Skip to content

Commit

Permalink
Implement new schema stitching API
Browse files Browse the repository at this point in the history
  • Loading branch information
freiksenet committed Oct 2, 2017
1 parent d21c841 commit 4a2e62f
Show file tree
Hide file tree
Showing 19 changed files with 2,773 additions and 98 deletions.
1 change: 1 addition & 0 deletions .npmignore
@@ -1,6 +1,7 @@
*
!dist/
!dist/*
!dist/stitching/*
!package.json
!*.md
!*.png
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "graphql-tools",
"version": "1.2.3",
"version": "2.0.0-alpha.25",
"description": "A set of useful tools for GraphQL",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
1 change: 0 additions & 1 deletion src/Interfaces.ts
Expand Up @@ -81,4 +81,3 @@ export interface IMockServer {
vars?: { [key: string]: any },
) => Promise<ExecutionResult>;
}

1 change: 1 addition & 0 deletions src/index.ts
@@ -1,2 +1,3 @@
export * from './schemaGenerator';
export * from './mock';
export * from './stitching';
12 changes: 12 additions & 0 deletions src/isEmptyObject.ts
@@ -0,0 +1,12 @@
export default function isEmptyObject(obj: Object): Boolean {
if (!obj) {
return true;
}

for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
return false;
}
}
return true;
}
123 changes: 123 additions & 0 deletions src/stitching/TypeRegistry.ts
@@ -0,0 +1,123 @@
import {
GraphQLSchema,
GraphQLNonNull,
GraphQLList,
GraphQLNamedType,
GraphQLType,
isNamedType,
getNamedType,
InlineFragmentNode,
Kind,
parse,
} from 'graphql';

export default class TypeRegistry {
public fragmentReplacements: {
[typeName: string]: { [fieldName: string]: InlineFragmentNode };
};
private types: { [key: string]: GraphQLNamedType };
private schemaByField: {
query: { [key: string]: GraphQLSchema };
mutation: { [key: string]: GraphQLSchema };
};

constructor() {
this.types = {};
this.schemaByField = {
query: {},
mutation: {},
};
this.fragmentReplacements = {};
}

public getSchemaByField(
operation: 'query' | 'mutation',
fieldName: string,
): GraphQLSchema {
return this.schemaByField[operation][fieldName];
}

public getAllTypes(): Array<GraphQLNamedType> {
return Object.keys(this.types).map(name => this.types[name]);
}

public getType(name: string): GraphQLNamedType {
if (!this.types[name]) {
throw new Error(`No such type: ${name}`);
}
return this.types[name];
}

public resolveType<T extends GraphQLType>(type: T): T {
if (type instanceof GraphQLList) {
return new GraphQLList(this.resolveType(type.ofType)) as T;
} else if (type instanceof GraphQLNonNull) {
return new GraphQLNonNull(this.resolveType(type.ofType)) as T;
} else if (isNamedType(type)) {
return this.getType(getNamedType(type).name) as T;
} else {
return type;
}
}

public addSchema(schema: GraphQLSchema) {
const query = schema.getQueryType();
if (query) {
const fieldNames = Object.keys(query.getFields());
fieldNames.forEach(field => {
this.schemaByField.query[field] = schema;
});
}

const mutation = schema.getMutationType();
if (mutation) {
const fieldNames = Object.keys(mutation.getFields());
fieldNames.forEach(field => {
this.schemaByField.mutation[field] = schema;
});
}
}

public addType(
name: string,
type: GraphQLNamedType,
onTypeConflict?: (
leftType: GraphQLNamedType,
rightType: GraphQLNamedType,
) => GraphQLNamedType,
): void {
if (this.types[name]) {
if (onTypeConflict) {
type = onTypeConflict(this.types[name], type);
} else {
throw new Error(`Type name conflict: ${name}`);
}
}
this.types[name] = type;
}

public addFragment(typeName: string, fieldName: string, fragment: string) {
if (!this.fragmentReplacements[typeName]) {
this.fragmentReplacements[typeName] = {};
}
this.fragmentReplacements[typeName][
fieldName
] = parseFragmentToInlineFragment(fragment);
}
}

function parseFragmentToInlineFragment(
definitions: string,
): InlineFragmentNode {
const document = parse(definitions);
for (const definition of document.definitions) {
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
return {
kind: Kind.INLINE_FRAGMENT,
typeCondition: definition.typeCondition,
selectionSet: definition.selectionSet,
};
}
}
throw new Error('Could not parse fragment');
}
5 changes: 5 additions & 0 deletions src/stitching/index.ts
@@ -0,0 +1,5 @@
import makeRemoteExecutableSchema from './makeRemoteExecutableSchema';
import introspectSchema from './introspectSchema';
import mergeSchemas from './mergeSchemas';

export { makeRemoteExecutableSchema, introspectSchema, mergeSchemas };
21 changes: 21 additions & 0 deletions src/stitching/introspectSchema.ts
@@ -0,0 +1,21 @@
import { GraphQLSchema } from 'graphql';
import { introspectionQuery, buildClientSchema } from 'graphql';
import { Fetcher } from './makeRemoteExecutableSchema';

export default async function introspectSchema(
fetcher: Fetcher,
context?: { [key: string]: any },
): Promise<GraphQLSchema> {
const introspectionResult = await fetcher({
query: introspectionQuery,
context,
});
if (introspectionResult.errors || !introspectionResult.data.__schema) {
throw introspectionResult.errors;
} else {
const schema = buildClientSchema(introspectionResult.data as {
__schema: any;
});
return schema;
}
}
161 changes: 161 additions & 0 deletions src/stitching/makeRemoteExecutableSchema.ts
@@ -0,0 +1,161 @@
import { printSchema, print, ExecutionResult, Kind, ValueNode } from 'graphql';
import {
GraphQLFieldResolver,
GraphQLSchema,
GraphQLInterfaceType,
GraphQLUnionType,
GraphQLID,
GraphQLString,
GraphQLFloat,
GraphQLBoolean,
GraphQLInt,
GraphQLScalarType,
} from 'graphql';
import isEmptyObject from '../isEmptyObject';
import { IResolvers, IResolverObject } from '../Interfaces';
import { makeExecutableSchema } from '../schemaGenerator';
import resolveParentFromTypename from './resolveFromParentTypename';

export type Fetcher = (
operation: {
query: string;
operationName?: string;
variables?: { [key: string]: any };
context?: { [key: string]: any };
},
) => Promise<ExecutionResult>;

export default function makeRemoteExecutableSchema({
schema,
fetcher,
}: {
schema: GraphQLSchema;
fetcher: Fetcher;
}): GraphQLSchema {
const queryType = schema.getQueryType();
const queries = queryType.getFields();
const queryResolvers: IResolverObject = {};
Object.keys(queries).forEach(key => {
queryResolvers[key] = createResolver(fetcher);
});
let mutationResolvers: IResolverObject = {};
const mutationType = schema.getMutationType();
if (mutationType) {
const mutations = mutationType.getFields();
Object.keys(mutations).forEach(key => {
mutationResolvers[key] = createResolver(fetcher);
});
}

const resolvers: IResolvers = { [queryType.name]: queryResolvers };

if (!isEmptyObject(mutationResolvers)) {
resolvers[mutationType.name] = mutationResolvers;
}

const typeMap = schema.getTypeMap();
const types = Object.keys(typeMap).map(name => typeMap[name]);
for (const type of types) {
if (
type instanceof GraphQLInterfaceType ||
type instanceof GraphQLUnionType
) {
resolvers[type.name] = {
__resolveType(parent, context, info) {
return resolveParentFromTypename(parent, info.schema);
},
};
} else if (type instanceof GraphQLScalarType) {
if (
!(
type === GraphQLID ||
type === GraphQLString ||
type === GraphQLFloat ||
type === GraphQLBoolean ||
type === GraphQLInt
)
) {
resolvers[type.name] = createPassThroughScalar(type);
}
}
}

const typeDefs = printSchema(schema);

return makeExecutableSchema({
typeDefs,
resolvers,
});
}

function createResolver(fetcher: Fetcher): GraphQLFieldResolver<any, any> {
return async (root, args, context, info) => {
const operation = print(info.operation);
const fragments = Object.keys(info.fragments)
.map(fragment => print(info.fragments[fragment]))
.join('\n');
const query = `${operation}\n${fragments}`;
const result = await fetcher({
query,
variables: info.variableValues,
context,
});
const fieldName = info.fieldNodes[0].alias
? info.fieldNodes[0].alias.value
: info.fieldName;
if (result.errors) {
const errorMessage = result.errors.map(error => error.message).join('\n');
throw new Error(errorMessage);
} else {
return result.data[fieldName];
}
};
}

function createPassThroughScalar({
name,
description,
}: {
name: string;
description: string;
}): GraphQLScalarType {
return new GraphQLScalarType({
name: name,
description: description,
serialize(value) {
return value;
},
parseValue(value) {
return value;
},
parseLiteral(ast) {
return parseLiteral(ast);
},
});
}

function parseLiteral(ast: ValueNode): any {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN: {
return ast.value;
}
case Kind.INT:
case Kind.FLOAT: {
return parseFloat(ast.value);
}
case Kind.OBJECT: {
const value = Object.create(null);
ast.fields.forEach(field => {
value[field.name.value] = parseLiteral(field.value);
});

return value;
}
case Kind.LIST: {
return ast.values.map(parseLiteral);
}
default:
return null;
}
}

0 comments on commit 4a2e62f

Please sign in to comment.