Skip to content

Commit 8cbfd3b

Browse files
committed
🚧 wip GQL parser
1 parent 678caa5 commit 8cbfd3b

File tree

10 files changed

+679
-5
lines changed

10 files changed

+679
-5
lines changed

src/GqlParser/GqlParserTreeToGql.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { getValueAsGqlStringNode } from '@/GqlParser/valueNode';
2+
import { Instances, TypeSystemDefinition } from '@/Models';
3+
import { GqlParserTree, VariableDefinitionWithoutLoc } from '@/Models/GqlParserTree';
4+
import { compileType } from '@/shared';
5+
export const GqlParserTreeToGql = (mainTree: GqlParserTree) => {
6+
const generateName = (tree: GqlParserTree): string => {
7+
return `${
8+
tree.operation
9+
? `${tree.operation}${tree.name ? ` ${tree.name}` : ''}${generateVariableDefinitions(tree)}`
10+
: tree.name
11+
}`;
12+
};
13+
const generateChildren = (tree: GqlParserTree): string => {
14+
return `${tree.children ? `{\n ${tree.children.map(generateGql).join('\n ')}\n}` : ''}`;
15+
};
16+
const generateValue = (tree: GqlParserTree): string => {
17+
return `${tree.value ? `: ${getValueAsGqlStringNode(tree.value)}` : ''}`;
18+
};
19+
const generateArguments = (tree: GqlParserTree): string => {
20+
return `${tree.arguments ? `(\n ${tree.arguments.map((a) => generateGql(a)).join(', ')})` : ''}`;
21+
};
22+
const generateGql = (tree: GqlParserTree): string => {
23+
return `${generateName(tree)}${generateValue(tree)}${generateArguments(tree)}${generateChildren(tree)}`;
24+
};
25+
const generateVariableDefinitions = (tree: GqlParserTree): string => {
26+
return `${
27+
tree.variableDefinitions?.length
28+
? `(${tree.variableDefinitions.map((tvd) => `$${tvd.name}: ${tvd.type}`).join(', ')})`
29+
: ''
30+
}`;
31+
};
32+
return generateGql(mainTree);
33+
};
34+
35+
export const enrichFieldNodeWithVariables = (
36+
fieldTree: GqlParserTree,
37+
variableDefinitionsUpdate: (
38+
fn: (variableDefinitions: VariableDefinitionWithoutLoc[]) => VariableDefinitionWithoutLoc[],
39+
) => void,
40+
): GqlParserTree => {
41+
if (
42+
fieldTree.node.data.type === TypeSystemDefinition.FieldDefinition ||
43+
fieldTree.node.data.type === Instances.Directive
44+
) {
45+
return {
46+
...fieldTree,
47+
arguments: fieldTree.node.args.map((a) => {
48+
const VarName = `${fieldTree.name}_${a.name}`;
49+
const VarNode: GqlParserTree = {
50+
name: a.name,
51+
node: a,
52+
value: {
53+
kind: 'Variable',
54+
value: VarName,
55+
},
56+
};
57+
variableDefinitionsUpdate((variableDefinitions) => [
58+
...variableDefinitions,
59+
{
60+
name: VarName,
61+
type: compileType(a.type.fieldType),
62+
},
63+
]);
64+
return VarNode;
65+
}),
66+
};
67+
}
68+
return fieldTree;
69+
};
70+
71+
export const enrichWholeTreeWithVars = (mainTree: GqlParserTree): GqlParserTree => {
72+
let variableDefinitions: VariableDefinitionWithoutLoc[] = [];
73+
const recursiveEnrich = (tree: GqlParserTree): GqlParserTree => {
74+
return enrichFieldNodeWithVariables(
75+
{
76+
...tree,
77+
...(tree.children
78+
? {
79+
children: tree.children.map(recursiveEnrich),
80+
}
81+
: {}),
82+
...(tree.directives
83+
? {
84+
directives: tree.directives.map(recursiveEnrich),
85+
}
86+
: {}),
87+
},
88+
(varsUpdate) => {
89+
variableDefinitions = varsUpdate(variableDefinitions);
90+
},
91+
);
92+
};
93+
return { ...recursiveEnrich(mainTree), variableDefinitions };
94+
};

