Skip to content

Commit 5574f2b

Browse files
committed
feat: fieldConfig validation moved from astToSchema() to directoryToAst() method.
BREAKING CHANGE: FieldConfig is now stored in AST.fieldConfig property and is used as mutable value (every ast transformer use this value and may change it).
1 parent 9bb5e2a commit 5574f2b

File tree

7 files changed

+93
-71
lines changed

7 files changed

+93
-71
lines changed

src/VisitInfo.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,10 @@ export class VisitInfo<TNode extends AstDirNode | AstFileNode | AstRootTypeNode,
133133
get fieldConfig(): FieldConfig {
134134
const node = this.node;
135135
if (node.kind === 'file') {
136-
return node.code?.default as FieldConfig;
136+
return node.fieldConfig;
137137
} else if (node.kind === 'dir' || this.node.kind === 'rootType') {
138-
return node.namespaceConfig?.code?.default as FieldConfig;
138+
// TODO: think about namespaceConfig (how to do it not null)
139+
return node.namespaceConfig?.fieldConfig as any;
139140
}
140141
throw new Error(`Cannot get fieldConfig. Node has some strange kind: ${node.kind}`);
141142
}

src/__tests__/VisitInfo-test.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ const nodeParent = {
1515
} as AstRootNode;
1616
const node = {
1717
absPath: 'schema/query/some_endpoint.ts',
18-
code: {
19-
default: {
20-
type: 'String',
21-
resolve: () => 'Hello!',
22-
},
23-
},
18+
code: {},
19+
fieldConfig: {
20+
type: 'String',
21+
resolve: () => 'Hello!',
22+
} as any,
2423
kind: 'file',
2524
name: 'some_endpoint',
2625
} as AstFileNode;
@@ -121,7 +120,7 @@ describe('VisitInfo', () => {
121120
operation: 'query',
122121
fieldName: 'ping',
123122
fieldPath: ['query.storage', 'viewer', 'utils.debug'],
124-
node,
123+
node: { ...node, fieldConfig: { ...node.fieldConfig } },
125124
nodeParent,
126125
schemaComposer,
127126
});

