UNPKG

nexus

Version:

Scalable, strongly typed GraphQL schema development

646 lines (563 loc) 20.4 kB
import * as fs from 'fs' import { GraphQLEnumType, GraphQLInputObjectType, GraphQLInterfaceType, GraphQLInterfaceTypeConfig, GraphQLNamedType, GraphQLObjectType, GraphQLResolveInfo, GraphQLScalarType, GraphQLSchema, GraphQLType, GraphQLUnionType, isAbstractType, isEnumType, isInputObjectType, isInterfaceType, isObjectType, isScalarType, isSpecifiedScalarType, isUnionType, isWrappingType, specifiedScalarTypes, } from 'graphql' import * as Path from 'path' import { decorateType } from './definitions/decorateType' import { isNexusMetaType, NexusMetaType, resolveNexusMetaType } from './definitions/nexusMeta' import { AllNexusArgsDefs, AllNexusNamedTypeDefs, AllNexusTypeDefs, isNexusWrappingType, isNexusArgDef, AllNamedInputTypeDefs, } from './definitions/wrapping' import { Maybe, MissingType, NexusFeatures, NexusGraphQLSchema, NexusTypes, TypingImport, withNexusSymbol, } from './definitions/_types' export const isInterfaceField = (type: GraphQLObjectType, fieldName: string) => { return type.getInterfaces().some((i) => Boolean(i.getFields()[fieldName])) } // ---------------------------- /** Copied from graphql-js: */ /** * Given an invalid input string and a list of valid options, returns a filtered list of valid options sorted * based on their similarity with the input. */ export function suggestionList(input: string = '', options: string[] = []): string[] { var optionsByDistance = Object.create(null) var oLength = options.length var inputThreshold = input.length / 2 for (var i = 0; i < oLength; i++) { var distance = lexicalDistance(input, options[i]) var threshold = Math.max(inputThreshold, options[i].length / 2, 1) if (distance <= threshold) { optionsByDistance[options[i]] = distance } } return Object.keys(optionsByDistance).sort(function (a, b) { return optionsByDistance[a] - optionsByDistance[b] }) } /** * Computes the lexical distance between strings A and B. * * The "distance" between two strings is given by counting the minimum number of edits needed to transform * string A into string B. An edit can be an insertion, deletion, or substitution of a single character, or a * swap of two adjacent characters. * * Includes a custom alteration from Damerau-Levenshtein to treat case changes as a single edit which helps * identify mis-cased values with an edit distance of 1. * * This distance can be useful for detecting typos in input or sorting */ function lexicalDistance(aStr: string, bStr: string): number { if (aStr === bStr) { return 0 } let i: number let j: number const d: number[][] = [] const a = aStr.toLowerCase() const b = bStr.toLowerCase() const aLength = a.length const bLength = b.length // Any case change counts as a single edit if (a === b) { return 1 } for (i = 0; i <= aLength; i++) { d[i] = [i] } for (j = 1; j <= bLength; j++) { d[0][j] = j } for (i = 1; i <= aLength; i++) { for (j = 1; j <= bLength; j++) { var cost = a[i - 1] === b[j - 1] ? 0 : 1 d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost) if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) { d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost) } } } return d[aLength][bLength] } // ---------------------------- export function objValues<T>(obj: Record<string, T>): T[] { return Object.keys(obj).reduce((result: T[], key) => { result.push(obj[key]) return result }, []) } export function mapObj<T, R>(obj: Record<string, T>, mapper: (val: T, key: string, index: number) => R) { return Object.keys(obj).map((key, index) => mapper(obj[key], key, index)) } export function mapValues<T, R>(obj: Record<string, T>, mapper: (val: T, key: string, index: number) => R) { const result: Record<string, any> = {} Object.keys(obj).forEach((key, index) => (result[key] = mapper(obj[key], key, index))) return result } export function eachObj<T>(obj: Record<string, T>, iter: (val: T, key: string, index: number) => void) { Object.keys(obj).forEach((name, i) => iter(obj[name], name, i)) } export const isObject = (obj: any): boolean => obj !== null && typeof obj === 'object' export const assertAbsolutePath = (pathName: string, property: string) => { if (!Path.isAbsolute(pathName)) { throw new Error(`Expected path for "${property}" to be an absolute path, saw "${pathName}"`) } return pathName } export interface GroupedTypes { input: GraphQLInputObjectType[] interface: GraphQLInterfaceType[] object: GraphQLObjectType[] union: GraphQLUnionType[] enum: GraphQLEnumType[] scalar: Array<GraphQLScalarType & { asNexusMethod?: string }> } export function groupTypes(schema: GraphQLSchema) { const groupedTypes: GroupedTypes = { input: [], interface: [], object: [], union: [], enum: [], scalar: Array.from(specifiedScalarTypes), } const schemaTypeMap = schema.getTypeMap() Object.keys(schemaTypeMap) .sort() .forEach((typeName) => { if (typeName.startsWith('__')) { return } const type = schema.getType(typeName) if (isObjectType(type)) { groupedTypes.object.push(type) } else if (isInputObjectType(type)) { groupedTypes.input.push(type) } else if (isScalarType(type) && !isSpecifiedScalarType(type) && !isUnknownType(type)) { groupedTypes.scalar.push(type) } else if (isUnionType(type)) { groupedTypes.union.push(type) } else if (isInterfaceType(type)) { groupedTypes.interface.push(type) } else if (isEnumType(type)) { groupedTypes.enum.push(type) } }) return groupedTypes } export function isUnknownType(type: GraphQLNamedType) { return type.name === UNKNOWN_TYPE_SCALAR.name } export function firstDefined<T>(...args: Array<T | undefined>): T { for (let i = 0; i < args.length; i++) { const arg = args[i] if (typeof arg !== 'undefined') { return arg } } /* istanbul ignore next */ throw new Error('At least one of the values should be defined') } export function isPromiseLike(value: any): value is PromiseLike<any> { return Boolean(value && typeof value.then === 'function') } export const typeScriptFileExtension = /(\.d)?\.ts$/ function makeRelativePathExplicitlyRelative(path: string) { if (Path.isAbsolute(path)) return path if (path.startsWith('./')) return path return `./${path}` } function nixifyPathSlashes(path: string): string { return path.replace(/\\+/g, '/') } /** * Format a path so it is suitable to be used as a module import. * * - Implicitly relative is made explicitly relative - TypeScript file extension is stripped - Windows slashes * converted into *nix slashes * * Do not pass Node module IDs here as they will be treated as relative paths e.g. "react" "@types/react" etc. */ export function formatPathForModuleImport(path: string) { return nixifyPathSlashes(makeRelativePathExplicitlyRelative(path).replace(typeScriptFileExtension, '')) } export function relativePathTo(absolutePath: string, fromPath: string): string { const filename = Path.basename(absolutePath) const relative = Path.relative(Path.dirname(fromPath), Path.dirname(absolutePath)) return formatPathForModuleImport(Path.join(relative, filename)) } export interface PrintedGenTypingImportConfig { module: string default?: string bindings?: Array<string | [string, string]> // import { X } or import { X as Y } } export class PrintedGenTypingImport { constructor(readonly config: PrintedGenTypingImportConfig) {} } withNexusSymbol(PrintedGenTypingImport, NexusTypes.PrintedGenTypingImport) export function printedGenTypingImport(config: PrintedGenTypingImportConfig) { return new PrintedGenTypingImport(config) } export interface PrintedGenTypingConfig { name: string optional: boolean type: string description?: Maybe<string> imports?: PrintedGenTypingImport[] } export class PrintedGenTyping { constructor(protected config: PrintedGenTypingConfig) {} get imports() { return this.config.imports || [] } toString() { let str = `` if (this.config.description) { const descriptionLines = this.config.description .split('\n') .map((s) => s.trim()) .filter((s) => s) .map((s) => ` * ${s}`) .join('\n') str = `/**\n${descriptionLines}\n */\n` } const field = `${this.config.name}${this.config.optional ? '?' : ''}` str += `${field}: ${this.config.type}` return str } } withNexusSymbol(PrintedGenTyping, NexusTypes.PrintedGenTyping) export function printedGenTyping(config: PrintedGenTypingConfig) { return new PrintedGenTyping(config) } export function assertNoMissingTypes(schema: GraphQLSchema, missingTypes: Record<string, MissingType>) { const missingTypesNames = Object.keys(missingTypes) const schemaTypeMap = schema.getTypeMap() const schemaTypeNames = Object.keys(schemaTypeMap).filter( (typeName) => !isUnknownType(schemaTypeMap[typeName]) ) if (missingTypesNames.length > 0) { const errors = missingTypesNames .map((typeName) => { const { fromObject } = missingTypes[typeName] if (fromObject) { return `- Looks like you forgot to import ${typeName} in the root "types" passed to Nexus makeSchema` } const suggestions = suggestionList(typeName, schemaTypeNames) let suggestionsString = '' if (suggestions.length > 0) { suggestionsString = ` or mean ${suggestions.join(', ')}` } return `- Missing type ${typeName}, did you forget to import a type to the root query${suggestionsString}?` }) .join('\n') throw new Error('\n' + errors) } } export function runAbstractTypeRuntimeChecks(schema: NexusGraphQLSchema, features: NexusFeatures) { if (features.abstractTypeRuntimeChecks === false) { return } const abstractTypes = Object.values(schema.getTypeMap()).filter(isAbstractType) abstractTypes.forEach((type) => { const kind = isInterfaceType(type) ? 'Interface' : 'Union' const resolveTypeImplemented = type.resolveType !== undefined const typesWithoutIsTypeOf = schema.getPossibleTypes(type).filter((type) => type.isTypeOf === undefined) // if no resolveType implemented but resolveType strategy enabled and isTypeOf strategy disabled if ( resolveTypeImplemented === false && features.abstractTypeStrategies.resolveType === true && features.abstractTypeStrategies.isTypeOf === false ) { const messagePrefix = `You have a faulty implementation for your ${kind.toLowerCase()} type "${ type.name }".` const message = `${messagePrefix} It is missing a \`resolveType\` implementation.` raiseProgrammerError(new Error(message)) } // if some isTypeOf implementations are missing but isTypeOf strategy enabled if ( typesWithoutIsTypeOf.length > 0 && features.abstractTypeStrategies.isTypeOf === true && features.abstractTypeStrategies.resolveType === false ) { const messageBadTypes = typesWithoutIsTypeOf.map((t) => `"${t.name}"`).join(', ') const messagePrefix = `You have a faulty implementation for your ${kind.toLowerCase()} type "${ type.name }".` const messageSuffix = `are missing an \`isTypeOf\` implementation: ${messageBadTypes}` let message if (kind === 'Union') { message = `${messagePrefix} Some members of union type "${type.name}" ${messageSuffix}` } else if (kind === 'Interface') { message = `${messagePrefix} Some objects implementing the interface type "${type.name}" ${messageSuffix}` } else { casesHandled(kind) } raiseProgrammerError(new Error(message)) } // if some isTypeOf or resolveType implementations are missing but isTypeOf and resolveType strategy enabled if ( (resolveTypeImplemented === false || typesWithoutIsTypeOf.length > 0) && features.abstractTypeStrategies.isTypeOf === true && features.abstractTypeStrategies.resolveType === true ) { const messageBadTypes = typesWithoutIsTypeOf.map((t) => `"${t.name}"`).join(', ') const messagePrefix = `You have a faulty implementation for your ${kind.toLowerCase()} type "${ type.name }". Either implement its \`resolveType\` or implement \`isTypeOf\` on each object` const messageSuffix = `These objects are missing an \`isTypeOf\` implementation: ${messageBadTypes}` let message if (kind === 'Union') { message = `${messagePrefix} in the union. ${messageSuffix}` } else if (kind === 'Interface') { message = `${messagePrefix} that implements this interface. ${messageSuffix}` } else { casesHandled(kind) } raiseProgrammerError(new Error(message)) } }) } export function consoleWarn(msg: string) { console.warn(msg) } export function log(msg: string) { console.log(`Nexus Schema: ${msg}`) } /** * Calculate the venn diagram between two iterables based on reference equality checks. The returned tripple * contains items thusly: * * * items only in arg 1 --> first tripple slot * * items in args 1 & 2 --> second tripple slot * * items only in arg 2 --> third tripple slot */ export function venn<T>(xs: Iterable<T>, ys: Iterable<T>): [Set<T>, Set<T>, Set<T>] { const lefts: Set<T> = new Set(xs) const boths: Set<T> = new Set() const rights: Set<T> = new Set(ys) lefts.forEach((l) => { if (rights.has(l)) { boths.add(l) lefts.delete(l) rights.delete(l) } }) return [lefts, boths, rights] } export const UNKNOWN_TYPE_SCALAR = decorateType( new GraphQLScalarType({ name: 'NEXUS__UNKNOWN__TYPE', description: ` This scalar should never make it into production. It is used as a placeholder for situations where GraphQL Nexus encounters a missing type. We don't want to error immediately, otherwise the TypeScript definitions will not be updated. `, parseValue(value) { throw new Error('Error: NEXUS__UNKNOWN__TYPE is not a valid scalar.') }, parseLiteral(value) { throw new Error('Error: NEXUS__UNKNOWN__TYPE is not a valid scalar.') }, serialize(value) { throw new Error('Error: NEXUS__UNKNOWN__TYPE is not a valid scalar.') }, }), { sourceType: 'never', } ) export function pathToArray(infoPath: GraphQLResolveInfo['path']): Array<string | number> { const flattened = [] let curr: GraphQLResolveInfo['path'] | undefined = infoPath while (curr) { flattened.push(curr.key) curr = curr.prev } return flattened.reverse() } export function getOwnPackage(): { name: string } { return require('../package.json') } /** Use this to make assertion at end of if-else chain that all members of a union have been accounted for. */ export function casesHandled(x: never): never { throw new Error(`A case was not handled for value: "${x}"`) } /** Quickly log objects */ export function dump(x: any) { console.log(require('util').inspect(x, { depth: null })) } function isNodeModule(path: string) { // Avoid treating absolute windows paths as Node packages e.g. D:/a/b/c return !Path.isAbsolute(path) && /^([A-z0-9@])/.test(path) } export function resolveImportPath(rootType: TypingImport, typeName: string, outputPath: string) { const rootTypePath = rootType.module if (typeof rootTypePath !== 'string' || (!Path.isAbsolute(rootTypePath) && !isNodeModule(rootTypePath))) { throw new Error( `Expected an absolute path or Node package for the root typing path of the type "${typeName}", saw "${rootTypePath}"` ) } if (isNodeModule(rootTypePath)) { try { require.resolve(rootTypePath) } catch (e) { throw new Error(`Module "${rootTypePath}" for the type "${typeName}" does not exist`) } } else if (!fs.existsSync(rootTypePath)) { throw new Error(`Root typing path "${rootTypePath}" for the type "${typeName}" does not exist`) } if (isNodeModule(rootTypePath)) { return rootTypePath } if (Path.isAbsolute(rootTypePath)) { return relativePathTo(rootTypePath, outputPath) } return rootTypePath } /** Given the right hand side of an arg definition, returns the underlying "named type" for us to add to the builder */ export function getArgNamedType(argDef: AllNexusArgsDefs | string): AllNamedInputTypeDefs | string { let finalValue = argDef if (typeof finalValue === 'string') { return finalValue } while (isNexusWrappingType(finalValue) || isWrappingType(finalValue) || isNexusArgDef(finalValue)) { if (isNexusArgDef(finalValue)) { finalValue = finalValue.value.type } else if (isNexusWrappingType(finalValue)) { finalValue = finalValue.ofNexusType } else if (isWrappingType(finalValue)) { finalValue = finalValue.ofType } } return finalValue } export function getNexusNamedType( type: AllNexusTypeDefs | NexusMetaType | GraphQLType | string ): AllNexusNamedTypeDefs | GraphQLNamedType | string { if (typeof type === 'string') { return type } let namedType = type while (isNexusWrappingType(namedType) || isWrappingType(namedType) || isNexusMetaType(namedType)) { if (isNexusWrappingType(namedType)) { namedType = namedType.ofNexusType } if (isWrappingType(namedType)) { namedType = namedType.ofType } if (isNexusMetaType(namedType)) { namedType = resolveNexusMetaType(namedType) } } return namedType as AllNexusNamedTypeDefs | GraphQLNamedType | string } /** Assertion utility with nexus-aware feedback for users. */ export function invariantGuard(val: any) { /* istanbul ignore next */ if (Boolean(val) === false) { throw new Error( 'Nexus Error: This should never happen, ' + 'please check your code or if you think this is a bug open a GitHub issue https://github.com/graphql-nexus/schema/issues/new.' ) } } /** Is the current stage production? If NODE_ENV envar is set to "production" or "prod" then yes it is. */ export function isProductionStage() { return process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'prod' } /** Throw a programmer error in production but only log it in development. */ export function raiseProgrammerError(error: Error) { if (isProductionStage()) { throw error } else { console.error(error) } } export class Unreachable extends Error { /* istanbul ignore next */ constructor(val: never) { super(`Unreachable case or branch, unexpected ${val}`) } } export function graphql15InterfaceConfig<T extends GraphQLInterfaceTypeConfig<any, any>>( config: T ): T & { interfaces: GraphQLInterfaceType[] } { return { ...config, interfaces: [], } } export function graphql15InterfaceType<T extends GraphQLInterfaceType>( type: T & { getInterfaces?: () => ReadonlyArray<GraphQLInterfaceType> } ): T & { getInterfaces(): ReadonlyArray<GraphQLInterfaceType> } { if (typeof type.getInterfaces !== 'function') { type.getInterfaces = () => [] } return type as T & { getInterfaces(): ReadonlyArray<GraphQLInterfaceType> } } /** @internal */ export function unpack<T extends object>(val: T | (() => T)): T { if (val instanceof Function) { return val() } return val } /** * A specially typed version of `Array.isArray` to work around [this * issue](https://github.com/microsoft/TypeScript/issues/17002). */ export function isArray<T>( arg: T | {} ): arg is T extends readonly any[] ? (unknown extends T ? never : readonly any[]) : any[] { return Array.isArray(arg) } export const ownProp = { has<O extends object, K extends keyof O>(obj: O, key: K): boolean { return Boolean(Object.getOwnPropertyDescriptor(obj, key)) }, set<O extends object, K extends keyof O>(obj: O, key: K, value: O[K]): O[K] { Object.defineProperty(obj, key, { value }) return value }, get<O extends object, K extends keyof O>(obj: O, key: K): O[K] | undefined { return Object.getOwnPropertyDescriptor(obj, key)?.value }, } export function result<T>(val: T | (() => T)): T { if (val instanceof Function) { return val() } return val as T }