Skip to content

Commit 07b0047

Browse files
committed
refactor: some code restructuring
1 parent 2e01225 commit 07b0047

11 files changed

+1050
-907
lines changed

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"@typescript-eslint/explicit-member-accessibility": 0,
3535
"@typescript-eslint/no-explicit-any": 0,
3636
"@typescript-eslint/no-inferrable-types": 0,
37-
"@typescript-eslint/explicit-function-return-type": 0
37+
"@typescript-eslint/explicit-function-return-type": 0,
38+
"@typescript-eslint/no-use-before-define": 0
3839
},
3940
"env": {
4041
"jasmine": true,

package.json

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,24 @@
1010
"lib"
1111
],
1212
"peerDependencies": {
13-
"graphql-compose": "^7.0.0"
13+
"graphql-compose": "^7.6.0"
1414
},
1515
"devDependencies": {
1616
"@types/glob": "7.1.1",
1717
"@types/jest": "24.0.23",
1818
"@types/node": "12.12.11",
1919
"@typescript-eslint/eslint-plugin": "2.8.0",
2020
"@typescript-eslint/parser": "2.8.0",
21-
"apollo-server-express": "^2.4.8",
21+
"apollo-server": "^2.9.12",
2222
"eslint": "6.6.0",
2323
"eslint-config-prettier": "6.7.0",
2424
"eslint-plugin-prettier": "3.1.1",
2525
"graphql": "^14.0.0",
26-
"graphql-compose": "^7.4.3",
26+
"graphql-compose": "^7.6.0",
2727
"jest": "24.9.0",
2828
"prettier": "1.19.1",
2929
"rimraf": "3.0.0",
30-
"ts-jest": "24.1.0",
30+
"ts-jest": "24.2.0",
3131
"typescript": "3.7.2"
3232
},
3333
"scripts": {
@@ -37,7 +37,10 @@
3737
"lint": "eslint 'src/**/*.{js,ts}'",
3838
"test": "npm run lint && npm run tscheck && npm run coverage",
3939
"tscheck": "tsc --noEmit",
40-
"start-example": "cd ./examples/forTests && yarn install && yarn watch"
40+
"start-example1": "cd ./examples/simple && yarn install && yarn watch",
41+
"start-example2": "cd ./examples/forTests && yarn install && yarn watch"
4142
},
42-
"dependencies": {}
43+
"dependencies": {
44+
"dedent": "^0.7.0"
45+
}
4346
}

