@luvio/graphql-parser 0.99.0 → 0.99.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gql.d.ts +49 -0
- package/dist/luvioGraphqlParser.js +322 -2
- package/dist/luvioGraphqlParser.mjs +319 -3
- package/dist/main.d.ts +2 -0
- package/dist/metaschema.d.ts +10 -0
- package/dist/util/language.d.ts +9 -0
- package/package.json +22 -17
- package/src/__tests__/ast.json +403 -0
- package/src/__tests__/astNoLoc.json +147 -0
- package/src/__tests__/gql.spec.ts +684 -0
- package/src/__tests__/metaschema.spec.ts +230 -0
- package/src/gql.ts +237 -0
- package/src/main.ts +7 -0
- package/src/metaschema.ts +162 -0
- package/src/util/language.ts +10 -0
- package/src/__tests__/__snapshots__/schema.spec.ts.snap +0 -2132
- package/src/__tests__/data/github.graphql +0 -48492
- package/src/__tests__/data/swapi.graphql +0 -1166
- package/src/__tests__/schema.spec.ts +0 -21
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { gql, referenceMap } from '../gql';
|
|
2
|
+
import { metaschemaMapper } from '../metaschema';
|
|
3
|
+
|
|
4
|
+
describe('metaschema mapper', () => {
|
|
5
|
+
it('should produce a equivalent metaschema AST when called with the legacy AST', () => {
|
|
6
|
+
const ref1 = gql`
|
|
7
|
+
query {
|
|
8
|
+
uiapi {
|
|
9
|
+
query {
|
|
10
|
+
Account(where: { Name: { like: "Account1" } }) @connection {
|
|
11
|
+
edges {
|
|
12
|
+
node @resource(type: "Record") {
|
|
13
|
+
Name {
|
|
14
|
+
value
|
|
15
|
+
displayValue
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
...conFragment
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fragment conFragment on AccountConnection {
|
|
26
|
+
edges {
|
|
27
|
+
node @resource(type: "Record") {
|
|
28
|
+
CreatedBy @resource(type: "Record") {
|
|
29
|
+
Id
|
|
30
|
+
Name {
|
|
31
|
+
value
|
|
32
|
+
displayValue
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
const doc1 = referenceMap.get(ref1);
|
|
41
|
+
|
|
42
|
+
const ref2 = gql`
|
|
43
|
+
query {
|
|
44
|
+
uiapi {
|
|
45
|
+
query {
|
|
46
|
+
Account(where: { Name: { like: "Account1" } })
|
|
47
|
+
@category(name: "recordQuery") {
|
|
48
|
+
edges {
|
|
49
|
+
node {
|
|
50
|
+
Name {
|
|
51
|
+
value
|
|
52
|
+
displayValue
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
...conFragment
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fragment conFragment on AccountConnection {
|
|
63
|
+
edges {
|
|
64
|
+
node {
|
|
65
|
+
CreatedBy @category(name: "parentRelationship") {
|
|
66
|
+
Id
|
|
67
|
+
Name {
|
|
68
|
+
value
|
|
69
|
+
displayValue
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
const doc2 = referenceMap.get(ref2);
|
|
77
|
+
|
|
78
|
+
metaschemaMapper(doc1);
|
|
79
|
+
|
|
80
|
+
expect(doc1).toStrictEqual(doc2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should produce a equivalent metaschema AST when query has multiple entities', () => {
|
|
84
|
+
const ref1 = gql`
|
|
85
|
+
query {
|
|
86
|
+
uiapi {
|
|
87
|
+
query {
|
|
88
|
+
TimeSheet @connection {
|
|
89
|
+
edges {
|
|
90
|
+
node @resource(type: "Record") {
|
|
91
|
+
CreatedBy @resource(type: "Record") {
|
|
92
|
+
Email {
|
|
93
|
+
value
|
|
94
|
+
displayValue
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
User @connection {
|
|
101
|
+
edges {
|
|
102
|
+
node @resource(type: "Record") {
|
|
103
|
+
CreatedBy @resource(type: "Record") {
|
|
104
|
+
Email {
|
|
105
|
+
value
|
|
106
|
+
displayValue
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
const doc1 = referenceMap.get(ref1);
|
|
117
|
+
|
|
118
|
+
const ref2 = gql`
|
|
119
|
+
query {
|
|
120
|
+
uiapi {
|
|
121
|
+
query {
|
|
122
|
+
TimeSheet @category(name: "recordQuery") {
|
|
123
|
+
edges {
|
|
124
|
+
node {
|
|
125
|
+
CreatedBy @category(name: "parentRelationship") {
|
|
126
|
+
Email {
|
|
127
|
+
value
|
|
128
|
+
displayValue
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
User @category(name: "recordQuery") {
|
|
135
|
+
edges {
|
|
136
|
+
node {
|
|
137
|
+
CreatedBy @category(name: "parentRelationship") {
|
|
138
|
+
Email {
|
|
139
|
+
value
|
|
140
|
+
displayValue
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
`;
|
|
150
|
+
const doc2 = referenceMap.get(ref2);
|
|
151
|
+
|
|
152
|
+
metaschemaMapper(doc1);
|
|
153
|
+
|
|
154
|
+
expect(doc1).toStrictEqual(doc2);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should leave any non - legacy directives intact', () => {
|
|
158
|
+
const ref1 = gql`
|
|
159
|
+
query myQuery($var1: Boolean!, $var2: Boolean!) {
|
|
160
|
+
uiapi {
|
|
161
|
+
query {
|
|
162
|
+
TimeSheet @include(if: $var1) @connection {
|
|
163
|
+
edges {
|
|
164
|
+
node @resource(type: "Record") {
|
|
165
|
+
CreatedBy @resource(type: "Record") {
|
|
166
|
+
Email {
|
|
167
|
+
value
|
|
168
|
+
displayValue
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
User @connection @skip(if: $var2) {
|
|
175
|
+
edges {
|
|
176
|
+
node @resource(type: "Record") {
|
|
177
|
+
CreatedBy @resource(type: "Record") {
|
|
178
|
+
Email {
|
|
179
|
+
value
|
|
180
|
+
displayValue
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
`;
|
|
190
|
+
const doc1 = referenceMap.get(ref1);
|
|
191
|
+
|
|
192
|
+
const ref2 = gql`
|
|
193
|
+
query myQuery($var1: Boolean!, $var2: Boolean!) {
|
|
194
|
+
uiapi {
|
|
195
|
+
query {
|
|
196
|
+
TimeSheet @include(if: $var1) @category(name: "recordQuery") {
|
|
197
|
+
edges {
|
|
198
|
+
node {
|
|
199
|
+
CreatedBy @category(name: "parentRelationship") {
|
|
200
|
+
Email {
|
|
201
|
+
value
|
|
202
|
+
displayValue
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
User @category(name: "recordQuery") @skip(if: $var2) {
|
|
209
|
+
edges {
|
|
210
|
+
node {
|
|
211
|
+
CreatedBy @category(name: "parentRelationship") {
|
|
212
|
+
Email {
|
|
213
|
+
value
|
|
214
|
+
displayValue
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
`;
|
|
224
|
+
const doc2 = referenceMap.get(ref2);
|
|
225
|
+
|
|
226
|
+
metaschemaMapper(doc1);
|
|
227
|
+
|
|
228
|
+
expect(doc1).toStrictEqual(doc2);
|
|
229
|
+
});
|
|
230
|
+
});
|
package/src/gql.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exposes a tagged template literal to parse graphql operation string
|
|
3
|
+
* "gql" is the only publicly exposed method
|
|
4
|
+
* @module gql
|
|
5
|
+
*/
|
|
6
|
+
import type { DefinitionNode, DocumentNode } from 'graphql/language';
|
|
7
|
+
import { parse } from 'graphql/language';
|
|
8
|
+
import { stripIgnoredCharacters } from 'graphql/utilities';
|
|
9
|
+
import { ObjectCreate, ObjectKeys } from './util/language';
|
|
10
|
+
import { metaschemaMapper } from './metaschema';
|
|
11
|
+
|
|
12
|
+
export type AstResolver = (astReference: any) => DocumentNode | undefined;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* we should look into optimizing this before it turns into a memory hog
|
|
16
|
+
* weakmaps, or limiting the size of the cache, or something
|
|
17
|
+
*/
|
|
18
|
+
export const docMap = new Map<string, DocumentNode>();
|
|
19
|
+
/**
|
|
20
|
+
* Opaque reference map to return keys to userland
|
|
21
|
+
* As a user shouldn't have access to the Document
|
|
22
|
+
*/
|
|
23
|
+
export const referenceMap = new WeakMap<Object, DocumentNode>();
|
|
24
|
+
|
|
25
|
+
export let addMetaschemaDirectives: boolean = false;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Strips characters that are not significant to the validity or execution
|
|
29
|
+
* of a GraphQL document:
|
|
30
|
+
* - UnicodeBOM
|
|
31
|
+
* - WhiteSpace
|
|
32
|
+
* - LineTerminator
|
|
33
|
+
* - Comment
|
|
34
|
+
* - Comma
|
|
35
|
+
* - BlockString indentation
|
|
36
|
+
*/
|
|
37
|
+
function operationKeyBuilder(inputString: string): string {
|
|
38
|
+
return stripIgnoredCharacters(inputString);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns document node if cached or else update the cache and return the document node
|
|
43
|
+
* @param inputString - operation string
|
|
44
|
+
* @returns DocumentNode
|
|
45
|
+
*/
|
|
46
|
+
function parseDocument(inputString: string): DocumentNode | null {
|
|
47
|
+
const operationKey = operationKeyBuilder(inputString);
|
|
48
|
+
const cachedDoc = docMap.get(operationKey);
|
|
49
|
+
if (cachedDoc !== undefined) {
|
|
50
|
+
return cachedDoc;
|
|
51
|
+
}
|
|
52
|
+
// parse throws an GraphQLError in case of invalid query, should this be in try/catch?
|
|
53
|
+
const parsedDoc = parse(inputString);
|
|
54
|
+
if (!parsedDoc || parsedDoc.kind !== 'Document') {
|
|
55
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
56
|
+
throw new Error('Invalid graphql doc');
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// in-place substitution for removal of legacy and adding metaschema directives
|
|
62
|
+
if (addMetaschemaDirectives) {
|
|
63
|
+
metaschemaMapper(parsedDoc);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const parsedDocNoLoc = stripLocation(parsedDoc);
|
|
67
|
+
|
|
68
|
+
docMap.set(operationKey, parsedDocNoLoc);
|
|
69
|
+
|
|
70
|
+
return parsedDocNoLoc;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* If the input string has fragment substitution
|
|
75
|
+
* Insert the fragments AST to the query document node
|
|
76
|
+
*/
|
|
77
|
+
function insertFragments(doc: DocumentNode, fragments: DefinitionNode[]): DocumentNode {
|
|
78
|
+
fragments.forEach((fragment) => {
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
// graphql describes definitions as "readonly"
|
|
81
|
+
// so we aren't supposed to mutate the document node
|
|
82
|
+
// but instead of parsing the fragment again, we substitute it's definition in the document node
|
|
83
|
+
doc.definitions.push(fragment);
|
|
84
|
+
});
|
|
85
|
+
return doc;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createReference(document: DocumentNode): object {
|
|
89
|
+
const key = ObjectCreate(null);
|
|
90
|
+
referenceMap.set(key, document);
|
|
91
|
+
|
|
92
|
+
return key;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Insert string and fragment substitutions with the actual nodes
|
|
97
|
+
* @param inputString
|
|
98
|
+
* @param substitutions - string | fragment DocumentNode
|
|
99
|
+
* @returns { operation string, fragment docs [] }
|
|
100
|
+
*/
|
|
101
|
+
export function processSubstitutions(
|
|
102
|
+
inputString: ReadonlyArray<string>,
|
|
103
|
+
substitutions: (string | object)[]
|
|
104
|
+
): { operationString: string; fragments: DefinitionNode[] } | null {
|
|
105
|
+
let outputString = '';
|
|
106
|
+
const fragments: DefinitionNode[] = [];
|
|
107
|
+
const subLength = substitutions.length;
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < subLength; i++) {
|
|
110
|
+
const substitution = substitutions[i];
|
|
111
|
+
outputString += inputString[i];
|
|
112
|
+
|
|
113
|
+
if (typeof substitution === 'string' || typeof substitution === 'number') {
|
|
114
|
+
outputString += substitution;
|
|
115
|
+
} else if (typeof substitution === 'object') {
|
|
116
|
+
const doc = referenceMap.get(substitution);
|
|
117
|
+
|
|
118
|
+
if (doc === undefined) {
|
|
119
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
120
|
+
throw new Error('Invalid substitution fragment');
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const def of doc.definitions) {
|
|
126
|
+
fragments.push(def);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
130
|
+
throw new Error('Unsupported substitution type');
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
operationString: outputString + inputString[subLength],
|
|
138
|
+
fragments,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* strips Document node and nested definitions of location references
|
|
144
|
+
*/
|
|
145
|
+
export function stripLocation(node: any) {
|
|
146
|
+
if (node.loc) {
|
|
147
|
+
delete node.loc;
|
|
148
|
+
}
|
|
149
|
+
const keys = ObjectKeys(node);
|
|
150
|
+
const keysLength = keys.length;
|
|
151
|
+
|
|
152
|
+
if (keysLength === 0) {
|
|
153
|
+
return node;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const key of keys) {
|
|
157
|
+
const subNode = node[key];
|
|
158
|
+
if (subNode && typeof subNode === 'object') {
|
|
159
|
+
stripLocation(subNode);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return node;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
*
|
|
168
|
+
* @param astReference - ast reference passed from user land
|
|
169
|
+
*/
|
|
170
|
+
export const astResolver: AstResolver = function (astReference: any) {
|
|
171
|
+
return referenceMap.get(astReference);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
*
|
|
176
|
+
* @param literals - operation query string
|
|
177
|
+
* @param subs - all other substitutions
|
|
178
|
+
* @returns an opaque reference to the parsed document
|
|
179
|
+
*/
|
|
180
|
+
export function gql(
|
|
181
|
+
literals: ReadonlyArray<string> | string,
|
|
182
|
+
...subs: (string | object)[]
|
|
183
|
+
): object | null {
|
|
184
|
+
let inputString: string;
|
|
185
|
+
let inputSubstitutionFragments: DefinitionNode[];
|
|
186
|
+
|
|
187
|
+
if (!literals) {
|
|
188
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
189
|
+
throw new Error('Invalid query');
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
} else if (typeof literals === 'string') {
|
|
193
|
+
inputString = literals.trim();
|
|
194
|
+
inputSubstitutionFragments = [];
|
|
195
|
+
} else {
|
|
196
|
+
// called as template literal
|
|
197
|
+
const sub = processSubstitutions(literals, subs);
|
|
198
|
+
// if invalid fragment references found
|
|
199
|
+
if (sub === null) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const { operationString, fragments } = sub;
|
|
204
|
+
inputString = operationString.trim();
|
|
205
|
+
inputSubstitutionFragments = fragments;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (inputString.length === 0) {
|
|
209
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
210
|
+
throw new Error('Invalid query');
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const document = parseDocument(inputString);
|
|
216
|
+
|
|
217
|
+
if (document === null) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (inputSubstitutionFragments.length === 0) {
|
|
222
|
+
return createReference(document);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return createReference(insertFragments(document, inputSubstitutionFragments));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Enable the parser to add corresponding metaschema directives for backwards compatibility
|
|
230
|
+
*/
|
|
231
|
+
export function enableAddMetaschemaDirective() {
|
|
232
|
+
addMetaschemaDirectives = true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function disableAddMetaschemaDirective() {
|
|
236
|
+
addMetaschemaDirectives = false;
|
|
237
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -64,6 +64,13 @@ export type {
|
|
|
64
64
|
export { ASTVisitor, parse, Kind, print, visit } from 'graphql/language';
|
|
65
65
|
export { isScalarType } from 'graphql/type';
|
|
66
66
|
export { stripIgnoredCharacters } from 'graphql/utilities';
|
|
67
|
+
export {
|
|
68
|
+
gql,
|
|
69
|
+
enableAddMetaschemaDirective,
|
|
70
|
+
disableAddMetaschemaDirective,
|
|
71
|
+
astResolver,
|
|
72
|
+
} from './gql';
|
|
73
|
+
export type { AstResolver } from './gql';
|
|
67
74
|
|
|
68
75
|
/**
|
|
69
76
|
* @deprecated - Schema-backed adapters will use standard graphql types re-exported from @luvio/graphql
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add metaschema annotations to their corresponding custom notation counterparts
|
|
3
|
+
* @module metaschemaMapper
|
|
4
|
+
*/
|
|
5
|
+
import type {
|
|
6
|
+
DefinitionNode,
|
|
7
|
+
DirectiveNode,
|
|
8
|
+
DocumentNode,
|
|
9
|
+
FragmentSpreadNode,
|
|
10
|
+
SelectionNode,
|
|
11
|
+
SelectionSetNode,
|
|
12
|
+
TypeSystemDefinitionNode,
|
|
13
|
+
TypeSystemExtensionNode,
|
|
14
|
+
} from 'graphql/language';
|
|
15
|
+
import { CUSTOM_DIRECTIVE_CONNECTION, CUSTOM_DIRECTIVE_RESOURCE } from './constants';
|
|
16
|
+
|
|
17
|
+
const DIRECTIVE_RECORD_CATEGORY = {
|
|
18
|
+
kind: 'Directive',
|
|
19
|
+
name: {
|
|
20
|
+
kind: 'Name',
|
|
21
|
+
value: 'category',
|
|
22
|
+
},
|
|
23
|
+
arguments: [
|
|
24
|
+
{
|
|
25
|
+
kind: 'Argument',
|
|
26
|
+
name: {
|
|
27
|
+
kind: 'Name',
|
|
28
|
+
value: 'name',
|
|
29
|
+
},
|
|
30
|
+
value: {
|
|
31
|
+
kind: 'StringValue',
|
|
32
|
+
value: 'recordQuery',
|
|
33
|
+
block: false,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const DIRECTIVE_PARENT_CATEGORY = {
|
|
40
|
+
kind: 'Directive',
|
|
41
|
+
name: {
|
|
42
|
+
kind: 'Name',
|
|
43
|
+
value: 'category',
|
|
44
|
+
},
|
|
45
|
+
arguments: [
|
|
46
|
+
{
|
|
47
|
+
kind: 'Argument',
|
|
48
|
+
name: {
|
|
49
|
+
kind: 'Name',
|
|
50
|
+
value: 'name',
|
|
51
|
+
},
|
|
52
|
+
value: {
|
|
53
|
+
kind: 'StringValue',
|
|
54
|
+
value: 'parentRelationship',
|
|
55
|
+
block: false,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function substituteDirectives(
|
|
62
|
+
directives: ReadonlyArray<DirectiveNode>,
|
|
63
|
+
index: number,
|
|
64
|
+
nodeName: string | undefined
|
|
65
|
+
): void {
|
|
66
|
+
if (directives[index].name.value === CUSTOM_DIRECTIVE_CONNECTION) {
|
|
67
|
+
// replace the custom directive node with the metaschema directive node
|
|
68
|
+
// @ts-ignore - Document is read only
|
|
69
|
+
directives[index] = DIRECTIVE_RECORD_CATEGORY;
|
|
70
|
+
} else if (directives[index].name.value === CUSTOM_DIRECTIVE_RESOURCE) {
|
|
71
|
+
// node gets its type from @category recordQuery
|
|
72
|
+
if (nodeName === 'node') {
|
|
73
|
+
// @ts-ignore - Document is read only
|
|
74
|
+
directives.splice(index, 1);
|
|
75
|
+
} else {
|
|
76
|
+
// @ts-ignore - Document is read only
|
|
77
|
+
directives[index] = DIRECTIVE_PARENT_CATEGORY;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Returns true if the directive node is of legacy type
|
|
84
|
+
* @param node : Directive node
|
|
85
|
+
* @returns
|
|
86
|
+
*/
|
|
87
|
+
function isCustomDirective(node: DirectiveNode): boolean {
|
|
88
|
+
return (
|
|
89
|
+
node.name.value === CUSTOM_DIRECTIVE_CONNECTION ||
|
|
90
|
+
node.name.value === CUSTOM_DIRECTIVE_RESOURCE
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type TraverseSelectionSetNode = SelectionSetNode | undefined;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Traverses a selection set and it's nested selections,
|
|
98
|
+
* to find any legacy custom directives and substitute them with metaschema directives
|
|
99
|
+
* @param node - SelectionSetNode
|
|
100
|
+
* @returns SelectionSetNode
|
|
101
|
+
*/
|
|
102
|
+
function traverseSelectionSet(node: TraverseSelectionSetNode): TraverseSelectionSetNode {
|
|
103
|
+
if (node === undefined) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const selection of node.selections) {
|
|
108
|
+
// FragmentSpreadNode doesn't have a selection set
|
|
109
|
+
// which should be handled at this methods entry point
|
|
110
|
+
const { directives, selectionSet } = selection as Exclude<
|
|
111
|
+
SelectionNode,
|
|
112
|
+
FragmentSpreadNode
|
|
113
|
+
>;
|
|
114
|
+
let selectionName: string | undefined;
|
|
115
|
+
|
|
116
|
+
if (selection.kind !== 'InlineFragment') {
|
|
117
|
+
selectionName = selection.name.value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (directives !== undefined && directives.length > 0) {
|
|
121
|
+
// we follow this pattern instead of map to preserve the order of directives
|
|
122
|
+
// order of directives may be significant as per graphql spec
|
|
123
|
+
const index = directives.findIndex(isCustomDirective);
|
|
124
|
+
|
|
125
|
+
if (index !== -1) {
|
|
126
|
+
substituteDirectives(directives, index, selectionName);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
traverseSelectionSet(selectionSet);
|
|
131
|
+
}
|
|
132
|
+
return node;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Accepts a document node and replaces the legacy custom directives with metaschema directives "in-place"
|
|
137
|
+
* @param doc
|
|
138
|
+
*/
|
|
139
|
+
export function metaschemaMapper(doc: DocumentNode): void {
|
|
140
|
+
// this method is only callable for Executable definitions
|
|
141
|
+
// such as Operations and Fragments
|
|
142
|
+
// so we have to explicitly cast the definitions for ts
|
|
143
|
+
const { definitions }: { definitions: unknown } = doc;
|
|
144
|
+
|
|
145
|
+
for (const def of definitions as ReadonlyArray<
|
|
146
|
+
Exclude<DefinitionNode, TypeSystemDefinitionNode | TypeSystemExtensionNode>
|
|
147
|
+
>) {
|
|
148
|
+
const { directives, selectionSet } = def;
|
|
149
|
+
|
|
150
|
+
if (directives !== undefined && directives.length > 0) {
|
|
151
|
+
// we are making an assumption here that only one custom directive can be applied to a node
|
|
152
|
+
// we can revisit if this condition changes
|
|
153
|
+
const index = directives.findIndex(isCustomDirective);
|
|
154
|
+
|
|
155
|
+
if (index !== -1) {
|
|
156
|
+
substituteDirectives(directives, index, undefined);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
traverseSelectionSet(selectionSet);
|
|
161
|
+
}
|
|
162
|
+
}
|