Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Do not merge] 2.0 with schema stitching #382

Merged
merged 4 commits into from Oct 3, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
}