UNPKG

graphile-build

Version:

Build a GraphQL schema from plugins

478 lines 30.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.makeNewWithHooks = makeNewWithHooks; const grafast_1 = require("grafast"); const graphql_1 = require("grafast/graphql"); const util_1 = require("util"); const utils_ts_1 = require("../utils.js"); const isString = (str) => typeof str === "string"; const knownTypes = [ graphql_1.GraphQLSchema, graphql_1.GraphQLObjectType, graphql_1.GraphQLInterfaceType, graphql_1.GraphQLUnionType, graphql_1.GraphQLInputObjectType, graphql_1.GraphQLEnumType, graphql_1.GraphQLScalarType, ]; const knownTypeNames = knownTypes.map((k) => k.name); const identity = (0, utils_ts_1.EXPORTABLE)(() => function identity(value) { return value; }, []); /** * Returns a 'newWithHooks' function suitable for creating GraphQL types with * the graphile-build plugin system applied. */ function makeNewWithHooks({ builder }) { const newWithHooks = function newWithHooks(build, Type, inSpec, inScope) { if (!inScope) { // eslint-disable-next-line no-console console.warn(`No scope was provided to new ${Type.name}${"name" in inSpec ? `[name=${inSpec.name}]` : ``}, it's highly recommended that you add a scope so other hooks can easily reference your object - please check usage of 'newWithHooks'. To mute this message, just pass an empty object.`); } if (!Type) { throw new Error("No type specified!"); } if (knownTypes.indexOf(Type) === -1 && knownTypeNames.indexOf(Type.name) >= 0) { throw new Error(`GraphQL conflict for '${Type.name}' detected! Multiple versions of graphql exist in your node_` + /* yarn doctor */ `modules?`); } const Result = (() => { switch (Type) { case graphql_1.GraphQLSchema: { const rawSpec = inSpec; const scope = (inScope || Object.create(null)); const context = { type: "GraphQLSchema", scope, }; const finalSpec = builder.applyHooks("GraphQLSchema", rawSpec, build, context); finalSpec.types = builder.applyHooks("GraphQLSchema_types", [...(finalSpec.types ?? [])], build, { ...context, config: finalSpec, }); const Self = new graphql_1.GraphQLSchema(finalSpec); return Self; } case graphql_1.GraphQLObjectType: { const rawObjectSpec = inSpec; const scope = (inScope || Object.create(null)); const objectContext = { type: "GraphQLObjectType", scope, }; const { name: baseName, interfaces: baseInterfaces, fields: baseFields, planType, extensions, assertStep, ...restOfConfig } = builder.applyHooks("GraphQLObjectType", rawObjectSpec, build, objectContext, `|${rawObjectSpec.name}`); const typeName = build.assertValidName(baseName, `Attempted to define an object type with invalid name $0.`); const finalSpec = { name: typeName, ...(extensions || planType || assertStep ? { extensions: { ...extensions, ...(planType || assertStep ? { grafast: { ...extensions?.grafast, ...(planType ? { planType } : null), ...(assertStep ? { assertStep } : null), }, } : null), }, } : undefined), ...restOfConfig, interfaces: () => { const interfacesContext = { ...objectContext, Self, }; let rawInterfaces = baseInterfaces || []; if (typeof rawInterfaces === "function") { rawInterfaces = rawInterfaces(interfacesContext); } return builder.applyHooks("GraphQLObjectType_interfaces", rawInterfaces, build, interfacesContext, `|${typeName}`); }, fields: () => { const processedFields = []; const fieldWithHooks = (fieldScope, fieldSpec) => { if (!isString(fieldScope.fieldName)) { throw new Error("It looks like you forgot to pass the fieldName to `fieldWithHooks`, we're sorry this is currently necessary."); } const fieldName = build.assertValidName(fieldScope.fieldName, `Object type '$1' attempted to define a field with invalid name $0.`, [typeName]); build.extend(fieldScope, scope, "Adding the object type scope to the field's scope"); if (!fieldScope) { throw new Error("All calls to `fieldWithHooks` must specify a `fieldScope` " + "argument that gives additional context about the field so " + "that further plugins may more easily understand the field. " + "Keys within this object should contain the phrase 'field' " + "since they will be merged into the parent objects scope and " + "are not allowed to clash. If you really have no additional " + "information to give, please just pass `{}`."); } const fieldContext = { ...fieldsContext, scope: fieldScope, }; let resolvedFieldSpec = typeof fieldSpec === "function" ? fieldSpec(fieldContext) : fieldSpec; resolvedFieldSpec = builder.applyHooks("GraphQLObjectType_fields_field", resolvedFieldSpec, build, fieldContext, `|${typeName}.fields.${fieldName}`); resolvedFieldSpec.args = resolvedFieldSpec.args ?? Object.create(null); const argsContext = { ...fieldContext }; const finalFieldSpec = { ...resolvedFieldSpec, args: builder.applyHooks("GraphQLObjectType_fields_field_args", resolvedFieldSpec.args, build, argsContext, `|${typeName}.fields.${fieldName}.args`), }; for (const [rawArgName, argSpec] of Object.entries(finalFieldSpec.args)) { const argName = build.assertValidName(rawArgName, `Object type '$1' attempted to define an argument for field '$2' with invalid name $0.`, [typeName, fieldName]); const argContext = { ...argsContext, scope: { ...argsContext.scope, argName, }, }; finalFieldSpec.args[argName] = builder.applyHooks("GraphQLObjectType_fields_field_args_arg", argSpec, build, argContext, `|${typeName}.fields.${fieldName}.args.${argName}`); } processedFields.push(finalFieldSpec); return finalFieldSpec; }; const fieldsContext = { ...objectContext, Self: Self, fieldWithHooks, }; const rawFields = typeof baseFields === "function" ? baseFields(fieldsContext) : baseFields || {}; const fieldsSpec = builder.applyHooks("GraphQLObjectType_fields", build.extend(Object.create(null), rawFields, `Default field included in newWithHooks call for '${typeName}'. ${inScope.__origin || ""}`), build, fieldsContext, `|${typeName}`); // Finally, check through all the fields that they've all been // processed; any that have not we should do so now. for (const [fieldName, fieldSpec] of Object.entries(fieldsSpec)) { if (!fieldName) { throw new Error(`Attempted to add empty/falsy fieldName to GraphQLObjectType ${typeName}; ${(0, util_1.inspect)(fieldSpec)}`); } if (processedFields.indexOf(fieldSpec) < 0) { // We've not processed this yet; process it now! fieldsSpec[fieldName] = fieldsContext.fieldWithHooks( // We don't have any additional information { fieldName }, fieldSpec); } } return fieldsSpec; }, }; const Self = new graphql_1.GraphQLObjectType((0, grafast_1.objectSpec)(finalSpec)); return Self; } case graphql_1.GraphQLInterfaceType: { const rawInterfaceSpec = inSpec; const scope = (inScope || Object.create(null)); const interfaceContext = { type: "GraphQLInterfaceType", scope, }; const { name: baseName, fields: baseFields, interfaces: baseInterfaces, extensions, planType, toSpecifier, ...restOfConfig } = builder.applyHooks("GraphQLInterfaceType", rawInterfaceSpec, build, interfaceContext, `|${rawInterfaceSpec.name}`); const typeName = build.assertValidName(baseName, `Attempted to define an interface type with invalid name $0.`); const finalSpec = { name: typeName, ...(extensions || planType || toSpecifier ? { extensions: { ...extensions, ...(planType || toSpecifier ? { grafast: { ...extensions?.grafast, ...(planType ? { planType } : null), ...(toSpecifier ? { toSpecifier } : null), }, } : null), }, } : undefined), ...restOfConfig, fields: () => { const processedFields = []; const fieldsContext = { ...interfaceContext, Self, fieldWithHooks: (fieldScope, fieldSpec) => { if (!isString(fieldScope.fieldName)) { throw new Error("It looks like you forgot to pass the fieldName to `fieldWithHooks`, we're sorry this is currently necessary."); } const fieldName = build.assertValidName(fieldScope.fieldName, `Interface type '$1' attempted to define a field with invalid name $0.`, [typeName]); build.extend(fieldScope, scope, "Adding interface scope to interface's field scope"); if (!fieldScope) { throw new Error("All calls to `fieldWithHooks` must specify a `fieldScope` " + "argument that gives additional context about the field so " + "that further plugins may more easily understand the field. " + "Keys within this object should contain the phrase 'field' " + "since they will be merged into the parent objects scope and " + "are not allowed to clash. If you really have no additional " + "information to give, please just pass `{}`."); } const fieldContext = { ...fieldsContext, scope: fieldScope, }; let newSpec = typeof fieldSpec === "function" ? fieldSpec(fieldContext) : fieldSpec; newSpec = builder.applyHooks("GraphQLInterfaceType_fields_field", newSpec, build, fieldContext, `|${typeName}.fields.${fieldName}`); newSpec.args = newSpec.args || Object.create(null); const argsContext = { ...fieldContext, }; newSpec = { ...newSpec, args: builder.applyHooks("GraphQLInterfaceType_fields_field_args", newSpec.args ?? Object.create(null), build, argsContext, `|${typeName}.fields.${fieldName}.args`), }; const finalFieldSpec = newSpec; for (const [rawArgName, argSpec] of Object.entries(finalFieldSpec.args)) { const argName = build.assertValidName(rawArgName, `Interface type '$1' attempted to define an argument for field '$2' with invalid name $0.`, [typeName, fieldName]); const argContext = { ...argsContext, scope: { ...argsContext.scope, argName, }, }; const finalArgSpec = builder.applyHooks("GraphQLInterfaceType_fields_field_args_arg", argSpec, build, argContext, `|${typeName}.fields.${fieldName}.args.${argName}`); finalFieldSpec.args[argName] = finalArgSpec; } processedFields.push(finalFieldSpec); return finalFieldSpec; }, }; const rawFields = (typeof baseFields === "function" ? baseFields(fieldsContext) : baseFields) || {}; const fieldsSpec = builder.applyHooks("GraphQLInterfaceType_fields", build.extend(Object.create(null), rawFields, `Default field included in newWithHooks call for '${typeName}'. ${inScope.__origin || ""}`), build, fieldsContext, `|${typeName}`); // Finally, check through all the fields that they've all been processed; any that have not we should do so now. for (const [fieldName, fieldSpec] of Object.entries(fieldsSpec)) { if (!fieldName) { throw new Error(`Attempted to add empty/falsy fieldName to GraphQLInterfaceType ${typeName}; ${(0, util_1.inspect)(fieldSpec)}`); } if (processedFields.indexOf(fieldSpec) < 0) { // We've not processed this yet; process it now! fieldsSpec[fieldName] = fieldsContext.fieldWithHooks( // We don't have any additional information { fieldName }, fieldSpec); } } return fieldsSpec; }, interfaces: () => { const interfacesContext = { ...interfaceContext, Self, }; const rawInterfaces = (typeof baseInterfaces === "function" ? baseInterfaces(interfacesContext) : baseInterfaces) || []; const interfacesSpec = builder.applyHooks("GraphQLInterfaceType_interfaces", rawInterfaces, build, interfacesContext, `|${typeName}`); return interfacesSpec; }, }; const Self = new graphql_1.GraphQLInterfaceType(finalSpec); return Self; } case graphql_1.GraphQLUnionType: { const rawUnionSpec = inSpec; const scope = (inScope || Object.create(null)); const commonContext = { type: "GraphQLUnionType", scope, }; const { name: baseName, types: baseTypes, toSpecifier, planType, extensions, ...restOfConfig } = builder.applyHooks("GraphQLUnionType", rawUnionSpec, build, commonContext, `|${rawUnionSpec.name}`); const typeName = build.assertValidName(baseName, `Attempted to define an union type with invalid name $0.`); const finalSpec = { name: typeName, ...(extensions || planType || toSpecifier ? { extensions: { ...extensions, ...(planType || toSpecifier ? { grafast: { ...extensions?.grafast, ...(planType ? { planType } : null), ...(toSpecifier ? { toSpecifier } : null), }, } : null), }, } : undefined), ...restOfConfig, types: () => { const typesContext = { ...commonContext, Self, }; const rawTypes = (typeof baseTypes === "function" ? baseTypes(typesContext) : baseTypes) || []; return builder.applyHooks("GraphQLUnionType_types", rawTypes, build, typesContext, `|${typeName}`); }, }; const Self = new graphql_1.GraphQLUnionType(finalSpec); return Self; } case graphql_1.GraphQLInputObjectType: { const rawInputObjectSpec = inSpec; const scope = (inScope || Object.create(null)); const inputObjectContext = { type: "GraphQLInputObjectType", scope, }; const { name: baseName, fields: baseFields, ...restOfConfig } = builder.applyHooks("GraphQLInputObjectType", rawInputObjectSpec, build, inputObjectContext, `|${rawInputObjectSpec.name}`); const typeName = build.assertValidName(baseName, `Attempted to define an input object type with invalid name $0.`); const finalSpec = { name: typeName, ...restOfConfig, fields: () => { const processedFields = []; const fieldWithHooks = (fieldScope, spec) => { if (!isString(fieldScope.fieldName)) { throw new Error("It looks like you forgot to pass the fieldName to `fieldWithHooks`, we're sorry this is currently necessary."); } const fieldName = build.assertValidName(fieldScope.fieldName, `Input object type '$1' attempted to define a field with invalid name $0.`, [typeName]); const finalFieldScope = build.extend(fieldScope, scope, `Extending scope for field '${fieldName}' within context for GraphQLInputObjectType '${typeName}'`); const fieldContext = { ...fieldsContext, scope: finalFieldScope, }; let newSpec = typeof spec === "function" ? spec(fieldContext) : spec; newSpec = builder.applyHooks("GraphQLInputObjectType_fields_field", newSpec, build, fieldContext, `|${typeName}.fields.${fieldName}`); const finalSpec = newSpec; processedFields.push(finalSpec); return finalSpec; }; const fieldsContext = { ...inputObjectContext, Self, fieldWithHooks, }; const rawFields = (typeof baseFields === "function" ? baseFields(fieldsContext) : baseFields) || {}; const fieldsList = build.extend(Object.create(null), rawFields, `Default field included in newWithHooks call for '${typeName}'. ${inScope.__origin || ""}`); const fieldsSpec = builder.applyHooks("GraphQLInputObjectType_fields", fieldsList, build, fieldsContext, `|${typeName}`); // Finally, check through all the fields that they've all been processed; any that have not we should do so now. for (const [fieldName, fieldSpec] of Object.entries(fieldsSpec)) { if (!fieldName) { throw new Error(`Attempted to add empty/falsy fieldName to GraphQLInputObjectType ${typeName}; ${(0, util_1.inspect)(fieldSpec)}`); } if (processedFields.indexOf(fieldSpec) < 0) { // We've not processed this yet; process it now! fieldsSpec[fieldName] = fieldsContext.fieldWithHooks( // We don't have any additional information { fieldName }, fieldSpec); } fieldsSpec[fieldName] = (0, grafast_1.inputObjectFieldSpec)(fieldsSpec[fieldName], `${typeName}.${fieldName}`); } return fieldsSpec; }, }; const Self = new graphql_1.GraphQLInputObjectType(finalSpec); return Self; } case graphql_1.GraphQLScalarType: { const rawScalarSpec = inSpec; const scope = (inScope || Object.create(null)); const scalarContext = { type: "GraphQLScalarType", scope, }; const { name: baseName, parseValue: baseParseValue, parseLiteral: baseParseLiteral, ...restOfConfig } = builder.applyHooks("GraphQLScalarType", rawScalarSpec, build, scalarContext, `|${rawScalarSpec.name}`); const typeName = build.assertValidName(baseName, `Attempted to define a scalar type with invalid name $0.`); const finalSpec = { name: typeName, ...restOfConfig, parseValue: (() => { return baseParseValue ?? identity; })(), // parseLiteral in GraphQL defaults to a dynamic function; that's not // exportable... So we must handle this ourselves. parseLiteral: (() => { if (baseParseLiteral) { return baseParseLiteral; } const parseValue = baseParseValue ?? identity; return (0, utils_ts_1.EXPORTABLE)((parseValue, valueFromASTUntyped) => ((node, variables) => { return parseValue(valueFromASTUntyped(node, variables)); }), [parseValue, graphql_1.valueFromASTUntyped]); })(), }; const Self = new graphql_1.GraphQLScalarType(finalSpec); return Self; } case graphql_1.GraphQLEnumType: { const rawEnumConfig = inSpec; const scope = (inScope || Object.create(null)); const enumContext = { type: "GraphQLEnumType", scope, }; const { name: baseName, values: baseValues, ...restOfConfig } = builder.applyHooks("GraphQLEnumType", rawEnumConfig, build, enumContext, `|${rawEnumConfig.name}`); const typeName = build.assertValidName(baseName, `Attempted to define an enum type with invalid name $0.`); const finalSpec = { name: typeName, ...restOfConfig, values() { const valuesContext = { ...enumContext, Self, }; const rawValues = typeof baseValues === "function" ? baseValues() : baseValues; // Mark up the values with an origin const intermediateValues = build.extend(Object.create(null), rawValues, `Default field included in newWithHooks call for '${baseName}'. ${inScope.__origin || ""}`); const finalValues = builder.applyHooks("GraphQLEnumType_values", intermediateValues, build, valuesContext, `|${typeName}`); // Hook each value for (const [valueKey, value] of Object.entries(finalValues)) { const valueName = build.assertValidName(valueKey, `Enum type '$1' attempted to define a value with invalid name $0.`, [typeName]); const finalValueScope = build.extend({ valueName }, scope, `Extending scope for value '${valueName}' within context for GraphQLEnumType '${typeName}'`); const valueContext = { ...valuesContext, scope: finalValueScope, }; const newValue = builder.applyHooks("GraphQLEnumType_values_value", value, build, valueContext, `|${typeName}|${valueName}`); // TODO: remove this code const ext = newValue.extensions?.grafast; if (ext && ext.applyPlan) { throw new Error(`Enum value ${typeName}.${valueName} has applyPlan set; this property no longer does anything and should be removed.`); } finalValues[valueKey] = newValue; } return finalValues; }, }; const Self = new graphql_1.GraphQLEnumType(finalSpec); return Self; } default: { throw new Error(`Cannot handle ${Type}`); } } })(); if ((0, graphql_1.isNamedType)(Result)) { build.scopeByType.set(Result, inScope); } return Result; }; return { newWithHooks }; } //# sourceMappingURL=index.js.map