From 403ed4507eff7cd509f410f7542a702da72e1a9a Mon Sep 17 00:00:00 2001 From: Nicolas Lagoutte Date: Fri, 2 Sep 2022 15:55:16 +0100 Subject: [PATCH] Get arguments with directive (#4661) * First pass: a functionning implementation * cleaner * add changeset * fix compilation * Move test file to correct location * empty to trigger ci --- .changeset/twenty-chairs-judge.md | 5 + .../src/get-arguments-with-directives.ts | 50 ++++++ .../utils/src/get-fields-with-directives.ts | 30 +--- packages/utils/src/index.ts | 1 + packages/utils/src/types.ts | 3 + .../get-arguments-with-directives.spec.ts | 162 ++++++++++++++++++ 6 files changed, 224 insertions(+), 27 deletions(-) create mode 100644 .changeset/twenty-chairs-judge.md create mode 100644 packages/utils/src/get-arguments-with-directives.ts create mode 100644 packages/utils/tests/get-arguments-with-directives.spec.ts diff --git a/.changeset/twenty-chairs-judge.md b/.changeset/twenty-chairs-judge.md new file mode 100644 index 0000000000..d3bd395bb2 --- /dev/null +++ b/.changeset/twenty-chairs-judge.md @@ -0,0 +1,5 @@ +--- +'@graphql-tools/utils': minor +--- + +Add getArgumentsWithDirectives diff --git a/packages/utils/src/get-arguments-with-directives.ts b/packages/utils/src/get-arguments-with-directives.ts new file mode 100644 index 0000000000..68e1f400b1 --- /dev/null +++ b/packages/utils/src/get-arguments-with-directives.ts @@ -0,0 +1,50 @@ +import { DirectiveUsage } from './types.js'; + +import { ASTNode, DocumentNode, Kind, ObjectTypeDefinitionNode, valueFromASTUntyped } from 'graphql'; + +function isTypeWithFields(t: ASTNode): t is ObjectTypeDefinitionNode { + return t.kind === Kind.OBJECT_TYPE_DEFINITION || t.kind === Kind.OBJECT_TYPE_EXTENSION; +} + +export type ArgumentToDirectives = { + [argumentName: string]: DirectiveUsage[]; +}; +export type TypeAndFieldToArgumentDirectives = { + [typeAndField: string]: ArgumentToDirectives; +}; + +export function getArgumentsWithDirectives(documentNode: DocumentNode): TypeAndFieldToArgumentDirectives { + const result: TypeAndFieldToArgumentDirectives = {}; + + const allTypes = documentNode.definitions.filter(isTypeWithFields); + + for (const type of allTypes) { + if (type.fields == null) { + continue; + } + + for (const field of type.fields) { + const argsWithDirectives = field.arguments?.filter(arg => arg.directives?.length); + + if (!argsWithDirectives?.length) { + continue; + } + + const typeFieldResult = (result[`${type.name.value}.${field.name.value}`] = {}); + + for (const arg of argsWithDirectives) { + const directives: DirectiveUsage[] = arg.directives!.map(d => ({ + name: d.name.value, + args: (d.arguments || []).reduce( + (prev, dArg) => ({ ...prev, [dArg.name.value]: valueFromASTUntyped(dArg.value) }), + {} + ), + })); + + typeFieldResult[arg.name.value] = directives; + } + } + } + + return result; +} diff --git a/packages/utils/src/get-fields-with-directives.ts b/packages/utils/src/get-fields-with-directives.ts index 5f6eeb9d31..2e43b2a650 100644 --- a/packages/utils/src/get-fields-with-directives.ts +++ b/packages/utils/src/get-fields-with-directives.ts @@ -4,12 +4,10 @@ import { ObjectTypeExtensionNode, InputObjectTypeDefinitionNode, InputObjectTypeExtensionNode, - ValueNode, - Kind, + valueFromASTUntyped, } from 'graphql'; +import { DirectiveUsage } from './types.js'; -export type DirectiveArgs = { [name: string]: any }; -export type DirectiveUsage = { name: string; args: DirectiveArgs }; export type TypeAndFieldToDirectives = { [typeAndField: string]: DirectiveUsage[]; }; @@ -24,28 +22,6 @@ type SelectedNodes = | InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode; -function parseDirectiveValue(value: ValueNode): any { - switch (value.kind) { - case Kind.INT: - return parseInt(value.value); - case Kind.FLOAT: - return parseFloat(value.value); - case Kind.BOOLEAN: - return Boolean(value.value); - case Kind.STRING: - case Kind.ENUM: - return value.value; - case Kind.LIST: - return value.values.map(v => parseDirectiveValue(v)); - case Kind.OBJECT: - return value.fields.reduce((prev, v) => ({ ...prev, [v.name.value]: parseDirectiveValue(v.value) }), {}); - case Kind.NULL: - return null; - default: - return null; - } -} - export function getFieldsWithDirectives(documentNode: DocumentNode, options: Options = {}): TypeAndFieldToDirectives { const result: TypeAndFieldToDirectives = {}; @@ -71,7 +47,7 @@ export function getFieldsWithDirectives(documentNode: DocumentNode, options: Opt const directives: DirectiveUsage[] = field.directives.map(d => ({ name: d.name.value, args: (d.arguments || []).reduce( - (prev, arg) => ({ ...prev, [arg.name.value]: parseDirectiveValue(arg.value) }), + (prev, arg) => ({ ...prev, [arg.name.value]: valueFromASTUntyped(arg.value) }), {} ), })); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index b25d642a7c..4445105649 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -2,6 +2,7 @@ export * from './loaders.js'; export * from './helpers.js'; export * from './get-directives.js'; export * from './get-fields-with-directives.js'; +export * from './get-arguments-with-directives.js'; export * from './get-implementing-types.js'; export * from './print-schema-with-directives.js'; export * from './get-fields-with-directives.js'; diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 4d325d9e9b..2d70298bae 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -131,3 +131,6 @@ export type SchemaExtensions = { schemaExtensions: ExtensionsObject; types: Record; }; + +export type DirectiveArgs = { [name: string]: any }; +export type DirectiveUsage = { name: string; args: DirectiveArgs }; diff --git a/packages/utils/tests/get-arguments-with-directives.spec.ts b/packages/utils/tests/get-arguments-with-directives.spec.ts new file mode 100644 index 0000000000..94aea54943 --- /dev/null +++ b/packages/utils/tests/get-arguments-with-directives.spec.ts @@ -0,0 +1,162 @@ +import { parse } from 'graphql'; +import { getArgumentsWithDirectives } from '../src/index.js'; + +describe('getArgumentsWithDirectives', () => { + it('Should detect single basic directive', () => { + const node = parse(/* GraphQL */ ` + type A { + f1(anArg: String @a): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: {} }] }); + }); + + it('Should detect single basic directive in a type extension', () => { + const node = parse(/* GraphQL */ ` + extend type A { + f1(anArg: String @a): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: {} }] }); + }); + + it('Should parse string argument correctly', () => { + const node = parse(/* GraphQL */ ` + type A { + f1(anArg: String @a(f: "1")): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: { f: '1' } }] }); + }); + + it('Should parse multiple arguments correctly', () => { + const node = parse(/* GraphQL */ ` + type A { + f1(anArg: String @a(a1: "1", a2: 10)): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: { a1: '1', a2: 10 } }] }); + }); + + it('Should parse object arg correctly', () => { + const node = parse(/* GraphQL */ ` + type A { + f1(anArg: String @a(a1: { foo: "bar" })): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: { a1: { foo: 'bar' } } }] }); + }); + + it('Should parse array arg correctly', () => { + const node = parse(/* GraphQL */ ` + type A { + f1(anArg: String @a(a1: [1, 2, 3])): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: { a1: [1, 2, 3] } }] }); + }); + + it('Should parse complex array arg correctly', () => { + const node = parse(/* GraphQL */ ` + type A { + f1(anArg: String @a(a1: ["a", 1, { c: 3, d: true }])): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: { a1: ['a', 1, { c: 3, d: true }] } }] }); + }); + + it('Should detect multiple directives', () => { + const node = parse(/* GraphQL */ ` + type A { + f1(anArg: String @a @b): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ + anArg: [ + { name: 'a', args: {} }, + { name: 'b', args: {} }, + ], + }); + }); + + it('Should detect multiple directives and multiple arguments', () => { + const node = parse(/* GraphQL */ ` + type A { + f1(anArg: String @a @b, anotherArg: String @c): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ + anArg: [ + { name: 'a', args: {} }, + { name: 'b', args: {} }, + ], + anotherArg: [{ name: 'c', args: {} }], + }); + }); + + it('Should detect multiple directives and multiple fields', () => { + const node = parse(/* GraphQL */ ` + type A { + f1(anArg: String @a @b): Int + f2(anotherArg: String @c): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ + anArg: [ + { name: 'a', args: {} }, + { name: 'b', args: {} }, + ], + }); + expect(result['A.f2']).toEqual({ anotherArg: [{ name: 'c', args: {} }] }); + }); + + it('Should detect multiple types', () => { + const node = parse(/* GraphQL */ ` + type A { + f1(anArg: String @a): Int + } + + type B { + f2(anArg: String @a): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: {} }] }); + expect(result['B.f2']).toEqual({ anArg: [{ name: 'a', args: {} }] }); + }); + + it('Should include only fields with arguments with directives', () => { + const node = parse(/* GraphQL */ ` + type A { + f1: String @a + f2(anArg: Int): Int + f3(anArg: String @a): Int + } + `); + + const result = getArgumentsWithDirectives(node); + expect(result['A.f3']).toBeDefined(); + expect(Object.keys(result).length).toBe(1); + }); +});