src/GqlParser/index.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { getValueWithoutLoc } from '@/GqlParser/valueNode';
2+
import { OperationType, ParserField } from '@/Models';
3+
import { GqlParserTree, VariableDefinitionWithoutLoc } from '@/Models/GqlParserTree';
4+
import { Parser } from '@/Parser';
5+
import { TypeResolver } from '@/Parser/typeResolver';
6+
import { compileType } from '@/shared';
7+
import {
8+
DefinitionNode,
9+
parse,
10+
OperationDefinitionNode,
11+
SelectionNode,
12+
ArgumentNode,
13+
DirectiveNode,
14+
FieldNode,
15+
VariableDefinitionNode,
16+
} from 'graphql';
17+
export const GqlParser = (gql: string, schema: string) => {
18+
const { definitions } = parse(schema + '\n' + gql);
19+
const ops = definitions.filter(onlyOperations);
20+
const { nodes } = Parser.parse(schema);
21+
22+
const composeDefinition = (d: OperationDefinitionNode): GqlParserTree => {
23+
const node = nodes.find((n) => n.type.operations?.includes(d.operation as OperationType));
24+
if (!node) {
25+
throw new Error(`Operation ${d.name} does not exist in schema`);
26+
}
27+
return {
28+
name: d.name?.value,
29+
node,
30+
...(d.operation
31+
? {
32+
operation:
33+
d.operation === 'query'
34+
? OperationType.query
35+
: d.operation === 'mutation'
36+
? OperationType.mutation
37+
: OperationType.subscription,
38+
}
39+
: {}),
40+
...(d.variableDefinitions?.length
41+
? {
42+
variableDefinitions: d.variableDefinitions.map((vd) => composeVariableDefinition(vd)),
43+
}
44+
: {}),
45+
children: d.selectionSet.selections.filter(onlyFieldNodes).map((s) => composeSelectionNode(s, node)),
46+
};
47+
};
48+
49+
const composeVariableDefinition = (v: VariableDefinitionNode): VariableDefinitionWithoutLoc => {
50+
const t = TypeResolver.resolveSingleFieldType(v.type);
51+
return {
52+
name: v.variable.name.value,
53+
type: compileType(t),
54+
};
55+
};
56+
57+
const composeArgumentNode = (a: ArgumentNode, parentNode: ParserField): GqlParserTree => {
58+
const node = parentNode.args.find((ar) => ar.name === a.name.value);
59+
if (!node) {
60+
throw new Error(`Invalid argument name ${a.name}`);
61+
}
62+
return {
63+
name: a.name.value,
64+
node,
65+
value: getValueWithoutLoc(a.value),
66+
};
67+
};
68+
const composeDirectiveNode = (a: DirectiveNode, parentNode: ParserField): GqlParserTree => {
69+
const node = parentNode.args.find((ar) => ar.name === a.name.value);
70+
if (!node) {
71+
throw new Error(`Invalid argument name ${a.name}`);
72+
}
73+
74+
return {
75+
node,
76+
name: a.name.value,
77+
arguments: a.arguments?.map((a) => composeArgumentNode(a, node)),
78+
};
79+
};
80+
81+
const composeSelectionNode = (s: FieldNode, parentNode: ParserField): GqlParserTree => {
82+
const fieldNode = parentNode.args.find((a) => a.name === s.name.value);
83+
if (!fieldNode) {
84+
throw new Error('Field does not exist in schema');
85+
}
86+
return {
87+
node: fieldNode,
88+
name: s.name.value,
89+
arguments: s.arguments?.map((a) => composeArgumentNode(a, fieldNode)),
90+
directives: s.directives?.map((a) => composeDirectiveNode(a, fieldNode)),
91+
children: s.selectionSet?.selections.filter(onlyFieldNodes).map((a) => composeSelectionNode(a, fieldNode)),
92+
} as GqlParserTree;
93+
};
94+
return ops.map((o) => composeDefinition(o));
95+
};
96+
97+
const onlyOperations = (definition: DefinitionNode): definition is OperationDefinitionNode => {
98+
return definition.kind === 'OperationDefinition';
99+
};
100+
const onlyFieldNodes = (definition: SelectionNode): definition is FieldNode => {
101+
return definition.kind === 'Field';
102+
};

