Skip to content

Commit 403ed45

Browse files
authored
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
1 parent 72dadfe commit 403ed45

File tree

6 files changed

+224
-27
lines changed

6 files changed

+224
-27
lines changed

.changeset/twenty-chairs-judge.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-tools/utils': minor
3+
---
4+
5+
Add getArgumentsWithDirectives
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { DirectiveUsage } from './types.js';
2+
3+
import { ASTNode, DocumentNode, Kind, ObjectTypeDefinitionNode, valueFromASTUntyped } from 'graphql';
4+
5+
function isTypeWithFields(t: ASTNode): t is ObjectTypeDefinitionNode {
6+
return t.kind === Kind.OBJECT_TYPE_DEFINITION || t.kind === Kind.OBJECT_TYPE_EXTENSION;
7+
}
8+
9+
export type ArgumentToDirectives = {
10+
[argumentName: string]: DirectiveUsage[];
11+
};
12+
export type TypeAndFieldToArgumentDirectives = {
13+
[typeAndField: string]: ArgumentToDirectives;
14+
};
15+
16+
export function getArgumentsWithDirectives(documentNode: DocumentNode): TypeAndFieldToArgumentDirectives {
17+
const result: TypeAndFieldToArgumentDirectives = {};
18+
19+
const allTypes = documentNode.definitions.filter(isTypeWithFields);
20+
21+
for (const type of allTypes) {
22+
if (type.fields == null) {
23+
continue;
24+
}
25+
26+
for (const field of type.fields) {
27+
const argsWithDirectives = field.arguments?.filter(arg => arg.directives?.length);
28+
29+
if (!argsWithDirectives?.length) {
30+
continue;
31+
}
32+
33+
const typeFieldResult = (result[`${type.name.value}.${field.name.value}`] = {});
34+
35+
for (const arg of argsWithDirectives) {
36+
const directives: DirectiveUsage[] = arg.directives!.map(d => ({
37+
name: d.name.value,
38+
args: (d.arguments || []).reduce(
39+
(prev, dArg) => ({ ...prev, [dArg.name.value]: valueFromASTUntyped(dArg.value) }),
40+
{}
41+
),
42+
}));
43+
44+
typeFieldResult[arg.name.value] = directives;
45+
}
46+
}
47+
}
48+
49+
return result;
50+
}

packages/utils/src/get-fields-with-directives.ts

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import {
44
ObjectTypeExtensionNode,
55
InputObjectTypeDefinitionNode,
66
InputObjectTypeExtensionNode,
7-
ValueNode,
8-
Kind,
7+
valueFromASTUntyped,
98
} from 'graphql';
9+
import { DirectiveUsage } from './types.js';
1010

11-
export type DirectiveArgs = { [name: string]: any };
12-
export type DirectiveUsage = { name: string; args: DirectiveArgs };
1311
export type TypeAndFieldToDirectives = {
1412
[typeAndField: string]: DirectiveUsage[];
1513
};
@@ -24,28 +22,6 @@ type SelectedNodes =
2422
| InputObjectTypeDefinitionNode
2523
| InputObjectTypeExtensionNode;
2624

