From ea9981fb46f1cddfc73d9f20e9cba26b27795b47 Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Fri, 29 Nov 2024 23:16:04 -0800 Subject: [PATCH] [directive plugin] improve ast representation of directive args --- .changeset/fuzzy-moons-move.md | 5 ++ .changeset/pink-gifts-wash.md | 5 ++ packages/plugin-directives/src/mock-ast.ts | 82 +++++++++++------- .../tests/__snapshots__/index.test.ts.snap | 83 ++++++++++++++++++- .../tests/example/builder.ts | 8 ++ .../tests/example/schema/index.ts | 65 ++++++++++++++- .../plugin-directives/tests/index.test.ts | 7 +- pnpm-lock.yaml | 1 + 8 files changed, 225 insertions(+), 31 deletions(-) create mode 100644 .changeset/fuzzy-moons-move.md create mode 100644 .changeset/pink-gifts-wash.md diff --git a/.changeset/fuzzy-moons-move.md b/.changeset/fuzzy-moons-move.md new file mode 100644 index 000000000..f59c5ea13 --- /dev/null +++ b/.changeset/fuzzy-moons-move.md @@ -0,0 +1,5 @@ +--- +"@pothos/plugin-prisma": minor +--- + +Support prisma 6.0 diff --git a/.changeset/pink-gifts-wash.md b/.changeset/pink-gifts-wash.md new file mode 100644 index 000000000..454f5c62e --- /dev/null +++ b/.changeset/pink-gifts-wash.md @@ -0,0 +1,5 @@ +--- +"@pothos/plugin-directives": minor +--- + +Improve ast representation of direvtive arguments diff --git a/packages/plugin-directives/src/mock-ast.ts b/packages/plugin-directives/src/mock-ast.ts index d8b507bcf..e7a2b4c2c 100644 --- a/packages/plugin-directives/src/mock-ast.ts +++ b/packages/plugin-directives/src/mock-ast.ts @@ -40,7 +40,7 @@ export default function mockAst(schema: GraphQLSchema) { schema.extensionASTNodes = [ { kind: Kind.SCHEMA_EXTENSION, - directives: directiveNodes(schema.extensions?.directives as DirectiveList), + directives: directiveNodes(schema.extensions?.directives as DirectiveList, null, schema), operationTypes: ( [ { @@ -78,8 +78,8 @@ export default function mockAst(schema: GraphQLSchema) { name: { kind: Kind.NAME, value: typeName }, description: type.description ? { kind: Kind.STRING, value: type.description } : undefined, interfaces: type.getInterfaces().map((iface) => typeNode(iface) as NamedTypeNode), - fields: fieldNodes(type.getFields()), - directives: directiveNodes(type.extensions?.directives as DirectiveList), + fields: fieldNodes(type.getFields(), schema), + directives: directiveNodes(type.extensions?.directives as DirectiveList, null, schema), }; } else if (type instanceof GraphQLInterfaceType) { type.astNode = { @@ -87,8 +87,8 @@ export default function mockAst(schema: GraphQLSchema) { name: { kind: Kind.NAME, value: typeName }, description: type.description ? { kind: Kind.STRING, value: type.description } : undefined, interfaces: type.getInterfaces().map((iface) => typeNode(iface) as NamedTypeNode), - fields: fieldNodes(type.getFields()), - directives: directiveNodes(type.extensions?.directives as DirectiveList), + fields: fieldNodes(type.getFields(), schema), + directives: directiveNodes(type.extensions?.directives as DirectiveList, null, schema), }; } else if (type instanceof GraphQLUnionType) { type.astNode = { @@ -96,30 +96,30 @@ export default function mockAst(schema: GraphQLSchema) { name: { kind: Kind.NAME, value: typeName }, description: type.description ? { kind: Kind.STRING, value: type.description } : undefined, types: type.getTypes().map((iface) => typeNode(iface) as NamedTypeNode), - directives: directiveNodes(type.extensions?.directives as DirectiveList), + directives: directiveNodes(type.extensions?.directives as DirectiveList, null, schema), }; } else if (type instanceof GraphQLEnumType) { type.astNode = { kind: Kind.ENUM_TYPE_DEFINITION, name: { kind: Kind.NAME, value: typeName }, description: type.description ? { kind: Kind.STRING, value: type.description } : undefined, - values: enumValueNodes(type.getValues()), - directives: directiveNodes(type.extensions?.directives as DirectiveList), + values: enumValueNodes(type.getValues(), schema), + directives: directiveNodes(type.extensions?.directives as DirectiveList, null, schema), }; } else if (type instanceof GraphQLScalarType) { type.astNode = { kind: Kind.SCALAR_TYPE_DEFINITION, name: { kind: Kind.NAME, value: typeName }, description: type.description ? { kind: Kind.STRING, value: type.description } : undefined, - directives: directiveNodes(type.extensions?.directives as DirectiveList), + directives: directiveNodes(type.extensions?.directives as DirectiveList, null, schema), }; } else if (type instanceof GraphQLInputObjectType) { type.astNode = { kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, name: { kind: Kind.NAME, value: typeName }, description: type.description ? { kind: Kind.STRING, value: type.description } : undefined, - fields: inputFieldNodes(type.getFields()), - directives: directiveNodes(type.extensions?.directives as DirectiveList), + fields: inputFieldNodes(type.getFields(), schema), + directives: directiveNodes(type.extensions?.directives as DirectiveList, null, schema), }; } } @@ -140,13 +140,17 @@ function typeNode(type: GraphQLType): TypeNode { return { kind: Kind.NAMED_TYPE, name: { kind: Kind.NAME, value: type.name } }; } -function valueNode(value: unknown): ValueNode { +function valueNode(value: unknown, arg?: GraphQLArgument): ValueNode { if (value == null) { return { kind: Kind.NULL }; } + if (arg) { + return astFromValue(value, arg.type) as ValueNode; + } + if (Array.isArray(value)) { - return { kind: Kind.LIST, values: value.map(valueNode) }; + return { kind: Kind.LIST, values: value.map((val) => valueNode(val)) }; } switch (typeof value) { @@ -166,7 +170,8 @@ function valueNode(value: unknown): ValueNode { function directiveNodes( directives: DirectiveList | Record | undefined, - deprecationReason?: string | null, + deprecationReason: string | null, + schema: GraphQLSchema, ): readonly ConstDirectiveNode[] { if (!directives) { return []; @@ -195,8 +200,10 @@ function directiveNodes( }); } - return directiveList.map( - (directive): DirectiveNode => ({ + return directiveList.map((directive): DirectiveNode => { + const directiveDef = schema.getDirective(directive.name); + directiveDef?.args.find((arg) => arg.name); + return { kind: Kind.DIRECTIVE, name: { kind: Kind.NAME, value: directive.name }, arguments: @@ -205,14 +212,20 @@ function directiveNodes( (argName): ArgumentNode => ({ kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: argName }, - value: valueNode((directive.args as Record)[argName]), + value: valueNode( + (directive.args as Record)[argName], + directiveDef?.args.find((arg) => arg.name === argName), + ), }), ), - }), - ) as readonly ConstDirectiveNode[]; + }; + }) as readonly ConstDirectiveNode[]; } -function fieldNodes(fields: GraphQLFieldMap): FieldDefinitionNode[] { +function fieldNodes( + fields: GraphQLFieldMap, + schema: GraphQLSchema, +): FieldDefinitionNode[] { return Object.keys(fields).map((fieldName) => { const field: GraphQLField = fields[fieldName]; @@ -220,11 +233,12 @@ function fieldNodes(fields: GraphQLFieldMap): FieldDefinitionN kind: Kind.FIELD_DEFINITION, description: field.description ? { kind: Kind.STRING, value: field.description } : undefined, name: { kind: Kind.NAME, value: fieldName }, - arguments: argumentNodes(field.args), + arguments: argumentNodes(field.args, schema), type: typeNode(field.type), directives: directiveNodes( field.extensions?.directives as DirectiveList, - field.deprecationReason, + field.deprecationReason ?? null, + schema, ), }; @@ -232,7 +246,10 @@ function fieldNodes(fields: GraphQLFieldMap): FieldDefinitionN }); } -function inputFieldNodes(fields: GraphQLInputFieldMap): InputValueDefinitionNode[] { +function inputFieldNodes( + fields: GraphQLInputFieldMap, + schema: GraphQLSchema, +): InputValueDefinitionNode[] { return Object.keys(fields).map((fieldName) => { const field: GraphQLInputField = fields[fieldName]; @@ -246,7 +263,8 @@ function inputFieldNodes(fields: GraphQLInputFieldMap): InputValueDefinitionNode defaultValue: field.defaultValue === undefined ? undefined : defaultValueNode, directives: directiveNodes( field.extensions?.directives as DirectiveList, - field.deprecationReason, + field.deprecationReason ?? null, + schema, ), }; @@ -254,7 +272,10 @@ function inputFieldNodes(fields: GraphQLInputFieldMap): InputValueDefinitionNode }); } -function argumentNodes(args: readonly GraphQLArgument[]): InputValueDefinitionNode[] { +function argumentNodes( + args: readonly GraphQLArgument[], + schema: GraphQLSchema, +): InputValueDefinitionNode[] { return args.map((arg): InputValueDefinitionNode => { const defaultValueNode = astFromValue(arg.defaultValue, arg.type) as ConstValueNode; @@ -266,7 +287,8 @@ function argumentNodes(args: readonly GraphQLArgument[]): InputValueDefinitionNo defaultValue: arg.defaultValue === undefined ? undefined : defaultValueNode, directives: directiveNodes( arg.extensions?.directives as DirectiveList, - arg.deprecationReason, + arg.deprecationReason ?? null, + schema, ), }; @@ -274,7 +296,10 @@ function argumentNodes(args: readonly GraphQLArgument[]): InputValueDefinitionNo }); } -function enumValueNodes(values: readonly GraphQLEnumValue[]): readonly EnumValueDefinitionNode[] { +function enumValueNodes( + values: readonly GraphQLEnumValue[], + schema: GraphQLSchema, +): readonly EnumValueDefinitionNode[] { return values.map((value): EnumValueDefinitionNode => { value.astNode = { kind: Kind.ENUM_VALUE_DEFINITION, @@ -282,7 +307,8 @@ function enumValueNodes(values: readonly GraphQLEnumValue[]): readonly EnumValue name: { kind: Kind.NAME, value: value.name }, directives: directiveNodes( value.extensions?.directives as DirectiveList, - value.deprecationReason, + value.deprecationReason ?? null, + schema, ), }; diff --git a/packages/plugin-directives/tests/__snapshots__/index.test.ts.snap b/packages/plugin-directives/tests/__snapshots__/index.test.ts.snap index 2beda75bb..dd7114fe4 100644 --- a/packages/plugin-directives/tests/__snapshots__/index.test.ts.snap +++ b/packages/plugin-directives/tests/__snapshots__/index.test.ts.snap @@ -1,7 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`extends example schema generates expected schema 1`] = ` -"scalar Date +""""Marks a field as cacheable""" +directive @cacheControl( + """Inherit max age from parent""" + inheritMaxAge: Boolean + + """The maximum age of the cache in seconds""" + maxAge: Int + + """The scope for the cache""" + scope: CacheControlScope = PRIVATE +) on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + +enum CacheControlScope { + PRIVATE + PUBLIC +} + +scalar Date enum EN { ONE @@ -35,8 +52,72 @@ type Obj { } type Query { + cacheControlPrivate: String + cacheControlPublic: String test(arg1: String, myInput: MyInput, myOtherInput: MyOtherInput = {}): String } union UN = Obj" `; + +exports[`extends example schema generates expected schema with directives 1`] = ` +"schema { + query: Query +} + +"""Marks a field as cacheable""" +directive @cacheControl( + """Inherit max age from parent""" + inheritMaxAge: Boolean + """The maximum age of the cache in seconds""" + maxAge: Int + """The scope for the cache""" + scope: CacheControlScope = PRIVATE +) on FIELD_DEFINITION | INTERFACE | OBJECT | UNION + +enum CacheControlScope { + PRIVATE + PUBLIC +} + +scalar Date @s(foo: 123) + +enum EN @e(foo: 123) { + ONE @ev(foo: 123) + TWO +} + +interface IF @i(foo: 123) { + field: String +} + +input In @io(foo: 123) { + test: String @if(foo: 123) +} + +input MyInput { + booleanWithDefault: Boolean = false + enumWithDefault: EN = TWO + id: ID! + idWithDefault: ID = 123 + ids: [ID!]! + idsWithDefault: [ID!] = [123, 456] + stringWithDefault: String = "default string" +} + +input MyOtherInput { + booleanWithDefault: Boolean = false +} + +type Obj @o(foo: 123) { + field: String +} + +type Query @o(foo: 123) @rateLimit(limit: 1, duration: 5) { + cacheControlPrivate: String @cacheControl(scope: PRIVATE, maxAge: 100, inheritMaxAge: true) + cacheControlPublic: String @cacheControl(scope: PUBLIC, maxAge: 100, inheritMaxAge: true) + test(arg1: String @a(foo: 123), myInput: MyInput, myOtherInput: MyOtherInput = {}): String @f(foo: 123) +} + +union UN @u(foo: 123) = Obj" +`; diff --git a/packages/plugin-directives/tests/example/builder.ts b/packages/plugin-directives/tests/example/builder.ts index 8b6e94e6a..4afcb3548 100644 --- a/packages/plugin-directives/tests/example/builder.ts +++ b/packages/plugin-directives/tests/example/builder.ts @@ -9,6 +9,14 @@ type DirectiveTypes = { duration: number; }; }; + cacheControl: { + locations: 'FIELD_DEFINITION' | 'OBJECT' | 'INTERFACE' | 'UNION'; + args: { + scope?: 'PRIVATE' | 'PUBLIC'; + maxAge?: number; + inheritMaxAge?: boolean; + }; + }; s: { locations: 'SCALAR'; args: { foo: number }; diff --git a/packages/plugin-directives/tests/example/schema/index.ts b/packages/plugin-directives/tests/example/schema/index.ts index 181b26108..5dd3076d4 100644 --- a/packages/plugin-directives/tests/example/schema/index.ts +++ b/packages/plugin-directives/tests/example/schema/index.ts @@ -1,3 +1,10 @@ +import { + DirectiveLocation, + GraphQLBoolean, + GraphQLDirective, + GraphQLEnumType, + GraphQLInt, +} from 'graphql'; import { rateLimitDirective } from 'graphql-rate-limit-directive'; import builder from '../builder'; @@ -50,6 +57,26 @@ builder.queryType({ }, }, fields: (t) => ({ + cacheControlPrivate: t.string({ + directives: { + cacheControl: { + scope: 'PRIVATE', + maxAge: 100, + inheritMaxAge: true, + }, + }, + resolve: () => 'hi', + }), + cacheControlPublic: t.string({ + directives: { + cacheControl: { + scope: 'PUBLIC', + maxAge: 100, + inheritMaxAge: true, + }, + }, + resolve: () => 'hi', + }), test: t.string({ directives: [ { @@ -121,7 +148,43 @@ builder.scalarType('Date', { serialize: () => new Date(), }); +export const cacheControlDirective = new GraphQLDirective({ + locations: [ + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.OBJECT, + DirectiveLocation.INTERFACE, + DirectiveLocation.UNION, + ], + name: 'cacheControl', + description: 'Marks a field as cacheable', + args: { + scope: { + defaultValue: 'PRIVATE', + description: 'The scope for the cache', + type: new GraphQLEnumType({ + name: 'CacheControlScope', + values: { + PUBLIC: { value: 'PUBLIC' }, + PRIVATE: { value: 'PRIVATE' }, + }, + }), + }, + maxAge: { + description: 'The maximum age of the cache in seconds', + type: GraphQLInt, + }, + inheritMaxAge: { + description: 'Inherit max age from parent', + type: GraphQLBoolean, + }, + }, +}); + const { rateLimitDirectiveTransformer } = rateLimitDirective(); -const schema = rateLimitDirectiveTransformer(builder.toSchema()); +const schema = rateLimitDirectiveTransformer( + builder.toSchema({ + directives: [cacheControlDirective], + }), +); export default schema; diff --git a/packages/plugin-directives/tests/index.test.ts b/packages/plugin-directives/tests/index.test.ts index 59c034f46..12dfda688 100644 --- a/packages/plugin-directives/tests/index.test.ts +++ b/packages/plugin-directives/tests/index.test.ts @@ -1,3 +1,4 @@ +import { printSchemaWithDirectives } from '@graphql-tools/utils'; import SchemaBuilder from '@pothos/core'; import { type GraphQLEnumType, @@ -13,7 +14,11 @@ import schema from './example/schema'; describe('extends example schema', () => { it('generates expected schema', () => { - expect(printSchema(lexicographicSortSchema(schema))).toMatchSnapshot(); + expect(printSchema(schema)).toMatchSnapshot(); + }); + + it('generates expected schema with directives', () => { + expect(printSchemaWithDirectives(schema)).toMatchSnapshot(); }); it('has expected directives in extensions', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d6c03266..3f1d03b61 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7732,6 +7732,7 @@ packages: libsql@0.4.7: resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} + cpu: [x64, arm64, wasm32] os: [darwin, linux, win32] light-my-request@6.3.0: