nexus
Version:
Scalable, strongly typed GraphQL schema development
1,416 lines (1,315 loc) • 64.1 kB
text/typescript
import {
assertValidName,
defaultFieldResolver,
GraphQLBoolean,
GraphQLEnumType,
GraphQLEnumValueConfigMap,
GraphQLFieldConfig,
GraphQLFieldConfigArgumentMap,
GraphQLFieldConfigMap,
GraphQLFieldResolver,
GraphQLFloat,
GraphQLID,
GraphQLInputFieldConfig,
GraphQLInputFieldConfigMap,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLInt,
GraphQLInterfaceType,
GraphQLList,
GraphQLNamedType,
GraphQLNonNull,
GraphQLObjectType,
GraphQLOutputType,
GraphQLScalarType,
GraphQLSchema,
GraphQLSchemaConfig,
GraphQLString,
GraphQLType,
GraphQLUnionType,
isInputObjectType,
isInputType,
isInterfaceType,
isLeafType,
isNamedType,
isObjectType,
isOutputType,
isSchema,
isWrappingType,
printSchema,
} from 'graphql'
import type { ArgsRecord, NexusFinalArgConfig } from './definitions/args'
import {
InputDefinitionBlock,
NexusInputFieldDef,
NexusOutputFieldConfig,
NexusOutputFieldDef,
OutputDefinitionBlock,
} from './definitions/definitionBlocks'
import type { NexusEnumTypeConfig } from './definitions/enumType'
import type { NexusExtendInputTypeConfig, NexusExtendInputTypeDef } from './definitions/extendInputType'
import type { NexusExtendTypeConfig, NexusExtendTypeDef } from './definitions/extendType'
import type { NexusInputObjectTypeConfig } from './definitions/inputObjectType'
import {
FieldModificationDef,
Implemented,
InterfaceDefinitionBlock,
NexusInterfaceTypeConfig,
NexusInterfaceTypeDef,
} from './definitions/interfaceType'
import { NexusObjectTypeConfig, NexusObjectTypeDef, ObjectDefinitionBlock } from './definitions/objectType'
import type { NexusScalarTypeConfig } from './definitions/scalarType'
import { NexusUnionTypeConfig, UnionDefinitionBlock, UnionMembers } from './definitions/unionType'
import {
AllNexusArgsDefs,
AllNexusNamedInputTypeDefs,
AllNexusNamedOutputTypeDefs,
AllNexusNamedTypeDefs,
AllNexusOutputTypeDefs,
finalizeWrapping,
isNexusDynamicInputMethod,
isNexusDynamicOutputMethod,
isNexusDynamicOutputProperty,
isNexusEnumTypeDef,
isNexusExtendInputTypeDef,
isNexusExtendTypeDef,
isNexusInputObjectTypeDef,
isNexusInterfaceTypeDef,
isNexusNamedInputTypeDef,
isNexusNamedOuputTypeDef,
isNexusNamedTypeDef,
isNexusObjectTypeDef,
isNexusPlugin,
isNexusScalarTypeDef,
isNexusUnionTypeDef,
isNexusWrappingType,
NexusFinalWrapKind,
NexusWrapKind,
normalizeArgWrapping,
rewrapAsGraphQLType,
unwrapGraphQLDef,
unwrapNexusDef,
} from './definitions/wrapping'
import type {
MissingType,
NexusFeaturesInput,
NexusGraphQLFieldConfig,
NexusGraphQLInputObjectTypeConfig,
NexusGraphQLInterfaceTypeConfig,
NexusGraphQLObjectTypeConfig,
NexusGraphQLSchema,
NonNullConfig,
SourceTypings,
TypingImport,
} from './definitions/_types'
import type { DynamicInputMethodDef, DynamicOutputMethodDef } from './dynamicMethod'
import type { DynamicOutputPropertyDef } from './dynamicProperty'
import {
hasNexusExtension,
NexusFieldExtension,
NexusInputObjectTypeExtension,
NexusInterfaceTypeExtension,
NexusObjectTypeExtension,
NexusSchemaExtension,
} from './extensions'
import { messages } from './messages'
import {
composeMiddlewareFns,
CreateFieldResolverInfo,
MiddlewareFn,
NexusPlugin,
PluginConfig,
} from './plugin'
import { declarativeWrappingPlugin } from './plugins'
import { fieldAuthorizePlugin } from './plugins/fieldAuthorizePlugin'
import type { SourceTypesConfigOptions } from './typegenAutoConfig'
import type { TypegenFormatFn } from './typegenFormatPrettier'
import type { AbstractTypeResolver, GetGen } from './typegenTypeHelpers'
import type { RequiredDeeply } from './typeHelpersInternal'
import {
casesHandled,
consoleWarn,
eachObj,
getArgNamedType,
getNexusNamedType,
graphql15InterfaceType,
invariantGuard,
isArray,
isObject,
objValues,
UNKNOWN_TYPE_SCALAR,
} from './utils'
import {
NEXUS_BUILD,
isNexusMetaBuild,
isNexusMeta,
isNexusMetaType,
NexusMeta,
resolveNexusMetaType,
} from './definitions/nexusMeta'
import { rebuildNamedType, RebuildConfig } from './rebuildType'
type NexusShapedOutput = {
name: string
definition: (t: ObjectDefinitionBlock<string>) => void
}
type NexusShapedInput = {
name: string
definition: (t: InputDefinitionBlock<string>) => void
}
const SCALARS: Record<string, GraphQLScalarType> = {
String: GraphQLString,
Int: GraphQLInt,
Float: GraphQLFloat,
ID: GraphQLID,
Boolean: GraphQLBoolean,
}
type PossibleOutputType =
| string
| AllNexusNamedOutputTypeDefs
| Exclude<GraphQLOutputType, GraphQLNonNull<any> | GraphQLList<any>>
type PossibleInputType = string | AllNexusNamedInputTypeDefs | GraphQLType
export interface ConfiguredTypegen {
/** Path for the generated type defs */
outputPath: string
/**
* Determine the path the "globals" are output, useful when you have a monorepo setup and need to isolate
* the globals from the rest of the types in order to have multiple schemas/ts projects
*/
globalsPath?: string
/**
* If globalsPath is defined, these headers are added to the "globals" generated file, rather than the
* typegen generated file
*/
globalsHeaders?: string[]
/**
* If "true", declares dedicated interfaces for any inputs / args
*
* @default false
*/
declareInputs?: boolean
}
export interface MergeSchemaConfig {
/**
* GraphQL Schema to merge into the Nexus type definitions.
*
* We unwrap each type, preserve the "nullable/nonNull" status of any fields & arguments, and then combine
* with the local Nexus GraphQL types.
*
* If you have multiple schemas
*/
schema: GraphQLSchema
/**
* If we want to "merge" specific types, provide a list of the types you wish to merge here.
*
* @default 'Query', 'Mutation'
*/
mergeTypes?: string[] | true
/**
* If there are types that we don't want to include from the external schema in our final Nexus generated
* schema, provide them here.
*/
skipTypes?: string[]
/**
* If there are certain "fields" that we want to skip, we can specify the fields here and we'll ensure they
* don't get merged into the schema
*/
skipFields?: Record<string, string[]>
/**
* If there are certain arguments for any type fields that we want to skip, we can specify the fields here &
* ensure they don't get merged into the final schema.
*
* @example
* skipArgs: {
* Mutation: {
* createAccount: ['internalId']
* }
* }
*/
skipArgs?: Record<string, Record<string, string[]>>
}
export interface BuilderConfigInput {
/**
* If we have an external schema that we want to "merge into" our local Nexus schema definitions, we can
* configure it here.
*
* If you have more than one schema that needs merging, you can look into using graphql-tools to pre-merge
* into a single schema: https://www.graphql-tools.com/docs/schema-merging
*/
mergeSchema?: MergeSchemaConfig
/**
* Generated artifact settings. Set to false to disable all. Set to true to enable all and use default
* paths. Leave undefined for default behaviour of each artifact.
*/
outputs?:
| boolean
| {
/**
* TypeScript declaration file generation settings. This file contains types reflected off your source
* code. It is how Nexus imbues dynamic code with static guarantees.
*
* Defaults to being enabled when `process.env.NODE_ENV !== "production"`. Set to true to enable and
* emit into default path (see below). Set to false to disable. Set to a string to specify absolute path.
*
* The default path is node_modules/@types/nexus-typegen/index.d.ts. This is chosen because TypeScript
* will pick it up without any configuration needed by you. For more details about the @types system
* refer to https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#types-typeroots-and-types
*/
typegen?: boolean | string | ConfiguredTypegen
/**
* GraphQL SDL file generation toggle and location.
*
* Set to a string to enable and output to an absolute path. Set to true to enable at default path
* (schema.graphql in the current working directory) Set to false to disable
*
* Defaults to true in development and false otherwise.
*
* This file is not necessary but may be nice for teams wishing to review SDL in pull-requests or just
* generally transitioning from a schema-first workflow.
*/
schema?: boolean | string
}
/**
* Whether the schema & types are generated when the server starts. Default is !process.env.NODE_ENV ||
* process.env.NODE_ENV === "development"
*/
shouldGenerateArtifacts?: boolean
/** Register the Source Types */
sourceTypes?: SourceTypesConfigOptions
/**
* Adjust the Prettier options used while running prettier over the generated output.
*
* Can be an absolute path to a Prettier config file like .prettierrc or package.json with "prettier" field,
* or an object of Prettier options.
*
* If provided, you must have prettier available as an importable dep in your project.
*/
prettierConfig?: string | object
/**
* Manually apply a formatter to the generated content before saving, see the `prettierConfig` option if you
* want to use Prettier.
*/
formatTypegen?: TypegenFormatFn
/**
* Configures the default "nonNullDefaults" for the entire schema the type. Read more about how nexus
* handles nullability
*/
nonNullDefaults?: NonNullConfig
/** List of plugins to apply to Nexus, with before/after hooks executed first to last: before -> resolve -> after */
plugins?: NexusPlugin[]
/** Provide if you wish to customize the behavior of the schema printing. Otherwise, uses `printSchema` from graphql-js */
customPrintSchemaFn?: typeof printSchema
/** Customize and toggle on or off various features of Nexus. */
features?: NexusFeaturesInput
/**
* Path to the module where your context type is exported
*
* @example
* contextType: { module: path.join(__dirname, 'context.ts'), export: 'MyContextType' }
*/
contextType?: TypingImport
/**
* If we wish to override the "Root" type for the schema, we can do so by specifying the rootTypes option,
* which will replace the default roots of Query / Mutation / Subscription
*/
schemaRoots?: {
query?: GetGen<'allOutputTypes', string> | AllNexusOutputTypeDefs
mutation?: GetGen<'allOutputTypes', string> | AllNexusOutputTypeDefs
subscription?: GetGen<'allOutputTypes', string> | AllNexusOutputTypeDefs
}
}
export interface BuilderConfig extends Omit<BuilderConfigInput, 'nonNullDefaults' | 'features' | 'plugins'> {
nonNullDefaults: RequiredDeeply<BuilderConfigInput['nonNullDefaults']>
features: RequiredDeeply<BuilderConfigInput['features']>
plugins: RequiredDeeply<BuilderConfigInput['plugins']>
}
export type SchemaConfig = BuilderConfigInput & {
/**
* All of the GraphQL types. This is an any for simplicity of developer experience, if it's an object we get
* the values, if it's an array we flatten out the valid types, ignoring invalid ones.
*/
types: any
/**
* Whether we should process.exit after the artifacts are generated. Useful if you wish to explicitly
* generate the test artifacts at a certain stage in a startup or build process.
*
* @default false
*/
shouldExitAfterGenerateArtifacts?: boolean
/**
* Custom extensions, as [supported in
* graphql-js](https://github.com/graphql/graphql-js/blob/master/src/type/__tests__/extensions-test.js)
*/
extensions?: GraphQLSchemaConfig['extensions']
} & NexusGenPluginSchemaConfig
export interface TypegenInfo {
/** Headers attached to the generate type output */
headers: string[]
/** All imports for the source types / context */
imports: string[]
/** A map of all GraphQL types and what TypeScript types they should be represented by. */
sourceTypeMap: { [K in GetGen<'objectNames'>]?: string }
/** Info about where to import the context from */
contextTypeImport: TypingImport | undefined
/**
* The path to the nexus package for typegen.
*
* This setting is only necessary when nexus is being wrapped by another library/framework such that `nexus`
* is not expected to be a direct dependency at the application level.
*/
nexusSchemaImportId?: string
}
export type TypeToWalk =
| { type: 'input'; value: NexusShapedInput }
| { type: 'object'; value: NexusShapedOutput }
| { type: 'interface'; value: NexusInterfaceTypeConfig<any> }
export type DynamicInputFields = Record<string, DynamicInputMethodDef<string> | string>
export type DynamicOutputFields = Record<string, DynamicOutputMethodDef<string> | string>
export type DynamicOutputProperties = Record<string, DynamicOutputPropertyDef<string>>
export type TypeDef =
| GraphQLNamedType
| AllNexusNamedTypeDefs
| NexusExtendInputTypeDef<string>
| NexusExtendTypeDef<string>
export type DynamicBlockDef =
| DynamicInputMethodDef<string>
| DynamicOutputMethodDef<string>
| DynamicOutputPropertyDef<string>
export type NexusAcceptedTypeDef = TypeDef | DynamicBlockDef | NexusMeta
export type PluginBuilderLens = {
hasType: SchemaBuilder['hasType']
addType: SchemaBuilder['addType']
setConfigOption: SchemaBuilder['setConfigOption']
hasConfigOption: SchemaBuilder['hasConfigOption']
getConfigOption: SchemaBuilder['getConfigOption']
}
/**
* Builds all of the types, properly accounts for any using "mix". Since the enum types are resolved
* synchronously, these need to guard for circular references at this step, while fields will guard for it
* during lazy evaluation.
*/
export class SchemaBuilder {
/** All objects containing a NEXUS_BUILD / NEXUS_TYPE symbol */
private nexusMetaObjects = new Set()
/** Used to check for circular references. */
private buildingTypes = new Set()
/** The "final type" map contains all types as they are built. */
private finalTypeMap: Record<string, GraphQLNamedType> = {}
/**
* The "defined type" map keeps track of all of the types that were defined directly as `GraphQL*Type`
* objects, so we don't accidentally overwrite any.
*/
private definedTypeMap: Record<string, GraphQLNamedType> = {}
/**
* The "pending type" map keeps track of all types that were defined w/ GraphQL Nexus and haven't been
* processed into concrete types yet.
*/
private pendingTypeMap: Record<string, AllNexusNamedTypeDefs | null> = {}
/** All "extensions" to types (adding fields on types from many locations) */
private typeExtendMap: Record<string, NexusExtendTypeConfig<string>[] | null> = {}
/** All "extensions" to input types (adding fields on types from many locations) */
private inputTypeExtendMap: Record<string, NexusExtendInputTypeConfig<string>[] | null> = {}
/**
* When we encounter "named" types from graphql-js, we keep them separate from Nexus definitions. This way
* we can have Nexus definitions take precedence without worrying about conflicts, particularly when we're
* looking to override behavior from inherited types.
*/
private graphqlNamedTypeMap: Record<string, AllNexusNamedTypeDefs> = {}
/**
* If we're merging against a remote schema, the types from the schema are kept here, for fallbacks /
* merging when we're building the actual Schema
*/
private graphqlMergeSchemaMap: Record<string, AllNexusNamedTypeDefs> = {}
private dynamicInputFields: DynamicInputFields = {}
private dynamicOutputFields: DynamicOutputFields = {}
private dynamicOutputProperties: DynamicOutputProperties = {}
private plugins: NexusPlugin[] = []
/** All types that need to be traversed for children types */
private typesToWalk: TypeToWalk[] = []
/** Root type mapping information annotated on the type definitions */
private sourceTypings: SourceTypings = {}
/** Array of missing types */
private missingTypes: Record<string, MissingType> = {}
/** Methods we are able to access to read/modify builder state from plugins */
private builderLens: PluginBuilderLens
/** Created just before types are walked, this keeps track of all of the resolvers */
private onMissingTypeFns: Exclude<PluginConfig['onMissingType'], undefined>[] = []
/** Executed just before types are walked */
private onBeforeBuildFns: Exclude<PluginConfig['onBeforeBuild'], undefined>[] = []
/** Executed as the field resolvers are included on the field */
private onCreateResolverFns: Exclude<PluginConfig['onCreateFieldResolver'], undefined>[] = []
/** Executed as the field "subscribe" fields are included on the schema */
private onCreateSubscribeFns: Exclude<PluginConfig['onCreateFieldSubscribe'], undefined>[] = []
/** Executed after the schema is constructed, for any final verification */
private onAfterBuildFns: Exclude<PluginConfig['onAfterBuild'], undefined>[] = []
/** Executed after the object is defined, allowing us to add additional fields to the object */
private onObjectDefinitionFns: Exclude<PluginConfig['onObjectDefinition'], undefined>[] = []
/** Executed after the object is defined, allowing us to add additional fields to the object */
private onInputObjectDefinitionFns: Exclude<PluginConfig['onInputObjectDefinition'], undefined>[] = []
/** Called immediately after the field is defined, allows for using metadata to define the shape of the field. */
private onAddArgFns: Exclude<PluginConfig['onAddArg'], undefined>[] = []
/** Called immediately after the field is defined, allows for using metadata to define the shape of the field. */
private onAddOutputFieldFns: Exclude<PluginConfig['onAddOutputField'], undefined>[] = []
/** Called immediately after the field is defined, allows for using metadata to define the shape of the field. */
private onAddInputFieldFns: Exclude<PluginConfig['onAddInputField'], undefined>[] = []
/** The `schemaExtension` is created just after the types are walked, but before the fields are materialized. */
private _schemaExtension?: NexusSchemaExtension
private config: BuilderConfig
private get schemaExtension() {
/* istanbul ignore next */
if (!this._schemaExtension) {
throw new Error('Cannot reference schemaExtension before it is created')
}
return this._schemaExtension
}
constructor(config: BuilderConfigInput) {
this.config = setConfigDefaults(config)
/** This array of plugin is used to keep retro-compatibility w/ older versions of nexus */
this.plugins = this.config.plugins.length > 0 ? this.config.plugins : [fieldAuthorizePlugin()]
if (!this.plugins.find((f) => f.config.name === 'declarativeWrapping')) {
this.plugins.push(declarativeWrappingPlugin({ disable: true }))
}
this.builderLens = Object.freeze({
hasType: this.hasType,
addType: this.addType,
setConfigOption: this.setConfigOption,
hasConfigOption: this.hasConfigOption,
getConfigOption: this.getConfigOption,
})
if (config.mergeSchema) {
this.graphqlMergeSchemaMap = this.handleMergeSchema(config.mergeSchema)
}
}
setConfigOption = <K extends keyof BuilderConfigInput>(key: K, value: BuilderConfigInput[K]) => {
this.config = {
...this.config,
[key]: value,
}
}
hasConfigOption = (key: keyof BuilderConfigInput): boolean => {
return this.config.hasOwnProperty(key)
}
getConfigOption = <K extends keyof BuilderConfigInput>(key: K): BuilderConfigInput[K] => {
return this.config[key]
}
hasType = (typeName: string): boolean => {
return Boolean(
this.pendingTypeMap[typeName] ||
this.finalTypeMap[typeName] ||
this.graphqlNamedTypeMap[typeName] ||
this.graphqlMergeSchemaMap[typeName]
)
}
/**
* Add type takes a Nexus type, or a GraphQL type and pulls it into an internal "type registry". It also
* does an initial pass on any types that are referenced on the "types" field and pulls those in too, so
* you can define types anonymously, without exporting them.
*/
private addType = (typeDef: NexusAcceptedTypeDef) => {
if (isNexusDynamicInputMethod(typeDef)) {
this.dynamicInputFields[typeDef.name] = typeDef
return
}
if (isNexusDynamicOutputMethod(typeDef)) {
this.dynamicOutputFields[typeDef.name] = typeDef
return
}
if (isNexusDynamicOutputProperty(typeDef)) {
this.dynamicOutputProperties[typeDef.name] = typeDef
return
}
if (isNexusMeta(typeDef)) {
this.addToNexusMeta(typeDef)
return
}
// Don't worry about internal types.
if (typeDef.name?.startsWith('__')) {
return
}
if (isNexusExtendTypeDef(typeDef)) {
const typeExtensions = (this.typeExtendMap[typeDef.name] = this.typeExtendMap[typeDef.name] || [])
typeExtensions.push(typeDef.value)
this.typesToWalk.push({ type: 'object', value: typeDef.value })
return
}
if (isNexusExtendInputTypeDef(typeDef)) {
const typeExtensions = (this.inputTypeExtendMap[typeDef.name] =
this.inputTypeExtendMap[typeDef.name] || [])
typeExtensions.push(typeDef.value)
this.typesToWalk.push({ type: 'input', value: typeDef.value })
return
}
// Check the "defined" type map for existing Nexus types. We are able to conflict with external types,
// as we assume that locally defined types take precedence.
const existingType = this.pendingTypeMap[typeDef.name]
// If we already have a "Nexus" type, but it's not the same, trigger mark as an error,
// otherwise early exit
if (existingType) {
if (existingType !== typeDef) {
throw extendError(typeDef.name)
}
return
}
if (isNexusNamedTypeDef(typeDef)) {
if (isNexusNamedOuputTypeDef(typeDef) && typeDef.value.asNexusMethod) {
this.dynamicOutputFields[typeDef.value.asNexusMethod] = typeDef.name
}
if (isNexusNamedInputTypeDef(typeDef) && typeDef.value.asNexusMethod) {
this.dynamicInputFields[typeDef.value.asNexusMethod] = typeDef.name
}
if (isNexusScalarTypeDef(typeDef) && typeDef.value.sourceType) {
this.sourceTypings[typeDef.name] = typeDef.value.sourceType
}
}
// If it's a concrete GraphQL type, we handle it directly by convering the
// type to a Nexus structure, and capturing all of the referenced types
// while we're reconstructing.
if (isNamedType(typeDef)) {
// If we've already captured the named type, we can skip it
if (this.graphqlNamedTypeMap[typeDef.name]) {
return
}
// If we've used decorateType to wrap, then we can grab the types off
if (typeDef.extensions?.nexus) {
const { asNexusMethod, sourceType } = Object(typeDef.extensions.nexus)
if (asNexusMethod) {
if (isInputType(typeDef)) {
this.dynamicInputFields[asNexusMethod] = typeDef.name
}
if (isOutputType(typeDef)) {
this.dynamicOutputFields[asNexusMethod] = typeDef.name
}
}
if (sourceType) {
this.sourceTypings[typeDef.name] = sourceType
}
}
this.graphqlNamedTypeMap[typeDef.name] = this.handleNativeType(typeDef, {
captureLeafType: (t) => {
if (!this.graphqlNamedTypeMap[t.name] && t.name !== typeDef.name) {
this.addType(t)
}
},
})
if (typeDef.extensions?.nexus) {
this.addType(this.graphqlNamedTypeMap[typeDef.name])
}
return
}
this.pendingTypeMap[typeDef.name] = typeDef
if (isNexusInputObjectTypeDef(typeDef)) {
this.typesToWalk.push({ type: 'input', value: typeDef.value })
}
if (isNexusObjectTypeDef(typeDef)) {
this.typesToWalk.push({ type: 'object', value: typeDef.value })
}
if (isNexusInterfaceTypeDef(typeDef)) {
this.typesToWalk.push({ type: 'interface', value: typeDef.value })
}
}
addTypes(types: any) {
if (!types) {
return
}
if (isSchema(types)) {
if (this.config.mergeSchema?.schema === types) {
return
} else if (!this.config.mergeSchema) {
if (Object.keys(this.graphqlMergeSchemaMap).length) {
console.error(
new Error(
`It looks like you're trying to merge multiple GraphQL schemas.\n Please open a GitHub ticket with more info about your use case.`
)
)
}
this.graphqlMergeSchemaMap = this.handleMergeSchema({ schema: types })
} else {
this.addTypes(types.getTypeMap())
}
return
}
if (isNexusPlugin(types)) {
if (!this.plugins?.includes(types)) {
throw new Error(
`Nexus plugin ${types.config.name} was seen in the "types" config, but should instead be provided to the "plugins" array.`
)
}
return
}
if (
isNexusNamedTypeDef(types) ||
isNexusExtendTypeDef(types) ||
isNexusExtendInputTypeDef(types) ||
isNamedType(types) ||
isNexusDynamicInputMethod(types) ||
isNexusDynamicOutputMethod(types) ||
isNexusDynamicOutputProperty(types) ||
isNexusMeta(types)
) {
this.addType(types)
} else if (Array.isArray(types)) {
types.forEach((typeDef) => this.addTypes(typeDef))
} else if (isObject(types)) {
Object.keys(types).forEach((key) => this.addTypes(types[key]))
}
}
private addToNexusMeta(type: NexusMeta) {
if (this.nexusMetaObjects.has(type)) {
return
}
this.nexusMetaObjects.add(type)
if (isNexusMetaBuild(type)) {
const types = type[NEXUS_BUILD]()
this.addTypes(types)
}
if (isNexusMetaType(type)) {
this.addType(resolveNexusMetaType(type))
}
}
private walkTypes() {
let obj
while ((obj = this.typesToWalk.shift())) {
switch (obj.type) {
case 'input':
this.walkInputType(obj.value)
break
case 'interface':
this.walkInterfaceType(obj.value)
break
case 'object':
this.walkOutputType(obj.value)
break
default:
casesHandled(obj)
}
}
}
private beforeWalkTypes() {
this.plugins.forEach((obj, i) => {
if (!isNexusPlugin(obj)) {
throw new Error(`Expected a plugin in plugins[${i}], saw ${obj}`)
}
const { config: pluginConfig } = obj
if (pluginConfig.onInstall) {
// TODO(tim): remove anys/warning at 1.0
const installResult = pluginConfig.onInstall(this.builderLens) as any
if (Array.isArray(installResult?.types)) {
throw new Error(
`Nexus no longer supports a return value from onInstall, you should instead use the hasType/addType api (seen in plugin ${pluginConfig.name}). `
)
}
}
if (pluginConfig.onCreateFieldResolver) {
this.onCreateResolverFns.push(pluginConfig.onCreateFieldResolver)
}
if (pluginConfig.onCreateFieldSubscribe) {
this.onCreateSubscribeFns.push(pluginConfig.onCreateFieldSubscribe)
}
if (pluginConfig.onBeforeBuild) {
this.onBeforeBuildFns.push(pluginConfig.onBeforeBuild)
}
if (pluginConfig.onMissingType) {
this.onMissingTypeFns.push(pluginConfig.onMissingType)
}
if (pluginConfig.onAfterBuild) {
this.onAfterBuildFns.push(pluginConfig.onAfterBuild)
}
if (pluginConfig.onObjectDefinition) {
this.onObjectDefinitionFns.push(pluginConfig.onObjectDefinition)
}
if (pluginConfig.onAddOutputField) {
this.onAddOutputFieldFns.push(pluginConfig.onAddOutputField)
}
if (pluginConfig.onAddInputField) {
this.onAddInputFieldFns.push(pluginConfig.onAddInputField)
}
if (pluginConfig.onAddArg) {
this.onAddArgFns.push(pluginConfig.onAddArg)
}
if (pluginConfig.onInputObjectDefinition) {
this.onInputObjectDefinitionFns.push(pluginConfig.onInputObjectDefinition)
}
})
}
private beforeBuildTypes() {
this.onBeforeBuildFns.forEach((fn) => {
fn(this.builderLens)
if (this.typesToWalk.length > 0) {
this.walkTypes()
}
})
}
private checkForInterfaceCircularDependencies() {
const interfaces: Record<string, NexusInterfaceTypeConfig<any>> = {}
Object.keys(this.pendingTypeMap)
.map((key) => this.pendingTypeMap[key])
.filter(isNexusInterfaceTypeDef)
.forEach((type) => {
interfaces[type.name] = type.value
})
const alreadyChecked: Record<string, boolean> = {}
const walkType = (
obj: NexusInterfaceTypeConfig<any>,
path: string[],
visited: Record<string, boolean>
) => {
if (alreadyChecked[obj.name]) {
return
}
if (visited[obj.name]) {
if (obj.name === path[path.length - 1]) {
throw new Error(`GraphQL Nexus: Interface ${obj.name} can't implement itself`)
} else {
throw new Error(
`GraphQL Nexus: Interface circular dependency detected ${[
...path.slice(path.lastIndexOf(obj.name)),
obj.name,
].join(' -> ')}`
)
}
}
const definitionBlock = new InterfaceDefinitionBlock({
typeName: obj.name,
addInterfaces: (i) =>
i.forEach((config) => {
const name = typeof config === 'string' ? config : config.value.name
walkType(interfaces[name], [...path, obj.name], { ...visited, [obj.name]: true })
}),
addModification: () => {},
addField: () => {},
addDynamicOutputMembers: (block, wrapping) => this.addDynamicOutputMembers(block, 'walk', wrapping),
warn: () => {},
})
obj.definition(definitionBlock)
alreadyChecked[obj.name] = true
}
Object.keys(interfaces).forEach((name) => {
walkType(interfaces[name], [], {})
})
}
private buildNexusTypes() {
// If Query isn't defined, set it to null so it falls through to "missingType"
if (!this.pendingTypeMap.Query && !this.config.schemaRoots?.query && !this.typeExtendMap.Query) {
this.pendingTypeMap.Query = null as any
}
Object.keys(this.pendingTypeMap).forEach((key) => {
if (this.typesToWalk.length > 0) {
this.walkTypes()
}
// If we've already constructed the type by this point,
// via circular dependency resolution don't worry about building it.
if (this.finalTypeMap[key]) {
return
}
if (this.definedTypeMap[key]) {
throw extendError(key)
}
this.finalTypeMap[key] = this.getOrBuildType(key)
this.buildingTypes.clear()
})
Object.keys(this.typeExtendMap).forEach((key) => {
// If we haven't defined the type, assume it's an object type
if (this.typeExtendMap[key] !== null && !this.hasType(key)) {
this.buildObjectType({
name: key,
definition() {},
})
}
})
Object.keys(this.inputTypeExtendMap).forEach((key) => {
// If we haven't defined the type, assume it's an input object type
if (this.inputTypeExtendMap[key] !== null && !this.hasType(key)) {
this.buildInputObjectType({
name: key,
definition() {},
})
}
})
}
private createSchemaExtension() {
this._schemaExtension = new NexusSchemaExtension({
...this.config,
dynamicFields: {
dynamicInputFields: this.dynamicInputFields,
dynamicOutputFields: this.dynamicOutputFields,
dynamicOutputProperties: this.dynamicOutputProperties,
},
sourceTypings: this.sourceTypings,
})
}
getFinalTypeMap(): BuildTypes<any> {
this.beforeWalkTypes()
this.createSchemaExtension()
this.walkTypes()
this.beforeBuildTypes()
this.checkForInterfaceCircularDependencies()
this.buildNexusTypes()
return {
finalConfig: this.config,
typeMap: this.finalTypeMap,
schemaExtension: this.schemaExtension!,
missingTypes: this.missingTypes,
onAfterBuildFns: this.onAfterBuildFns,
}
}
private shouldMerge(typeName: string) {
if (!this.config.mergeSchema) {
return false
}
const { mergeTypes = ['Query', 'Mutation'] } = this.config.mergeSchema
return Boolean(mergeTypes === true || mergeTypes.includes(typeName))
}
private buildInputObjectType(config: NexusInputObjectTypeConfig<any>): GraphQLInputObjectType {
const fields: NexusInputFieldDef[] = []
const definitionBlock = new InputDefinitionBlock({
typeName: config.name,
addField: (field) => fields.push(this.addInputField(field)),
addDynamicInputFields: (block, wrapping) => this.addDynamicInputFields(block, wrapping),
warn: consoleWarn,
})
const externalNamedType = this.graphqlMergeSchemaMap[config.name]
if (this.shouldMerge(config.name) && isNexusInputObjectTypeDef(externalNamedType)) {
externalNamedType.value.definition(definitionBlock)
}
config.definition(definitionBlock)
this.onInputObjectDefinitionFns.forEach((fn) => {
fn(definitionBlock, config)
})
const extensions = this.inputTypeExtendMap[config.name]
if (extensions) {
extensions.forEach((extension) => {
extension.definition(definitionBlock)
})
}
this.inputTypeExtendMap[config.name] = null
const inputObjectTypeConfig: NexusGraphQLInputObjectTypeConfig = {
name: config.name,
fields: () => this.buildInputObjectFields(fields, inputObjectTypeConfig),
description: config.description,
extensions: {
...config.extensions,
nexus: new NexusInputObjectTypeExtension(config),
},
}
return this.finalize(new GraphQLInputObjectType(inputObjectTypeConfig))
}
private buildObjectType(config: NexusObjectTypeConfig<string>) {
const fields: NexusOutputFieldDef[] = []
const interfaces: Implemented[] = []
const modifications: Record<string, FieldModificationDef<any, any>> = {}
const definitionBlock = new ObjectDefinitionBlock({
typeName: config.name,
addField: (fieldDef) => fields.push(this.addOutputField(fieldDef)),
addInterfaces: (interfaceDefs) => interfaces.push(...interfaceDefs),
addModification: (modification) => (modifications[modification.field] = modification),
addDynamicOutputMembers: (block, wrapping) => this.addDynamicOutputMembers(block, 'build', wrapping),
warn: consoleWarn,
})
const externalNamedType = this.graphqlMergeSchemaMap[config.name]
if (this.shouldMerge(config.name) && isNexusObjectTypeDef(externalNamedType)) {
externalNamedType.value.definition(definitionBlock)
}
config.definition(definitionBlock)
this.onObjectDefinitionFns.forEach((fn) => {
fn(definitionBlock, config)
})
const extensions = this.typeExtendMap[config.name]
if (extensions) {
extensions.forEach((extension) => {
extension.definition(definitionBlock)
})
}
this.typeExtendMap[config.name] = null
if (config.sourceType) {
this.sourceTypings[config.name] = config.sourceType
}
const objectTypeConfig: NexusGraphQLObjectTypeConfig = {
name: config.name,
interfaces: () => this.buildInterfaceList(interfaces),
description: config.description,
fields: () =>
this.buildOutputFields(
fields,
objectTypeConfig,
this.buildInterfaceFields(objectTypeConfig, interfaces, modifications)
),
isTypeOf: (config as any).isTypeOf,
extensions: {
...config.extensions,
nexus: new NexusObjectTypeExtension(config),
},
}
return this.finalize(new GraphQLObjectType(objectTypeConfig))
}
private buildInterfaceType(config: NexusInterfaceTypeConfig<any>) {
const { name, description } = config
let resolveType: AbstractTypeResolver<string> | undefined = (config as any).resolveType
const fields: NexusOutputFieldDef[] = []
const interfaces: Implemented[] = []
const modifications: Record<string, FieldModificationDef<any, any>> = {}
const definitionBlock = new InterfaceDefinitionBlock({
typeName: config.name,
addField: (field) => fields.push(this.addOutputField(field)),
addInterfaces: (interfaceDefs) => interfaces.push(...interfaceDefs),
addModification: (modification) => (modifications[modification.field] = modification),
addDynamicOutputMembers: (block, wrapping) => this.addDynamicOutputMembers(block, 'build', wrapping),
warn: consoleWarn,
})
const externalNamedType = this.graphqlMergeSchemaMap[config.name]
if (this.shouldMerge(config.name) && isNexusInterfaceTypeDef(externalNamedType)) {
externalNamedType.value.definition(definitionBlock)
}
config.definition(definitionBlock)
if (config.sourceType) {
this.sourceTypings[config.name] = config.sourceType
}
const interfaceTypeConfig: NexusGraphQLInterfaceTypeConfig = {
name,
interfaces: () => this.buildInterfaceList(interfaces),
resolveType,
description,
fields: () =>
this.buildOutputFields(
fields,
interfaceTypeConfig,
this.buildInterfaceFields(interfaceTypeConfig, interfaces, modifications)
),
extensions: {
...config.extensions,
nexus: new NexusInterfaceTypeExtension(config),
},
}
return this.finalize(new GraphQLInterfaceType(interfaceTypeConfig))
}
private addOutputField(field: NexusOutputFieldDef): NexusOutputFieldDef {
this.onAddOutputFieldFns.forEach((fn) => {
const result = fn(field)
if (result) {
field = result
}
})
return field
}
private addInputField(field: NexusInputFieldDef): NexusInputFieldDef {
this.onAddInputFieldFns.forEach((fn) => {
const result = fn(field)
if (result) {
field = result
}
})
return field
}
private buildEnumType(config: NexusEnumTypeConfig<any>) {
const { members } = config
const values: GraphQLEnumValueConfigMap = {}
if (isArray(members)) {
members.forEach((m) => {
if (typeof m === 'string') {
values[m] = { value: m }
} else {
values[m.name] = {
value: typeof m.value === 'undefined' ? m.name : m.value,
deprecationReason: m.deprecation,
description: m.description,
extensions: {
...m.extensions,
nexus: m.extensions?.nexus ?? {},
},
}
}
})
} else {
Object.keys(members)
// members can potentially be a TypeScript enum.
// The compiled version of this enum will be the members object,
// numeric enums members also get a reverse mapping from enum values to enum names.
// In these cases we have to ensure we don't include these reverse mapping keys.
// See: https://www.typescriptlang.org/docs/handbook/enums.html
.filter((key) => isNaN(+key))
.forEach((key) => {
assertValidName(key)
values[key] = {
value: (members as Record<string, string | number | symbol>)[key],
}
})
}
if (!Object.keys(values).length) {
throw new Error(`GraphQL Nexus: Enum ${config.name} must have at least one member`)
}
if (config.sourceType) {
this.sourceTypings[config.name] = config.sourceType
}
return this.finalize(
new GraphQLEnumType({
name: config.name,
values: values,
description: config.description,
extensions: {
...config.extensions,
nexus: config.extensions?.nexus ?? {},
},
})
)
}
private buildUnionType(config: NexusUnionTypeConfig<any>) {
let members: UnionMembers | undefined
let resolveType: AbstractTypeResolver<string> | undefined = (config as any).resolveType
config.definition(
new UnionDefinitionBlock({
typeName: config.name,
addUnionMembers: (unionMembers) => (members = unionMembers),
})
)
if (config.sourceType) {
this.sourceTypings[config.name] = config.sourceType
}
return this.finalize(
new GraphQLUnionType({
name: config.name,
resolveType,
description: config.description,
types: () => this.buildUnionMembers(config.name, members),
extensions: {
...config.extensions,
nexus: config.extensions?.nexus ?? {},
},
})
)
}
private buildScalarType(config: NexusScalarTypeConfig<string>): GraphQLScalarType {
if (config.sourceType) {
this.sourceTypings[config.name] = config.sourceType
}
return this.finalize(
new GraphQLScalarType({
...config,
extensions: {
...config.extensions,
nexus: config.extensions?.nexus ?? {},
},
})
)
}
private finalize<T extends GraphQLNamedType>(type: T): T {
this.finalTypeMap[type.name] = type
return type
}
private missingType(typeName: string, fromObject: boolean = false): GraphQLNamedType {
invariantGuard(typeName)
if (this.onMissingTypeFns.length) {
for (let i = 0; i < this.onMissingTypeFns.length; i++) {
const fn = this.onMissingTypeFns[i]
const replacementType = fn(typeName, this.builderLens)
if (replacementType && replacementType.name) {
this.addType(replacementType)
return this.getOrBuildType(replacementType)
}
}
}
if (typeName === 'Query') {
return new GraphQLObjectType({
name: 'Query',
fields: {
ok: {
type: new GraphQLNonNull(GraphQLBoolean),
resolve: () => true,
},
},
})
}
if (!this.missingTypes[typeName]) {
this.missingTypes[typeName] = { fromObject }
}
this.addType(UNKNOWN_TYPE_SCALAR)
return this.getOrBuildType(UNKNOWN_TYPE_SCALAR)
}
private buildUnionMembers(unionName: string, members: UnionMembers | undefined) {
const unionMembers: GraphQLObjectType[] = []
/* istanbul ignore next */
if (!members) {
throw new Error(
`Missing Union members for ${unionName}.` +
`Make sure to call the t.members(...) method in the union blocks`
)
}
members.forEach((member) => {
unionMembers.push(this.getObjectType(member))
})
/* istanbul ignore next */
if (!unionMembers.length) {
throw new Error(`GraphQL Nexus: Union ${unionName} must have at least one member type`)
}
return unionMembers
}
private buildInterfaceList(interfaces: (string | NexusInterfaceTypeDef<any>)[]) {
const list: GraphQLInterfaceType[] = []
interfaces.forEach((i) => {
const type = this.getInterface(i)
list.push(type, ...graphql15InterfaceType(type).getInterfaces())
})
return Array.from(new Set(list))
}
private buildInterfaceFields(
forTypeConfig: NexusGraphQLObjectTypeConfig | NexusGraphQLInterfaceTypeConfig,
interfaces: (string | NexusInterfaceTypeDef<any>)[],
modifications: Record<string, FieldModificationDef<any, any>>
) {
const interfaceFieldsMap: GraphQLFieldConfigMap<any, any> = {}
interfaces.forEach((i) => {
const config = this.getInterface(i).toConfig()
Object.keys(config.fields).forEach((field) => {
const interfaceField = config.fields[field]
interfaceFieldsMap[field] = interfaceField
if (modifications[field]) {
// TODO(tim): Refactor this whole mess
const { type, field: _field, args, extensions, ...rest } = modifications[field]
const extensionConfig: NexusOutputFieldConfig<any, any> = hasNexusExtension(extensions?.nexus)
? extensions?.nexus?.config ?? {}
: {}
interfaceFieldsMap[field] = {
...interfaceFieldsMap[field],
...rest,
extensions: {
...interfaceField.extensions,
...extensions,
nexus: hasNexusExtension(interfaceField.extensions?.nexus)
? interfaceField.extensions?.nexus?.modify(extensionConfig)
: new NexusFieldExtension(extensionConfig),
},
}
if (typeof type !== 'undefined') {
let interfaceReplacement: GraphQLOutputType
if (isNexusWrappingType(type)) {
const { wrapping, namedType } = unwrapNexusDef(type)
interfaceReplacement = rewrapAsGraphQLType(
this.getOrBuildType(namedType as any),
wrapping as NexusFinalWrapKind[]
) as GraphQLOutputType
} else {
const { wrapping } = unwrapGraphQLDef(config.fields[field].type)
interfaceReplacement = rewrapAsGraphQLType(
this.getOutputType(type),
wrapping
) as GraphQLOutputType
}
interfaceFieldsMap[field].type = interfaceReplacement
}
if (typeof args !== 'undefined') {
interfaceFieldsMap[field].args = {
...this.buildArgs(args ?? {}, forTypeConfig, field),
...interfaceFieldsMap[field].args,
}
}
}
})
})
return interfaceFieldsMap
}
private buildOutputFields(
fields: NexusOutputFieldDef[],
typeConfig: NexusGraphQLInterfaceTypeConfig | NexusGraphQLObjectTypeConfig,
intoObject: GraphQLFieldConfigMap<any, any>
) {
fields.forEach((field) => {
intoObject[field.name] = this.buildOutputField(field, typeConfig)
})
return intoObject
}
private buildInputObjectFields(
fields: NexusInputFieldDef[],
typeConfig: NexusGraphQLInputObjectTypeConfig
): GraphQLInputFieldConfigMap {
const fieldMap: GraphQLInputFieldConfigMap = {}
fields.forEach((field) => {
fieldMap[field.name] = this.buildInputObjectField(field, typeConfig)
})
return fieldMap
}
private getNonNullDefault(
nonNullDefaultConfig: { nonNullDefaults?: NonNullConfig } | undefined,
kind: 'input' | 'output'
): boolean {
const { nonNullDefaults = {} } = nonNullDefaultConfig ?? {}
return nonNullDefaults[kind] ?? this.config.nonNullDefaults[kind] ?? false
}
private buildOutputField(
fieldConfig: NexusOutputFieldDef,
typeConfig: NexusGraphQLObjectTypeConfig | NexusGraphQLInterfaceTypeConfig
): GraphQLFieldConfig<any, any> {
if (!fieldConfig.type) {
/* istanbul ignore next */
throw new Error(`Missing required "type" field for ${typeConfig.name}.${fieldConfig.name}`)
}
const fieldExtension = new NexusFieldExtension(fieldConfig)
const nonNullDefault = this.getNonNullDefault(typeConfig.extensions?.nexus?.config, 'output')
const { namedType, wrapping } = unwrapNexusDef(fieldConfig.type)
const finalWrap = finalizeWrapping(nonNullDefault, wrapping, fieldConfig.wrapping)
const builderFieldConfig: Omit<NexusGraphQLFieldConfig, 'resolve' | 'subscribe'> = {
name: fieldConfig.name,
type: rewrapAsGraphQLType(
this.getOutputType(namedType as PossibleOutputType),
finalWrap
) as GraphQLOutputType,
args: this.buildArgs(fieldConfig.args || {}, typeConfig, fieldConfig.name),
description: fieldConfig.description,
deprecationReason: fieldConfig.deprecation,
extensions: {
...fieldConfig.extensions,
nexus: fieldExtension,
},
}
return {
resolve: this.makeFinalResolver(
{
builder: this.builderLens,
fieldConfig: builderFieldConfig,
parentTypeConfig: typeConfig as any, // TODO(tim): remove as any when we drop support for 14.x
schemaConfig: this.config,
schemaExtension: this.schemaExtension,
},
fieldConfig.resolve
),
subscribe: fieldConfig.subscribe,
...builderFieldConfig,
}
}
private makeFinalResolver(info: CreateFieldResolverInfo, resolver?: GraphQLFieldResolver<any, any>) {
const resolveFn = resolver || defaultFieldResolver
if (this.onCreateResolverFns.length) {
const toCompose = this.onCreateResolverFns.map((fn) => fn(info)).filter((f) => f) as MiddlewareFn[]
if (toCompose.length) {
return composeMiddlewareFns(toCompose, resolveFn)
}
}
return resolveFn
}
private buildInputObjectField(
fieldConfig: NexusInputFieldDef,
typeConfig: NexusGraphQLInputObjectTypeConfig
): GraphQLInputFieldConfig {
const nonNullDefault = this.getNonNullDefault(typeConfig.extensions?.nexus?.config, 'input')
const { namedType, wrapping } = unwrapNexusDef(fieldConfig.type)
const finalWrap = finalizeWrapping(nonNullDefault, wrapping, fieldConfig.wrapping)
return {
type: rewrapAsGraphQLType(
this.getInputType(namedType as PossibleInputType),
finalWrap
) as GraphQLInputType,
defaultValue: fieldConfig.default,
description: fieldConfig.description,
extensions: {
...fieldConfig.extensions,
nexus: fieldConfig.extensions?.nexus ?? {},
},
}
}
private buildArgs(
args: ArgsRecord,
typeConfig: NexusGraphQLObjectTypeConfig | NexusGraphQLInterfaceTypeConfig,
fieldName: string
): GraphQLFieldConfigArgumentMap {
const allArgs: GraphQLFieldConfigArgumentMap = {}
for (const [argName, arg] of Object.entries(args)) {
const nonNullDefault = this.getNonNullDefault(typeConfig.extensions?.nexus?.config, 'input')
let finalArgDef: NexusFinalArgConfig = {
...normalizeArgWrapping(arg).value,
fieldName,