27-
function parseDirectiveValue(value: ValueNode): any {
28-
switch (value.kind) {
29-
case Kind.INT:
30-
return parseInt(value.value);
31-
case Kind.FLOAT:
32-
return parseFloat(value.value);
33-
case Kind.BOOLEAN:
34-
return Boolean(value.value);
35-
case Kind.STRING:
36-
case Kind.ENUM:
37-
return value.value;
38-
case Kind.LIST:
39-
return value.values.map(v => parseDirectiveValue(v));
40-
case Kind.OBJECT:
41-
return value.fields.reduce((prev, v) => ({ ...prev, [v.name.value]: parseDirectiveValue(v.value) }), {});
42-
case Kind.NULL:
43-
return null;
44-
default:
45-
return null;
46-
}
47-
}
48-
4925
export function getFieldsWithDirectives(documentNode: DocumentNode, options: Options = {}): TypeAndFieldToDirectives {
5026
const result: TypeAndFieldToDirectives = {};
5127

@@ -71,7 +47,7 @@ export function getFieldsWithDirectives(documentNode: DocumentNode, options: Opt
7147
const directives: DirectiveUsage[] = field.directives.map(d => ({
7248
name: d.name.value,
7349
args: (d.arguments || []).reduce(
74-
(prev, arg) => ({ ...prev, [arg.name.value]: parseDirectiveValue(arg.value) }),
50+
(prev, arg) => ({ ...prev, [arg.name.value]: valueFromASTUntyped(arg.value) }),
7551
{}
7652
),
7753
}));

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './loaders.js';
22
export * from './helpers.js';
33
export * from './get-directives.js';
44
export * from './get-fields-with-directives.js';
5+
export * from './get-arguments-with-directives.js';
56
export * from './get-implementing-types.js';
67
export * from './print-schema-with-directives.js';
78
export * from './get-fields-with-directives.js';

packages/utils/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,6 @@ export type SchemaExtensions = {
131131
schemaExtensions: ExtensionsObject;
132132
types: Record<string, { extensions: ExtensionsObject } & PossibleTypeExtensions>;
133133
};
134+
135+
export type DirectiveArgs = { [name: string]: any };
136+
export type DirectiveUsage = { name: string; args: DirectiveArgs };
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { parse } from 'graphql';
2+
import { getArgumentsWithDirectives } from '../src/index.js';
3+
4+
describe('getArgumentsWithDirectives', () => {
5+
it('Should detect single basic directive', () => {
6+
const node = parse(/* GraphQL */ `
7+
type A {
8+
f1(anArg: String @a): Int
9+
}
10+
`);
11+
12+
const result = getArgumentsWithDirectives(node);
13+
expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: {} }] });
14+
});
15+
16+
it('Should detect single basic directive in a type extension', () => {
17+
const node = parse(/* GraphQL */ `
18+
extend type A {
19+
f1(anArg: String @a): Int
20+
}
21+
`);
22+
23+
const result = getArgumentsWithDirectives(node);
24+
expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: {} }] });
25+
});
26+
27+
it('Should parse string argument correctly', () => {
28+
const node = parse(/* GraphQL */ `
29+
type A {
30+
f1(anArg: String @a(f: "1")): Int
31+
}
32+
`);
33+
34+
const result = getArgumentsWithDirectives(node);
35+
expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: { f: '1' } }] });
36+
});
37+
38+
it('Should parse multiple arguments correctly', () => {
39+
const node = parse(/* GraphQL */ `
40+
type A {
41+
f1(anArg: String @a(a1: "1", a2: 10)): Int
42+
}
43+
`);
44+
45+
const result = getArgumentsWithDirectives(node);
46+
expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: { a1: '1', a2: 10 } }] });
47+
});
48+
49+
it('Should parse object arg correctly', () => {
50+
const node = parse(/* GraphQL */ `
51+
type A {
52+
f1(anArg: String @a(a1: { foo: "bar" })): Int
53+
}
54+
`);
55+
56+
const result = getArgumentsWithDirectives(node);
57+
expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: { a1: { foo: 'bar' } } }] });
58+
});
59+
60+
it('Should parse array arg correctly', () => {
61+
const node = parse(/* GraphQL */ `
62+
type A {
63+
f1(anArg: String @a(a1: [1, 2, 3])): Int
64+
}
65+
`);
66+
67+
const result = getArgumentsWithDirectives(node);
68+
expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: { a1: [1, 2, 3] } }] });
69+
});
70+
71+
it('Should parse complex array arg correctly', () => {
72+
const node = parse(/* GraphQL */ `
73+
type A {
74+
f1(anArg: String @a(a1: ["a", 1, { c: 3, d: true }])): Int
75+
}
76+
`);
77+
78+
const result = getArgumentsWithDirectives(node);
79+
expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: { a1: ['a', 1, { c: 3, d: true }] } }] });
80+
});
81+
82+
it('Should detect multiple directives', () => {
83+
const node = parse(/* GraphQL */ `
84+
type A {
85+
f1(anArg: String @a @b): Int
86+
}
87+
`);
88+
89+
const result = getArgumentsWithDirectives(node);
90+
expect(result['A.f1']).toEqual({
91+
anArg: [
92+
{ name: 'a', args: {} },
93+
{ name: 'b', args: {} },
94+
],
95+
});
96+
});
97+
98+
it('Should detect multiple directives and multiple arguments', () => {
99+
const node = parse(/* GraphQL */ `
100+
type A {
101+
f1(anArg: String @a @b, anotherArg: String @c): Int
102+
}
103+
`);
104+
105+
const result = getArgumentsWithDirectives(node);
106+
expect(result['A.f1']).toEqual({
107+
anArg: [
108+
{ name: 'a', args: {} },
109+
{ name: 'b', args: {} },
110+
],
111+
anotherArg: [{ name: 'c', args: {} }],
112+
});
113+
});
114+
115+
it('Should detect multiple directives and multiple fields', () => {
116+
const node = parse(/* GraphQL */ `
117+
type A {
118+
f1(anArg: String @a @b): Int
119+
f2(anotherArg: String @c): Int
120+
}
121+
`);
122+
123+
const result = getArgumentsWithDirectives(node);
124+
expect(result['A.f1']).toEqual({
125+
anArg: [
126+
{ name: 'a', args: {} },
127+
{ name: 'b', args: {} },
128+
],
129+
});
130+
expect(result['A.f2']).toEqual({ anotherArg: [{ name: 'c', args: {} }] });
131+
});
132+
133+
it('Should detect multiple types', () => {
134+
const node = parse(/* GraphQL */ `
135+
type A {
136+
f1(anArg: String @a): Int
137+
}
138+
139+
type B {
140+
f2(anArg: String @a): Int
141+
}
142+
`);
143+
144+
const result = getArgumentsWithDirectives(node);
145+
expect(result['A.f1']).toEqual({ anArg: [{ name: 'a', args: {} }] });
146+
expect(result['B.f2']).toEqual({ anArg: [{ name: 'a', args: {} }] });
147+
});
148+
149+
it('Should include only fields with arguments with directives', () => {
150+
const node = parse(/* GraphQL */ `
151+
type A {
152+
f1: String @a
153+
f2(anArg: Int): Int
154+
f3(anArg: String @a): Int
155+
}
156+
`);
157+
158+
const result = getArgumentsWithDirectives(node);
159+
expect(result['A.f3']).toBeDefined();
160+
expect(Object.keys(result).length).toBe(1);
161+
});
162+
});

0 commit comments

Comments
 (0)