src/__tests__/__snapshots__/directoryToAst-test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ Object {
9393
"namespaceConfig": Object {
9494
"absPath": Any<String>,
9595
"code": Any<Object>,
96+
"fieldConfig": Any<Object>,
9697
"kind": "file",
9798
"name": "index",
9899
},

src/__tests__/directoryToAst-test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('directoryToAst()', () => {
3737
name: 'index',
3838
absPath: expect.any(String),
3939
code: expect.any(Object),
40+
fieldConfig: expect.any(Object),
4041
},
4142
},
4243
'some.nested': expect.objectContaining({ kind: 'file' }),

src/astToSchema.ts

Lines changed: 2 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@ import {
55
ObjectTypeComposerFieldConfig,
66
isOutputTypeDefinitionString,
77
isWrappedTypeNameString,
8-
isComposeOutputType,
9-
isSomeOutputTypeDefinitionString,
108
inspect,
11-
isFunction,
129
} from 'graphql-compose';
1310
import { AstRootNode, AstRootTypeNode, AstDirNode, AstFileNode } from './directoryToAst';
1411
import dedent from 'dedent';
1512
import { GraphQLObjectType } from 'graphql';
16-
import { FieldConfig, NamespaceConfig } from './typeDefs';
1713

1814
export interface AstToSchemaOptions {
1915
/**
@@ -109,7 +105,7 @@ export function createFields(
109105

110106
if (ast.kind === 'file') {
111107
parent.addNestedFields({
112-
[name]: prepareFieldConfig(ast) as FieldConfig,
108+
[name]: ast.fieldConfig,
113109
});
114110
return;
115111
}
@@ -171,18 +167,7 @@ function prepareNamespaceFieldConfig(
171167
ast: AstFileNode,
172168
typename: string
173169
): ObjectTypeComposerFieldConfig<any, any> {
174-
if (!ast.code.default) {
175-
throw new Error(dedent`
176-
NamespaceModule MUST return FieldConfig as default export in '${ast.absPath}'.
177-
Eg:
178-
export default {
179-
type: 'SomeObjectTypeName',
180-
resolve: () => ({}),
181-
};
182-
`);
183-
}
184-
185-
const fc: any = ast.code.default;
170+
const fc = ast.fieldConfig as any;
186171

187172
if (!fc.type) {
188173
fc.type = sc.createObjectTC(typename);
@@ -218,46 +203,3 @@ function prepareNamespaceFieldConfig(
218203

219204
return fc;
220205
}
221-
222-
function prepareFieldConfig(ast: AstFileNode): FieldConfig | NamespaceConfig {
223-
const fc = ast.code.default;
224-
225-
if (!fc) {
226-
throw new Error(dedent`
227-
Module MUST return FieldConfig as default export in '${ast.absPath}'.
228-
Eg:
229-
export default {
230-
type: 'String',
231-
resolve: () => Date.now(),
232-
};
233-
`);
234-
}
235-
236-
if (!fc.type || !isSomeOutputTypeDefinition(fc.type)) {
237-
throw new Error(dedent`
238-
Module MUST return FieldConfig with correct 'type: xxx' property in '${ast.absPath}'.
239-
Eg:
240-
export default {
241-
type: 'String'
242-
};
243-
`);
244-
}
245-
246-
return fc;
247-
}
248-
249-
function isSomeOutputTypeDefinition(type: any): boolean {
250-
if (typeof type === 'string') {
251-
// type: 'String'
252-
return isSomeOutputTypeDefinitionString(type) || isWrappedTypeNameString(type);
253-
} else if (Array.isArray(type)) {
254-
// type: ['String']
255-
return isSomeOutputTypeDefinition(type[0]);
256-
} else if (isFunction(type)) {
257-
// pass thunked type without internal checks
258-
return true;
259-
} else {
260-
// type: 'type User { name: String }'
261-
return isComposeOutputType(type);
262-
}
263-
}

src/directoryToAst.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import fs from 'fs';
2+
import {
3+
dedent,
4+
isComposeOutputType,
5+
isFunction,
6+
isSomeOutputTypeDefinitionString,
7+
isWrappedTypeNameString,
8+
Resolver,
9+
} from 'graphql-compose';
210
import { join, resolve, dirname, basename } from 'path';
311
import { FieldConfig, NamespaceConfig } from './typeDefs';
412

@@ -31,6 +39,12 @@ export interface AstFileNode extends AstBaseNode {
3139
code: {
3240
default?: FieldConfig | NamespaceConfig;
3341
};
42+
/**
43+
* This FieldConfig loaded from `code` and validated.
44+
* This property is used by ast transformers and stores the last version of modified config.
45+
* This value will be used by astToSchema method.
46+
*/
47+
fieldConfig: FieldConfig;
3448
}
3549

3650
export type RootTypeNames = 'query' | 'mutation' | 'subscription';
@@ -195,11 +209,16 @@ export function getAstForFile(
195209
if (absPath !== m.filename && checkInclusion(absPath, 'file', filename, options)) {
196210
// module name shouldn't include file extension
197211
const moduleName = filename.substring(0, filename.lastIndexOf('.'));
212+
// namespace configs may not have `type` property
213+
const checkType = moduleName !== 'index';
214+
const code = m.require(absPath);
215+
const fieldConfig = prepareFieldConfig(code, absPath, checkType);
198216
return {
199217
kind: 'file',
200218
name: moduleName,
201219
absPath,
202-
code: m.require(absPath),
220+
code,
221+
fieldConfig,
203222
};
204223
}
205224
}
@@ -258,3 +277,62 @@ function checkInclusion(
258277

259278
return true;
260279
}
280+
281+
function prepareFieldConfig(code: any, absPath: string, checkType = true): FieldConfig {
282+
const _fc = code?.default;
283+
if (!_fc || typeof _fc !== 'object') {
284+
throw new Error(dedent`
285+
GraphQL entrypoint MUST return FieldConfig as default export in '${absPath}'.
286+
Eg:
287+
export default {
288+
type: 'String',
289+
resolve: () => Date.now(),
290+
};
291+
`);
292+
}
293+
294+
let fc: FieldConfig;
295+
if (code.default instanceof Resolver) {
296+
fc = (code.default as Resolver).getFieldConfig() as any;
297+
} else {
298+
// recreate object for immutability purposes (do not change object in module definition)
299+
// NB. I don't know should we here recreate (args, extensions) but let's keep them as is for now.
300+
fc = { ...code.default };
301+
}
302+
303+
if (checkType) {
304+
if (!fc.type || !isSomeOutputTypeDefinition(fc.type)) {
305+
throw new Error(dedent`
306+
Module MUST return FieldConfig with correct 'type: xxx' property in '${absPath}'.
307+
Eg:
308+
export default {
309+
type: 'String'
310+
};
311+
`);
312+
}
313+
}
314+
315+
if (fc.resolve && typeof fc.resolve !== 'function') {
316+
throw new Error(
317+
`Cannot load entrypoint config from ${absPath}. 'resolve' property must be a function or undefined.`
318+
);
319+
}
320+
321+
return fc;
322+
}
323+
324+
function isSomeOutputTypeDefinition(type: any): boolean {
325+
if (typeof type === 'string') {
326+
// type: 'String'
327+
return isSomeOutputTypeDefinitionString(type) || isWrappedTypeNameString(type);
328+
} else if (Array.isArray(type)) {
329+
// type: ['String']
330+
return isSomeOutputTypeDefinition(type[0]);
331+
} else if (isFunction(type)) {
332+
// pass thunked type without internal checks
333+
return true;
334+
} else {
335+
// type: 'type User { name: String }'
336+
return isComposeOutputType(type);
337+
}
338+
}

src/testHelpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function _getArgsForQuery(
5252
varNames.forEach((varName) => {
5353
if (!argNames.includes(varName)) {
5454
throw new Error(
55-
`FieldConfig does not have '${varName}' argument. Avaliable arguments: '${argNames.join(
55+
`FieldConfig does not have '${varName}' argument. Available arguments: '${argNames.join(
5656
"', '"
5757
)}'.`
5858
);

0 commit comments

Comments
 (0)