From 8d3fb2722ae7dc9033931cb41435f0d4d0747963 Mon Sep 17 00:00:00 2001 From: Tim Griesser Date: Wed, 10 Apr 2019 17:28:59 +0200 Subject: [PATCH] Parse graphql-js types into Nexus structures Allows better interop between the two Fixes: #70, #88 --- package.json | 2 +- src/builder.ts | 16 ++- src/toConfig.ts | 326 ++++++++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 30 +++++ yarn.lock | 13 +- 5 files changed, 379 insertions(+), 8 deletions(-) create mode 100644 src/toConfig.ts diff --git a/package.json b/package.json index f9b6e092..5e9c49a7 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "tslib": "^1.9.3" }, "devDependencies": { - "@types/graphql": "^14.0.7", + "@types/graphql": "^14.2.0", "@types/jest": "^23.3.7", "@types/node": "^10.12.2", "@types/prettier": "^1.15.2", diff --git a/src/builder.ts b/src/builder.ts index 871fe085..bfceabcf 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -33,6 +33,7 @@ import { isUnionType, isScalarType, defaultFieldResolver, + isSchema, } from "graphql"; import { NexusArgConfig, NexusArgDef } from "./definitions/args"; import { @@ -98,6 +99,7 @@ import { NexusExtendInputTypeDef, NexusExtendInputTypeConfig, } from "./definitions/extendInputType"; +import { toNexusConfig, schemaToNexusConfig } from "./toConfig"; export type Maybe = T | null; @@ -261,7 +263,15 @@ export class SchemaBuilder { | NexusExtendInputTypeDef | NexusExtendTypeDef | GraphQLNamedType + | GraphQLSchema ) { + if (isSchema(typeDef)) { + schemaToNexusConfig(typeDef).forEach((type) => { + this.pendingTypeMap[type.name] = type; + }); + return; + } + const existingType = this.finalTypeMap[typeDef.name] || this.pendingTypeMap[typeDef.name]; @@ -305,8 +315,7 @@ export class SchemaBuilder { } if (isNamedType(typeDef)) { - this.finalTypeMap[typeDef.name] = typeDef; - this.definedTypeMap[typeDef.name] = typeDef; + this.pendingTypeMap[typeDef.name] = toNexusConfig(typeDef); } else { this.pendingTypeMap[typeDef.name] = typeDef; } @@ -1028,7 +1037,8 @@ function addTypes(builder: SchemaBuilder, types: any) { isNexusNamedTypeDef(types) || isNexusExtendTypeDef(types) || isNexusExtendInputTypeDef(types) || - isNamedType(types) + isNamedType(types) || + isSchema(types) ) { builder.addType(types); } else if (Array.isArray(types)) { diff --git a/src/toConfig.ts b/src/toConfig.ts new file mode 100644 index 00000000..8236c3f7 --- /dev/null +++ b/src/toConfig.ts @@ -0,0 +1,326 @@ +import { + GraphQLScalarType, + GraphQLObjectType, + GraphQLArgument, + GraphQLFieldConfigArgumentMap, + GraphQLFieldMap, + GraphQLInterfaceType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLSchema, + GraphQLSchemaConfig, + GraphQLNamedType, + isObjectType, + isInterfaceType, + isScalarType, + isUnionType, + isEnumType, + isInputObjectType, + GraphQLOutputType, + GraphQLInputType, + isNonNullType, + isListType, +} from "graphql"; +import { keyValMap, mapValue, objValues, eachObj } from "./utils"; +import { AllNexusNamedTypeDefs } from "./definitions/wrapping"; +import { objectType } from "./definitions/objectType"; +import { scalarType } from "./definitions/scalarType"; +import { interfaceType } from "./definitions/interfaceType"; +import { unionType } from "./definitions/unionType"; +import { enumType } from "./definitions/enumType"; +import { inputObjectType } from "./definitions/inputObjectType"; +import { arg } from "./definitions/args"; + +/** + * Converts a schema to an array of Nexus compatible types. + */ +export function schemaToNexusConfig( + schema: GraphQLSchema +): AllNexusNamedTypeDefs[] { + const { types } = polyfillSchemaToConfig(schema); + return types.map((t) => toNexusConfig(t)); +} + +/** + * Converts the native GraphQL object types first toConfig, polyfilling + * if necessary - then to Nexus' syntax so it's consistent with how we process + * the types internally. + */ +export function toNexusConfig(type: GraphQLNamedType): AllNexusNamedTypeDefs { + if (isScalarType(type)) { + const config = polyfillScalarToConfig(type); + return scalarType(config); + } + if (isObjectType(type)) { + const config = polyfillObjectToConfig(type); + return objectType({ + name: config.name, + definition(t) { + eachObj(config.fields, (val, key) => { + t.field(key, { + type: extractTypeName(val.type), + list: extractList(val.type), + nullable: !isNonNullType(val.type), + resolve: val.resolve, + args: extractArgs(val.args), + }); + }); + }, + }); + } + if (isInterfaceType(type)) { + const config = polyfillInterfaceToConfig(type); + return interfaceType({ + name: config.name, + definition(t) { + eachObj(config.fields, (val, key) => { + t.field(key, { + type: extractTypeName(val.type), + list: extractList(val.type), + nullable: !isNonNullType(val.type), + resolve: val.resolve, + args: extractArgs(val.args), + }); + }); + }, + }); + } + if (isUnionType(type)) { + const config = polyfillUnionToConfig(type); + return unionType({ + name: config.name, + definition(t) { + if (typeof config.resolveType === "function") { + t.resolveType(config.resolveType); + } + t.members(...config.types.map(({ name }) => name)); + }, + }); + } + if (isEnumType(type)) { + const config = polyfillEnumToConfig(type); + return enumType({ + name: type.name, + members: config.values, + }); + } + if (isInputObjectType(type)) { + const config = polyfillInputObjectToConfig(type); + return inputObjectType({ + name: type.name, + definition(t) { + eachObj(config.fields, (val, key) => { + t.field(key, { + type: extractTypeName(val.type), + list: extractList(val.type), + nullable: !isNonNullType(val.type), + description: val.description, + }); + }); + }, + }); + } + throw new Error(`Invalid type ${type}`); +} + +/** + * Extracts the core "type" name, removing nullable / list + */ +export function extractTypeName( + type: GraphQLOutputType | GraphQLInputType +): string { + while (isNonNullType(type) || isListType(type)) { + type = type.ofType; + } + return type.name; +} + +/** + * Extract a "list" array as it + */ +export function extractList( + type: GraphQLOutputType | GraphQLInputType +): boolean[] | undefined { + if (isNonNullType(type)) { + type = type.ofType; + } + const list: boolean[] = []; + while (isListType(type)) { + type = type.ofType; + if (isNonNullType(type)) { + type = type.ofType; + list.push(true); + } else { + list.push(false); + } + } + return list.length ? list : undefined; +} + +export function extractArgs(args: GraphQLFieldConfigArgumentMap = {}) { + return mapValue(args, (val, key) => { + return arg({ + type: extractTypeName(val.type), + list: extractList(val.type), + description: val.description, + default: val.defaultValue, + }); + }); +} + +export function polyfillSchemaToConfig( + schema: GraphQLSchema +): GraphQLSchemaConfig & ReturnType { + if (schema.toConfig) { + return schema.toConfig(); + } + return { + types: objValues(schema.getTypeMap()), + directives: schema.getDirectives().slice(), + query: schema.getQueryType(), + mutation: schema.getMutationType(), + subscription: schema.getSubscriptionType(), + astNode: schema.astNode, + extensionASTNodes: schema.extensionASTNodes || [], + }; +} + +export function polyfillScalarToConfig( + type: GraphQLScalarType +): ReturnType { + if (type.toConfig) { + return type.toConfig(); + } + return { + name: type.name, + description: type.description, + serialize: type.serialize, + parseValue: type.parseValue, + parseLiteral: type.parseLiteral, + astNode: type.astNode, + extensionASTNodes: type.extensionASTNodes || [], + }; +} + +export function polyfillObjectToConfig( + type: GraphQLObjectType +): ReturnType { + if (type.toConfig) { + return type.toConfig(); + } + return { + name: type.name, + description: type.description, + isTypeOf: type.isTypeOf, + interfaces: type.getInterfaces(), + fields: fieldsToFieldsConfig(type.getFields()), + astNode: type.astNode, + extensionASTNodes: type.extensionASTNodes || [], + }; +} + +export function polyfillInterfaceToConfig( + type: GraphQLInterfaceType +): ReturnType { + if (type.toConfig) { + return type.toConfig(); + } + return { + name: type.name, + description: type.description, + resolveType: type.resolveType, + fields: fieldsToFieldsConfig(type.getFields()), + astNode: type.astNode, + extensionASTNodes: type.extensionASTNodes || [], + }; +} + +export function polyfillUnionToConfig(type: GraphQLUnionType) { + if (type.toConfig) { + return type.toConfig(); + } + return { + name: type.name, + description: type.description, + resolveType: type.resolveType, + types: type.getTypes(), + astNode: type.astNode, + extensionASTNodes: type.extensionASTNodes || [], + }; +} + +export function polyfillEnumToConfig( + type: GraphQLEnumType +): ReturnType { + if (type.toConfig) { + return type.toConfig(); + } + const values = keyValMap( + type.getValues(), + (value) => value.name, + (value) => ({ + description: value.description, + value: value.value, + deprecationReason: value.deprecationReason, + astNode: value.astNode, + }) + ); + + return { + name: type.name, + description: type.description, + values, + astNode: type.astNode, + extensionASTNodes: type.extensionASTNodes || [], + }; +} + +export function polyfillInputObjectToConfig( + type: GraphQLInputObjectType +): ReturnType { + if (type.toConfig) { + return type.toConfig(); + } + const fields = mapValue(type.getFields(), (field) => ({ + description: field.description, + type: field.type, + defaultValue: field.defaultValue, + astNode: field.astNode, + })); + + return { + name: type.name, + description: type.description, + fields, + astNode: type.astNode, + extensionASTNodes: type.extensionASTNodes || [], + }; +} + +function fieldsToFieldsConfig(fields: GraphQLFieldMap) { + return mapValue(fields, (field) => ({ + type: field.type, + args: argsToArgsConfig(field.args), + resolve: field.resolve, + subscribe: field.subscribe, + deprecationReason: field.deprecationReason, + description: field.description, + astNode: field.astNode, + })); +} + +function argsToArgsConfig( + args: Array +): GraphQLFieldConfigArgumentMap { + return keyValMap( + args, + (a) => a.name, + (a) => ({ + type: a.type, + defaultValue: a.defaultValue, + description: a.description, + astNode: a.astNode, + }) + ); +} diff --git a/src/utils.ts b/src/utils.ts index 1409fb96..b852c23b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -214,3 +214,33 @@ export function firstDefined(...args: Array): T { export function isPromise(value: any): value is PromiseLike { return Boolean(value && typeof value.then === "function"); } + +/** + * Creates a keyed JS object from an array, given a function to produce the keys + * and a function to produce the values from each item in the array. + */ +export function keyValMap( + list: ReadonlyArray, + keyFn: (item: T) => string, + valFn: (item: T) => V +): Record { + return list.reduce( + (map, item) => ((map[keyFn(item)] = valFn(item)), map), + Object.create(null) + ); +} + +/** + * Creates an object map with the same keys as `map` and values generated by + * running each value of `map` thru `fn`. + */ +export function mapValue( + map: Record, + fn: (value: T, key: string) => V +): Record { + const result = Object.create(null); + eachObj(map, (value, key) => { + result[key] = fn(value, key); + }); + return result; +} diff --git a/yarn.lock b/yarn.lock index a0bfeecf..fcf71dad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,10 +25,10 @@ dependencies: any-observable "^0.3.0" -"@types/graphql@^14.0.7": - version "14.0.7" - resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.7.tgz#daa09397220a68ce1cbb3f76a315ff3cd92312f6" - integrity sha512-BoLDjdvLQsXPZLJux3lEZANwGr3Xag56Ngy0U3y8uoRSDdeLcn43H3oBcgZlnd++iOQElBpaRVDHPzEDekyvXQ== +"@types/graphql@^14.2.0": + version "14.2.0" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.2.0.tgz#74e1da5f2a4a744ac6eb3ed57b48242ea9367202" + integrity sha512-lELg5m6eBOmATWyCZl8qULEOvnPIUG6B443yXKj930glXIgwQirIBPp5rthP2amJW0YSzUg2s5sfgba4mRRCNw== "@types/jest@^23.3.7": version "23.3.10" @@ -735,6 +735,11 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" +cssom@0.3.x: + version "0.3.6" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.6.tgz#f85206cee04efa841f3c5982a74ba96ab20d65ad" + integrity sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A== + "cssom@>= 0.3.2 < 0.4.0": version "0.3.4" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797"