Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d21c841
commit 4a2e62f
Showing
19 changed files
with
2,773 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
* | ||
!dist/ | ||
!dist/* | ||
!dist/stitching/* | ||
!package.json | ||
!*.md | ||
!*.png |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -81,4 +81,3 @@ export interface IMockServer { | |
vars?: { [key: string]: any }, | ||
) => Promise<ExecutionResult>; | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './schemaGenerator'; | ||
export * from './mock'; | ||
export * from './stitching'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import makeRemoteExecutableSchema from './makeRemoteExecutableSchema'; | ||
import introspectSchema from './introspectSchema'; | ||
import mergeSchemas from './mergeSchemas'; | ||
|
||
export { makeRemoteExecutableSchema, introspectSchema, mergeSchemas }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Oops, something went wrong.