Skip to content

Commit

Permalink
Add support for new federation directives
Browse files Browse the repository at this point in the history
  • Loading branch information
hayes committed Feb 24, 2023
1 parent 196c632 commit 487b810
Show file tree
Hide file tree
Showing 22 changed files with 449 additions and 51 deletions.
9 changes: 9 additions & 0 deletions .changeset/heavy-schools-relate.md
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions .changeset/ten-spoons-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@pothos/plugin-directives': minor
---

Add schemaDirectives option to `toSchema` for defining SCHEMA directives
4 changes: 2 additions & 2 deletions examples/prisma-federation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
2 changes: 1 addition & 1 deletion examples/prisma-smart-subscriptions-apollo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
10 changes: 9 additions & 1 deletion packages/deno/packages/plugin-directives/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.
3 changes: 3 additions & 0 deletions packages/deno/packages/plugin-directives/global-types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/deno/packages/plugin-directives/index.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion packages/plugin-directives/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.
4 changes: 4 additions & 0 deletions packages/plugin-directives/src/global-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,9 @@ declare global {
export interface EnumValueConfig<Types extends SchemaTypes = SchemaTypes> {
directives?: Directives<Types, 'ENUM_VALUE'>;
}

export interface BuildSchemaOptions<Types extends SchemaTypes> {
schemaDirectives?: Directives<Types, 'SCHEMA'>;
}
}
}
10 changes: 10 additions & 0 deletions packages/plugin-directives/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ export class PothosDirectivesPlugin<Types extends SchemaTypes> extends BasePlugi
}

override afterBuild(schema: GraphQLSchema) {
schema.extensions = {
...schema.extensions,
directives: this.normalizeDirectives(
this.mergeDirectives(
(schema.extensions?.directives as Record<string, {}>) ?? {},
this.options.schemaDirectives as unknown as Record<string, {}>,
),
),
};

mockAst(schema);

return schema;
Expand Down
78 changes: 78 additions & 0 deletions packages/plugin-federation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}),
],
});
```
4 changes: 2 additions & 2 deletions packages/plugin-federation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
7 changes: 6 additions & 1 deletion packages/plugin-federation/src/global-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,18 @@ declare global {
toSubGraphSchema: (
options: BuildSchemaOptions<Types> & {
linkUrl?: string;
composeDirectives?: `@${string}`[];
},
) => GraphQLSchema;

asEntity: <Param extends ObjectRef<unknown>, KeySelection extends Selection<object>>(
asEntity: <
Param extends ObjectRef<unknown> | InterfaceRef<unknown>,
KeySelection extends Selection<object>,
>(
param: Param,
options: {
key: KeySelection | KeySelection[];
interfaceObject?: Param extends ObjectRef<unknown> ? boolean : never;
resolveReference: (
parent: KeySelection[typeof selectionShapeKey],
context: Types['Context'],
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-federation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class PothosFederationPlugin<Types extends SchemaTypes> extends BasePlugi
directives: mergeDirectives(typeConfig.extensions?.directives as [], [
...(entityConfig ? keyDirective(entityConfig.key) : []),
...commonDirectives,
...(entityConfig?.interfaceObject ? [{ name: 'interfaceObject', args: {} }] : []),
]),
},
};
Expand Down
47 changes: 31 additions & 16 deletions packages/plugin-federation/src/schema-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SchemaTypes>;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -137,6 +151,7 @@ export const entityMapping = new WeakMap<
string,
{
key: Selection<object> | Selection<object>[];
interfaceObject?: boolean;
resolveReference: (val: object, context: {}, info: GraphQLResolveInfo) => unknown;
}
>
Expand Down
Loading

0 comments on commit 487b810

Please sign in to comment.