Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add __assertStep support to makeGrafastSchema and makeExtendSchemaPlugin #1916

Merged
merged 5 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/nasty-dragons-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"graphile-utils": patch
"grafast": patch
---

Add `__assertStep` registration support to makeGrafastSchema and PostGraphile's
makeExtendSchemaPlugin.
8 changes: 7 additions & 1 deletion grafast/grafast/src/makeGrafastSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export type FieldPlans =
* The plans/config for each field of a GraphQL object type.
*/
export type ObjectPlans = {
__Step?: { new (...args: any[]): ExecutableStep };
__assertStep?:
| ((step: ExecutableStep) => asserts step is ExecutableStep)
| { new (...args: any[]): ExecutableStep };
} & {
[fieldName: string]: FieldPlans;
};
Expand Down Expand Up @@ -142,6 +144,10 @@ export function makeGrafastSchema(details: {
type.extensions as graphql.GraphQLObjectTypeExtensions<any, any>
).grafast = { assertStep: fieldSpec as any };
continue;
} else if (fieldName.startsWith("__")) {
throw new Error(
`Unsupported field name '${fieldName}'; perhaps you meant '__assertStep'?`,
);
}

const field = fields[fieldName];
Expand Down
140 changes: 139 additions & 1 deletion grafast/website/grafast/plan-resolvers.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,144 @@ with a `$`.
Of course the actual body of the plan resolver function will vary based on your
own application's needs.

## Specifying a field plan resolver

When building a GraphQL schema programatically, plan resolvers are stored into
`extensions.grafast.plan` of the field; for example:

```ts {9-15}
import { GraphQLSchema, GraphQLObjectType, GraphQLInt } from "graphql";
import { constant } from "grafast";

const Query = new GraphQLObjectType({
name: "Query",
fields: {
meaningOfLife: {
type: GraphQLInt,
extensions: {
grafast: {
plan() {
return constant(42);
},
},
},
},
},
});

export const schema = new GraphQLSchema({
query: Query,
});
```

If you are using `makeGrafastSchema` then the field plan resolver for the field
`fieldName` on the object type `typeName` would be indicated via the
`plans[typeName][fieldName]` property:

```ts {11-13}
import { makeGrafastSchema, constant } from "grafast";

export const schema = makeGrafastSchema({
typeDefs: /* GraphQL */ `
type Query {
meaningOfLife: Int
}
`,
plans: {
Query: {
meaningOfLife() {
return constant(42);
},
},
},
});
```

### Asserting an object type's step

Object types in Gra*fast* can indicate that they must be represented by a
particular step or set of steps to guarantee that the methods on those steps
are available to the field plan resolvers; this can help to catch bugs early.

This indication takes one of two forms, either it's explicitly the step class
itself, or it's an assertion function that checks that the incoming step is of
an appropriate type and throws an error otherwise.

When defining a schema programatically, `assertStep` is defined via
`objectTypeConfig.extensions.grafast.assertStep`, for example:

```ts {8-14}
import { GraphQLObjectType } from "graphql";
import { ObjectStep } from "grafast";

const MyObject = new GraphQLObjectType({
name: "MyObject",
extensions: {
grafast: {
assertStep: ObjectStep,
/* Or:
assertStep($step) {
if ($step instanceof ObjectStep) return;
throw new Error(`Type 'MyObject' expects a step of type ObjectStep; instead received a step of type '${$step.constructor.name}'`);
}
*/
},
},
fields: {
a: {
extensions: {
grafast: {
plan($obj: ObjectStep) {
return $obj.get("a");
},
},
},
},
},
});
```

When defined via `makeGrafastSchema` we cannot call the property `assertStep`
directly as it might conflict with a field name, so instead we use
`__assertStep`, knowing that GraphQL forbids fields to start with `__` (two
underscores) since those names are reserved for introspection:

```ts {11-17}
import { makeGrafastSchema, ObjectStep } from "grafast";

const schema = makeGrafastSchema({
typeDefs: /* GraphQL */ `
type MyObject {
a: Int
}
`,
plans: {
MyObject: {
__assertStep: ObjectStep,
/* Or:
__assertStep($step) {
if ($step instanceof ObjectStep) return;
throw new Error(`Type 'MyObject' expects a step of type ObjectStep; instead received a step of type '${$step.constructor.name}'`);
}
*/
a($obj: ObjectStep) {
return $obj.get("a");
},
},
},
});
```

:::tip

Generally adding a step assertion is optional; however when there's a union or
interface type all types within it must agree whether a step is expected or
not. If you want to require steps everywhere but you don't care for a particular
type what the step actually is, you can use `__assertStep: ExecutableStep` or
`__assertStep: () => true`.

