From 1dd9c30e8b92ebcba6f0dc7d785a0f15e69bc4f2 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 19 Jan 2024 12:03:26 +0000 Subject: [PATCH 1/5] Add __assertStep support to makeExtendSchemaPlugin --- grafast/grafast/src/makeGrafastSchema.ts | 8 +++++++- .../src/makeExtendSchemaPlugin.ts | 20 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/grafast/grafast/src/makeGrafastSchema.ts b/grafast/grafast/src/makeGrafastSchema.ts index 9e64015c2f..5cae73f9a0 100644 --- a/grafast/grafast/src/makeGrafastSchema.ts +++ b/grafast/grafast/src/makeGrafastSchema.ts @@ -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; }; @@ -142,6 +144,10 @@ export function makeGrafastSchema(details: { type.extensions as graphql.GraphQLObjectTypeExtensions ).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]; diff --git a/graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts b/graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts index 26fa8d64ee..35dc007ec1 100644 --- a/graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts +++ b/graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts @@ -1,4 +1,4 @@ -import type { FieldPlanResolver } from "grafast"; +import type { ExecutableStep, FieldPlanResolver } from "grafast"; import type { DefinitionNode, DirectiveDefinitionNode, @@ -14,6 +14,7 @@ import type { // Config: GraphQLEnumValueConfigMap, GraphQLFieldConfigMap, + GraphQLObjectTypeExtensions, // Resolvers: GraphQLFieldResolver, GraphQLInputFieldConfigMap, @@ -69,11 +70,15 @@ export interface ObjectResolver { | ObjectFieldConfig; } -export interface ObjectPlan { +export type ObjectPlan = { + __assertStep?: + | ((step: ExecutableStep) => asserts step is ExecutableStep) + | { new (...args: any[]): ExecutableStep }; +} & { [key: string]: | FieldPlanResolver | ObjectFieldConfig; -} +}; export interface EnumResolver { [key: string]: string | number | Array | Record | symbol; @@ -412,6 +417,15 @@ export function makeExtendSchemaPlugin( description, } : null), + ...(plans?.[name]?.__assertStep + ? { + extensions: { + grafast: { + assertStep: plans[name].__assertStep as any, + }, + } as GraphQLObjectTypeExtensions, + } + : null), }), uniquePluginName, ); From a2176ea324db0801249661b30e9c9d314c6fb159 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 19 Jan 2024 12:04:24 +0000 Subject: [PATCH 2/5] docs(changeset): Add `__assertStep` registration support to makeGrafastSchema and PostGraphile's makeExtendSchemaPlugin. --- .changeset/nasty-dragons-rush.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/nasty-dragons-rush.md diff --git a/.changeset/nasty-dragons-rush.md b/.changeset/nasty-dragons-rush.md new file mode 100644 index 0000000000..1e15b2e68b --- /dev/null +++ b/.changeset/nasty-dragons-rush.md @@ -0,0 +1,7 @@ +--- +"graphile-utils": patch +"grafast": patch +--- + +Add `__assertStep` registration support to makeGrafastSchema and PostGraphile's +makeExtendSchemaPlugin. From d8c22593a39e0e7c9e36cc32e3ea39f7ea681e11 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 19 Jan 2024 12:58:42 +0000 Subject: [PATCH 3/5] Add some docs --- grafast/website/grafast/plan-resolvers.md | 140 +++++++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/grafast/website/grafast/plan-resolvers.md b/grafast/website/grafast/plan-resolvers.md index 9c9a390fda..ec179b8d5b 100644 --- a/grafast/website/grafast/plan-resolvers.md +++ b/grafast/website/grafast/plan-resolvers.md @@ -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 @@ -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. From f87d320e218238052d491ba242b59de103bf8bee Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 19 Jan 2024 13:54:01 +0000 Subject: [PATCH 4/5] Docs for PostGraphile --- .../postgraphile/make-extend-schema-plugin.md | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/postgraphile/website/postgraphile/make-extend-schema-plugin.md b/postgraphile/website/postgraphile/make-extend-schema-plugin.md index 3c97f4db2d..56d39e78db 100644 --- a/postgraphile/website/postgraphile/make-extend-schema-plugin.md +++ b/postgraphile/website/postgraphile/make-extend-schema-plugin.md @@ -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 @@ -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: From af3d8ddc7b999a7ad9c56170ee90635ba96edd95 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Fri, 19 Jan 2024 13:56:02 +0000 Subject: [PATCH 5/5] Reorder imports --- graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts b/graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts index 35dc007ec1..13d8d85482 100644 --- a/graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts +++ b/graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts @@ -14,7 +14,6 @@ import type { // Config: GraphQLEnumValueConfigMap, GraphQLFieldConfigMap, - GraphQLObjectTypeExtensions, // Resolvers: GraphQLFieldResolver, GraphQLInputFieldConfigMap, @@ -25,6 +24,7 @@ import type { GraphQLIsTypeOfFn, GraphQLNamedType, GraphQLObjectType, + GraphQLObjectTypeExtensions, GraphQLOutputType, GraphQLScalarType, GraphQLScalarTypeConfig,