UNPKG

grafast

Version:

Cutting edge GraphQL planning and execution engine

562 lines 28.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.makeGrafastSchema = makeGrafastSchema; const tslib_1 = require("tslib"); const graphql_1 = require("graphql"); const graphql = tslib_1.__importStar(require("graphql")); const utils_js_1 = require("./utils.js"); const { buildASTSchema, isEnumType, isInputObjectType, isInterfaceType, isObjectType, isScalarType, isUnionType, parse, } = graphql; /** * Takes a GraphQL schema definition in Interface Definition Language (IDL/SDL) * syntax and configs for the types in it and returns a GraphQL schema. */ function makeGrafastSchema(details) { const { typeDefs, plans: rawPlans, objects, unions, interfaces, inputObjects, scalars, enums, enableDeferStream = false, } = details; const document = typeof typeDefs === "string" ? parse(typeDefs) : Array.isArray(typeDefs) ? { kind: graphql.Kind.DOCUMENT, definitions: typeDefs.flatMap((t) => t.definitions), } : typeDefs; if (!document || document.kind !== "Document") { throw new Error("The first argument to makeGrafastSchema must be an object containing a `typeDefs` field; the value for this field should be a parsed GraphQL document, array of these, or a string."); } const astSchema = buildASTSchema(document, { enableDeferStream, }); let plans; if (rawPlans) { if (objects || unions || interfaces || inputObjects || scalars || enums) { throw new Error(`plans is deprecated and may not be specified alongside newer approaches`); } plans = rawPlans; } else { // Hackily convert the new format into the old format. We'll do away with // this in future, but for now it's the easiest way to ensure compatibility plans = {}; const assertLocation = (typeName, expectedLocation) => { const t = astSchema.getType(typeName); if (!t) { throw new Error(`You detailed '${expectedLocation}.${typeName}', but the '${typeName}' type does not exist in the schema.`); } const [description, attr] = (() => { if (isObjectType(t)) { return ["an object type", "objects"]; } else if (isInterfaceType(t)) { return ["an interface type", "interfaces"]; } else if (isUnionType(t)) { return ["a union type", "unions"]; } else if (isInputObjectType(t)) { return ["an input object type", "inputObjects"]; } else if (isScalarType(t)) { return ["a scalar type", "scalars"]; } else if (isEnumType(t)) { return ["an enum type", "enums"]; } else { throw new Error(`Type ${t} not understood`); } })(); if (expectedLocation !== attr) { throw new Error(`You defined '${t}' under '${expectedLocation}', but it is ${description} so it should be defined under '${attr}'.`); } return t; }; for (const [typeName, spec] of Object.entries(objects ?? {})) { const t = assertLocation(typeName, "objects"); const o = {}; plans[typeName] = o; const { plans: planResolvers = {}, ...rest } = spec; for (const [key, val] of Object.entries(rest)) { o[`__${key}`] = val; } for (const [key, val] of Object.entries(planResolvers)) { if (!t.getFields()[key]) { throw new Error(`Object type '${t}' has no field '${key}'.`); } o[key] = val; } } for (const [typeName, spec] of Object.entries(inputObjects ?? {})) { const t = assertLocation(typeName, "inputObjects"); const o = {}; plans[typeName] = o; const { plans: planResolvers = {}, ...rest } = spec; for (const [key, val] of Object.entries(rest)) { o[`__${key}`] = val; } for (const [key, val] of Object.entries(planResolvers)) { if (!t.getFields()[key]) { throw new Error(`Input object type '${t}' has no input field '${key}'.`); } o[key] = val; } } for (const [typeName, spec] of Object.entries(unions ?? {})) { assertLocation(typeName, "unions"); const o = {}; plans[typeName] = o; for (const [key, val] of Object.entries(spec)) { o[`__${key}`] = val; } } for (const [typeName, spec] of Object.entries(interfaces ?? {})) { assertLocation(typeName, "interfaces"); const o = {}; plans[typeName] = o; for (const [key, val] of Object.entries(spec)) { o[`__${key}`] = val; } } for (const [typeName, spec] of Object.entries(scalars ?? {})) { assertLocation(typeName, "scalars"); plans[typeName] = spec; } for (const [typeName, spec] of Object.entries(enums ?? {})) { const t = assertLocation(typeName, "enums"); const o = {}; plans[typeName] = o; const { values = {}, ...rest } = spec; if ("plans" in rest) { throw new Error(`Enum type '${t}' cannot have field plans, please use 'values'.`); } for (const [key, val] of Object.entries(rest)) { o[`__${key}`] = val; } for (const [key, val] of Object.entries(values)) { if (!t.getValues().find((v) => v.name === key)) { throw new Error(`Enum type '${t}' has no value '${key}'.`); } o[key] = val; } } } const schemaConfig = astSchema.toConfig(); const typeByName = new Map(); function mapType(type) { if (graphql.isNonNullType(type)) { return new graphql.GraphQLNonNull(mapType(type.ofType)); } else if (graphql.isListType(type)) { return new graphql.GraphQLList(mapType(type.ofType)); } else { const replacementType = typeByName.get(type.name); if (!replacementType) { throw new Error(`Failed to find replaced type '${type.name}'`); } return replacementType; } } for (const [typeName, _spec] of Object.entries(plans)) { const astTypeIndex = schemaConfig.types.findIndex((t) => t.name === typeName); const astType = schemaConfig.types[astTypeIndex]; if (!astType) { console.warn(`'plans' specified configuration for type '${typeName}', but that type was not present in the schema`); continue; } } const BUILT_IN_TYPE_NAMES = ["String", "Int", "Float", "Boolean", "ID"]; // Now mod the types const rawTypes = schemaConfig.types; schemaConfig.types = rawTypes.map((astType) => { const typeName = astType.name; const replacementType = (() => { if (typeName.startsWith("__") || BUILT_IN_TYPE_NAMES.includes(typeName)) { return astType; } if (isObjectType(astType)) { const rawConfig = astType.toConfig(); const objectPlan = plans[astType.name]; const rawFields = rawConfig.fields; const rawInterfaces = rawConfig.interfaces; const config = { ...rawConfig, extensions: { ...rawConfig.extensions, }, }; const ext = config.extensions; if (objectPlan) { for (const [fieldName, rawFieldSpec] of Object.entries(objectPlan)) { if (fieldName === "__assertStep") { (0, utils_js_1.exportNameHint)(rawFieldSpec, `${typeName}_assertStep`); ext.grafast ??= {}; config.extensions.grafast.assertStep = rawFieldSpec; continue; } else if (fieldName === "__planType") { (0, utils_js_1.exportNameHint)(rawFieldSpec, `${typeName}_planType`); ext.grafast ??= {}; config.extensions.grafast.planType = rawFieldSpec; continue; } else if (fieldName === "__isTypeOf") { (0, utils_js_1.exportNameHint)(rawFieldSpec, `${typeName}_isTypeOf`); config.isTypeOf = rawFieldSpec; continue; } else if (fieldName.startsWith("__")) { throw new Error(`Unsupported field name '${fieldName}'; perhaps you meant '__assertStep'?`); } const fieldSpec = rawFieldSpec; const field = rawFields[fieldName]; if (!field) { console.warn(`'plans' specified configuration for object type '${typeName}' field '${fieldName}', but that field was not present in the type`); continue; } if ("args" in fieldSpec && typeof fieldSpec.args === "object" && fieldSpec.args != null) { for (const [argName, _argSpec] of Object.entries(fieldSpec.args)) { const arg = field.args?.[argName]; if (!arg) { console.warn(`'plans' specified configuration for object type '${typeName}' field '${fieldName}' arg '${argName}', but that arg was not present on the field`); continue; } } } } } config.interfaces = function () { return rawInterfaces.map((t) => mapType(t)); }; config.fields = function () { const fields = Object.create(null); for (const [fieldName, rawFieldSpec] of Object.entries(rawFields)) { if (fieldName.startsWith("__")) { continue; } const fieldSpec = objectPlan && Object.hasOwn(objectPlan, fieldName) ? objectPlan[fieldName] : undefined; const fieldConfig = { ...rawFieldSpec, type: mapType(rawFieldSpec.type), }; fields[fieldName] = fieldConfig; if (fieldConfig.args) { for (const [_argName, arg] of Object.entries(fieldConfig.args)) { arg.type = mapType(arg.type); } } if (fieldSpec) { if (typeof fieldSpec === "function") { (0, utils_js_1.exportNameHint)(fieldSpec, `${typeName}_${fieldName}_plan`); // it's a plan fieldConfig.extensions.grafast = { plan: fieldSpec, }; } else { // it's a spec const grafastExtensions = Object.create(null); fieldConfig.extensions.grafast = grafastExtensions; if (typeof fieldSpec.resolve === "function") { (0, utils_js_1.exportNameHint)(fieldSpec.resolve, `${typeName}_${fieldName}_resolve`); fieldConfig.resolve = fieldSpec.resolve; } if (typeof fieldSpec.subscribe === "function") { (0, utils_js_1.exportNameHint)(fieldSpec.subscribe, `${typeName}_${fieldName}_subscribe`); fieldConfig.subscribe = fieldSpec.subscribe; } if (typeof fieldSpec.plan === "function") { (0, utils_js_1.exportNameHint)(fieldSpec.plan, `${typeName}_${fieldName}_plan`); grafastExtensions.plan = fieldSpec.plan; } if (typeof fieldSpec.subscribePlan === "function") { (0, utils_js_1.exportNameHint)(fieldSpec.subscribePlan, `${typeName}_${fieldName}_subscribePlan`); grafastExtensions.subscribePlan = fieldSpec.subscribePlan; } if (fieldConfig.args) { for (const [argName, arg] of Object.entries(fieldConfig.args)) { const argSpec = fieldSpec.args?.[argName]; if (typeof argSpec === "function") { const applyPlan = argSpec; (0, utils_js_1.exportNameHint)(applyPlan, `${typeName}_${fieldName}_${argName}_applyPlan`); Object.assign(arg.extensions, { grafast: { applyPlan }, }); } else if (typeof argSpec === "object" && argSpec !== null) { const { extensions, applyPlan, applySubscribePlan } = argSpec; if (extensions) { Object.assign(arg.extensions, extensions); } if (applyPlan || applySubscribePlan) { (0, utils_js_1.exportNameHint)(applyPlan, `${typeName}_${fieldName}_${argName}_applyPlan`); (0, utils_js_1.exportNameHint)(applySubscribePlan, `${typeName}_${fieldName}_${argName}_applySubscribePlan`); Object.assign(arg.extensions, { grafast: { applyPlan, applySubscribePlan, }, }); } } } } } } } return fields; }; return new graphql.GraphQLObjectType(config); } else if (isInputObjectType(astType)) { const rawConfig = astType.toConfig(); const config = { ...rawConfig, extensions: { ...rawConfig.extensions, grafast: { ...rawConfig.extensions?.grafast, }, }, }; const inputObjectPlan = plans[astType.name]; if (inputObjectPlan) { for (const [fieldName, fieldSpec] of Object.entries(inputObjectPlan)) { if (fieldName === "__baked") { config.extensions.grafast.baked = fieldSpec; continue; } if (config.extensions?.grafast?.baked) { (0, utils_js_1.exportNameHint)(config.extensions.grafast.baked, `${typeName}__baked`); } const field = rawConfig.fields[fieldName]; if (!field) { console.warn(`'plans' specified configuration for input object type '${typeName}' field '${fieldName}', but that field was not present in the type`); continue; } } } const rawFields = rawConfig.fields; config.fields = function () { const fields = Object.create(null); for (const [fieldName, rawFieldConfig] of Object.entries(rawFields)) { const fieldSpec = inputObjectPlan && Object.hasOwn(inputObjectPlan, fieldName) ? inputObjectPlan[fieldName] : undefined; const fieldConfig = { ...rawFieldConfig, type: mapType(rawFieldConfig.type), }; fields[fieldName] = fieldConfig; if (fieldSpec) { const grafastExtensions = Object.create(null); fieldConfig.extensions.grafast = grafastExtensions; if (typeof fieldSpec === "function") { (0, utils_js_1.exportNameHint)(fieldSpec, `${typeName}_${fieldName}_apply`); grafastExtensions.apply = fieldSpec; } else { const { apply, extensions } = fieldSpec; if (extensions) { Object.assign(fieldConfig.extensions, extensions); } if (apply) { (0, utils_js_1.exportNameHint)(fieldSpec.apply, `${typeName}_${fieldName}_apply`); Object.assign(grafastExtensions, { apply }); } } } } return fields; }; return new graphql.GraphQLInputObjectType(config); } else if (isInterfaceType(astType)) { const rawConfig = astType.toConfig(); const config = { ...rawConfig, }; const rawFields = rawConfig.fields; config.fields = function () { const fields = Object.create(null); for (const [fieldName, rawFieldSpec] of Object.entries(rawFields)) { const fieldConfig = { ...rawFieldSpec, type: mapType(rawFieldSpec.type), }; fields[fieldName] = fieldConfig; if (fieldConfig.args) { for (const [_argName, arg] of Object.entries(fieldConfig.args)) { arg.type = mapType(arg.type); } } } return fields; }; const rawInterfaces = rawConfig.interfaces; config.interfaces = function () { return rawInterfaces.map((t) => mapType(t)); }; const polyPlans = plans[astType.name]; if (polyPlans?.__resolveType) { (0, utils_js_1.exportNameHint)(polyPlans.__resolveType, `${typeName}_resolveType`); config.resolveType = polyPlans.__resolveType; } if (polyPlans?.__toSpecifier) { (0, utils_js_1.exportNameHint)(polyPlans.__toSpecifier, `${typeName}_toSpecifier`); config.extensions ??= Object.create(null); config.extensions.grafast ??= Object.create(null); config.extensions.grafast.toSpecifier = polyPlans.__toSpecifier; } if (polyPlans?.__planType) { (0, utils_js_1.exportNameHint)(polyPlans.__planType, `${typeName}_planType`); config.extensions ??= Object.create(null); config.extensions.grafast ??= Object.create(null); config.extensions.grafast.planType = polyPlans.__planType; } return new graphql.GraphQLInterfaceType(config); } else if (isUnionType(astType)) { const rawConfig = astType.toConfig(); const config = { ...rawConfig, }; const rawTypes = rawConfig.types; config.types = function () { return rawTypes.map((t) => mapType(t)); }; const polyPlans = plans[astType.name]; if (polyPlans?.__resolveType) { (0, utils_js_1.exportNameHint)(polyPlans.__resolveType, `${typeName}_resolveType`); config.resolveType = polyPlans.__resolveType; } if (polyPlans?.__toSpecifier) { (0, utils_js_1.exportNameHint)(polyPlans.__toSpecifier, `${typeName}_toSpecifier`); config.extensions ??= Object.create(null); config.extensions.grafast ??= Object.create(null); config.extensions.grafast.toSpecifier = polyPlans.__toSpecifier; } if (polyPlans?.__planType) { (0, utils_js_1.exportNameHint)(polyPlans.__planType, `${typeName}_planType`); config.extensions ??= Object.create(null); config.extensions.grafast ??= Object.create(null); config.extensions.grafast.planType = polyPlans.__planType; } return new graphql.GraphQLUnionType(config); } else if (isScalarType(astType)) { const rawConfig = astType.toConfig(); const config = { ...rawConfig, extensions: { ...rawConfig.extensions, }, }; const scalarPlan = plans[astType.name]; if (typeof scalarPlan?.serialize === "function") { (0, utils_js_1.exportNameHint)(scalarPlan.serialize, `${typeName}_serialize`); config.serialize = scalarPlan.serialize; } if (typeof scalarPlan?.parseValue === "function") { (0, utils_js_1.exportNameHint)(scalarPlan.parseValue, `${typeName}_parseValue`); config.parseValue = scalarPlan.parseValue; } if (typeof scalarPlan?.parseLiteral === "function") { (0, utils_js_1.exportNameHint)(scalarPlan.parseLiteral, `${typeName}_parseLiteral`); config.parseLiteral = scalarPlan.parseLiteral; } if (typeof scalarPlan?.plan === "function") { (0, utils_js_1.exportNameHint)(scalarPlan.plan, `${typeName}_plan`); config.extensions.grafast = { plan: scalarPlan.plan }; } return new graphql.GraphQLScalarType(config); } else if (isEnumType(astType)) { const rawConfig = astType.toConfig(); const config = { ...rawConfig, }; const enumPlan = plans[astType.name]; const enumValues = config.values; if (enumPlan) { for (const [enumValueName, enumValueSpec] of Object.entries(enumPlan)) { const enumValue = enumValues[enumValueName]; if (!enumValue) { console.warn(`'plans' specified configuration for enum type '${typeName}' value '${enumValueName}', but that value was not present in the type`); continue; } if (typeof enumValueSpec === "function") { (0, utils_js_1.exportNameHint)(enumValueSpec, `${typeName}_${enumValueName}_apply`); // It's a plan if (!enumValue.extensions) { enumValue.extensions = Object.create(null); } enumValue.extensions.grafast = { apply: enumValueSpec, }; } else if (typeof enumValueSpec === "object" && enumValueSpec != null) { // It's a full spec if (enumValueSpec.extensions) { (0, utils_js_1.exportNameHint)(enumValueSpec.extensions, `${typeName}_${enumValueName}_extensions`); Object.assign(enumValue.extensions, enumValueSpec.extensions); } if (enumValueSpec.apply) { (0, utils_js_1.exportNameHint)(enumValueSpec.apply, `${typeName}_${enumValueName}_apply`); enumValue.extensions.grafast = { apply: enumValueSpec.apply, }; } if ("value" in enumValueSpec) { enumValue.value = enumValueSpec.value; } } else { // It must be the value enumValue.value = enumValueSpec; } } } return new graphql.GraphQLEnumType(config); } else { const never = astType; throw new Error(`Unhandled type ${never}`); } })(); typeByName.set(typeName, replacementType); return replacementType; }); if (schemaConfig.query) { schemaConfig.query = mapType(schemaConfig.query); } if (schemaConfig.mutation) { schemaConfig.mutation = mapType(schemaConfig.mutation); } if (schemaConfig.subscription) { schemaConfig.subscription = mapType(schemaConfig.subscription); } if (schemaConfig.directives) { for (const directiveConfig of schemaConfig.directives) { for (const argConfig of directiveConfig.args) { argConfig.type = mapType(argConfig.type); } } } const schema = new graphql_1.GraphQLSchema(schemaConfig); const errors = graphql.validateSchema(schema); if (errors.length === 1) { throw errors[0]; } else if (errors.length > 1) { throw new AggregateError(errors, `Invalid schema; first few errors:\n${errors.slice(0, 5).join("\n")}`); } return schema; } //# sourceMappingURL=makeGrafastSchema.js.map