nexus
Version:
Scalable, strongly typed GraphQL schema development
394 lines (358 loc) • 11.4 kB
text/typescript
import {
buildSchema,
GraphQLArgument,
GraphQLEnumType,
GraphQLField,
GraphQLInputField,
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLNamedType,
GraphQLObjectType,
GraphQLOutputType,
GraphQLScalarType,
GraphQLSchema,
GraphQLUnionType,
GraphQLWrappingType,
isObjectType,
isScalarType,
isSpecifiedScalarType,
} from 'graphql'
import { eachObj, GroupedTypes, groupTypes, isInterfaceField, objValues } from './utils'
import { unwrapGraphQLDef, NexusFinalWrapKind } from './definitions/wrapping'
export function convertSDL(sdl: string, commonjs: null | boolean = false, json = JSON) {
try {
return new SDLConverter(sdl, commonjs, json).print()
} catch (e) {
return `Error Parsing SDL into Schema: ${e.stack}`
}
}
/** Convert an existing SDL schema into a GraphQL Nexus format */
export class SDLConverter {
protected export: string
protected schema: GraphQLSchema | null
protected groupedTypes: GroupedTypes
protected usedImports: Set<string> = new Set()
protected exports: Set<string> = new Set()
constructor(sdl: string, protected commonjs: null | boolean = false, protected json: JSON = JSON) {
this.export = commonjs === null || commonjs ? 'const ' : 'export const '
this.schema = buildSchema(sdl)
this.groupedTypes = groupTypes(this.schema)
}
print() {
const body = [
this.printObjectTypes(),
this.printInterfaceTypes(),
this.printInputObjectTypes(),
this.printUnionTypes(),
this.printEnumTypes(),
this.printScalarTypes(),
this.printExports(),
]
return [this.printUsedImports()]
.concat(body)
.filter((f) => f)
.join('\n\n')
}
printUsedImports() {
if (this.commonjs) {
return `const { ${Array.from(this.usedImports).join(', ')} } = require('nexus');`
}
return `import { ${Array.from(this.usedImports).join(', ')} } from 'nexus';`
}
printObjectTypes() {
if (this.groupedTypes.object.length > 0) {
this.usedImports.add('objectType')
return this.groupedTypes.object.map((t) => this.printObjectType(t)).join('\n')
}
return ''
}
printObjectType(type: GraphQLObjectType): string {
const implementing = type.getInterfaces().map((i) => i.name)
const implementsInterfaces = implementing.length > 0 ? ` t.implements(${implementing.join(', ')})` : ''
this.exports.add(type.name)
return this.printBlock([
`${this.export}${type.name} = objectType({`,
` name: "${type.name}",`,
this.maybeDescription(type),
` definition(t) {`,
implementsInterfaces,
this.printObjectFields(type),
` }`,
`})`,
])
}
printObjectFields(type: GraphQLObjectType | GraphQLInterfaceType) {
return objValues(type.getFields())
.map((field) => {
if (isObjectType(type) && isInterfaceField(type, field.name)) {
return
}
return this.printField('output', field)
})
.filter((f) => f)
.join('\n')
}
printInputObjectFields(type: GraphQLInputObjectType) {
return objValues(type.getFields())
.map((field) => this.printField('input', field))
.filter((f) => f)
.join('\n')
}
printField(source: 'input' | 'output', field: GraphQLField<any, any> | GraphQLInputField) {
const { namedType, wrapping } = unwrapGraphQLDef(field.type)
let prefix = 't.'
let typeString: string | undefined = undefined
;[...wrapping].reverse().forEach((w) => {
if (w === 'List') {
prefix += `list.`
} else {
prefix += `nonNull.`
}
})
return ` ${prefix}${this.printFieldMethod(source, field, namedType, typeString)}`
}
printFieldMethod(
source: 'input' | 'output',
field: GraphQLField<any, any> | GraphQLInputField,
type:
| Exclude<GraphQLOutputType, GraphQLWrappingType>
| Exclude<GraphQLInputObjectType, GraphQLWrappingType>,
typeString?: string
) {
const objectMeta: Record<string, any> = {}
let str = ''
if (isCommonScalar(type) && !typeString) {
str += `${type.name.toLowerCase()}("${field.name}"`
} else {
objectMeta.type = typeString ?? type
str += `field("${field.name}"`
}
if ('deprecationReason' in field && field.deprecationReason) {
objectMeta.deprecation = field.deprecationReason
}
if (field.description) {
objectMeta.description = field.description
}
if (source === 'output') {
const outputField = field as GraphQLField<any, any>
if (outputField.args.length) {
objectMeta.args = outputField.args
}
} else {
const inputField = field as GraphQLInputField
if (inputField.defaultValue !== undefined) {
objectMeta.default = inputField.defaultValue
}
}
const metaKeys = Object.keys(objectMeta)
if (metaKeys.length > 0) {
if (metaKeys.length === 1 && !objectMeta.args) {
const key = metaKeys[0]
str += `, { ${key}: ${this.printMeta(objectMeta[key], key)} }`
} else {
str += `, {\n`
eachObj(objectMeta, (val, key) => {
str += ` ${key}: ${this.printMeta(val, key)},\n`
})
str += ` }`
}
}
return `${str})`
}
printMeta(val: any, key: string) {
if (key === 'type') {
return val
}
if (key === 'args') {
let str = `{\n`
;(val as GraphQLArgument[]).forEach((arg) => {
str += ` ${arg.name}: ${this.printArg(arg)},\n`
})
str += ` }`
return str
}
return this.json.stringify(val)
}
printArg(arg: GraphQLArgument) {
const description = arg.description
const defaultValue = arg.defaultValue
const { namedType: type, wrapping } = unwrapGraphQLDef(arg.type)
const isArg = !isSpecifiedScalarType(type)
let str = ''
if (isArg) {
this.usedImports.add('arg')
str += `arg(`
} else {
this.usedImports.add(`${type.toString().toLowerCase()}Arg`)
str += `${type.toString().toLowerCase()}Arg(`
}
const metaToAdd = []
let wrappedType = type.name
if (isArg) {
metaToAdd.push(`type: ${this.addWrapping(wrappedType, wrapping)}`)
}
if (description) {
metaToAdd.push(`description: ${JSON.stringify(description)}`)
}
if (defaultValue) {
metaToAdd.push(`default: ${JSON.stringify(defaultValue)}`)
}
str +=
metaToAdd.length > 1
? `{\n ${metaToAdd.join(',\n ')}\n })`
: metaToAdd.length
? `{ ${metaToAdd[0]} })`
: ')'
return isArg ? str : this.addWrapping(str, wrapping)
}
addWrapping(toWrap: string, wrapping: NexusFinalWrapKind[]) {
let wrappedVal = toWrap
wrapping.forEach((w) => {
if (w === 'NonNull') {
this.usedImports.add('nonNull')
wrappedVal = `nonNull(${wrappedVal})`
} else if (w === 'List') {
this.usedImports.add('list')
wrappedVal = `list(${wrappedVal})`
}
})
return wrappedVal
}
printInterfaceTypes() {
if (this.groupedTypes.interface.length) {
this.usedImports.add('interfaceType')
return this.groupedTypes.interface.map((t) => this.printInterfaceType(t)).join('\n')
}
return ''
}
printInterfaceType(type: GraphQLInterfaceType): string {
const implementing: string[] =
// @ts-ignore
typeof type.getInterfaces === 'function' ? type.getInterfaces().map((i) => i.name) : []
const implementsInterfaces = implementing.length > 0 ? ` t.implements(${implementing.join(', ')})` : ''
this.exports.add(type.name)
return this.printBlock([
`${this.export}${type.name} = interfaceType({`,
` name: "${type.name}",`,
this.maybeDescription(type),
` definition(t) {`,
implementsInterfaces,
this.printObjectFields(type),
` }`,
`});`,
])
}
printEnumTypes() {
if (this.groupedTypes.enum.length) {
this.usedImports.add('enumType')
return this.groupedTypes.enum.map((t) => this.printEnumType(t)).join('\n')
}
return ''
}
printEnumType(type: GraphQLEnumType): string {
const members = type.getValues().map((val) => {
const { description, name, deprecationReason, value } = val
if (!description && !deprecationReason && name === value) {
return val.name
}
return { description, name, deprecation: deprecationReason, value }
})
this.exports.add(type.name)
return this.printBlock([
`${this.export}${type.name} = enumType({`,
` name: "${type.name}",`,
this.maybeDescription(type),
` members: ${this.json.stringify(members)},`,
`});`,
])
}
printInputObjectTypes() {
if (this.groupedTypes.input.length) {
this.usedImports.add('inputObjectType')
return this.groupedTypes.input.map((t) => this.printInputObjectType(t)).join('\n')
}
return ''
}
printInputObjectType(type: GraphQLInputObjectType): string {
this.exports.add(type.name)
return this.printBlock([
`${this.export}${type.name} = inputObjectType({`,
` name: "${type.name}",`,
this.maybeDescription(type),
` definition(t) {`,
this.printInputObjectFields(type),
` }`,
`});`,
])
}
printUnionTypes() {
if (this.groupedTypes.union.length) {
this.usedImports.add('unionType')
return this.groupedTypes.union.map((t) => this.printUnionType(t)).join('\n')
}
return ''
}
printUnionType(type: GraphQLUnionType): string {
this.exports.add(type.name)
return this.printBlock([
`${this.export}${type.name} = unionType({`,
` name: "${type.name}",`,
this.maybeDescription(type),
` definition(t) {`,
` t.members(${type.getTypes().join(', ')})`,
` }`,
`});`,
])
}
printScalarTypes() {
if (this.groupedTypes.scalar.length) {
this.usedImports.add('scalarType')
return this.groupedTypes.scalar
.filter((s) => !isSpecifiedScalarType(s))
.map((t) => this.printScalarType(t))
.join('\n')
}
return ''
}
printScalarType(type: GraphQLScalarType): string {
this.exports.add(type.name)
return this.printBlock([
`${this.export}${type.name} = scalarType({`,
` name: "${type.name}",`,
this.maybeDescription(type),
this.maybeAsNexusType(type),
` serialize() { /* Todo */ },`,
` parseValue() { /* Todo */ },`,
` parseLiteral() { /* Todo */ }`,
`});`,
])
}
printExports() {
if (!this.commonjs || this.exports.size === 0) {
return ''
}
const exports = Array.from(this.exports)
return this.printBlock(exports.map((exp) => `exports.${exp} = ${exp};`))
}
maybeAsNexusType(type: GraphQLScalarType) {
if (isCommonScalar(type)) {
return ` asNexusMethod: "${type.name.toLowerCase()}",`
}
return null
}
maybeDescription(type: GraphQLNamedType) {
if (type.description) {
return ` description: ${this.json.stringify(type.description)},`
}
return null
}
printBlock(block: (string | null)[]) {
return block.filter((t) => t !== null && t !== '').join('\n')
}
}
function isCommonScalar(field: GraphQLNamedType): boolean {
if (isScalarType(field)) {
return field.name === 'UUID' || field.name === 'Date' || isSpecifiedScalarType(field)
}
return false
}