Skip to content

Commit

Permalink
New loaders (#387)
Browse files Browse the repository at this point in the history
* Improvements for loaders

* WIP

* New implementation

* Use cache instead of preresolvedTypeDefs

* Support schema definition

* Fix comments

* Add tests for shadowed variable

* Use absolute: true
  • Loading branch information
ardatan authored Dec 29, 2019
1 parent e4b4704 commit d2fbf64
Show file tree
Hide file tree
Showing 149 changed files with 1,524 additions and 1,650 deletions.
38 changes: 19 additions & 19 deletions packages/common/src/fix-schema-ast.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { GraphQLSchema, buildASTSchema, parse, BuildSchemaOptions } from 'graphql';
import { GraphQLSchema, buildASTSchema, parse, BuildSchemaOptions, ParseOptions } from 'graphql';
import { printSchemaWithDirectives } from '.';

export function fixSchemaAst(schema: GraphQLSchema, options?: BuildSchemaOptions) {
if (!schema.astNode) {
Object.defineProperty(schema, 'astNode', {
get: () => {
return buildASTSchema(parse(printSchemaWithDirectives(schema)), {
commentDescriptions: true,
...(options || {}),
}).astNode;
},
});
Object.defineProperty(schema, 'extensionASTNodes', {
get: () => {
return buildASTSchema(parse(printSchemaWithDirectives(schema)), {
commentDescriptions: true,
...(options || {}),
}).extensionASTNodes;
},
});
export function fixSchemaAst(schema: GraphQLSchema, options: BuildSchemaOptions) {
if (!schema.astNode || !schema.extensionASTNodes) {
const schemaWithValidAst = buildASTSchema(
parse(printSchemaWithDirectives(schema), {
noLocation: true,
...(options || {}),
}),
{
commentDescriptions: true,
...(options || {}),
}
);
if (!schema.astNode) {
schema.astNode = schemaWithValidAst.astNode;
}
if (!schema.extensionASTNodes) {
schema.extensionASTNodes = schemaWithValidAst.extensionASTNodes;
}
}
return schema;
}
2 changes: 2 additions & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export * from './get-fields-with-directives';
export * from './validate-documents';
export * from './resolvers-composition';
export * from './fix-schema-ast';
export * from './parse-graphql-json';
export * from './parse-graphql-sdl';
22 changes: 15 additions & 7 deletions packages/common/src/loaders.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { DocumentNode, GraphQLSchema } from 'graphql';
import { DocumentNode, GraphQLSchema, ParseOptions, BuildSchemaOptions } from 'graphql';
import { GraphQLSchemaValidationOptions } from 'graphql/type/schema';

export declare class Source {
document: DocumentNode;
document?: DocumentNode;
schema?: GraphQLSchema;
rawSDL?: string;
location?: string;
constructor({ document, location, schema }: { document: DocumentNode; location?: string; schema?: GraphQLSchema });
constructor({ document, location, schema }: { document?: DocumentNode; location?: string; schema?: GraphQLSchema });
}

export type SingleFileOptions = ParseOptions &
GraphQLSchemaValidationOptions &
BuildSchemaOptions & {
noRequire?: boolean;
cwd?: string;
};

export type WithList<T> = T | T[];
export type ElementOf<TList> = TList extends Array<infer TElement> ? TElement : never;
export type SchemaPointer = WithList<string>;
Expand All @@ -16,12 +24,12 @@ export type DocumentGlobPathPointer = string;
export type DocumentPointer = WithList<DocumentGlobPathPointer>;
export type DocumentPointerSingle = ElementOf<DocumentPointer>;

export interface Loader<TPointer = string, TOptions = any> {
export interface Loader<TPointer = string, TOptions extends SingleFileOptions = SingleFileOptions> {
loaderId(): string;
canLoad(pointer: TPointer, options?: TOptions): Promise<boolean>;
load(pointer: TPointer, options?: TOptions): Promise<Source | null>;
}

export type SchemaLoader<TOptions = any> = Loader<SchemaPointerSingle, TOptions>;
export type DocumentLoader<TOptions = any> = Loader<DocumentPointerSingle, TOptions>;
export type UniversalLoader<TOptions = any> = Loader<SchemaPointerSingle | DocumentPointerSingle, TOptions>;
export type SchemaLoader<TOptions extends SingleFileOptions = SingleFileOptions> = Loader<SchemaPointerSingle, TOptions>;
export type DocumentLoader<TOptions extends SingleFileOptions = SingleFileOptions> = Loader<DocumentPointerSingle, TOptions>;
export type UniversalLoader<TOptions extends SingleFileOptions = SingleFileOptions> = Loader<SchemaPointerSingle | DocumentPointerSingle, TOptions>;
43 changes: 43 additions & 0 deletions packages/common/src/parse-graphql-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Source as GraphQLSource, buildClientSchema, parse, ParseOptions } from 'graphql';
import { printSchemaWithDirectives, Source } from '.';
import { GraphQLSchemaValidationOptions } from 'graphql/type/schema';

function stripBOM(content: string): string {
content = content.toString();
// Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
// because the buffer-to-string conversion in `fs.readFileSync()`
// translates it to FEFF, the UTF-16 BOM.
if (content.charCodeAt(0) === 0xfeff) {
content = content.slice(1);
}

return content;
}

function parseBOM(content: string): any {
return JSON.parse(stripBOM(content));
}

export function parseGraphQLJSON(location: string, jsonContent: string, options: ParseOptions & GraphQLSchemaValidationOptions): Source {
let parsedJson = parseBOM(jsonContent);

if (parsedJson['data']) {
parsedJson = parsedJson['data'];
}

if (parsedJson.kind === 'Document') {
const document = parsedJson;
return {
location,
document,
};
} else if (parsedJson.__schema) {
const schema = buildClientSchema(parsedJson, options);
return {
location,
document: parse(printSchemaWithDirectives(schema), options),
schema,
};
}
throw new Error(`Not valid JSON content`);
}
22 changes: 22 additions & 0 deletions packages/common/src/parse-graphql-sdl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ParseOptions, DocumentNode, parse, Kind, Source as GraphQLSource } from 'graphql';

export function parseGraphQLSDL(location: string, rawSDL: string, options: ParseOptions) {
let document: DocumentNode;
try {
document = parse(new GraphQLSource(rawSDL, location), options);
} catch (e) {
if (e.message.includes('EOF')) {
document = {
kind: Kind.DOCUMENT,
definitions: [],
};
} else {
throw e;
}
}
return {
location,
document,
rawSDL,
};
}
1 change: 0 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"@graphql-toolkit/schema-merging": "0.7.5",
"aggregate-error": "3.0.1",
"globby": "10.0.1",
"graphql-import": "0.7.1",
"is-glob": "4.0.1",
"tslib": "1.10.0",
"valid-url": "1.0.9",
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/documents.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Loader, Source } from '@graphql-toolkit/common';
import { Source } from '@graphql-toolkit/common';
import { Kind } from 'graphql';
import { LoadTypedefsOptions, loadTypedefsUsingLoaders, UnnormalizedTypeDefPointer } from './load-typedefs';
import { LoadTypedefsOptions, loadTypedefs, UnnormalizedTypeDefPointer } from './load-typedefs';

export const OPERATION_KINDS = [Kind.OPERATION_DEFINITION, Kind.FRAGMENT_DEFINITION];
export const NON_OPERATION_KINDS = Object.keys(Kind)
.reduce((prev, v) => [...prev, Kind[v]], [])
.filter(v => !OPERATION_KINDS.includes(v));

export async function loadDocumentsUsingLoaders(loaders: Loader[], documentDef: UnnormalizedTypeDefPointer | UnnormalizedTypeDefPointer[], options: LoadTypedefsOptions = {}, cwd = process.cwd()): Promise<Source[]> {
return await loadTypedefsUsingLoaders(loaders, documentDef, { ...options, skipGraphQLImport: true, noRequire: true }, NON_OPERATION_KINDS, cwd);
export function loadDocuments(documentDef: UnnormalizedTypeDefPointer | UnnormalizedTypeDefPointer[], options: LoadTypedefsOptions): Promise<Source[]> {
return loadTypedefs(documentDef, { noRequire: true, filterKinds: NON_OPERATION_KINDS, ...options });
}
33 changes: 23 additions & 10 deletions packages/core/src/import-parser/definition.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { keyBy, uniqBy, includes } from 'lodash';
import { TypeDefinitionNode, TypeNode, NamedTypeNode, DirectiveNode, DirectiveDefinitionNode, InputValueDefinitionNode, FieldDefinitionNode } from 'graphql';
import { keyBy, uniqBy, includes, reverse } from 'lodash';
import { TypeDefinitionNode, TypeNode, NamedTypeNode, DirectiveNode, DirectiveDefinitionNode, InputValueDefinitionNode, FieldDefinitionNode, SchemaDefinitionNode } from 'graphql';

const builtinTypes = ['String', 'Float', 'Int', 'Boolean', 'ID'];

const builtinDirectives = ['deprecated', 'skip', 'include', 'key', 'external', 'requires', 'provides'];

export type ValidDefinitionNode = DirectiveDefinitionNode | TypeDefinitionNode;
export type ValidDefinitionNode = DirectiveDefinitionNode | TypeDefinitionNode | SchemaDefinitionNode;

export interface DefinitionMap {
[key: string]: ValidDefinitionNode;
Expand All @@ -23,17 +23,17 @@ export interface DefinitionMap {
export function completeDefinitionPool(allDefinitions: ValidDefinitionNode[], definitionPool: ValidDefinitionNode[], newTypeDefinitions: ValidDefinitionNode[]): ValidDefinitionNode[] {
const visitedDefinitions: { [name: string]: boolean } = {};
while (newTypeDefinitions.length > 0) {
const schemaMap: DefinitionMap = keyBy(allDefinitions, d => d.name.value);
const schemaMap: DefinitionMap = keyBy(reverse(allDefinitions), d => ('name' in d ? d.name.value : 'schema'));
const newDefinition = newTypeDefinitions.shift();
if (visitedDefinitions[newDefinition.name.value]) {
if (visitedDefinitions['name' in newDefinition ? newDefinition.name.value : 'schema']) {
continue;
}

const collectedTypedDefinitions = collectNewTypeDefinitions(allDefinitions, definitionPool, newDefinition, schemaMap);
newTypeDefinitions.push(...collectedTypedDefinitions);
definitionPool.push(...collectedTypedDefinitions);

visitedDefinitions[newDefinition.name.value] = true;
visitedDefinitions['name' in newDefinition ? newDefinition.name.value : 'schema'] = true;
}

return uniqBy(definitionPool, 'name.value');
Expand Down Expand Up @@ -73,7 +73,7 @@ function collectNewTypeDefinitions(allDefinitions: ValidDefinitionNode[], defini

if (newDefinition.kind === 'UnionTypeDefinition') {
newDefinition.types.forEach(type => {
if (!definitionPool.some(d => d.name.value === type.name.value)) {
if (!definitionPool.some(d => 'name' in d && d.name.value === type.name.value)) {
const typeName = type.name.value;
const typeMatch = schemaMap[typeName];
if (!typeMatch) {
Expand All @@ -87,7 +87,7 @@ function collectNewTypeDefinitions(allDefinitions: ValidDefinitionNode[], defini
if (newDefinition.kind === 'ObjectTypeDefinition') {
// collect missing interfaces
newDefinition.interfaces.forEach(int => {
if (!definitionPool.some(d => d.name.value === int.name.value)) {
if (!definitionPool.some(d => 'name' in d && d.name.value === int.name.value)) {
const interfaceName = int.name.value;
const interfaceMatch = schemaMap[interfaceName];
if (!interfaceMatch) {
Expand All @@ -105,14 +105,27 @@ function collectNewTypeDefinitions(allDefinitions: ValidDefinitionNode[], defini
});
}

if (newDefinition.kind === 'SchemaDefinition') {
newDefinition.operationTypes.forEach(operationType => {
if (!definitionPool.some(d => 'name' in d && d.name.value === operationType.type.name.value)) {
const typeName = operationType.type.name.value;
const typeMatch = schemaMap[typeName];
if (!typeMatch) {
throw new Error(`Couldn't find type ${typeName} in any of the schemas.`);
}
newTypeDefinitions.push(schemaMap[operationType.type.name.value]);
}
});
}

return newTypeDefinitions;

function collectNode(node: FieldDefinitionNode | InputValueDefinitionNode) {
const nodeType = getNamedType(node.type);
const nodeTypeName = nodeType.name.value;

// collect missing argument input types
if (!definitionPool.some(d => d.name.value === nodeTypeName) && !includes(builtinTypes, nodeTypeName)) {
if (!definitionPool.some(d => 'name' in d && d.name.value === nodeTypeName) && !includes(builtinTypes, nodeTypeName)) {
const argTypeMatch = schemaMap[nodeTypeName];
if (!argTypeMatch) {
throw new Error(`Field ${node.name.value}: Couldn't find type ${nodeTypeName} in any of the schemas.`);
Expand All @@ -125,7 +138,7 @@ function collectNewTypeDefinitions(allDefinitions: ValidDefinitionNode[], defini

function collectDirective(directive: DirectiveNode) {
const directiveName = directive.name.value;
if (!definitionPool.some(d => d.name.value === directiveName) && !includes(builtinDirectives, directiveName)) {
if (!definitionPool.some(d => 'name' in d && d.name.value === directiveName) && !includes(builtinDirectives, directiveName)) {
const directive = schemaMap[directiveName] as DirectiveDefinitionNode;
if (!directive) {
throw new Error(`Directive ${directiveName}: Couldn't find type ${directiveName} in any of the schemas.`);
Expand Down
Loading

0 comments on commit d2fbf64

Please sign in to comment.