Skip to content

Commit

Permalink
fix: nested typenames & code refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
nodkz committed Dec 21, 2019
1 parent 69929fa commit 499a72e
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 208 deletions.
38 changes: 17 additions & 21 deletions src/requireAstToSchema.ts → src/astToSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,16 @@ import {
isSomeOutputTypeDefinitionString,
inspect,
} from 'graphql-compose';
import {
RequireAstResult,
RequireAstRootTypeNode,
RequireAstDirNode,
RequireAstFileNode,
} from './requireSchemaDirectory';
import { AstResult, AstRootTypeNode, AstDirNode, AstFileNode } from './directoryToAst';
import dedent from 'dedent';
import { GraphQLObjectType } from 'graphql';

export interface AstOptions {
schemaComposer?: SchemaComposer<any>;
}

export function requireAstToSchema<TContext = any>(
ast: RequireAstResult,
export function astToSchema<TContext = any>(
ast: AstResult,
opts: AstOptions = {}
): SchemaComposer<TContext> {
let sc: SchemaComposer<any>;
Expand Down Expand Up @@ -51,20 +46,22 @@ export function requireAstToSchema<TContext = any>(
function populateRoot(
sc: SchemaComposer<any>,
rootName: 'Query' | 'Mutation' | 'Subscription',
astRootNode: RequireAstRootTypeNode
astRootNode: AstRootTypeNode
) {
const tc = sc[rootName];
Object.keys(astRootNode.children).forEach((key) => {
createFields(sc, astRootNode.children[key], rootName, tc);
});
}

function createFields(
export function createFields(
sc: SchemaComposer<any>,
ast: RequireAstDirNode | RequireAstFileNode,
ast: AstDirNode | AstFileNode | void,
prefix: string,
parent: ObjectTypeComposer
): void {
if (!ast) return;

const name = ast.name;
if (!/^[._a-zA-Z0-9]+$/.test(name)) {
throw new Error(
Expand All @@ -73,14 +70,13 @@ function createFields(
} name '${name}', it should meet RegExp(/^[._a-zA-Z0-9]+$/) for '${ast.absPath}'`
);
}
const typename = getTypename(ast);

if (ast.kind === 'file') {
if (name !== 'index') {
if (name.endsWith('.index')) {
const fieldName = name.slice(0, -6); // remove ".index" from field name
parent.addNestedFields({
[fieldName]: prepareNamespaceFieldConfig(sc, ast, prefix, typename),
[fieldName]: prepareNamespaceFieldConfig(sc, ast, `${prefix}${getTypename(ast)}`),
});
} else {
parent.addNestedFields({
Expand All @@ -92,11 +88,12 @@ function createFields(
}

if (ast.kind === 'dir') {
const typename = `${prefix}${getTypename(ast)}`;
let fc: ObjectTypeComposerFieldConfig<any, any>;
if (ast.children['index'] && ast.children['index'].kind === 'file') {
fc = prepareNamespaceFieldConfig(sc, ast.children['index'], prefix, typename);
fc = prepareNamespaceFieldConfig(sc, ast.children['index'], typename);
} else {
fc = { type: sc.createObjectTC(`${prefix}${typename}`) };
fc = { type: sc.createObjectTC(typename) };
}

parent.addNestedFields({
Expand All @@ -107,12 +104,12 @@ function createFields(
});

Object.keys(ast.children).forEach((key) => {
createFields(sc, ast.children[key], name, fc.type as any);
createFields(sc, ast.children[key], typename, fc.type as any);
});
}
}

function getTypename(ast: RequireAstDirNode | RequireAstFileNode): string {
function getTypename(ast: AstDirNode | AstFileNode): string {
const name = ast.name;

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

function prepareNamespaceFieldConfig(
sc: SchemaComposer<any>,
ast: RequireAstFileNode,
prefix: string,
ast: AstFileNode,
typename: string
): ObjectTypeComposerFieldConfig<any, any> {
if (!ast.code.default) {
Expand All @@ -152,7 +148,7 @@ function prepareNamespaceFieldConfig(
const fc: any = ast.code.default;

if (!fc.type) {
fc.type = sc.createObjectTC(`${prefix}${typename}`);
fc.type = sc.createObjectTC(typename);
} else {
if (typeof fc.type === 'string') {
if (!isOutputTypeDefinitionString(fc.type) && !isTypeNameString(fc.type)) {
Expand Down Expand Up @@ -188,7 +184,7 @@ function prepareNamespaceFieldConfig(

function prepareFieldConfig(
sc: SchemaComposer<any>,
ast: RequireAstFileNode
ast: AstFileNode
): ObjectTypeComposerFieldConfig<any, any> {
const fc = ast.code.default as any;

Expand Down
217 changes: 217 additions & 0 deletions src/directoryToAst.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import fs from 'fs';
import { join, resolve, dirname, basename } from 'path';

export interface Options {
relativePath?: string;
extensions?: string[];
include?: RegExp | ((path: string, kind: 'dir' | 'file', filename: string) => boolean);
exclude?: RegExp | ((path: string, kind: 'dir' | 'file', filename: string) => boolean);
}

type AstNodeKinds = 'rootType' | 'dir' | 'file';

interface AstBaseNode {
kind: AstNodeKinds;
name: string;
absPath: string;
}

export interface AstRootTypeNode extends AstBaseNode {
kind: 'rootType';
children: {
[key: string]: AstDirNode | AstFileNode;
};
}

export interface AstDirNode extends AstBaseNode {
kind: 'dir';
children: {
[key: string]: AstDirNode | AstFileNode;
};
}

export interface AstFileNode extends AstBaseNode {
kind: 'file';
code: {
default?: any;
};
}

export interface AstResult {
query?: AstRootTypeNode;
mutation?: AstRootTypeNode;
subscription?: AstRootTypeNode;
}

export const defaultOptions: Options = {
extensions: ['js', 'ts'],
};

export function directoryToAst(m: NodeModule, options: Options = defaultOptions): AstResult {
// if no path was passed in, assume the equivelant of __dirname from caller
// otherwise, resolve path relative to the equivalent of __dirname
const schemaPath = options?.relativePath
? resolve(dirname(m.filename), options.relativePath)
: dirname(m.filename);

// setup default options
Object.keys(defaultOptions).forEach((prop) => {
if (typeof (options as any)[prop] === 'undefined') {
(options as any)[prop] = (defaultOptions as any)[prop];
}
});

const result = {} as AstResult;

fs.readdirSync(schemaPath).forEach((filename) => {
const absPath = join(schemaPath, filename);

if (fs.statSync(absPath).isDirectory()) {
const dirName = filename;
const re = /^(query|mutation|subscription)(\.(.*))?$/i;
const found = dirName.match(re);
if (found) {
const opType = found[1].toLowerCase() as keyof AstResult;
let rootTypeAst = result[opType];
if (!rootTypeAst)
rootTypeAst = {
kind: 'rootType',
name: opType,
absPath,
children: {},
} as AstRootTypeNode;

const astDir = getAstForDir(m, absPath, options);
if (astDir) {
const subField = found[3]; // any part after dot (eg for `query.me` will be `me`)
if (subField) {
rootTypeAst.children[subField] = {
...astDir,
name: subField,
absPath,
};
} else {
rootTypeAst.children = astDir.children;
}
result[opType] = rootTypeAst;
}
}
}
});

return result;
}

export function getAstForDir(
m: NodeModule,
absPath: string,
options: Options = defaultOptions
): AstDirNode | void {
const name = basename(absPath);

if (!checkInclusion(absPath, 'dir', name, options)) return;

const result: AstDirNode = {
kind: 'dir',
absPath,
name,
children: {},
};

// get the path of each file in specified directory, append to current tree node, recurse
fs.readdirSync(absPath).forEach((filename) => {
const absFilePath = join(absPath, filename);

const stat = fs.statSync(absFilePath);
if (stat.isDirectory()) {
// this node is a directory; recurse
if (result.children[filename]) {
throw new Error(
`You have a folder and file with same name "${filename}" by the following path ${absPath}. Please remove one of them.`
);
}
const astDir = getAstForDir(m, absFilePath, options);
if (astDir) {
result.children[filename] = astDir;
}
} else if (stat.isFile()) {
// this node is a file
const fileAst = getAstForFile(m, absFilePath, options);
if (fileAst) {
if (result.children[fileAst.name]) {
throw new Error(
`You have a folder and file with same name "${fileAst.name}" by the following path ${absPath}. Please remove one of them.`
);
} else {
result.children[fileAst.name] = fileAst;
}
}
}
});

return result;
}

export function getAstForFile(
m: NodeModule,
absPath: string,
options: Options = defaultOptions
): AstFileNode | void {
const filename = basename(absPath);
if (absPath !== m.filename && checkInclusion(absPath, 'file', filename, options)) {
// hash node key shouldn't include file extension
const moduleName = filename.substring(0, filename.lastIndexOf('.'));
return {
kind: 'file',
name: moduleName,
absPath,
code: m.require(absPath),
};
}
}

function checkInclusion(
absPath: string,
kind: 'dir' | 'file',
filename: string,
options: Options
): boolean {
// Skip dir/files started from double underscore
if (/^__.*/i.test(filename)) {
return false;
}

if (kind === 'file') {
if (
// Verify file has valid extension
!new RegExp('\\.(' + (options?.extensions || ['js', 'ts']).join('|') + ')$', 'i').test(
filename
) ||
// Hardcoded skip file extensions
new RegExp('(\\.d\\.ts)$', 'i').test(filename)
)
return false;
}

if (options.include) {
if (options.include instanceof RegExp) {
// if options.include is a RegExp, evaluate it and make sure the path passes
if (!options.include.test(absPath)) return false;
} else if (typeof options.include === 'function') {
// if options.include is a function, evaluate it and make sure the path passes
if (!options.include(absPath, kind, filename)) return false;
}
}

if (options.exclude) {
if (options.exclude instanceof RegExp) {
// if options.exclude is a RegExp, evaluate it and make sure the path doesn't pass
if (options.exclude.test(absPath)) return false;
} else if (typeof options.exclude === 'function') {
// if options.exclude is a function, evaluate it and make sure the path doesn't pass
if (options.exclude(absPath, kind, filename)) return false;
}
}

return true;
}
12 changes: 6 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { requireSchemaDirectory, RequireOptions } from './requireSchemaDirectory';
import { requireAstToSchema, AstOptions } from './requireAstToSchema';
import { directoryToAst, Options } from './directoryToAst';
import { astToSchema, AstOptions } from './astToSchema';

export interface BuildOptions extends RequireOptions, AstOptions {}
export interface BuildOptions extends Options, AstOptions {}

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

export function loadSchemaComposer(module: NodeModule, opts: BuildOptions) {
const ast = requireSchemaDirectory(module, opts);
const sc = requireAstToSchema(ast, opts);
const ast = directoryToAst(module, opts);
const sc = astToSchema(ast, opts);
return sc;
}

export { requireSchemaDirectory, requireAstToSchema };
export { directoryToAst, astToSchema };
Loading

0 comments on commit 499a72e

Please sign in to comment.