diff --git a/examples/prisma-federation/README.md b/examples/prisma-federation/README.md index aba664d99..89f4a5441 100644 --- a/examples/prisma-federation/README.md +++ b/examples/prisma-federation/README.md @@ -164,7 +164,10 @@ ReviewType.implement({ ```typescript // Use new `toSubGraphSchema` method to add subGraph specific types and queries to the schema -const schema = builder.toSubGraphSchema({}); +const schema = builder.toSubGraphSchema({ + // defaults to v2.3 + linkUrl: 'https://specs.apollo.dev/federation/v2.3', +}); const server = new ApolloServer({ schema, @@ -181,3 +184,88 @@ startStandaloneServer(server, { listen: { port: 4000 } }) For a functional example that combines multiple graphs built with Pothos into a single schema see [https://github.com/hayes/pothos/tree/main/packages/plugin-federation/tests/example](https://github.com/hayes/pothos/tree/main/packages/plugin-federation/tests/example) + +### Printing the schema + +If you are printing the schema as a string for any reason, and then using the printed schema for +Apollo Federation(submitting if using Managed Federation, or composing manually with `rover`), you +must use `printSubgraphSchema`(from `@apollo/subgraph`) or another compatible way of printing the +schema(that includes directives) in order for it to work. + +### Field directives directives + +Several federation directives can be configured directly when defining a field includes +`@shareable`, `@tag`, `@inaccessible`, and `@override`. + +```ts +t.field({ + type: 'String', + shareable: true, + tag: ['someTag'], + inaccessible: true, + override: { from: 'users' }, +}); +``` + +For more details on these directives, see the official Federation documentation. + +### interface entities and @interfaceObject + +Federation 2.3 introduces new features for federating interface definitions. + +You can now pass interfaces to `asEntity` to defined keys for an interface: + +```ts +const Media = builder.interfaceRef<{ id: string }>('Media').implement({ + fields: (t) => ({ + id: t.exposeID('id'), + ... + }), +}); + +builder.asEntity(Media, { + key: builder.selection<{ id: string }>('id'), + resolveReference: ({ id }) => loadMediaById(id), +}); +``` + +You can also extend interfaces from another subGraph by creating an `interfaceObject`: + +```ts +const Media = builder.objectRef<{ id: string }>('Media').implement({ + fields: (t) => ({ + id: t.exposeID('id'), + // add new MediaFields here that are available on all implementors of the `Media` type + }), +}); + +builder.asEntity(Media, { + interfaceObject: true, + key: builder.selection<{ id: string }>('id'), + resolveReference: (ref) => ref, +}); +``` + +See federation documentation for more details on `interfaceObject`s + +### composeDirective + +You can apply the `composeDirective` directive when building the subgraph schema: + +```ts +export const schema = builder.toSubGraphSchema({ + // This adds the @composeDirective directive + composeDirectives: ['@custom'], + // composeDirective requires an @link directive on the schema pointing the the url for your directive + schemaDirectives: { + link: { url: 'https://myspecs.dev/myCustomDirective/v1.0', import: ['@custom'] }, + }, + // You currently also need to provide an actual implementation for your Directive + directives: [ + new GraphQLDirective({ + locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE], + name: 'custom', + }), + ], +}); +``` diff --git a/packages/plugin-federation/README.md b/packages/plugin-federation/README.md index 5dd23b94c..89f4a5441 100644 --- a/packages/plugin-federation/README.md +++ b/packages/plugin-federation/README.md @@ -248,7 +248,7 @@ builder.asEntity(Media, { See federation documentation for more details on `interfaceObject`s -### composeDirective = +### composeDirective You can apply the `composeDirective` directive when building the subgraph schema: diff --git a/packages/plugin-prisma-utils/README.md b/packages/plugin-prisma-utils/README.md index dc145b199..2f097b78e 100644 --- a/packages/plugin-prisma-utils/README.md +++ b/packages/plugin-prisma-utils/README.md @@ -1,6 +1,6 @@ # Prisma utils for Pothos -This package is highly experimental and not recommended for production use +This package is highly experimental and not recommended for production use The plugin adds new helpers for creating prisma compatible input types. It is NOT required to use the normal prisma plugin. @@ -65,7 +65,7 @@ operations). ```typescript const StringFilter = builder.prismaFilter('String', { - ops: ['contains', 'equals', 'startsWith', 'not', 'equals'], + ops: ['contains', 'equals', 'startsWith', 'not'], }); export const IDFilter = builder.prismaFilter('Int', { @@ -268,7 +268,7 @@ There are 2 main approaches: 2. Dynamic Generation: Types are generated dynamically at runtime through helpers imported from your App -### Static generator: +### Static generator You can find an [example static generator here](https://github.com/hayes/pothos/blob/main/packages/plugin-prisma-utils/tests/examples/codegen/generator.ts) diff --git a/packages/plugin-relay/README.md b/packages/plugin-relay/README.md index 5a7340778..cb7b4cd95 100644 --- a/packages/plugin-relay/README.md +++ b/packages/plugin-relay/README.md @@ -17,24 +17,19 @@ yarn add @pothos/plugin-relay import RelayPlugin from '@pothos/plugin-relay'; const builder = new SchemaBuilder({ plugins: [RelayPlugin], - relayOptions: { - // These will become the defaults in the next major version - clientMutationId: 'omit', - cursorType: 'String', - }, + relay: {}, }); ``` ### Options -The `relayOptions` object passed to builder can contain the following properties: +The `relay` options object passed to builder can contain the following properties: - `idFieldName`: The name of the field that contains the global id for the node. Defaults to `id`. - `idFieldOptions`: Options to pass to the id field. -- `clientMutationId`: `required` (default) | `omit` | `optional`. Determines if clientMutationId +- `clientMutationId`: `omit` (default) | `required` | `optional`. Determines if clientMutationId fields are created on `relayMutationFields`, and if they are required. -- `cursorType`: `String` | `ID`. Determines type used for cursor fields. Defaults behavior due to - legacy reasons is `String` for everything except for connection arguments which use `ID`. +- `cursorType`: `String` | `ID`. Determines type used for cursor fields. Defaults to `String` Overwriting this default is highly encouraged. - `nodeQueryOptions`: Options for the `node` field on the query object, set to false to omit the field @@ -64,6 +59,85 @@ The `relayOptions` object passed to builder can contain the following properties - `defaultMutationInputTypeOptions`: default options for the mutation `Input` types. - `nodesOnConnection`: If true, the `nodes` field will be added to the `Connection` object types. - `defaultConnectionFieldOptions`: Default options for connection fields defined with t.connection +- `brandLoadedObjects`: Defaults to `true`. This will add a hidden symbol to objects returned from + the `load` methods of Nodes that allows the default `resolveType` implementation to identify the + type of the node. When this is enabled, you will not need to implement an `isTypeOf` check for + most common patterns. + +### Creating Nodes + +To create objects that extend the `Node` interface, you can use the new `builder.node` method. + +```typescript +// Using object refs +const User = builder.objectRef('User'); +// Or using a class +class User { + id: string; + name: string; +} + +builder.node(User, { + // define an id field + id: { + resolve: (user) => user.id, + // other options for id field can be added here + }, + + // Define only one of the following methods for loading nodes by id + loadOne: (id) => loadUserByID(id), + loadMany: (ids) => loadUsers(ids), + loadWithoutCache: (id) => loadUserByID(id), + loadManyWithoutCache: (ids) => loadUsers(ids), + + // if using a class instaed of a ref, you will need to provide a name + name: 'User', + fields: (t) => ({ + name: t.exposeString('name'), + }), +}); +``` + +`builder.node` will create an object type that implements the `Node` interface. It will also create +the `Node` interface the first time it is used. The `resolve` function for `id` should return a +number or string, which will be converted to a globalID. The relay plugin adds to new query fields +`node` and `nodes` which can be used to directly fetch nodes using global IDs by calling the +provided `loadOne` or `loadMany` method. Each node will only be loaded once by id, and cached if the +same node is loaded multiple times inn the same request. You can provide `loadWithoutCache` or +`loadManyWithoutCache` instead if caching is not desired, or you are already using a caching +datasource like a dataloader. + +Nodes may also implement an `isTypeOf` method which can be used to resolve the correct type for +lists of generic nodes. When using a class as the type parameter, the `isTypeOf` method defaults to +using an `instanceof` check, and falls back to checking the constructor property on the prototype. +The means that for many cases if you are using classes in your type parameters, and all your values +are instances of those classes, you won't need to implement an `isTypeOf` method, but it is usually +better to explicitly define that behavior. + +By default (unless `brandLoadedObjects` is set to `false`) any nodes loaded through one of the +`load*` methods will be branded so that the default `resolveType` method can identify the GraphQL +type for the loaded object. This means `isTypeOf` is only required for `union` and `interface` +fields that return node objects that are manually loaded, where the union or interface does not have +a custom `resolveType` method that knows how to resolve the node type. + +#### parsing node ids + +By default all node ids are parsed as string. This behavior can be customized by providing a custom +parse function for your node's ID field: + +```ts +const User = builder.objectRef('User') +builder.node(User, { + // define an id field + id: { + resolve: (user) => user.id, + parse: (id) => Number.parseInt(id, 10), + }, + // the ID is now a number + loadOne: (id) => loadUserByID(id), + ... +}); +``` ### Global IDs @@ -76,7 +150,7 @@ import { encodeGlobalID } from '@pothos/plugin-relay'; builder.queryFields((t) => ({ singleID: t.globalID({ resolve: (parent, args, context) => { - return encodeGlobalID('SomeType', 123); + return { id: 123, type: 'SomeType' }; }, }), listOfIDs: t.globalIDList({ @@ -138,76 +212,6 @@ builder.queryType({ }); ``` -### Creating Nodes - -To create objects that extend the `Node` interface, you can use the new `builder.node` method. - -```typescript -class NumberThing { - id: number; - - binary: string; - - constructor(n: number) { - this.id = n; - this.binary = n.toString(2); - } -} - -builder.node(NumberThing, { - // define an id field - id: { - resolve: (num) => num.id, - // other options for id field can be added here - }, - - // Define only one of the following methods for loading nodes by id - loadOne: (id) => new NumberThing(parseInt(id)), - loadMany: (ids) => ids.map((id) => new NumberThing(parseInt(id))), - loadWithoutCache: (id) => new NumberThing(parseInt(id)), - loadManyWithoutCache: (ids) => ids.map((id) => new NumberThing(parseInt(id))), - - name: 'Number', - fields: (t) => ({ - binary: t.exposeString('binary', {}), - }), -}); -``` - -`builder.node` will create an object type that implements the `Node` interface. It will also create -the `Node` interface the first time it is used. The `resolve` function for `id` should return a -number or string, which will be converted to a globalID. The relay plugin adds to new query fields -`node` and `nodes` which can be used to directly fetch nodes using global IDs by calling the -provided `loadOne` or `loadMany` method. Each node will only be loaded once by id, and cached if the -same node is loaded multiple times inn the same request. You can provide `loadWithoutCache` or -`loadManyWithoutCache` instead if caching is not desired, or you are already using a caching -datasource like a dataloader. - -Nodes may also implement an `isTypeOf` method which can be used to resolve the correct type for -lists of generic nodes. When using a class as the type parameter, the `isTypeOf` method defaults to -using an `instanceof` check, and falls back to checking the constructor property on the prototype. -The means that for many cases if you are using classes in your type parameters, and all your values -are instances of those classes, you won't need to implement an `isTypeOf` method, but it is usually -better to explicitly define that behavior. - -#### parsing node ids - -By default all node ids are parsed as string. This behavior can be customized by providing a custom -parse function for your node's ID field: - -```ts -builder.node(NumberThing, { - // define an id field - id: { - resolve: (num) => num.id, - parse: (id) => Number.parseInt(id, 10), - }, - // the ID is now a number - loadOne: (id) => new NumberThing(id), - ... -}); -``` - ### Creating Connections The `t.connection` field builder method can be used to define connections. This method will @@ -237,9 +241,9 @@ builder.queryFields((t) => ({ cursor: 'def', node: new NumberThing(123), }, - ] - } - }), + ], + }; + }, }, { name: 'NameOfConnectionType', // optional, will use ParentObject + capitalize(FieldName) + "Connection" as the default @@ -247,7 +251,7 @@ builder.queryFields((t) => ({ // define extra fields on Connection // We need to use a new variable for the connection field builder (eg tc) to get the correct types }), - edgesField: {} // optional, allows customizing the edges field on the Connection Object + edgesField: {}, // optional, allows customizing the edges field on the Connection Object // Other options for connection object can be added here }, { @@ -257,7 +261,7 @@ builder.queryFields((t) => ({ // define extra fields on Edge // We need to use a new variable for the connection field builder (eg te) to get the correct types }), - nodeField: {} // optional, allows customizing the node field on the Edge Object + nodeField: {}, // optional, allows customizing the node field on the Edge Object }, ), })); @@ -273,7 +277,7 @@ import { resolveOffsetConnection } from '@pothos/plugin-relay'; builder.queryFields((t) => ({ things: t.connection({ - type: SomeThings, + type: SomeThing, resolve: (parent, args) => { return resolveOffsetConnection({ args }, ({ limit, offset }) => { return getThings(offset, limit); diff --git a/website/components/Docs/Alert.tsx b/website/components/Docs/Alert.tsx index 980418df1..4e7477580 100644 --- a/website/components/Docs/Alert.tsx +++ b/website/components/Docs/Alert.tsx @@ -4,7 +4,7 @@ import { ExclamationTriangleIcon } from '@heroicons/react/24/solid'; export interface AlertProps extends HTMLProps {} -export default function Alert({ children, title }: AlertProps) { +export default function Alert({ children }: AlertProps) { return (
@@ -12,7 +12,7 @@ export default function Alert({ children, title }: AlertProps) {
-

{children}

+

{children}

diff --git a/website/pages/docs/plugins/federation.mdx b/website/pages/docs/plugins/federation.mdx index 71b0f75f9..06daa8605 100644 --- a/website/pages/docs/plugins/federation.mdx +++ b/website/pages/docs/plugins/federation.mdx @@ -262,7 +262,7 @@ builder.asEntity(Media, { See federation documentation for more details on `interfaceObject`s -### composeDirective = +### composeDirective You can apply the `composeDirective` directive when building the subgraph schema: diff --git a/website/pages/docs/plugins/prisma-utils.mdx b/website/pages/docs/plugins/prisma-utils.mdx index 06bf86347..c0de88e70 100644 --- a/website/pages/docs/plugins/prisma-utils.mdx +++ b/website/pages/docs/plugins/prisma-utils.mdx @@ -6,6 +6,7 @@ description: Prisma utils for creating input types --- import { DocsPage } from '../../../components/Docs/Page'; +import Alert from '../../../components/Docs/Alert'; import { buildNav } from '../../../util/build-nav'; export default DocsPage; @@ -14,7 +15,7 @@ export const getStaticProps = () => ({ props: { nav: buildNav() } }); # Prisma utils for Pothos -This package is highly experimental and not recommended for production use +This package is highly experimental and not recommended for production use The plugin adds new helpers for creating prisma compatible input types. It is NOT required to use the normal prisma plugin. diff --git a/website/pages/docs/plugins/relay.mdx b/website/pages/docs/plugins/relay.mdx index aa0f38fe0..693b48f43 100644 --- a/website/pages/docs/plugins/relay.mdx +++ b/website/pages/docs/plugins/relay.mdx @@ -31,24 +31,19 @@ yarn add @pothos/plugin-relay import RelayPlugin from '@pothos/plugin-relay'; const builder = new SchemaBuilder({ plugins: [RelayPlugin], - relayOptions: { - // These will become the defaults in the next major version - clientMutationId: 'omit', - cursorType: 'String', - }, + relay: {}, }); ``` ### Options -The `relayOptions` object passed to builder can contain the following properties: +The `relay` options object passed to builder can contain the following properties: - `idFieldName`: The name of the field that contains the global id for the node. Defaults to `id`. - `idFieldOptions`: Options to pass to the id field. -- `clientMutationId`: `required` (default) | `omit` | `optional`. Determines if clientMutationId +- `clientMutationId`: `omit` (default) | `required` | `optional`. Determines if clientMutationId fields are created on `relayMutationFields`, and if they are required. -- `cursorType`: `String` | `ID`. Determines type used for cursor fields. Defaults behavior due to - legacy reasons is `String` for everything except for connection arguments which use `ID`. +- `cursorType`: `String` | `ID`. Determines type used for cursor fields. Defaults to `String` Overwriting this default is highly encouraged. - `nodeQueryOptions`: Options for the `node` field on the query object, set to false to omit the field @@ -78,6 +73,85 @@ The `relayOptions` object passed to builder can contain the following properties - `defaultMutationInputTypeOptions`: default options for the mutation `Input` types. - `nodesOnConnection`: If true, the `nodes` field will be added to the `Connection` object types. - `defaultConnectionFieldOptions`: Default options for connection fields defined with t.connection +- `brandLoadedObjects`: Defaults to `true`. This will add a hidden symbol to objects returned from + the `load` methods of Nodes that allows the default `resolveType` implementation to identify the + type of the node. When this is enabled, you will not need to implement an `isTypeOf` check for + most common patterns. + +### Creating Nodes + +To create objects that extend the `Node` interface, you can use the new `builder.node` method. + +```typescript +// Using object refs +const User = builder.objectRef('User'); +// Or using a class +class User { + id: string; + name: string; +} + +builder.node(User, { + // define an id field + id: { + resolve: (user) => user.id, + // other options for id field can be added here + }, + + // Define only one of the following methods for loading nodes by id + loadOne: (id) => loadUserByID(id), + loadMany: (ids) => loadUsers(ids), + loadWithoutCache: (id) => loadUserByID(id), + loadManyWithoutCache: (ids) => loadUsers(ids), + + // if using a class instaed of a ref, you will need to provide a name + name: 'User', + fields: (t) => ({ + name: t.exposeString('name'), + }), +}); +``` + +`builder.node` will create an object type that implements the `Node` interface. It will also create +the `Node` interface the first time it is used. The `resolve` function for `id` should return a +number or string, which will be converted to a globalID. The relay plugin adds to new query fields +`node` and `nodes` which can be used to directly fetch nodes using global IDs by calling the +provided `loadOne` or `loadMany` method. Each node will only be loaded once by id, and cached if the +same node is loaded multiple times inn the same request. You can provide `loadWithoutCache` or +`loadManyWithoutCache` instead if caching is not desired, or you are already using a caching +datasource like a dataloader. + +Nodes may also implement an `isTypeOf` method which can be used to resolve the correct type for +lists of generic nodes. When using a class as the type parameter, the `isTypeOf` method defaults to +using an `instanceof` check, and falls back to checking the constructor property on the prototype. +The means that for many cases if you are using classes in your type parameters, and all your values +are instances of those classes, you won't need to implement an `isTypeOf` method, but it is usually +better to explicitly define that behavior. + +By default (unless `brandLoadedObjects` is set to `false`) any nodes loaded through one of the +`load*` methods will be branded so that the default `resolveType` method can identify the GraphQL +type for the loaded object. This means `isTypeOf` is only required for `union` and `interface` +fields that return node objects that are manually loaded, where the union or interface does not have +a custom `resolveType` method that knows how to resolve the node type. + +#### parsing node ids + +By default all node ids are parsed as string. This behavior can be customized by providing a custom +parse function for your node's ID field: + +```ts +const User = builder.objectRef('User') +builder.node(User, { + // define an id field + id: { + resolve: (user) => user.id, + parse: (id) => Number.parseInt(id, 10), + }, + // the ID is now a number + loadOne: (id) => loadUserByID(id), + ... +}); +``` ### Global IDs @@ -90,7 +164,7 @@ import { encodeGlobalID } from '@pothos/plugin-relay'; builder.queryFields((t) => ({ singleID: t.globalID({ resolve: (parent, args, context) => { - return encodeGlobalID('SomeType', 123); + return { id: 123, type: 'SomeType' }; }, }), listOfIDs: t.globalIDList({ @@ -152,76 +226,6 @@ builder.queryType({ }); ``` -### Creating Nodes - -To create objects that extend the `Node` interface, you can use the new `builder.node` method. - -```typescript -class NumberThing { - id: number; - - binary: string; - - constructor(n: number) { - this.id = n; - this.binary = n.toString(2); - } -} - -builder.node(NumberThing, { - // define an id field - id: { - resolve: (num) => num.id, - // other options for id field can be added here - }, - - // Define only one of the following methods for loading nodes by id - loadOne: (id) => new NumberThing(parseInt(id)), - loadMany: (ids) => ids.map((id) => new NumberThing(parseInt(id))), - loadWithoutCache: (id) => new NumberThing(parseInt(id)), - loadManyWithoutCache: (ids) => ids.map((id) => new NumberThing(parseInt(id))), - - name: 'Number', - fields: (t) => ({ - binary: t.exposeString('binary', {}), - }), -}); -``` - -`builder.node` will create an object type that implements the `Node` interface. It will also create -the `Node` interface the first time it is used. The `resolve` function for `id` should return a -number or string, which will be converted to a globalID. The relay plugin adds to new query fields -`node` and `nodes` which can be used to directly fetch nodes using global IDs by calling the -provided `loadOne` or `loadMany` method. Each node will only be loaded once by id, and cached if the -same node is loaded multiple times inn the same request. You can provide `loadWithoutCache` or -`loadManyWithoutCache` instead if caching is not desired, or you are already using a caching -datasource like a dataloader. - -Nodes may also implement an `isTypeOf` method which can be used to resolve the correct type for -lists of generic nodes. When using a class as the type parameter, the `isTypeOf` method defaults to -using an `instanceof` check, and falls back to checking the constructor property on the prototype. -The means that for many cases if you are using classes in your type parameters, and all your values -are instances of those classes, you won't need to implement an `isTypeOf` method, but it is usually -better to explicitly define that behavior. - -#### parsing node ids - -By default all node ids are parsed as string. This behavior can be customized by providing a custom -parse function for your node's ID field: - -```ts -builder.node(NumberThing, { - // define an id field - id: { - resolve: (num) => num.id, - parse: (id) => Number.parseInt(id, 10), - }, - // the ID is now a number - loadOne: (id) => new NumberThing(id), - ... -}); -``` - ### Creating Connections The `t.connection` field builder method can be used to define connections. This method will @@ -251,9 +255,9 @@ builder.queryFields((t) => ({ cursor: 'def', node: new NumberThing(123), }, - ] - } - }), + ], + }; + }, }, { name: 'NameOfConnectionType', // optional, will use ParentObject + capitalize(FieldName) + "Connection" as the default @@ -261,7 +265,7 @@ builder.queryFields((t) => ({ // define extra fields on Connection // We need to use a new variable for the connection field builder (eg tc) to get the correct types }), - edgesField: {} // optional, allows customizing the edges field on the Connection Object + edgesField: {}, // optional, allows customizing the edges field on the Connection Object // Other options for connection object can be added here }, { @@ -271,7 +275,7 @@ builder.queryFields((t) => ({ // define extra fields on Edge // We need to use a new variable for the connection field builder (eg te) to get the correct types }), - nodeField: {} // optional, allows customizing the node field on the Edge Object + nodeField: {}, // optional, allows customizing the node field on the Edge Object }, ), })); @@ -287,7 +291,7 @@ import { resolveOffsetConnection } from '@pothos/plugin-relay'; builder.queryFields((t) => ({ things: t.connection({ - type: SomeThings, + type: SomeThing, resolve: (parent, args) => { return resolveOffsetConnection({ args }, ({ limit, offset }) => { return getThings(offset, limit); diff --git a/website/pages/index.mdx b/website/pages/index.mdx index 69f269c19..44cb0e3e9 100644 --- a/website/pages/index.mdx +++ b/website/pages/index.mdx @@ -12,7 +12,7 @@ export default DocsPage; export const getStaticProps = () => ({ props: { nav: buildNav() } }); -![Pothos](https://pothos-graphql.dev/assets/logo-name-auto.svg) +![Pothos](/assets/logo-name-auto.svg) # Pothos GraphQL