Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nicolaslt committed Sep 2, 2022
1 parent 72dadfe commit 403ed45
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .changeset/twenty-chairs-judge.md
@@ -0,0 +1,5 @@
---
'@graphql-tools/utils': minor
---

Add getArgumentsWithDirectives
50 changes: 50 additions & 0 deletions 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;
}
30 changes: 3 additions & 27 deletions packages/utils/src/get-fields-with-directives.ts
Expand Up @@ -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[];
};
Expand All @@ -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 = {};

Expand All @@ -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) }),
{}
),
}));
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Expand Up @@ -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';
Expand Down
3 changes: 3 additions & 0 deletions packages/utils/src/types.ts
Expand Up @@ -131,3 +131,6 @@ export type SchemaExtensions = {
schemaExtensions: ExtensionsObject;
types: Record<string, { extensions: ExtensionsObject } & PossibleTypeExtensions>;
};

export type DirectiveArgs = { [name: string]: any };
export type DirectiveUsage = { name: string; args: DirectiveArgs };
162 changes: 162 additions & 0 deletions 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);
});
});

1 comment on commit 403ed45

@vercel
Copy link

@vercel vercel bot commented on 403ed45 Sep 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.