src/__tests__/__snapshots__/requireToSchema-test.ts.snap renamed to src/__tests__/__snapshots__/requireAstToSchema-test.ts.snap

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`requireToSchema() Schema ../../examples/forTests/schema schema 1`] = `
3+
exports[`requireAstToSchema() Schema ../../examples/forTests/schema schema 1`] = `
44
"# Provides default value for input field.
55
directive @default(value: JSON!) on INPUT_FIELD_DEFINITION
66
@@ -43,6 +43,26 @@ type MutationUser {
4343
4444
type Query {
4545
me: MeType
46+
some: QuerySome
47+
user: UserAwesomeType
48+
}
49+
50+
type QuerySome {
51+
type: SomeIndexFileType
52+
}
53+
54+
type SomeIndexFileType {
55+
awesomeValue: String
56+
}
57+
58+
type UserAwesomeType {
59+
firstName: String
60+
lastName: String
61+
extendedData: UserExtendedData
62+
}
63+
64+
type UserExtendedData {
65+
starsCount: Int
4666
}
4767
"
4868
`;

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,16 @@ Object {
8181
"kind": "dir",
8282
"name": "me",
8383
},
84+
"some.type.index": Any<Object>,
85+
"user": Object {
86+
"absPath": Any<String>,
87+
"children": Object {
88+
"extendedData": Any<Object>,
89+
"index": Any<Object>,
90+
},
91+
"kind": "dir",
92+
"name": "user",
93+
},
8494
},
8595
"kind": "rootType",
8696
"name": "query",

src/__tests__/requireToSchema-test.ts renamed to src/__tests__/requireAstToSchema-test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { requireSchemaDirectory } from '../requireSchemaDirectory';
2-
import { requireToSchema } from '../requireToSchema';
2+
import { requireAstToSchema } from '../requireAstToSchema';
33
import { SchemaComposer } from 'graphql-compose';
44
import { printSchema } from 'graphql/utilities';
55

6-
describe('requireToSchema()', () => {
6+
describe('requireAstToSchema()', () => {
77
describe('Schema ../../examples/forTests/schema', () => {
88
const ast = requireSchemaDirectory(module, '../../examples/forTests/schema');
9-
const sc = requireToSchema(ast);
9+
const sc = requireAstToSchema(ast);
1010

1111
it('should return schema composer', () => {
1212
expect(sc).toBeInstanceOf(SchemaComposer);

src/__tests__/requireSchemaDirectory-test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ describe('requireSchemaDirectory()', () => {
2929
},
3030
},
3131
index: { kind: 'file', absPath: expect.any(String), code: expect.any(Object) },
32+
'some.type.index': expect.any(Object),
33+
user: {
34+
absPath: expect.any(String),
35+
children: {
36+
extendedData: expect.any(Object),
37+
index: expect.any(Object),
38+
},
39+
},
3240
},
3341
});
3442
});

src/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,14 @@
1-
export { requireSchemaDirectory } from './requireSchemaDirectory';
1+
import { requireSchemaDirectory } from './requireSchemaDirectory';
2+
import { requireAstToSchema } from './requireAstToSchema';
3+
4+
export function buildSchema(module: NodeModule) {
5+
return loadSchemaComposer(module).buildSchema();
6+
}
7+
8+
export function loadSchemaComposer(module: NodeModule) {
9+
const ast = requireSchemaDirectory(module);
10+
const sc = requireAstToSchema(ast);
11+
return sc;
12+
}
13+
14+
export { requireSchemaDirectory, requireAstToSchema };

src/requireAstToSchema.ts

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import {
2+
SchemaComposer,
3+
ObjectTypeComposer,
4+
upperFirst,
5+
ObjectTypeComposerFieldConfig,
6+
isOutputTypeDefinitionString,
7+
isTypeNameString,
8+
isComposeOutputType,
9+
isSomeOutputTypeDefinitionString,
10+
inspect,
11+
} from 'graphql-compose';
12+
import {
13+
RequireAstResult,
14+
RequireAstRootTypeNode,
15+
RequireAstDirNode,
16+
RequireAstFileNode,
17+
} from './requireSchemaDirectory';
18+
import dedent from 'dedent';
19+
import { GraphQLObjectType } from 'graphql';
20+
21+
export function requireAstToSchema<TContext = any>(
22+
ast: RequireAstResult,
23+
schemaComposer?: SchemaComposer<TContext>
24+
): SchemaComposer<TContext> {
25+
const sc = schemaComposer || new SchemaComposer<TContext>();
26+
27+
if (ast.query) populateRoot(sc, 'Query', ast.query);
28+
if (ast.mutation) populateRoot(sc, 'Mutation', ast.mutation);
29+
if (ast.subscription) populateRoot(sc, 'Subscription', ast.subscription);
30+
31+
return sc;
32+
}
33+
34+
function populateRoot(
35+
sc: SchemaComposer<any>,
36+
rootName: 'Query' | 'Mutation' | 'Subscription',
37+
astRootNode: RequireAstRootTypeNode
38+
) {
39+
const tc = sc[rootName];
40+
Object.keys(astRootNode.children).forEach((key) => {
41+
createFields(sc, astRootNode.children[key], rootName, tc);
42+
});
43+
}
44+
45+
function createFields(
46+
sc: SchemaComposer<any>,
47+
ast: RequireAstDirNode | RequireAstFileNode,
48+
prefix: string,
49+
parent: ObjectTypeComposer
50+
): void {
51+
const name = ast.name;
52+
if (!/^[._a-zA-Z0-9]+$/.test(name)) {
53+
throw new Error(
54+
`You provide incorrect field name '${name}'. It should meet RegExp([._a-zA-Z0-9]+) for '${ast.absPath}'`
55+
);
56+
}
57+
const typename = getTypename(ast);
58+
59+
if (ast.kind === 'file') {
60+
if (name !== 'index') {
61+
if (name.endsWith('.index')) {
62+
const fieldName = name.slice(0, -6); // remove ".index" from field name
63+
parent.addNestedFields({
64+
[fieldName]: prepareNamespaceFieldConfig(sc, ast, prefix, typename),
65+
});
66+
} else {
67+
parent.addNestedFields({
68+
[name]: prepareFieldConfig(sc, ast),
69+
});
70+
}
71+
}
72+
return;
73+
}
74+
75+
if (ast.kind === 'dir') {
76+
let fc: ObjectTypeComposerFieldConfig<any, any>;
77+
if (ast.children['index'] && ast.children['index'].kind === 'file') {
78+
fc = prepareNamespaceFieldConfig(sc, ast.children['index'], prefix, typename);
79+
} else {
80+
fc = { type: sc.createObjectTC(`${prefix}${typename}`) };
81+
}
82+
83+
parent.addNestedFields({
84+
[name]: {
85+
resolve: () => ({}),
86+
...fc,
87+
},
88+
});
89+
90+
Object.keys(ast.children).forEach((key) => {
91+
createFields(sc, ast.children[key], name, fc.type as any);
92+
});
93+
}
94+
}
95+
96+
function getTypename(ast: RequireAstDirNode | RequireAstFileNode): string {
97+
const name = ast.name;
98+
99+
if (name.indexOf('.') !== -1) {
100+
const namesArray = name.split('.');
101+
102+
if (namesArray.some((n) => !n)) {
103+
throw new Error(
104+
`Field name '${ast.name}' contains dots in the wrong place for '${ast.absPath}'!`
105+
);
106+
}
107+
108+
return namesArray.reduce((prev, current) => {
109+
return prev + upperFirst(current);
110+
}, '');
111+
} else {
112+
return upperFirst(name);
113+
}
114+
}
115+
116+
function prepareNamespaceFieldConfig(
117+
sc: SchemaComposer<any>,
118+
ast: RequireAstFileNode,
119+
prefix: string,
120+
typename: string
121+
): ObjectTypeComposerFieldConfig<any, any> {
122+
if (!ast.code.default) {
123+
throw new Error(dedent`
124+
NamespaceModule MUST return FieldConfig as default export in '${ast.absPath}'.
125+
Eg:
126+
export default {
127+
type: 'SomeObjectTypeName',
128+
resolve: () => Date.now(),
129+
};
130+
`);
131+
}
132+
133+
const fc: any = ast.code.default;
134+
135+
if (!fc.type) {
136+
fc.type = sc.createObjectTC(`${prefix}${typename}`);
137+
} else {
138+
if (typeof fc.type === 'string') {
139+
if (!isOutputTypeDefinitionString(fc.type) && !isTypeNameString(fc.type)) {
140+
throw new Error(dedent`
141+
You provide incorrect output type definition:
142+
${fc.type}
143+
It must be valid TypeName or output type SDL definition:
144+
145+
Eg.
146+
type Payload { me: String }
147+
OR
148+
Payload
149+
`);
150+
}
151+
} else if (
152+
!(fc.type instanceof ObjectTypeComposer) &&
153+
!(fc.type instanceof GraphQLObjectType)
154+
) {
155+
throw new Error(dedent`
156+
You provide some strange value as 'type':
157+
${inspect(fc.type)}
158+
`);
159+
}
160+
fc.type = sc.createObjectTC(fc.type);
161+
}
162+
163+
if (!fc.resolve) {
164+
fc.resolve = () => ({});
165+
}
166+
167+
return fc;
168+
}
169+
170+
function prepareFieldConfig(
171+
sc: SchemaComposer<any>,
172+
ast: RequireAstFileNode
173+
): ObjectTypeComposerFieldConfig<any, any> {
174+
const fc = ast.code.default as any;
175+
176+
if (!fc) {
177+
throw new Error(dedent`
178+
Module MUST return FieldConfig as default export in '${ast.absPath}'.
179+
Eg:
180+
export default {
181+
type: 'String',
182+
resolve: () => Date.now(),
183+
};
184+
`);
185+
}
186+
187+
if (!fc.type || !isSomeOutputTypeDefinition(fc.type)) {
188+
throw new Error(dedent`
189+
Module MUST return FieldConfig with TYPE property in '${ast.absPath}'.
190+
Eg:
191+
export default {
192+
type: 'String'
193+
};
194+
`);
195+
}
196+
197+
return fc;
198+
}
199+
200+
function isSomeOutputTypeDefinition(type: any): boolean {
201+
return (
202+
(typeof type === 'string' &&
203+
(isSomeOutputTypeDefinitionString(type) || isTypeNameString(type))) ||
204+
isComposeOutputType(type)
205+
);
206+
}

src/requireSchemaDirectory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export interface RequireAstDirNode extends RequireAstBaseNode {
3232
export interface RequireAstFileNode extends RequireAstBaseNode {
3333
kind: 'file';
3434
code: {
35-
default?: Object;
35+
default?: any;
3636
};
3737
}
3838

@@ -48,7 +48,7 @@ export const defaultOptions = {
4848

4949
export function requireSchemaDirectory(
5050
m: NodeModule,
51-
path: string,
51+
path?: string,
5252
options: Options = defaultOptions
5353
): RequireAstResult {
5454
// if no path was passed in, assume the equivelant of __dirname from caller

0 commit comments

Comments
 (0)