:::

## Argument and input field plan resolvers

:::tip
Expand All @@ -86,7 +224,7 @@ on these behaviors if present.

:::

In addition to field plan resolvers, Grafast allows you to attach an `inputPlan`
In addition to field plan resolvers, Gra*fast* allows you to attach an `inputPlan`
and/or an `applyPlan` to individual arguments or to input fields on input
objects. These plan resolvers work a little differently.

Expand Down
20 changes: 17 additions & 3 deletions graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FieldPlanResolver } from "grafast";
import type { ExecutableStep, FieldPlanResolver } from "grafast";
import type {
DefinitionNode,
DirectiveDefinitionNode,
Expand All @@ -24,6 +24,7 @@ import type {
GraphQLIsTypeOfFn,
GraphQLNamedType,
GraphQLObjectType,
GraphQLObjectTypeExtensions,
GraphQLOutputType,
GraphQLScalarType,
GraphQLScalarTypeConfig,
Expand Down Expand Up @@ -69,11 +70,15 @@ export interface ObjectResolver<TSource = any, TContext = any> {
| ObjectFieldConfig<TSource, TContext>;
}

export interface ObjectPlan<TSource = any, TContext = any> {
export type ObjectPlan<TSource = any, TContext = any> = {
__assertStep?:
| ((step: ExecutableStep) => asserts step is ExecutableStep)
| { new (...args: any[]): ExecutableStep };
} & {
[key: string]:
| FieldPlanResolver<any, any, any>
| ObjectFieldConfig<TSource, TContext>;
}
};

export interface EnumResolver {
[key: string]: string | number | Array<any> | Record<string, any> | symbol;
Expand Down Expand Up @@ -412,6 +417,15 @@ export function makeExtendSchemaPlugin(
description,
}
: null),
...(plans?.[name]?.__assertStep
? {
extensions: {
grafast: {
assertStep: plans[name].__assertStep as any,
},
} as GraphQLObjectTypeExtensions<any, any>,
}
: null),
}),
uniquePluginName,
);
Expand Down
58 changes: 57 additions & 1 deletion postgraphile/website/postgraphile/make-extend-schema-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,62 @@ const typeDefs = gql`

-->

## "Special" fields

In GraphQL, it is forbidden to name any fields beginning with `__` (two
underscores) since that is reserved for introspection. We therefore use this
prefix to provide additional details to types. What additional information is
relevant depends on the type:

### Object types

Object types in Gra*fast* can [indicate that they must be represented by a
particular
step](https://grafast.org/grafast/plan-resolvers#asserting-an-object-types-step)
or one of a set of steps; this can help to catch bugs early. For example, in
PostGraphile a database table resource should be represented by a
`pgSelectSingle` or similar class; representing it with `object({id: 1})` or
similar would mean the step doesn't have the expected helper methods and
downstream fields may fail to plan because their expectations are broken.

Object types' `plans` entries may define an `__assertStep` property to indicate
the type of step the object type's fields' resolvers will be expecting; this is
equivalent to `typeConfig.extensions.grafast.assertStep` when defining a object
type programatically.

The value for `__assertStep` can either be a step class itself (e.g.
`PgSelectSingleStep`) or
it can be an "assertion function" that throws an error if the passed step is
not of the right type, e.g.:

```ts
import { makeExtendSchemaPlugin, gql } from "postgraphile/utils";

const schema = makeExtendSchemaPlugin({
typeDefs: gql`
type MyObject {
id: Int
}
`,
plans: {
MyObject: {
assertStep($step) {
if ($step instanceof PgSelectSingleStep) return true;
if ($step instanceof PgInsertSingleStep) return true;
if ($step instanceof PgUpdateSingleStep) return true;
throw new Error(
`Type 'User' expects a step of type PgSelectSingleStep, PgInsertSingleStep ` +
`or PgUpdateSingleStep; but found step of type '${$step.constructor.name}'.`,
);
},
a($obj: PgSelectSingleStep | PgInsertSingleStep | PgUpdateSingleStep) {
return $obj.get("id");
},
},
},
});
```

## Querying the database

You should read [the Gra*fast* introduction](https://grafast.org/grafast/) and
Expand Down Expand Up @@ -190,7 +246,7 @@ expecting, but you have a lot of freedom within your plan resolver as to how to
achieve that.

One common desire is to access the data in the GraphQL context. You can access
this in Grafast using the `context()` step; for example, you may have stored
this in Gra*fast* using the `context()` step; for example, you may have stored
the current user's ID on the GraphQL context via the `userId` property, to
retrieve these you might do this in your plan resolver function:

Expand Down