diff --git a/website/content/docs/plugins/prisma/indirect-relations.mdx b/website/content/docs/plugins/prisma/indirect-relations.mdx new file mode 100644 index 000000000..f48a0ec4d --- /dev/null +++ b/website/content/docs/plugins/prisma/indirect-relations.mdx @@ -0,0 +1,125 @@ +--- +title: Indirect relations +description: Indirect relations and join tables +--- +## Selecting fields from a nested GraphQL field + +By default, the `nestedSelection` function will return selections based on the type of the current +field. `nestedSelection` can also be used to get a selection from a field nested deeper inside other +fields. This is useful if the field returns a type that is not a `prismaObject`, but a field nested +inside the returned type is. + +```typescript +const PostRef = builder.prismaObject('Post', { + fields: (t) => ({ + title: t.exposeString('title'), + content: t.exposeString('content'), + author: t.relation('author'), + }), +}); + +const PostPreview = builder.objectRef('PostPreview').implement({ + fields: (t) => ({ + post: t.field({ + type: PostRef, + resolve: (post) => post, + }), + preview: t.string({ + nullable: true, + resolve: (post) => post.content?.slice(10), + }), + }), +}); + +builder.prismaObject('User', { + fields: (t) => ({ + id: t.exposeID('id'), + postPreviews: t.field({ + select: (args, ctx, nestedSelection) => ({ + posts: nestedSelection( + { + // limit the number of postPreviews to load + take: 2, + }, + // Look at the selections in postPreviews.post to determine what relations/fields to select + ['post'], + // (optional) If the field returns a union or interface, you can pass a typeName to get selections for a specific object type + 'Post', + ), + }), + type: [PostPreview], + resolve: (user) => user.posts, + }), + }), +}); +``` + +## Indirect relations (eg. Join tables) + +If you want to define a GraphQL field that directly exposes data from a nested relationship (many to +many relations using a custom join table is a common example of this) you can use the +`nestedSelection` function passed to `select`. + +Given a prisma schema like the following: + +``` +model Post { + id Int @id @default(autoincrement()) + title String + content String + media PostMedia[] +} + +model Media { + id Int @id @default(autoincrement()) + url String + posts PostMedia[] + uploadedBy User @relation(fields: [uploadedById], references: [id]) + uploadedById Int +} + +model PostMedia { + id Int @id @default(autoincrement()) + post Post @relation(fields: [postId], references: [id]) + media Media @relation(fields: [mediaId], references: [id]) + postId Int + mediaId Int +} +``` + +You can define a media field that can pre-load the correct relations based on the graphql query: + +```typescript +const PostDraft = builder.prismaObject('Post', { + fields: (t) => ({ + title: t.exposeString('title'), + media: t.field({ + select: (args, ctx, nestedSelection) => ({ + media: { + select: { + // This will look at what fields are queried on Media + // and automatically select uploadedBy if that relation is requested + media: nestedSelection( + // This arument is the default query for the media relation + // It could be something like: `{ select: { id: true } }` instead + true, + ), + }, + }, + }), + type: [Media], + resolve: (post) => post.media.map(({ media }) => media), + }), + }), +}); + +const Media = builder.prismaObject('Media', { + select: { + id: true, + }, + fields: (t) => ({ + url: t.exposeString('url'), + uploadedBy: t.relation('uploadedBy'), + }), +}); +``` diff --git a/website/content/docs/plugins/prisma/interfaces.mdx b/website/content/docs/plugins/prisma/interfaces.mdx new file mode 100644 index 000000000..f92a70b56 --- /dev/null +++ b/website/content/docs/plugins/prisma/interfaces.mdx @@ -0,0 +1,46 @@ +--- +title: Interfaces +description: Creating interfaces for prisma models that can be shared by variants +--- + +`builder.prismaInterface` works just like builder.prismaObject and can be used to define either the +primary type or a variant for a model. + +The following example creates a `User` interface, and 2 variants Admin and Member. The `resolveType` +method returns the typenames as strings to avoid issues with circular references. + +```typescript +builder.prismaInterface('User', { + name: 'User', + fields: (t) => ({ + id: t.exposeID('id'), + email: t.exposeString('email'), + }), + resolveType: (user) => { + return user.isAdmin ? 'Admin' : 'Member'; + }, +}); + +builder.prismaObject('User', { + variant: 'Admin', + interfaces: [User], + fields: (t) => ({ + isAdmin: t.exposeBoolean('isAdmin'), + }), +}); + +builder.prismaObject('User', { + variant: 'Member', + interfaces: [User], + fields: (t) => ({ + bio: t.exposeString('bio'), + }), +}); +``` + +When using select mode, it's recommended to add selections to both the interface and the object +types that implement them. Selections are not inherited and will fallback to the default selection +which includes all scalar columns. + +You will not be able to extend an interface for a different prisma model, doing so will result in an +error at build time. diff --git a/website/content/docs/plugins/prisma/meta.json b/website/content/docs/plugins/prisma/meta.json index 8579326bb..52e252e65 100644 --- a/website/content/docs/plugins/prisma/meta.json +++ b/website/content/docs/plugins/prisma/meta.json @@ -4,8 +4,12 @@ "setup", "objects", "relations", + "selections", "relay", "connections", + "variants", + "indirect-relations", + "interfaces", "prisma-utils", "without-a-plugin", "..." diff --git a/website/content/docs/plugins/prisma/objects.mdx b/website/content/docs/plugins/prisma/objects.mdx index 3d63e11ac..96d2d973c 100644 --- a/website/content/docs/plugins/prisma/objects.mdx +++ b/website/content/docs/plugins/prisma/objects.mdx @@ -67,3 +67,12 @@ take advantage of more efficient queries. The `query` object will contain an object with `include` or `select` options to pre-load data needed to resolve nested parts of the current query. The included/selected fields are based on which fields are being queried, and the options provided when defining those fields and types. + + +## Extending prisma objects + +The normal `builder.objectField(s)` methods can be used to extend prisma objects, but do not support +using selections, or exposing fields not in the default selection. To use these features, you can +use + +`builder.prismaObjectField` or `builder.prismaObjectFields` instead. diff --git a/website/content/docs/plugins/prisma/relations.mdx b/website/content/docs/plugins/prisma/relations.mdx index 8b4ee08a0..6bf023ddc 100644 --- a/website/content/docs/plugins/prisma/relations.mdx +++ b/website/content/docs/plugins/prisma/relations.mdx @@ -149,3 +149,25 @@ builder.prismaObject('User', { }), }); ``` + +### relationCount + +Prisma supports querying for +[relation counts](https://www.prisma.io/docs/concepts/components/prisma-client/aggregation-grouping-summarizing#count-relations) +which allow including counts for relations along side other `includes`. Before prisma 4.2.0, this +does not support any filters on the counts, but can give a total count for a relation. Starting from +prisma 4.2.0, filters on relation count are available under the `filteredRelationCount` preview +feature flag. + +```typescript +builder.prismaObject('User', { + fields: (t) => ({ + id: t.exposeID('id'), + postCount: t.relationCount('posts', { + where: { + published: true, + }, + }), + }), +}); +``` diff --git a/website/content/docs/plugins/prisma/relay.mdx b/website/content/docs/plugins/prisma/relay.mdx index d9299a58f..a1a6d476a 100644 --- a/website/content/docs/plugins/prisma/relay.mdx +++ b/website/content/docs/plugins/prisma/relay.mdx @@ -59,3 +59,4 @@ builder.prismaNode('Post', { }), }); ``` + diff --git a/website/content/docs/plugins/prisma/selections.mdx b/website/content/docs/plugins/prisma/selections.mdx new file mode 100644 index 000000000..eac6f66a1 --- /dev/null +++ b/website/content/docs/plugins/prisma/selections.mdx @@ -0,0 +1,177 @@ +--- +title: Selections +description: how to use custom includes and selections to optimize your prisma queries +--- + +## Includes on types + +In some cases, you may want to always pre-load certain relations. This can be helpful for defining +fields directly on type where the underlying data may come from a related table. + +```typescript +builder.prismaObject('User', { + // This will always include the profile when a user object is loaded. Deeply nested relations can + // also be included this way. + include: { + profile: true, + }, + fields: (t) => ({ + id: t.exposeID('id'), + email: t.exposeString('email'), + bio: t.string({ + // The profile relation will always be loaded, and user will now be typed to include the + // profile field so you can return the bio from the nested profile relation. + resolve: (user) => user.profile.bio, + }), + }), +}); +``` + +## Select mode for types + +By default, the prisma plugin will use `include` when including relations, or generating fallback +queries. This means we are always loading all columns of a table when loading it in a +`t.prismaField` or a `t.relation`. This is usually what we want, but in some cases, you may want to +select specific columns instead. This can be useful if you have tables with either a very large +number of columns, or specific columns with large payloads you want to avoid loading. + +To do this, you can add a `select` instead of an include to your `prismaObject`: + +```typescript +builder.prismaObject('User', { + select: { + id: true, + }, + fields: (t) => ({ + id: t.exposeID('id'), + email: t.exposeString('email'), + }), +}); +``` + +The `t.expose*` and `t.relation` methods will all automatically add selections for the exposed +fields _WHEN THEY ARE QUERIED_, ensuring that only the requested columns will be loaded from the +database. + +In addition to the `t.expose` and `t.relation`, you can also add custom selections to other fields: + +```typescript +builder.prismaObject('User', { + select: { + id: true, + }, + fields: (t) => ({ + id: t.exposeID('id'), + email: t.exposeString('email'), + bio: t.string({ + // This will select user.profile.bio when the the `bio` field is queried + select: { + profile: { + select: { + bio: true, + }, + }, + }, + resolve: (user) => user.profile.bio, + }), + }), +}); +``` + +## Using arguments or context in your selections + +The following is a slightly contrived example, but shows how arguments can be used when creating a +selection for a field: + +```typescript +const PostDraft = builder.prismaObject('Post', { + fields: (t) => ({ + title: t.exposeString('title'), + commentFromDate: t.string({ + args: { + date: t.arg({ type: 'Date', required: true }), + }, + select: (args) => ({ + comments: { + take: 1, + where: { + createdAt: { + gt: args.date, + }, + }, + }, + }), + resolve: (post) => post.comments[0]?.content, + }), + }), +}); +``` + +## Optimized queries without `t.prismaField` + +In some cases, it may be useful to get an optimized query for fields where you can't use +`t.prismaField`. + +This may be required for combining with other plugins, or because your query does not directly +return a `PrismaObject`. In these cases, you can use the `queryFromInfo` helper. An example of this +might be a mutation that wraps the prisma object in a result type. + +```typescript +const Post = builder.prismaObject('Post', {...}); + +builder.objectRef<{ + success: boolean; + post?: Post + }>('CreatePostResult').implement({ + fields: (t) => ({ + success: t.boolean(), + post: t.field({ + type: Post, + nullable: + resolve: (result) => result.post, + }), + }), +}); + +builder.mutationField( + 'createPost', + { + args: (t) => ({ + title: t.string({ required: true }), + ... + }), + }, + { + resolve: async (parent, args, context, info) => { + if (!validateCreatePostArgs(args)) { + return { + success: false, + } + } + + const post = prisma.city.create({ + ...queryFromInfo({ + context, + info, + // nested path where the selections for this type can be found + path: ['post'] + // optionally you can pass a custom initial selection, generally you wouldn't need this + // but if the field at `path` is not selected, the initial selection set may be empty + select: { + comments: true, + }, + }), + data: { + title: args.input.title, + ... + }, + }); + + return { + success: true, + post, + } + }, + }, +); +``` diff --git a/website/content/docs/plugins/prisma/setup.mdx b/website/content/docs/plugins/prisma/setup.mdx index 383c842d7..7b347ee44 100644 --- a/website/content/docs/plugins/prisma/setup.mdx +++ b/website/content/docs/plugins/prisma/setup.mdx @@ -157,3 +157,21 @@ const builder = new SchemaBuilder<{ }, }); ``` + +## Detecting unused query arguments + +Forgetting to spread the `query` argument from `t.prismaField` or `t.prismaConnection` into your +prisma query can result in inefficient queries, or even missing data. To help catch these issues, +the plugin can warn you when you are not using the query argument correctly. + +the `onUnusedQuery` option can be set to `warn` or `error` to enable this feature. When set to +`warn` it will log a warning to the console if Pothos detects that you have not properly used the +query in your resolver. Similarly if you set the option to `error` it will throw an error instead. +You can also pass a function which will receive the `info` object which can be used to log or throw +your own error. + +This check is fairly naive and works by wrapping the properties on the query with a getter that sets +a flag if the property is accessed. If no properties are accessed on the query object before the +resolver returns, it will trigger the `onUnusedQuery` condition. + +It's recommended to enable this check in development to more quickly find potential issues. diff --git a/website/content/docs/plugins/prisma/variants.mdx b/website/content/docs/plugins/prisma/variants.mdx new file mode 100644 index 000000000..a7ffe84f4 --- /dev/null +++ b/website/content/docs/plugins/prisma/variants.mdx @@ -0,0 +1,102 @@ +--- +title: Type variants +description: How to define multiple GraphQL types based on the same prisma model +--- + +The prisma plugin supports defining multiple GraphQL types based on the same prisma model. +Additional types are called `variants`. You will always need to have a "Primary" variant (defined as +described above). Additional variants can be defined by providing a `variant` option instead of a +`name` option when creating the type: + +```typescript +const Viewer = builder.prismaObject('User', { + variant: 'Viewer', + fields: (t) => ({ + id: t.exposeID('id'), + }); +}); +``` + +You can define variant fields that reference one variant from another: + +```typescript +const Viewer = builder.prismaObject('User', { + variant: 'Viewer', + fields: (t) => ({ + id: t.exposeID('id'), + // Using the model name ('User') will reference the primary variant + user: t.variant('User'), + }); +}); + +const User = builder.prismaNode('User', { + id: { + resolve: (user) => user.id, + }, + fields: (t) => ({ + // To reference another variant, use the returned object Ref instead of the model name: + viewer: t.variant(Viewer, { + // return null for viewer if the parent User is not the current user + isNull: (user, args, ctx) => user.id !== ctx.user.id, + }), + email: t.exposeString('email'), + }), +}); +``` + +You can also use variants when defining relations by providing a `type` option: + +```typescript +const PostDraft = builder.prismaNode('Post', { + variant: 'PostDraft' + // This set's what database field to use for the nodes id field + id: { field: 'id' }, + // fields work just like they do for builder.prismaObject + fields: (t) => ({ + title: t.exposeString('title'), + author: t.relation('author'), + }), +}); + +const Viewer = builder.prismaObject('User', { + variant: 'Viewer', + fields: (t) => ({ + id: t.exposeID('id'), + drafts: t.relation('posts', { + // This will cause this relation to use the PostDraft variant rather than the default Post variant + type: PostDraft, + query: { where: { draft: true } }, + }), + }); +}); +``` + +You may run into circular reference issues if you use 2 prisma object refs to reference each other. +To avoid this, you can split out the field definition for one of the relationships using +`builder.prismaObjectField` + +```typescript +const Viewer = builder.prismaObject('User', { + variant: 'Viewer', + fields: (t) => ({ + id: t.exposeID('id'), + user: t.variant(User), + }); +}); + +const User = builder.prismaNode('User', { + interfaces: [Named], + id: { + resolve: (user) => user.id, + }, + fields: (t) => ({ + email: t.exposeString('email'), + }), +}); + +// Viewer references the `User` ref in its field definition, +// referencing the `User` in fields would cause a circular type issue +builder.prismaObjectField(Viewer, 'user', t.variant(User)); +``` + +This same workaround applies when defining relations using variants.