diff --git a/.changeset/heavy-schools-relate.md b/.changeset/heavy-schools-relate.md new file mode 100644 index 000000000..ce26717ed --- /dev/null +++ b/.changeset/heavy-schools-relate.md @@ -0,0 +1,9 @@ +--- +'@pothos-examples/prisma-smart-subscriptions-apollo': minor +'@pothos-examples/prisma-federation': minor +'@pothos/plugin-directives': minor +'@pothos/plugin-federation': minor +'@pothos/deno': minor +--- + +Add support for @interfaceObject and @composeDirective diff --git a/.changeset/ten-spoons-sort.md b/.changeset/ten-spoons-sort.md new file mode 100644 index 000000000..69f72e62c --- /dev/null +++ b/.changeset/ten-spoons-sort.md @@ -0,0 +1,5 @@ +--- +'@pothos/plugin-directives': minor +--- + +Add schemaDirectives option to `toSchema` for defining SCHEMA directives diff --git a/examples/prisma-federation/package.json b/examples/prisma-federation/package.json index 40e166159..4aac3068f 100644 --- a/examples/prisma-federation/package.json +++ b/examples/prisma-federation/package.json @@ -15,8 +15,8 @@ }, "dependencies": { "@apollo/gateway": "2.3.1", - "@apollo/server": "^4.3.3", - "@apollo/subgraph": "2.3.1", + "@apollo/server": "^4.4.0", + "@apollo/subgraph": "2.3.2", "@faker-js/faker": "^7.6.0", "@pothos/core": "workspace:*", "@pothos/plugin-directives": "workspace:*", diff --git a/examples/prisma-smart-subscriptions-apollo/package.json b/examples/prisma-smart-subscriptions-apollo/package.json index 985000576..07c6a8230 100644 --- a/examples/prisma-smart-subscriptions-apollo/package.json +++ b/examples/prisma-smart-subscriptions-apollo/package.json @@ -14,7 +14,7 @@ "seed": "node -r @swc-node/register prisma/seed.ts" }, "dependencies": { - "@apollo/server": "^4.3.3", + "@apollo/server": "^4.4.0", "@faker-js/faker": "^7.6.0", "@pothos/core": "workspace:*", "@pothos/plugin-prisma": "workspace:*", diff --git a/packages/deno/packages/plugin-directives/README.md b/packages/deno/packages/plugin-directives/README.md index d0001970c..8ef589371 100644 --- a/packages/deno/packages/plugin-directives/README.md +++ b/packages/deno/packages/plugin-directives/README.md @@ -50,7 +50,7 @@ an object type representing the arguments the directive accepts. The valid locations for directives are: -- `ARGUMENT_DEFINITION` \| +- `ARGUMENT_DEFINITION` - `ENUM_VALUE` - `ENUM` - `FIELD_DEFINITION` @@ -104,3 +104,11 @@ supported locations using one of the following 2 formats: Each of these applies the same 2 directives. The first format is preferred, especially when using directives that are sensitive to ordering, or can be repeated multiple times for the same location. + +## Applying directives + +For most locations (On fields and types) the options object for the field or type will have a +`directives` option which can be used to define directives. + +To apply `SCHEMA` directives, you can use the `schemaDirectives` option on the `toSchema` method. +`directives` on `toSchema` is reserved for the Directive implementations. diff --git a/packages/deno/packages/plugin-directives/global-types.ts b/packages/deno/packages/plugin-directives/global-types.ts index cafcb2532..c3bb6e5d7 100644 --- a/packages/deno/packages/plugin-directives/global-types.ts +++ b/packages/deno/packages/plugin-directives/global-types.ts @@ -61,5 +61,8 @@ declare global { export interface EnumValueConfig { directives?: Directives; } + export interface BuildSchemaOptions { + schemaDirectives?: Directives; + } } } diff --git a/packages/deno/packages/plugin-directives/index.ts b/packages/deno/packages/plugin-directives/index.ts index 5bfeab2ad..13073ccc6 100644 --- a/packages/deno/packages/plugin-directives/index.ts +++ b/packages/deno/packages/plugin-directives/index.ts @@ -62,6 +62,10 @@ export class PothosDirectivesPlugin extends BasePlugi }; } override afterBuild(schema: GraphQLSchema) { + schema.extensions = { + ...schema.extensions, + directives: this.normalizeDirectives(this.mergeDirectives((schema.extensions?.directives as Record) ?? {}, this.options.schemaDirectives as unknown as Record)), + }; mockAst(schema); return schema; } diff --git a/packages/plugin-directives/README.md b/packages/plugin-directives/README.md index d0001970c..8ef589371 100644 --- a/packages/plugin-directives/README.md +++ b/packages/plugin-directives/README.md @@ -50,7 +50,7 @@ an object type representing the arguments the directive accepts. The valid locations for directives are: -- `ARGUMENT_DEFINITION` \| +- `ARGUMENT_DEFINITION` - `ENUM_VALUE` - `ENUM` - `FIELD_DEFINITION` @@ -104,3 +104,11 @@ supported locations using one of the following 2 formats: Each of these applies the same 2 directives. The first format is preferred, especially when using directives that are sensitive to ordering, or can be repeated multiple times for the same location. + +## Applying directives + +For most locations (On fields and types) the options object for the field or type will have a +`directives` option which can be used to define directives. + +To apply `SCHEMA` directives, you can use the `schemaDirectives` option on the `toSchema` method. +`directives` on `toSchema` is reserved for the Directive implementations. diff --git a/packages/plugin-directives/src/global-types.ts b/packages/plugin-directives/src/global-types.ts index 47a4b9f39..7624bd229 100644 --- a/packages/plugin-directives/src/global-types.ts +++ b/packages/plugin-directives/src/global-types.ts @@ -117,5 +117,9 @@ declare global { export interface EnumValueConfig { directives?: Directives; } + + export interface BuildSchemaOptions { + schemaDirectives?: Directives; + } } } diff --git a/packages/plugin-directives/src/index.ts b/packages/plugin-directives/src/index.ts index c93c2cb1c..8ced8186b 100644 --- a/packages/plugin-directives/src/index.ts +++ b/packages/plugin-directives/src/index.ts @@ -103,6 +103,16 @@ export class PothosDirectivesPlugin extends BasePlugi } override afterBuild(schema: GraphQLSchema) { + schema.extensions = { + ...schema.extensions, + directives: this.normalizeDirectives( + this.mergeDirectives( + (schema.extensions?.directives as Record) ?? {}, + this.options.schemaDirectives as unknown as Record, + ), + ), + }; + mockAst(schema); return schema; diff --git a/packages/plugin-federation/README.md b/packages/plugin-federation/README.md index d1c3388d1..5dd23b94c 100644 --- a/packages/plugin-federation/README.md +++ b/packages/plugin-federation/README.md @@ -191,3 +191,81 @@ If you are printing the schema as a string for any reason, and then using the pr 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/package.json b/packages/plugin-federation/package.json index 0adacff68..ade2a4bc1 100644 --- a/packages/plugin-federation/package.json +++ b/packages/plugin-federation/package.json @@ -44,8 +44,8 @@ }, "devDependencies": { "@apollo/gateway": "^2.3.1", - "@apollo/server": "^4.3.3", - "@apollo/subgraph": "2.3.1", + "@apollo/server": "^4.4.0", + "@apollo/subgraph": "2.3.2", "@graphql-tools/utils": "^9.2.1", "@pothos/core": "workspace:*", "@pothos/plugin-directives": "workspace:*", diff --git a/packages/plugin-federation/src/global-types.ts b/packages/plugin-federation/src/global-types.ts index 017166cb8..4bc455b27 100644 --- a/packages/plugin-federation/src/global-types.ts +++ b/packages/plugin-federation/src/global-types.ts @@ -86,13 +86,18 @@ declare global { toSubGraphSchema: ( options: BuildSchemaOptions & { linkUrl?: string; + composeDirectives?: `@${string}`[]; }, ) => GraphQLSchema; - asEntity: , KeySelection extends Selection>( + asEntity: < + Param extends ObjectRef | InterfaceRef, + KeySelection extends Selection, + >( param: Param, options: { key: KeySelection | KeySelection[]; + interfaceObject?: Param extends ObjectRef ? boolean : never; resolveReference: ( parent: KeySelection[typeof selectionShapeKey], context: Types['Context'], diff --git a/packages/plugin-federation/src/index.ts b/packages/plugin-federation/src/index.ts index 9733c8194..185e328dd 100644 --- a/packages/plugin-federation/src/index.ts +++ b/packages/plugin-federation/src/index.ts @@ -42,6 +42,7 @@ export class PothosFederationPlugin extends BasePlugi directives: mergeDirectives(typeConfig.extensions?.directives as [], [ ...(entityConfig ? keyDirective(entityConfig.key) : []), ...commonDirectives, + ...(entityConfig?.interfaceObject ? [{ name: 'interfaceObject', args: {} }] : []), ]), }, }; diff --git a/packages/plugin-federation/src/schema-builder.ts b/packages/plugin-federation/src/schema-builder.ts index d7fdc166a..bc6582d76 100644 --- a/packages/plugin-federation/src/schema-builder.ts +++ b/packages/plugin-federation/src/schema-builder.ts @@ -14,6 +14,7 @@ import { entitiesField, EntityType, serviceField } from '@apollo/subgraph/dist/t import SchemaBuilder, { MaybePromise, SchemaTypes } from '@pothos/core'; import { ExternalEntityRef } from './external-ref'; import { Selection, SelectionFromShape, selectionShapeKey } from './types'; +import { mergeDirectives } from './util'; const schemaBuilderProto = SchemaBuilder.prototype as PothosSchemaTypes.SchemaBuilder; @@ -50,27 +51,40 @@ schemaBuilderProto.toSubGraphSchema = function toSubGraphSchema({ linkUrl = 'https://specs.apollo.dev/federation/v2.3', ...options }) { + const hasInterfaceObjects = [...(entityMapping.get(this)?.values() ?? [])].some( + (m) => m.interfaceObject, + ); const schema = this.toSchema({ ...options, extensions: { ...options.extensions, - directives: { - ...(options?.extensions?.directives as {}), - link: { - url: linkUrl, - import: [ - '@key', - '@shareable', - '@inaccessible', - '@tag', - '@provides', - '@requires', - '@external', - '@extends', - '@override', - ], + directives: mergeDirectives(options?.extensions?.directives as {}, [ + { + name: 'link', + args: { + url: linkUrl, + import: [ + '@key', + '@shareable', + '@inaccessible', + '@tag', + '@provides', + '@requires', + '@external', + '@extends', + '@override', + options.composeDirectives ? '@composeDirective' : null, + hasInterfaceObjects ? '@interfaceObject' : null, + ].filter(Boolean), + }, }, - }, + ...(options.composeDirectives?.map((name) => ({ + name: 'composeDirective', + args: { + name, + }, + })) ?? []), + ]), }, }); const queryType = schema.getType('Query') as GraphQLObjectType | undefined; @@ -137,6 +151,7 @@ export const entityMapping = new WeakMap< string, { key: Selection | Selection[]; + interfaceObject?: boolean; resolveReference: (val: object, context: {}, info: GraphQLResolveInfo) => unknown; } > diff --git a/packages/plugin-federation/tests/__snapshots__/index.test.ts.snap b/packages/plugin-federation/tests/__snapshots__/index.test.ts.snap index 9f9dac9c1..ea26c73e2 100644 --- a/packages/plugin-federation/tests/__snapshots__/index.test.ts.snap +++ b/packages/plugin-federation/tests/__snapshots__/index.test.ts.snap @@ -2,7 +2,15 @@ exports[`federation accounts schema generates expected schema 1`] = ` "extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable", "@inaccessible", "@tag", "@provides", "@requires", "@external", "@extends", "@override"]) + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable", "@inaccessible", "@tag", "@provides", "@requires", "@external", "@extends", "@override", "@interfaceObject"]) + +type Media + @key(fields: "id") + @interfaceObject +{ + id: ID + test: String +} type Query { me: User @@ -18,7 +26,12 @@ type User `; exports[`federation accounts schema generates expected schema 2`] = ` -"type Query { +"type Media { + id: ID + test: String +} + +type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! me: User @@ -32,7 +45,7 @@ type User { scalar _Any -union _Entity = User +union _Entity = Media | User type _Service { """ @@ -86,10 +99,28 @@ type _Service { exports[`federation products schema generates expected schema 1`] = ` "extend schema - @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable", "@inaccessible", "@tag", "@provides", "@requires", "@external", "@extends", "@override"]) + @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable", "@inaccessible", "@tag", "@provides", "@requires", "@external", "@extends", "@override", "@composeDirective"]) + @link(url: "https://myspecs.dev/myCustomDirective/v1.0", import: ["@custom"]) + @composeDirective(name: "@custom") + +directive @custom on INTERFACE | OBJECT + +interface Media + @key(fields: "id") +{ + id: ID +} + +type Post implements Media + @key(fields: "id") +{ + id: ID + title: String +} type Product @key(fields: "upc") + @custom { upc: String name: String @@ -98,12 +129,24 @@ type Product } type Query { + post: Post topProducts(first: Int): [Product!] }" `; exports[`federation products schema generates expected schema 2`] = ` -"type Product { +"directive @custom on INTERFACE | OBJECT + +interface Media { + id: ID +} + +type Post implements Media { + id: ID + title: String +} + +type Product { name: String price: Float upc: String @@ -113,12 +156,13 @@ exports[`federation products schema generates expected schema 2`] = ` type Query { _entities(representations: [_Any!]!): [_Entity]! _service: _Service! + post: Post topProducts(first: Int = 5): [Product!] } scalar _Any -union _Entity = Product +union _Entity = Post | Product type _Service { """ diff --git a/packages/plugin-federation/tests/example/accounts/schema.ts b/packages/plugin-federation/tests/example/accounts/schema.ts index ef01f4b32..27b67c97a 100644 --- a/packages/plugin-federation/tests/example/accounts/schema.ts +++ b/packages/plugin-federation/tests/example/accounts/schema.ts @@ -56,4 +56,19 @@ builder.queryType({ }), }); +const Media = builder.objectRef<{ id: string }>('Media').implement({ + fields: (t) => ({ + id: t.exposeID('id'), + test: t.string({ + resolve: (media) => 'test field from interfaceObject', + }), + }), +}); + +builder.asEntity(Media, { + interfaceObject: true, + key: builder.selection<{ id: string }>('id'), + resolveReference: (ref) => ref, +}); + export const schema = builder.toSubGraphSchema({}); diff --git a/packages/plugin-federation/tests/example/products/schema.ts b/packages/plugin-federation/tests/example/products/schema.ts index 48900470a..9f5ad886d 100644 --- a/packages/plugin-federation/tests/example/products/schema.ts +++ b/packages/plugin-federation/tests/example/products/schema.ts @@ -1,9 +1,23 @@ +import { DirectiveLocation, GraphQLDirective } from 'graphql'; import SchemaBuilder from '@pothos/core'; import DirectivesPlugin from '@pothos/plugin-directives'; import FederationPlugin from '../../../src'; const builder = new SchemaBuilder<{ DefaultFieldNullability: true; + Directives: { + custom: { + locations: 'OBJECT' | 'INTERFACE'; + args: {}; + }; + link: { + locations: 'SCHEMA'; + args: { + url: string; + import: string[]; + }; + }; + }; }>({ plugins: [DirectivesPlugin, FederationPlugin], useGraphQLToolsUnorderedDirectives: true, @@ -38,7 +52,23 @@ const products: Product[] = [ }, ]; +const Media = builder.interfaceRef<{ id: string }>('Media').implement({ + fields: (t) => ({ + id: t.exposeID('id'), + }), +}); + +const Post = builder.objectRef<{ id: string; title: string }>('Post').implement({ + interfaces: [Media], + fields: (t) => ({ + title: t.exposeString('title'), + }), +}); + const ProductType = builder.objectRef('Product').implement({ + directives: { + custom: {}, + }, fields: (t) => ({ upc: t.exposeString('upc'), name: t.exposeString('name'), @@ -52,6 +82,26 @@ builder.asEntity(ProductType, { resolveReference: ({ upc }) => products.find((product) => product.upc === upc), }); +builder.asEntity(Media, { + key: builder.selection<{ id: string }>('id'), + resolveReference: ({ id }) => ({ id, __typename: 'Post', title: 'Title' }), +}); + +builder.asEntity(Post, { + key: builder.selection<{ id: string }>('id'), + resolveReference: ({ id }) => ({ id, title: 'Title' }), +}); + +builder.queryField('post', (t) => + t.field({ + type: Post, + resolve: () => ({ + id: '123', + title: 'Title', + }), + }), +); + builder.queryType({ fields: (t) => ({ topProducts: t.field({ @@ -66,4 +116,15 @@ builder.queryType({ }), }); -export const schema = builder.toSubGraphSchema({}); +export const schema = builder.toSubGraphSchema({ + composeDirectives: ['@custom'], + schemaDirectives: { + link: { url: 'https://myspecs.dev/myCustomDirective/v1.0', import: ['@custom'] }, + }, + directives: [ + new GraphQLDirective({ + locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE], + name: 'custom', + }), + ], +}); diff --git a/packages/plugin-federation/tests/index.test.ts b/packages/plugin-federation/tests/index.test.ts index 9fac56877..80eb60141 100644 --- a/packages/plugin-federation/tests/index.test.ts +++ b/packages/plugin-federation/tests/index.test.ts @@ -1,8 +1,8 @@ +import axios from 'axios'; +import { printSchema } from 'graphql'; import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; import { printSubgraphSchema } from '@apollo/subgraph'; -import axios from 'axios'; -import { printSchema } from 'graphql'; import { schema as accountsSchema } from './example/accounts/schema'; import { createGateway } from './example/gateway'; import { schema as inventorySchema } from './example/inventory/schema'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6714c0a6..0caf8f770 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -243,8 +243,8 @@ importers: examples/prisma-federation: specifiers: '@apollo/gateway': 2.3.1 - '@apollo/server': ^4.3.3 - '@apollo/subgraph': 2.3.1 + '@apollo/server': ^4.4.0 + '@apollo/subgraph': 2.3.2 '@faker-js/faker': ^7.6.0 '@pothos/core': workspace:* '@pothos/plugin-directives': workspace:* @@ -255,8 +255,8 @@ importers: prisma: ^4.9.0 dependencies: '@apollo/gateway': 2.3.1_graphql@16.6.0 - '@apollo/server': 4.3.3_graphql@16.6.0 - '@apollo/subgraph': 2.3.1_graphql@16.6.0 + '@apollo/server': 4.4.0_graphql@16.6.0 + '@apollo/subgraph': 2.3.2_graphql@16.6.0 '@faker-js/faker': 7.6.0 '@pothos/core': link:../../packages/core '@pothos/plugin-directives': link:../../packages/plugin-directives @@ -288,7 +288,7 @@ importers: examples/prisma-smart-subscriptions-apollo: specifiers: - '@apollo/server': ^4.3.3 + '@apollo/server': ^4.4.0 '@faker-js/faker': ^7.6.0 '@pothos/core': workspace:* '@pothos/plugin-prisma': workspace:* @@ -307,7 +307,7 @@ importers: prisma: ^4.9.0 ws: ^8.12.0 dependencies: - '@apollo/server': 4.3.3_graphql@16.6.0 + '@apollo/server': 4.4.0_graphql@16.6.0 '@faker-js/faker': 7.6.0 '@pothos/core': link:../../packages/core '@pothos/plugin-prisma': link:../../packages/plugin-prisma @@ -517,8 +517,8 @@ importers: packages/plugin-federation: specifiers: '@apollo/gateway': ^2.3.1 - '@apollo/server': ^4.3.3 - '@apollo/subgraph': 2.3.1 + '@apollo/server': ^4.4.0 + '@apollo/subgraph': 2.3.2 '@graphql-tools/utils': ^9.2.1 '@pothos/core': workspace:* '@pothos/plugin-directives': workspace:* @@ -528,8 +528,8 @@ importers: graphql-tag: ^2.12.6 devDependencies: '@apollo/gateway': 2.3.1_graphql@16.6.0 - '@apollo/server': 4.3.3_graphql@16.6.0 - '@apollo/subgraph': 2.3.1_graphql@16.6.0 + '@apollo/server': 4.4.0_graphql@16.6.0 + '@apollo/subgraph': 2.3.2_graphql@16.6.0 '@graphql-tools/utils': 9.2.1_graphql@16.6.0 '@pothos/core': link:../core '@pothos/plugin-directives': link:../plugin-directives @@ -978,6 +978,16 @@ packages: graphql: 16.6.0 js-levenshtein: 1.1.6 + /@apollo/federation-internals/2.3.2_graphql@16.6.0: + resolution: {integrity: sha512-XtXQag8sV75BoNlzu6ci5mn2U+QGNZdkRB8Igi5e31VqnBx4XSdvbyx6Ht1lvYru9GCYx6OqGWZqqPqAXG72/Q==} + engines: {node: '>=14.15.0'} + peerDependencies: + graphql: ^16.5.0 + dependencies: + chalk: 4.1.2 + graphql: 16.6.0 + js-levenshtein: 1.1.6 + /@apollo/gateway/2.3.1_graphql@16.6.0: resolution: {integrity: sha512-kkZP591To697XL8rH6JOvStOU7URcHInKWEU2q3Llimwt4Gm1mlshlNNnlRiHy2ri6tim0/wL1qLBtFG2LUfDQ==} engines: {node: '>=14.15.0'} @@ -1063,8 +1073,8 @@ packages: '@apollo/utils.logger': 2.0.0 graphql: 16.6.0 - /@apollo/server/4.3.3_graphql@16.6.0: - resolution: {integrity: sha512-2nigGTgXCAUk2PHHGybtofyuuVAA/QUZwRJzwuCbRFgY1fKkMT7J4fUPwNcA809lDlZyyYphcQnM/vQNbeiu6w==} + /@apollo/server/4.4.0_graphql@16.6.0: + resolution: {integrity: sha512-hjFq8fB3tO6jnvNj2030IUDlxjpKAHvcjXq0PhpdSzjRggZlAMVr9REqvn6Ni++lF/80T/Na+YYZW4L+gedHaQ==} engines: {node: '>=14.16.0'} peerDependencies: graphql: ^16.6.0 || ^16.5.0 @@ -1085,12 +1095,12 @@ packages: '@types/express-serve-static-core': 4.17.33 '@types/node-fetch': 2.6.2 async-retry: 1.3.3 - body-parser: 1.20.1 + body-parser: 1.20.2 cors: 2.8.5 express: 4.18.2 graphql: 16.6.0 loglevel: 1.8.1 - lru-cache: 7.14.1 + lru-cache: 7.17.0 negotiator: 0.6.3 node-abort-controller: 3.1.1 node-fetch: 2.6.9 @@ -1100,14 +1110,14 @@ packages: - encoding - supports-color - /@apollo/subgraph/2.3.1_graphql@16.6.0: - resolution: {integrity: sha512-xn9SZm1sJNJLfyqtXV8ZxI0tQSsikkjExGCoEfXYqHINDucXRqHtPVlCv5G6k0xhrDQhmmzlvGMzsCuWsUAv4Q==} + /@apollo/subgraph/2.3.2_graphql@16.6.0: + resolution: {integrity: sha512-PYSVD+tx49EVjII25Ip1QsrOCn/Bmh7KBqoqZsp/AT/8yGbqX5j9oAeRH37QcOxbl8TXBcJBDKbPO/KzUln1Dg==} engines: {node: '>=14.15.0'} peerDependencies: graphql: ^16.5.0 dependencies: '@apollo/cache-control-types': 1.0.2_graphql@16.6.0 - '@apollo/federation-internals': 2.3.1_graphql@16.6.0 + '@apollo/federation-internals': 2.3.2_graphql@16.6.0 graphql: 16.6.0 /@apollo/usage-reporting-protobuf/4.0.2: @@ -6358,6 +6368,25 @@ packages: transitivePeerDependencies: - supports-color + /body-parser/1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + /brace-expansion/1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -11598,6 +11627,10 @@ packages: resolution: {integrity: sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==} engines: {node: '>=12'} + /lru-cache/7.17.0: + resolution: {integrity: sha512-zSxlVVwOabhVyTi6E8gYv2cr6bXK+8ifYz5/uyJb9feXX6NACVDwY4p5Ut3WC3Ivo/QhpARHU3iujx2xGAYHbQ==} + engines: {node: '>=12'} + /lru_map/0.3.3: resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} dev: true @@ -13621,6 +13654,15 @@ packages: iconv-lite: 0.4.24 unpipe: 1.0.0 + /raw-body/2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + /react-dom/17.0.2_react@17.0.2: resolution: {integrity: sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==} peerDependencies: diff --git a/website/pages/docs/plugins/directives.mdx b/website/pages/docs/plugins/directives.mdx index d9c9ce243..8366a0d5e 100644 --- a/website/pages/docs/plugins/directives.mdx +++ b/website/pages/docs/plugins/directives.mdx @@ -64,7 +64,7 @@ an object type representing the arguments the directive accepts. The valid locations for directives are: -- `ARGUMENT_DEFINITION` \| +- `ARGUMENT_DEFINITION` - `ENUM_VALUE` - `ENUM` - `FIELD_DEFINITION` @@ -118,3 +118,11 @@ supported locations using one of the following 2 formats: Each of these applies the same 2 directives. The first format is preferred, especially when using directives that are sensitive to ordering, or can be repeated multiple times for the same location. + +## Applying directives + +For most locations (On fields and types) the options object for the field or type will have a +`directives` option which can be used to define directives. + +To apply `SCHEMA` directives, you can use the `schemaDirectives` option on the `toSchema` method. +`directives` on `toSchema` is reserved for the Directive implementations. diff --git a/website/pages/docs/plugins/federation.mdx b/website/pages/docs/plugins/federation.mdx index bbd819a54..71b0f75f9 100644 --- a/website/pages/docs/plugins/federation.mdx +++ b/website/pages/docs/plugins/federation.mdx @@ -205,3 +205,81 @@ If you are printing the schema as a string for any reason, and then using the pr 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', + }), + ], +}); +```