src/GqlParser/valueNode.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { ValueNodeWithoutLoc } from '@/Models/GqlParserTree';
2+
import { ValueNode } from 'graphql';
3+
4+
export const getValueAsGqlStringNode = (v: ValueNodeWithoutLoc): string => {
5+
if (v.kind === 'ListValue') {
6+
return `[${v.values.map((vv) => getValueAsGqlStringNode(vv)).join(', ')}]`;
7+
}
8+
if (v.kind === 'ObjectValue') {
9+
return `{\n ${v.fields.map((f) => `${f.name}: ${getValueAsGqlStringNode(f.value)}`).join(', \n')} \n}`;
10+
}
11+
if (v.kind === 'Variable') {
12+
return '$' + v.value;
13+
}
14+
if (v.kind === 'NullValue') {
15+
return `null`;
16+
}
17+
if (v.kind === 'StringValue') {
18+
return `"${v.value}"`;
19+
}
20+
if (v.kind === 'FloatValue') {
21+
return v.value;
22+
}
23+
if (v.kind === 'IntValue') {
24+
return v.value;
25+
}
26+
if (v.kind === 'BooleanValue') {
27+
if (v.value) return 'true';
28+
return 'false';
29+
}
30+
return v.value;
31+
};
32+
33+
export const getValueWithoutLoc = (v: ValueNode): ValueNodeWithoutLoc => {
34+
if (v.kind === 'ListValue') {
35+
return {
36+
kind: v.kind,
37+
values: v.values.map((vv) => getValueWithoutLoc(vv)),
38+
};
39+
}
40+
if (v.kind === 'ObjectValue') {
41+
return {
42+
kind: v.kind,
43+
fields: v.fields.map((f) => ({
44+
kind: 'ObjectField',
45+
name: f.name.value,
46+
value: getValueWithoutLoc(f.value),
47+
})),
48+
};
49+
}
50+
if (v.kind === 'Variable') {
51+
return {
52+
kind: v.kind,
53+
value: v.name.value,
54+
};
55+
}
56+
if (v.kind === 'NullValue') {
57+
return {
58+
kind: v.kind,
59+
value: null,
60+
};
61+
}
62+
if (v.kind === 'BooleanValue') {
63+
return {
64+
kind: v.kind,
65+
value: v.value,
66+
};
67+
}
68+
return {
69+
kind: v.kind,
70+
value: v.value,
71+
};
72+
};

src/Models/GqlParserTree.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ParserField } from '@/Models/ParserTree';
2+
import { OperationType } from '@/Models/Spec';
3+
import {
4+
BooleanValueNode,
5+
EnumValueNode,
6+
FloatValueNode,
7+
IntValueNode,
8+
ListValueNode,
9+
NullValueNode,
10+
ObjectFieldNode,
11+
ObjectValueNode,
12+
StringValueNode,
13+
VariableNode,
14+
} from 'graphql';
15+
16+
type ObjectFieldNodeWithoutLoc = Omit<ObjectFieldNode, 'loc' | 'name' | 'value'> & {
17+
name: string;
18+
value: ValueNodeWithoutLoc;
19+
};
20+
21+
export type ValueNodeWithoutLoc =
22+
| (Omit<VariableNode, 'loc' | 'name'> & { value: string })
23+
| Omit<IntValueNode, 'loc'>
24+
| Omit<FloatValueNode, 'loc'>
25+
| Omit<StringValueNode, 'loc'>
26+
| Omit<BooleanValueNode, 'loc'>
27+
| (Omit<NullValueNode, 'loc'> & { value: null })
28+
| Omit<EnumValueNode, 'loc'>
29+
| (Omit<ListValueNode, 'loc' | 'values'> & { values: ValueNodeWithoutLoc[] })
30+
| (Omit<ObjectValueNode, 'loc' | 'fields'> & {
31+
fields: ObjectFieldNodeWithoutLoc[];
32+
});
33+
34+
export type GqlParserTree = {
35+
name?: string;
36+
node: ParserField;
37+
children?: GqlParserTree[];
38+
arguments?: GqlParserTree[];
39+
directives?: GqlParserTree[];
40+
fragment?: boolean;
41+
value?: ValueNodeWithoutLoc;
42+
operation?: OperationType;
43+
variableDefinitions?: VariableDefinitionWithoutLoc[];
44+
};
45+
46+
export type VariableDefinitionWithoutLoc = {
47+
name: string;
48+
type: string;
49+
};

0 commit comments

Comments
 (0)