Skip to content

Commit 499a72e

Browse files
committed
fix: nested typenames & code refactor
1 parent 69929fa commit 499a72e

File tree

4 files changed

+240
-208
lines changed

4 files changed

+240
-208
lines changed

src/requireAstToSchema.ts renamed to src/astToSchema.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,16 @@ import {
99
isSomeOutputTypeDefinitionString,
1010
inspect,
1111
} from 'graphql-compose';
12-
import {
13-
RequireAstResult,
14-
RequireAstRootTypeNode,
15-
RequireAstDirNode,
16-
RequireAstFileNode,
17-
} from './requireSchemaDirectory';
12+
import { AstResult, AstRootTypeNode, AstDirNode, AstFileNode } from './directoryToAst';
1813
import dedent from 'dedent';
1914
import { GraphQLObjectType } from 'graphql';
2015

2116
export interface AstOptions {
2217
schemaComposer?: SchemaComposer<any>;
2318
}
2419

25-
export function requireAstToSchema<TContext = any>(
26-
ast: RequireAstResult,
20+
export function astToSchema<TContext = any>(
21+
ast: AstResult,
2722
opts: AstOptions = {}
2823
): SchemaComposer<TContext> {
2924
let sc: SchemaComposer<any>;
@@ -51,20 +46,22 @@ export function requireAstToSchema<TContext = any>(
5146
function populateRoot(
5247
sc: SchemaComposer<any>,
5348
rootName: 'Query' | 'Mutation' | 'Subscription',
54-
astRootNode: RequireAstRootTypeNode
49+
astRootNode: AstRootTypeNode
5550
) {
5651
const tc = sc[rootName];
5752
Object.keys(astRootNode.children).forEach((key) => {
5853
createFields(sc, astRootNode.children[key], rootName, tc);
5954
});
6055
}
6156

62-
function createFields(
57+
export function createFields(
6358
sc: SchemaComposer<any>,
64-
ast: RequireAstDirNode | RequireAstFileNode,
59+
ast: AstDirNode | AstFileNode | void,
6560
prefix: string,
6661
parent: ObjectTypeComposer
6762
): void {
63+
if (!ast) return;
64+
6865
const name = ast.name;
6966
if (!/^[._a-zA-Z0-9]+$/.test(name)) {
7067
throw new Error(
@@ -73,14 +70,13 @@ function createFields(
7370
} name '${name}', it should meet RegExp(/^[._a-zA-Z0-9]+$/) for '${ast.absPath}'`
7471
);
7572
}
76-
const typename = getTypename(ast);
7773

7874
if (ast.kind === 'file') {
7975
if (name !== 'index') {
8076
if (name.endsWith('.index')) {
8177
const fieldName = name.slice(0, -6); // remove ".index" from field name
8278
parent.addNestedFields({
83-
[fieldName]: prepareNamespaceFieldConfig(sc, ast, prefix, typename),
79+
[fieldName]: prepareNamespaceFieldConfig(sc, ast, `${prefix}${getTypename(ast)}`),
8480
});
8581
} else {
8682
parent.addNestedFields({
@@ -92,11 +88,12 @@ function createFields(
9288
}
9389

9490
if (ast.kind === 'dir') {
91+
const typename = `${prefix}${getTypename(ast)}`;
9592
let fc: ObjectTypeComposerFieldConfig<any, any>;
9693
if (ast.children['index'] && ast.children['index'].kind === 'file') {
97-
fc = prepareNamespaceFieldConfig(sc, ast.children['index'], prefix, typename);
94+
fc = prepareNamespaceFieldConfig(sc, ast.children['index'], typename);
9895
} else {
99-
fc = { type: sc.createObjectTC(`${prefix}${typename}`) };
96+
fc = { type: sc.createObjectTC(typename) };
10097
}
10198

10299
parent.addNestedFields({
@@ -107,12 +104,12 @@ function createFields(
107104
});
108105

109106
Object.keys(ast.children).forEach((key) => {
110-
createFields(sc, ast.children[key], name, fc.type as any);
107+
createFields(sc, ast.children[key], typename, fc.type as any);
111108
});
112109
}
113110
}
114111

115-
function getTypename(ast: RequireAstDirNode | RequireAstFileNode): string {
112+
function getTypename(ast: AstDirNode | AstFileNode): string {
116113
const name = ast.name;
117114

118115
if (name.indexOf('.') !== -1) {
@@ -134,8 +131,7 @@ function getTypename(ast: RequireAstDirNode | RequireAstFileNode): string {
134131

135132
function prepareNamespaceFieldConfig(
136133
sc: SchemaComposer<any>,
137-
ast: RequireAstFileNode,
138-
prefix: string,
134+
ast: AstFileNode,
139135
typename: string
140136
): ObjectTypeComposerFieldConfig<any, any> {
141137
if (!ast.code.default) {
@@ -152,7 +148,7 @@ function prepareNamespaceFieldConfig(
152148
const fc: any = ast.code.default;
153149

154150
if (!fc.type) {
155-
fc.type = sc.createObjectTC(`${prefix}${typename}`);
151+
fc.type = sc.createObjectTC(typename);
156152
} else {
157153
if (typeof fc.type === 'string') {
158154
if (!isOutputTypeDefinitionString(fc.type) && !isTypeNameString(fc.type)) {
@@ -188,7 +184,7 @@ function prepareNamespaceFieldConfig(
188184

189185
function prepareFieldConfig(
190186
sc: SchemaComposer<any>,
191-
ast: RequireAstFileNode
187+
ast: AstFileNode
192188
): ObjectTypeComposerFieldConfig<any, any> {
193189
const fc = ast.code.default as any;
194190

src/directoryToAst.ts

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import fs from 'fs';
2+
import { join, resolve, dirname, basename } from 'path';
3+
4+
export interface Options {
5+
relativePath?: string;
6+
extensions?: string[];
7+
include?: RegExp | ((path: string, kind: 'dir' | 'file', filename: string) => boolean);
8+
exclude?: RegExp | ((path: string, kind: 'dir' | 'file', filename: string) => boolean);
9+
}
10+
11+
type AstNodeKinds = 'rootType' | 'dir' | 'file';
12+
13+
interface AstBaseNode {
14+
kind: AstNodeKinds;
15+
name: string;
16+
absPath: string;
17+
}
18+
19+
export interface AstRootTypeNode extends AstBaseNode {
20+
kind: 'rootType';
21+
children: {
22+
[key: string]: AstDirNode | AstFileNode;
23+
};
24+
}
25+
26+
export interface AstDirNode extends AstBaseNode {
27+
kind: 'dir';
28+
children: {
29+
[key: string]: AstDirNode | AstFileNode;
30+
};
31+
}
32+
33+
export interface AstFileNode extends AstBaseNode {
34+
kind: 'file';
35+
code: {
36+
default?: any;
37+
};
38+
}
39+
40+
export interface AstResult {
41+
query?: AstRootTypeNode;
42+
mutation?: AstRootTypeNode;
43+
subscription?: AstRootTypeNode;
44+
}
45+
46+
export const defaultOptions: Options = {
47+
extensions: ['js', 'ts'],
48+
};
49+
50+
export function directoryToAst(m: NodeModule, options: Options = defaultOptions): AstResult {
51+
// if no path was passed in, assume the equivelant of __dirname from caller
52+
// otherwise, resolve path relative to the equivalent of __dirname
53+
const schemaPath = options?.relativePath
54+
? resolve(dirname(m.filename), options.relativePath)
55+
: dirname(m.filename);
56+
57+
// setup default options
58+
Object.keys(defaultOptions).forEach((prop) => {
59+
if (typeof (options as any)[prop] === 'undefined') {
60+
(options as any)[prop] = (defaultOptions as any)[prop];
61+
}
62+
});
63+
64+
const result = {} as AstResult;
65+
66+
fs.readdirSync(schemaPath).forEach((filename) => {
67+
const absPath = join(schemaPath, filename);
68+
69+
if (fs.statSync(absPath).isDirectory()) {
70+
const dirName = filename;
71+
const re = /^(query|mutation|subscription)(\.(.*))?$/i;
72+
const found = dirName.match(re);
73+
if (found) {
74+
const opType = found[1].toLowerCase() as keyof AstResult;
75+
let rootTypeAst = result[opType];
76+
if (!rootTypeAst)
77+
rootTypeAst = {
78+
kind: 'rootType',
79+
name: opType,
80+
absPath,
81+
children: {},
82+
} as AstRootTypeNode;
83+
84+
const astDir = getAstForDir(m, absPath, options);
85+
if (astDir) {
86+
const subField = found[3]; // any part after dot (eg for `query.me` will be `me`)
87+
if (subField) {
88+
rootTypeAst.children[subField] = {
89+
...astDir,
90+
name: subField,
91+
absPath,
92+
};
93+
} else {
94+
rootTypeAst.children = astDir.children;
95+
}
96+
result[opType] = rootTypeAst;
97+
}
98+
}
99+
}
100+
});
101+
102+
return result;
103+
}
104+
105+
export function getAstForDir(
106+
m: NodeModule,
107+
absPath: string,
108+
options: Options = defaultOptions
109+
): AstDirNode | void {
110+
const name = basename(absPath);
111+
112+
if (!checkInclusion(absPath, 'dir', name, options)) return;
113+
114+
const result: AstDirNode = {
115+
kind: 'dir',
116+
absPath,
117+
name,
118+
children: {},
119+
};
120+
121+
// get the path of each file in specified directory, append to current tree node, recurse
122+
fs.readdirSync(absPath).forEach((filename) => {
123+
const absFilePath = join(absPath, filename);
124+
125+
const stat = fs.statSync(absFilePath);
126+
if (stat.isDirectory()) {
127+
// this node is a directory; recurse
128+
if (result.children[filename]) {
129+
throw new Error(
130+
`You have a folder and file with same name "${filename}" by the following path ${absPath}. Please remove one of them.`
131+
);
132+
}
133+
const astDir = getAstForDir(m, absFilePath, options);
134+
if (astDir) {
135+
result.children[filename] = astDir;
136+
}
137+
} else if (stat.isFile()) {
138+
// this node is a file
139+
const fileAst = getAstForFile(m, absFilePath, options);
140+
if (fileAst) {
141+
if (result.children[fileAst.name]) {
142+
throw new Error(
143+
`You have a folder and file with same name "${fileAst.name}" by the following path ${absPath}. Please remove one of them.`
144+
);
145+
} else {
146+
result.children[fileAst.name] = fileAst;
147+
}
148+
}
149+
}
150+
});
151+
152+
return result;
153+
}
154+
155+
export function getAstForFile(
156+
m: NodeModule,
157+
absPath: string,
158+
options: Options = defaultOptions
159+
): AstFileNode | void {
160+
const filename = basename(absPath);
161+
if (absPath !== m.filename && checkInclusion(absPath, 'file', filename, options)) {
162+
// hash node key shouldn't include file extension
163+
const moduleName = filename.substring(0, filename.lastIndexOf('.'));
164+
return {
165+
kind: 'file',
166+
name: moduleName,
167+
absPath,
168+
code: m.require(absPath),
169+
};
170+
}
171+
}
172+
173+
function checkInclusion(
174+
absPath: string,
175+
kind: 'dir' | 'file',
176+
filename: string,
177+
options: Options
178+
): boolean {
179+
// Skip dir/files started from double underscore
180+
if (/^__.*/i.test(filename)) {
181+
return false;
182+
}
183+
184+
if (kind === 'file') {
185+
if (
186+
// Verify file has valid extension
187+
!new RegExp('\\.(' + (options?.extensions || ['js', 'ts']).join('|') + ')$', 'i').test(
188+
filename
189+
) ||
190+
// Hardcoded skip file extensions
191+
new RegExp('(\\.d\\.ts)$', 'i').test(filename)
192+
)
193+
return false;
194+
}
195+
196+
if (options.include) {
197+
if (options.include instanceof RegExp) {
198+
// if options.include is a RegExp, evaluate it and make sure the path passes
199+
if (!options.include.test(absPath)) return false;
200+
} else if (typeof options.include === 'function') {
201+
// if options.include is a function, evaluate it and make sure the path passes
202+
if (!options.include(absPath, kind, filename)) return false;
203+
}
204+
}
205+
206+
if (options.exclude) {
207+
if (options.exclude instanceof RegExp) {
208+
// if options.exclude is a RegExp, evaluate it and make sure the path doesn't pass
209+
if (options.exclude.test(absPath)) return false;
210+
} else if (typeof options.exclude === 'function') {
211+
// if options.exclude is a function, evaluate it and make sure the path doesn't pass
212+
if (options.exclude(absPath, kind, filename)) return false;
213+
}
214+
}
215+
216+
return true;
217+
}

src/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { requireSchemaDirectory, RequireOptions } from './requireSchemaDirectory';
2-
import { requireAstToSchema, AstOptions } from './requireAstToSchema';
1+
import { directoryToAst, Options } from './directoryToAst';
2+
import { astToSchema, AstOptions } from './astToSchema';
33

4-
export interface BuildOptions extends RequireOptions, AstOptions {}
4+
export interface BuildOptions extends Options, AstOptions {}
55

66
export function buildSchema(module: NodeModule, opts: BuildOptions = {}) {
77
return loadSchemaComposer(module, opts).buildSchema();
88
}
99

1010
export function loadSchemaComposer(module: NodeModule, opts: BuildOptions) {
11-
const ast = requireSchemaDirectory(module, opts);
12-
const sc = requireAstToSchema(ast, opts);
11+
const ast = directoryToAst(module, opts);
12+
const sc = astToSchema(ast, opts);
1313
return sc;
1414
}
1515

16-
export { requireSchemaDirectory, requireAstToSchema };
16+
export { directoryToAst, astToSchema };

0 commit comments

Comments
 (0)