From e07061445a0847d654c308019d3615b7cef8b539 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 25 Sep 2024 13:44:56 +0100 Subject: [PATCH] Implement new split inferred/override behavior system --- .../src/plugins/PgAttributesPlugin.ts | 74 +++-- .../src/plugins/PgBasicsPlugin.ts | 28 +- .../src/plugins/PgConditionArgumentPlugin.ts | 10 +- .../plugins/PgConditionCustomFieldsPlugin.ts | 5 +- .../plugins/PgConnectionArgOrderByPlugin.ts | 18 +- .../src/plugins/PgCustomTypeFieldPlugin.ts | 5 +- .../PgFirstLastBeforeAfterArgsPlugin.ts | 8 +- .../PgInterfaceModeUnionAllRowsPlugin.ts | 19 +- .../src/plugins/PgMutationCreatePlugin.ts | 36 +- .../src/plugins/PgOrderCustomFieldsPlugin.ts | 5 +- .../src/plugins/PgPolymorphismPlugin.ts | 39 +-- .../src/plugins/PgProceduresPlugin.ts | 32 +- .../src/plugins/PgRelationsPlugin.ts | 15 +- .../src/plugins/PgTableNodePlugin.ts | 46 +-- .../src/plugins/PgTablesPlugin.ts | 80 ++--- graphile-build/graphile-build/src/behavior.ts | 307 ++++++++++++------ graphile-build/graphile-build/src/index.ts | 24 +- .../src/plugins/CommonBehaviorsPlugin.ts | 6 + .../postgraphile/src/presets/relay.ts | 152 ++++----- postgraphile/postgraphile/src/presets/v4.ts | 40 +-- 20 files changed, 524 insertions(+), 425 deletions(-) diff --git a/graphile-build/graphile-build-pg/src/plugins/PgAttributesPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgAttributesPlugin.ts index 49aac3d3d..93372585f 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgAttributesPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgAttributesPlugin.ts @@ -333,44 +333,46 @@ export const PgAttributesPlugin: GraphileConfig.Plugin = { entityBehavior: { pgCodecAttribute: { - provides: ["default"], - before: ["inferred", "override"], - callback(behavior, [codec, attributeName]) { - const behaviors = new Set([ - "select", - "base", - "update", - "insert", - "filterBy", - "orderBy", - ]); - const attribute = codec.attributes[attributeName]; - function walk(codec: PgCodec) { - if (codec.arrayOfCodec) { - behaviors.add("-condition:attribute:filterBy"); - behaviors.add(`-attribute:orderBy`); - walk(codec.arrayOfCodec); - } else if (codec.rangeOfCodec) { - behaviors.add(`-condition:attribute:filterBy`); - behaviors.add(`-attribute:orderBy`); - walk(codec.rangeOfCodec); - } else if (codec.domainOfCodec) { - // No need to add a behavior for domain - walk(codec.domainOfCodec); - } else if (codec.attributes) { - behaviors.add(`-condition:attribute:filterBy`); - behaviors.add(`-attribute:orderBy`); - } else if (codec.isBinary) { - // Never filter, not in condition plugin nor any other - behaviors.add(`-attribute:filterBy`); - behaviors.add(`-attribute:orderBy`); - } else { - // Done + inferred: { + provides: ["default"], + before: ["inferred", "override"], + callback(behavior, [codec, attributeName]) { + const behaviors = new Set([ + "select", + "base", + "update", + "insert", + "filterBy", + "orderBy", + ]); + const attribute = codec.attributes[attributeName]; + function walk(codec: PgCodec) { + if (codec.arrayOfCodec) { + behaviors.add("-condition:attribute:filterBy"); + behaviors.add(`-attribute:orderBy`); + walk(codec.arrayOfCodec); + } else if (codec.rangeOfCodec) { + behaviors.add(`-condition:attribute:filterBy`); + behaviors.add(`-attribute:orderBy`); + walk(codec.rangeOfCodec); + } else if (codec.domainOfCodec) { + // No need to add a behavior for domain + walk(codec.domainOfCodec); + } else if (codec.attributes) { + behaviors.add(`-condition:attribute:filterBy`); + behaviors.add(`-attribute:orderBy`); + } else if (codec.isBinary) { + // Never filter, not in condition plugin nor any other + behaviors.add(`-attribute:filterBy`); + behaviors.add(`-attribute:orderBy`); + } else { + // Done + } } - } - walk(attribute.codec); + walk(attribute.codec); - return [...behaviors, behavior]; + return [...behaviors, behavior]; + }, }, }, }, diff --git a/graphile-build/graphile-build-pg/src/plugins/PgBasicsPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgBasicsPlugin.ts index 18ccf3679..460c49c82 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgBasicsPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgBasicsPlugin.ts @@ -219,16 +219,12 @@ export const PgBasicsPlugin: GraphileConfig.Plugin = { }, entityBehavior: { pgCodec: { - after: ["default", "inferred"], - provides: ["override"], - callback(behavior, codec) { + override(behavior, codec) { return [behavior, getBehavior(codec.extensions)]; }, }, pgCodecAttribute: { - after: ["default", "inferred"], - provides: ["override"], - callback(behavior, [codec, attributeName]) { + override(behavior, [codec, attributeName]) { if (typeof attributeName !== "string") { throw new Error( `pgCodecAttribute no longer accepts (codec, attribute) - it now accepts (codec, attributeName). Please update your code. Sorry! (Changed in PostGraphile V5 alpha 13.)`, @@ -242,9 +238,7 @@ export const PgBasicsPlugin: GraphileConfig.Plugin = { }, }, pgResource: { - after: ["default", "inferred"], - provides: ["override"], - callback(behavior, resource) { + override(behavior, resource) { return [ behavior, getBehavior([resource.codec.extensions, resource.extensions]), @@ -252,9 +246,7 @@ export const PgBasicsPlugin: GraphileConfig.Plugin = { }, }, pgResourceUnique: { - after: ["default", "inferred"], - provides: ["override"], - callback(behavior, [resource, unique]) { + override(behavior, [resource, unique]) { return [ behavior, getBehavior([ @@ -266,9 +258,7 @@ export const PgBasicsPlugin: GraphileConfig.Plugin = { }, }, pgCodecRelation: { - after: ["default", "inferred"], - provides: ["override"], - callback(behavior, relationSpec) { + override(behavior, relationSpec) { return [ behavior, // The behavior is the relation behavior PLUS the remote table @@ -282,9 +272,7 @@ export const PgBasicsPlugin: GraphileConfig.Plugin = { }, }, pgCodecRef: { - after: ["default", "inferred"], - provides: ["override"], - callback(behavior, [codec, refName]) { + override(behavior, [codec, refName]) { const ref = codec.refs?.[refName]; if (!ref) { throw new Error(`Codec ${codec.name} has no ref '${refName}'`); @@ -296,9 +284,7 @@ export const PgBasicsPlugin: GraphileConfig.Plugin = { }, }, pgRefDefinition: { - after: ["default", "inferred"], - provides: ["override"], - callback(behavior, refSpec) { + override(behavior, refSpec) { return [behavior, getBehavior(refSpec.extensions)]; }, }, diff --git a/graphile-build/graphile-build-pg/src/plugins/PgConditionArgumentPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgConditionArgumentPlugin.ts index eefcc00e0..e22e8723d 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgConditionArgumentPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgConditionArgumentPlugin.ts @@ -61,10 +61,12 @@ export const PgConditionArgumentPlugin: GraphileConfig.Plugin = { entityBehavior: { pgCodec: ["select", "filter"], pgResource: { - provides: ["default"], - before: ["inferred", "override"], - callback(behavior, resource) { - return resource.parameters ? [behavior] : ["filter", behavior]; + inferred: { + provides: ["default"], + before: ["inferred", "override"], + callback(behavior, resource) { + return resource.parameters ? [behavior] : ["filter", behavior]; + }, }, }, }, diff --git a/graphile-build/graphile-build-pg/src/plugins/PgConditionCustomFieldsPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgConditionCustomFieldsPlugin.ts index 87da3a352..8bb2f67ef 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgConditionCustomFieldsPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgConditionCustomFieldsPlugin.ts @@ -65,10 +65,7 @@ export const PgConditionCustomFieldsPlugin: GraphileConfig.Plugin = { }, entityBehavior: { pgResource: { - provides: ["inferred"], - after: ["default"], - before: ["override"], - callback(behavior, entity) { + inferred(behavior, entity) { if (isSimpleScalarComputedColumnLike(entity)) { return [behavior, "-proc:filterBy"]; } else { diff --git a/graphile-build/graphile-build-pg/src/plugins/PgConnectionArgOrderByPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgConnectionArgOrderByPlugin.ts index ae8d0943f..5e67ce9ab 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgConnectionArgOrderByPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgConnectionArgOrderByPlugin.ts @@ -71,14 +71,16 @@ export const PgConnectionArgOrderByPlugin: GraphileConfig.Plugin = { entityBehavior: { pgCodec: "order", pgResource: { - provides: ["default"], - before: ["inferred", "override"], - callback(behavior, resource) { - if (resource.parameters) { - return behavior; - } else { - return ["order", behavior]; - } + inferred: { + provides: ["default"], + before: ["inferred", "override"], + callback(behavior, resource) { + if (resource.parameters) { + return behavior; + } else { + return ["order", behavior]; + } + }, }, }, }, diff --git a/graphile-build/graphile-build-pg/src/plugins/PgCustomTypeFieldPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgCustomTypeFieldPlugin.ts index 5b2590409..a0ace4d79 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgCustomTypeFieldPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgCustomTypeFieldPlugin.ts @@ -394,10 +394,7 @@ export const PgCustomTypeFieldPlugin: GraphileConfig.Plugin = { }, entityBehavior: { pgResource: { - provides: ["inferred"], - after: ["defaults"], - before: ["overrides"], - callback(behavior, entity) { + inferred(behavior, entity) { if (entity.parameters) { return [behavior, defaultProcSourceBehavior(entity)]; } else { diff --git a/graphile-build/graphile-build-pg/src/plugins/PgFirstLastBeforeAfterArgsPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgFirstLastBeforeAfterArgsPlugin.ts index 393b448f6..1a8f8fcda 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgFirstLastBeforeAfterArgsPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgFirstLastBeforeAfterArgsPlugin.ts @@ -39,13 +39,7 @@ export const PgFirstLastBeforeAfterArgsPlugin: GraphileConfig.Plugin = { schema: { entityBehavior: { - pgResource: { - provides: ["default"], - before: ["inferred", "override"], - callback(behavior) { - return ["resource:connection:backwards", behavior]; - }, - }, + pgResource: "resource:connection:backwards", }, hooks: { GraphQLObjectType_fields_field_args: commonFn, diff --git a/graphile-build/graphile-build-pg/src/plugins/PgInterfaceModeUnionAllRowsPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgInterfaceModeUnionAllRowsPlugin.ts index 67799750d..95f23a10c 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgInterfaceModeUnionAllRowsPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgInterfaceModeUnionAllRowsPlugin.ts @@ -69,14 +69,17 @@ export const PgInterfaceModeUnionAllRowsPlugin: GraphileConfig.Plugin = { schema: { entityBehavior: { pgCodec: { - provides: ["default"], - before: ["inferred", "override"], - callback(behavior, entity) { - if (entity.polymorphism?.mode === "union") { - return ["connection", "-list", behavior]; - } else { - return behavior; - } + inferred: { + provides: ["default"], + before: ["inferred"], + callback(behavior, entity) { + if (entity.polymorphism?.mode === "union") { + // TODO: explain why this exists! Also, why is it default and not inferred? + return ["connection", "-list", behavior]; + } else { + return behavior; + } + }, }, }, }, diff --git a/graphile-build/graphile-build-pg/src/plugins/PgMutationCreatePlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgMutationCreatePlugin.ts index 50fb19871..9ce292137 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgMutationCreatePlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgMutationCreatePlugin.ts @@ -97,23 +97,25 @@ export const PgMutationCreatePlugin: GraphileConfig.Plugin = { entityBehavior: { pgResource: { - provides: ["default"], - before: ["inferred", "override"], - callback(behavior, resource) { - const newBehavior: GraphileBuild.BehaviorString[] = [ - behavior, - "insert:resource:select", - ]; - if ( - !resource.parameters && - !!resource.codec.attributes && - !resource.codec.polymorphism && - !resource.codec.isAnonymous - ) { - newBehavior.unshift("insert"); - newBehavior.unshift("record"); - } - return newBehavior; + inferred: { + provides: ["default"], + before: ["inferred", "override"], + callback(behavior, resource) { + const newBehavior: GraphileBuild.BehaviorString[] = [ + behavior, + "insert:resource:select", + ]; + if ( + !resource.parameters && + !!resource.codec.attributes && + !resource.codec.polymorphism && + !resource.codec.isAnonymous + ) { + newBehavior.unshift("insert"); + newBehavior.unshift("record"); + } + return newBehavior; + }, }, }, }, diff --git a/graphile-build/graphile-build-pg/src/plugins/PgOrderCustomFieldsPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgOrderCustomFieldsPlugin.ts index 6d83eb759..0fe8b1277 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgOrderCustomFieldsPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgOrderCustomFieldsPlugin.ts @@ -68,10 +68,7 @@ export const PgOrderCustomFieldsPlugin: GraphileConfig.Plugin = { }, entityBehavior: { pgResource: { - provides: ["inferred"], - after: ["default"], - before: ["override"], - callback(behavior, resource) { + inferred(behavior, resource) { if (isSimpleScalarComputedColumnLike(resource)) { return [behavior, "-orderBy"]; } else { diff --git a/graphile-build/graphile-build-pg/src/plugins/PgPolymorphismPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgPolymorphismPlugin.ts index 0d5f162c7..a65967c36 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgPolymorphismPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgPolymorphismPlugin.ts @@ -657,24 +657,23 @@ export const PgPolymorphismPlugin: GraphileConfig.Plugin = { schema: { entityBehavior: { pgCodec: { - provides: ["default"], - before: ["inferred", "override"], - callback(behavior, codec) { - return [ - "select", - "table", - ...((!codec.isAnonymous - ? ["insert", "update"] - : []) as GraphileBuild.BehaviorString[]), - behavior, - ]; + inferred: { + provides: ["default"], + before: ["inferred", "override"], + callback(behavior, codec) { + return [ + "select", + "table", + ...((!codec.isAnonymous + ? ["insert", "update"] + : []) as GraphileBuild.BehaviorString[]), + behavior, + ]; + }, }, }, pgCodecRelation: { - provides: ["inferred"], - after: ["default", "PgRelationsPlugin"], - before: ["override"], - callback(behavior, entity, build) { + inferred(behavior, entity, build) { const { input: { pgRegistry: { pgRelations }, @@ -719,10 +718,7 @@ export const PgPolymorphismPlugin: GraphileConfig.Plugin = { }, }, pgCodecAttribute: { - provides: ["inferred"], - after: ["default"], - before: ["override"], - callback(behavior, [codec, attributeName], build) { + inferred(behavior, [codec, attributeName], build) { // If this is the primary key of a related table of a // `@interface mode:relational` table, then omit it from the schema const tbl = build.pgTableResource(codec); @@ -758,10 +754,7 @@ export const PgPolymorphismPlugin: GraphileConfig.Plugin = { }, }, pgResource: { - provides: ["inferred"], - after: ["default"], - before: ["override"], - callback(behavior, resource, build) { + inferred(behavior, resource, build) { // Disable insert/update/delete on relational tables const newBehavior = [behavior]; if ( diff --git a/graphile-build/graphile-build-pg/src/plugins/PgProceduresPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgProceduresPlugin.ts index 256e66644..eccfdbd79 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgProceduresPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgProceduresPlugin.ts @@ -689,21 +689,23 @@ export const PgProceduresPlugin: GraphileConfig.Plugin = { entityBehavior: { pgResource: { - provides: ["default"], - before: [ - // By running before this, we actually override it because it - // prefixes further back in the behavior chain - "PgFirstLastBeforeAfterArgsPlugin", - "inferred", - "override", - ], - callback(behavior, resource) { - if (resource.parameters) { - // Default to no backwards pagination for functions - return ["-resource:connection:backwards", behavior]; - } else { - return behavior; - } + inferred: { + provides: ["default"], + before: [ + // By running before this, we actually override it because it + // prefixes further back in the behavior chain + "PgFirstLastBeforeAfterArgsPlugin", + "inferred", + "override", + ], + callback(behavior, resource) { + if (resource.parameters) { + // Default to no backwards pagination for functions + return ["-resource:connection:backwards", behavior]; + } else { + return behavior; + } + }, }, }, }, diff --git a/graphile-build/graphile-build-pg/src/plugins/PgRelationsPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgRelationsPlugin.ts index 195912fbd..59451effe 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgRelationsPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgRelationsPlugin.ts @@ -541,10 +541,7 @@ export const PgRelationsPlugin: GraphileConfig.Plugin = { }, entityBehavior: { pgCodecRelation: { - provides: ["inferred"], - before: ["override"], - after: ["default"], - callback(behavior, entity): GraphileBuild.BehaviorString[] { + inferred(behavior, entity): GraphileBuild.BehaviorString[] { if (entity.isUnique) { return [ behavior, @@ -558,10 +555,7 @@ export const PgRelationsPlugin: GraphileConfig.Plugin = { }, }, pgCodecRef: { - provides: ["inferred"], - before: ["override"], - after: ["default"], - callback(behavior, [codec, refName]) { + inferred(behavior, [codec, refName]) { const ref = codec.refs?.[refName]; if (ref?.definition.singular) { return [ @@ -576,10 +570,7 @@ export const PgRelationsPlugin: GraphileConfig.Plugin = { }, }, pgRefDefinition: { - provides: ["inferred"], - before: ["override"], - after: ["default"], - callback(behavior, entity) { + inferred(behavior, entity) { if (entity.singular) { return [ behavior, diff --git a/graphile-build/graphile-build-pg/src/plugins/PgTableNodePlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgTableNodePlugin.ts index 3797d64c6..4c09a0404 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgTableNodePlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgTableNodePlugin.ts @@ -46,31 +46,33 @@ export const PgTableNodePlugin: GraphileConfig.Plugin = { }, entityBehavior: { pgCodec: { - provides: ["default"], - before: ["inferred", "override"], - callback(behavior, codec, build) { - const newBehavior = [behavior]; - if ( - !codec.isAnonymous && - !!codec.attributes && - (!codec.polymorphism || - codec.polymorphism.mode === "single" || - codec.polymorphism.mode === "relational") - ) { - const resource = build.pgTableResource( - codec as PgCodecWithAttributes, - ); - if (resource && resource.uniques?.length >= 1) { - if (codec.polymorphism) { - newBehavior.push("interface:node"); + inferred: { + provides: ["default"], + before: ["inferred"], + callback(behavior, codec, build) { + const newBehavior = [behavior]; + if ( + !codec.isAnonymous && + !!codec.attributes && + (!codec.polymorphism || + codec.polymorphism.mode === "single" || + codec.polymorphism.mode === "relational") + ) { + const resource = build.pgTableResource( + codec as PgCodecWithAttributes, + ); + if (resource && resource.uniques?.length >= 1) { + if (codec.polymorphism) { + newBehavior.push("interface:node"); + } else { + newBehavior.push("type:node"); + } } else { - newBehavior.push("type:node"); + // Meh } - } else { - // Meh } - } - return newBehavior; + return newBehavior; + }, }, }, }, diff --git a/graphile-build/graphile-build-pg/src/plugins/PgTablesPlugin.ts b/graphile-build/graphile-build-pg/src/plugins/PgTablesPlugin.ts index f8242d87e..9ecbb906a 100644 --- a/graphile-build/graphile-build-pg/src/plugins/PgTablesPlugin.ts +++ b/graphile-build/graphile-build-pg/src/plugins/PgTablesPlugin.ts @@ -602,53 +602,57 @@ export const PgTablesPlugin: GraphileConfig.Plugin = { }, entityBehavior: { pgCodec: { - provides: ["default"], - before: ["inferred", "override"], - callback(behavior, codec) { - if (codec.attributes) { + inferred: { + provides: ["default"], + before: ["inferred", "override"], + callback(behavior, codec) { + if (codec.attributes) { + const isUnloggedOrTemp = + codec.extensions?.pg?.persistence === "u" || + codec.extensions?.pg?.persistence === "t"; + return [ + "resource:select", + "table", + ...((!codec.isAnonymous + ? ["resource:insert", "resource:update", "resource:delete"] + : []) as GraphileBuild.BehaviorString[]), + behavior, + ...((isUnloggedOrTemp + ? [ + "-resource:select", + "-resource:insert", + "-resource:update", + "-resource:delete", + ] + : []) as GraphileBuild.BehaviorString[]), + ]; + } else { + return [behavior]; + } + }, + }, + }, + pgResource: { + inferred: { + provides: ["default"], + before: ["inferred", "override"], + callback(behavior, resource) { + const isFunction = !!resource.parameters; const isUnloggedOrTemp = - codec.extensions?.pg?.persistence === "u" || - codec.extensions?.pg?.persistence === "t"; + resource.extensions?.pg?.persistence === "u" || + resource.extensions?.pg?.persistence === "t"; return [ - "resource:select", - "table", - ...((!codec.isAnonymous - ? ["resource:insert", "resource:update", "resource:delete"] + ...((!isFunction && !isUnloggedOrTemp + ? ["resource:select"] : []) as GraphileBuild.BehaviorString[]), behavior, ...((isUnloggedOrTemp ? [ - "-resource:select", - "-resource:insert", - "-resource:update", - "-resource:delete", + "-resource:select -resource:insert -resource:update -resource:delete", ] : []) as GraphileBuild.BehaviorString[]), ]; - } else { - return [behavior]; - } - }, - }, - pgResource: { - provides: ["default"], - before: ["inferred", "override"], - callback(behavior, resource) { - const isFunction = !!resource.parameters; - const isUnloggedOrTemp = - resource.extensions?.pg?.persistence === "u" || - resource.extensions?.pg?.persistence === "t"; - return [ - ...((!isFunction && !isUnloggedOrTemp - ? ["resource:select"] - : []) as GraphileBuild.BehaviorString[]), - behavior, - ...((isUnloggedOrTemp - ? [ - "-resource:select -resource:insert -resource:update -resource:delete", - ] - : []) as GraphileBuild.BehaviorString[]), - ]; + }, }, }, }, diff --git a/graphile-build/graphile-build/src/behavior.ts b/graphile-build/graphile-build/src/behavior.ts index d7abc3f10..c69012d1f 100644 --- a/graphile-build/graphile-build/src/behavior.ts +++ b/graphile-build/graphile-build/src/behavior.ts @@ -1,4 +1,4 @@ -import { arraysMatch } from "grafast"; +import { arraysMatch, isDev } from "grafast"; import { orderedApply, sortedPlugins } from "graphile-config"; import { version } from "./version.js"; @@ -10,42 +10,73 @@ interface BehaviorSpec { } const NULL_BEHAVIOR: ResolvedBehavior = Object.freeze({ - behaviorString: "", + behaviorString: "" as GraphileBuild.BehaviorString, stack: Object.freeze([]), }); -const getEntityBehaviorHooks = (plugin: GraphileConfig.Plugin) => { +const getEntityBehaviorHooks = ( + plugin: GraphileConfig.Plugin, + type: "inferred" | "override", +) => { const val = plugin.schema?.entityBehavior; if (!val) return val; // These might not all be hooks, some might be strings. We need to convert the strings into hooks. - const entries = Object.entries(val); - let changed = false; - for (const entry of entries) { - const lhs = entry[1] as GraphileBuild.BehaviorString; + const result: { + [entityType in keyof GraphileBuild.BehaviorEntities]: GraphileBuild.EntityBehaviorHook; + } = Object.create(null); + for (const [entityType, rhs] of Object.entries(val)) { const isArrayOfStrings = - Array.isArray(lhs) && lhs.every((t) => typeof t === "string"); - if (isArrayOfStrings || typeof lhs === "string") { - const hook: Exclude< - NonNullable< - NonNullable["entityBehavior"] - >[keyof GraphileBuild.BehaviorEntities], - string - > = { - provides: ["default"], - before: ["inferred", "override"], - callback: isArrayOfStrings - ? (behavior) => [...lhs, behavior] - : (behavior) => [lhs, behavior], - }; - entry[1] = hook; - changed = true; + Array.isArray(rhs) && rhs.every((t) => typeof t === "string"); + if (isArrayOfStrings || typeof rhs === "string") { + if (type === "inferred") { + const hook: GraphileBuild.EntityBehaviorHook< + keyof GraphileBuild.BehaviorEntities + > = { + provides: ["default"], + before: ["inferred"], + callback: isArrayOfStrings + ? (behavior) => [...rhs, behavior] + : (behavior) => [rhs as GraphileBuild.BehaviorString, behavior], + }; + result[entityType as keyof GraphileBuild.BehaviorEntities] = hook; + } else { + // noop + } + } else if (Array.isArray(rhs)) { + if (type === "inferred") { + throw new Error( + `Behavior of '${entityType}' was specified as an array, but not every element of the array was a string (plugin: ${plugin.name})`, + ); + } else { + // noop + } + } else if (typeof rhs === "function") { + if (type === "inferred") { + const hook: GraphileBuild.EntityBehaviorHook< + keyof GraphileBuild.BehaviorEntities + > = { + provides: ["inferred"], + after: ["default"], + callback: rhs, + }; + result[entityType as keyof GraphileBuild.BehaviorEntities] = hook; + } else { + // noop + } + } else { + const hook = rhs[type]; + if (hook) { + result[entityType as keyof GraphileBuild.BehaviorEntities] = hook; + } } } - if (changed) { - return Object.fromEntries(entries) as any; - } else { - return val; - } + return result; +}; +const getEntityBehaviorInferredHooks = (plugin: GraphileConfig.Plugin) => { + return getEntityBehaviorHooks(plugin, "inferred"); +}; +const getEntityBehaviorOverrideHooks = (plugin: GraphileConfig.Plugin) => { + return getEntityBehaviorHooks(plugin, "override"); }; export type BehaviorDynamicMethods = { @@ -64,7 +95,17 @@ export class Behavior { private behaviorEntities: { [entityType in keyof GraphileBuild.BehaviorEntities]: { behaviorStrings: Record; - behaviorCallbacks: Array< + inferredBehaviorCallbacks: Array< + [ + source: string, + callback: ( + behavior: string, + entity: GraphileBuild.BehaviorEntities[entityType], + build: GraphileBuild.Build, + ) => string | string[], + ] + >; + overrideBehaviorCallbacks: Array< [ source: string, callback: ( @@ -152,48 +193,7 @@ export class Behavior { } } - const defaultBehaviorByEntityTypeCache = new Map< - keyof GraphileBuild.BehaviorEntities, - string - >(); - const getDefaultBehaviorFor = ( - entityType: keyof GraphileBuild.BehaviorEntities, - ) => { - if (!defaultBehaviorByEntityTypeCache.has(entityType)) { - const supportedBehaviors = new Set(); - - for (const [behaviorString, spec] of Object.entries( - this.behaviorRegistry, - )) { - if ( - spec.entities[entityType] || - true /* This ` || true` is because of inheritance (e.g. unique inherits from resource inherits from codec); it causes a headache if we factor it in */ - ) { - const parts = behaviorString.split(":"); - const l = parts.length; - for (let i = 0; i < l; i++) { - const subparts = parts.slice(i, l); - // We need to add all of the parent behaviors, e.g. `foo:bar:baz` - // should also add `bar:baz` and `baz` - supportedBehaviors.add(subparts.join(":")); - } - } - } - - // TODO: scope this on an entity basis - const defaultBehaviors = this.globalDefaultBehavior; - - const behaviorString = ( - [...supportedBehaviors].sort().join(" ") + - " " + - defaultBehaviors.behaviorString - ).trim(); - defaultBehaviorByEntityTypeCache.set(entityType, behaviorString); - return behaviorString; - } - return defaultBehaviorByEntityTypeCache.get(entityType)!; - }; - + /* plugins.unshift({ name: "_GraphileBuildBehaviorSystemApplyPreferencesPlugin", version, @@ -228,22 +228,11 @@ export class Behavior { ), }, }); + */ - const initialBehavior = resolvedPreset.schema?.defaultBehavior ?? ""; this.globalDefaultBehavior = this.resolveBehavior( null, - initialBehavior - ? { - behaviorString: initialBehavior, - stack: [ - { - source: "preset.schema.defaultBehavior", - prefix: initialBehavior, - suffix: "", - }, - ], - } - : { behaviorString: "", stack: [] }, + null, plugins.map((p) => [ `${p.name}.schema.globalBehavior`, p.schema?.globalBehavior, @@ -253,15 +242,31 @@ export class Behavior { orderedApply( plugins, - getEntityBehaviorHooks, + getEntityBehaviorInferredHooks, + (hookName, hookFn, plugin) => { + const entityType = hookName as keyof GraphileBuild.BehaviorEntities; + if (!this.behaviorEntities[entityType]) { + this.registerEntity(entityType); + } + const t = this.behaviorEntities[entityType]; + t.inferredBehaviorCallbacks.push([ + `${plugin.name}.schema.entityBehavior.${entityType}.inferred`, + hookFn, + ]); + }, + ); + + orderedApply( + plugins, + getEntityBehaviorOverrideHooks, (hookName, hookFn, plugin) => { const entityType = hookName as keyof GraphileBuild.BehaviorEntities; if (!this.behaviorEntities[entityType]) { this.registerEntity(entityType); } const t = this.behaviorEntities[entityType]; - t.behaviorCallbacks.push([ - `${plugin.name}.schema.entityBehavior.${entityType}`, + t.overrideBehaviorCallbacks.push([ + `${plugin.name}.schema.entityBehavior.${entityType}.override`, hookFn, ]); }, @@ -288,7 +293,8 @@ export class Behavior { this.behaviorEntityTypes.push(entityType); this.behaviorEntities[entityType] = { behaviorStrings: Object.create(null), - behaviorCallbacks: [], + inferredBehaviorCallbacks: [], + overrideBehaviorCallbacks: [], listCache: new Map(), cacheWithDefault: new Map(), cacheWithoutDefault: new Map(), @@ -359,7 +365,7 @@ export class Behavior { entityType: TEntityType, rawEntity: GraphileBuild.BehaviorEntities[TEntityType], applyDefaultBehavior = true, - ) { + ): ResolvedBehavior { this.assertEntity(entityType); const { cacheWithDefault, cacheWithoutDefault, listCache } = this.behaviorEntities[entityType]; @@ -372,13 +378,45 @@ export class Behavior { return existing; } const behaviorEntity = this.behaviorEntities[entityType]; - const behavior = this.resolveBehavior( + const inferredBehavior = this.resolveBehavior( entityType, - applyDefaultBehavior ? this.globalDefaultBehavior : NULL_BEHAVIOR, - behaviorEntity.behaviorCallbacks, + applyDefaultBehavior, + behaviorEntity.inferredBehaviorCallbacks, entity, this.build, ); + const overrideBehavior = this.resolveBehavior( + entityType, + applyDefaultBehavior, + behaviorEntity.overrideBehaviorCallbacks, + entity, + this.build, + ); + const defaultBehavior = this.getDefaultBehaviorFor(entityType); + const inferredBehaviorWithPreferencesApplied = multiplyBehavior( + defaultBehavior, + inferredBehavior.behaviorString, + entityType, + ); + const behaviorString = joinBehaviors([ + inferredBehaviorWithPreferencesApplied, + overrideBehavior.behaviorString, + ]); + const behavior: ResolvedBehavior = { + stack: [ + ...inferredBehavior.stack, + { + source: "__ApplyBehaviors__", + prefix: "", + suffix: `-* ${inferredBehaviorWithPreferencesApplied}`, + }, + ...overrideBehavior.stack, + ], + behaviorString, + toString() { + return behaviorString; + }, + }; cache.set(entity, behavior); return behavior; } @@ -421,7 +459,7 @@ export class Behavior { private resolveBehavior( entityType: keyof GraphileBuild.BehaviorEntities | null, - initialBehavior: ResolvedBehavior, + applyDefaultBehavior: boolean | null, // Misnomer; also allows strings or nothings callbacks: ReadonlyArray< [ @@ -434,7 +472,23 @@ export class Behavior { ] >, ...args: TArgs - ) { + ): ResolvedBehavior { + const defaultBehaviorFromPreset = + this.resolvedPreset.schema?.defaultBehavior ?? ""; + const initialBehavior = applyDefaultBehavior + ? this.globalDefaultBehavior + : applyDefaultBehavior === null + ? { + behaviorString: defaultBehaviorFromPreset, + stack: [ + { + source: "preset.schema.defaultBehavior", + prefix: defaultBehaviorFromPreset, + suffix: "", + }, + ], + } + : NULL_BEHAVIOR; let behaviorString = initialBehavior.behaviorString; const stack: Array = [...initialBehavior.stack]; @@ -472,7 +526,7 @@ export class Behavior { } return { stack, - behaviorString, + behaviorString: behaviorString as GraphileBuild.BehaviorString, toString() { return behaviorString; }, @@ -505,14 +559,58 @@ export class Behavior { /*entities[entityType] &&*/ stringMatches(bhv, behavior), ) ) { + ARGH(source, behavior, entityType); + /* console.trace( `Behavior '${behavior}' is not registered for entity type '${entityType}'; it's only expected to be used with '${Object.keys( this.behaviorRegistry[behavior].entities, ).join("', '")}'. (Source: ${source})`, ); + */ } } } + + _defaultBehaviorByEntityTypeCache = new Map< + keyof GraphileBuild.BehaviorEntities, + string + >(); + getDefaultBehaviorFor(entityType: keyof GraphileBuild.BehaviorEntities) { + if (!this._defaultBehaviorByEntityTypeCache.has(entityType)) { + const supportedBehaviors = new Set(); + + for (const [behaviorString, spec] of Object.entries( + this.behaviorRegistry, + )) { + if ( + spec.entities[entityType] || + true /* This ` || true` is because of inheritance (e.g. unique inherits from resource inherits from codec); it causes a headache if we factor it in */ + ) { + const parts = behaviorString.split(":"); + const l = parts.length; + for (let i = 0; i < l; i++) { + const subparts = parts.slice(i, l); + // We need to add all of the parent behaviors, e.g. `foo:bar:baz` + // should also add `bar:baz` and `baz` + supportedBehaviors.add(subparts.join(":")); + } + } + } + + // TODO: scope this on an entity basis + const defaultBehaviors = this.globalDefaultBehavior; + + const behaviorString = ( + [...supportedBehaviors].sort().join(" ") + + " " + + defaultBehaviors.behaviorString + ).trim(); + this._defaultBehaviorByEntityTypeCache.set(entityType, behaviorString); + return behaviorString; + } + return this._defaultBehaviorByEntityTypeCache.get(entityType)!; + } +} } /** @@ -600,10 +698,13 @@ function scopeMatches( export function joinBehaviors( strings: ReadonlyArray, -): string { +): GraphileBuild.BehaviorString { let str = ""; for (const string of strings) { if (string != null && string !== "") { + if (isDev && !isValidBehaviorString(string)) { + throw new Error(`'${string}' is not a valid behavior string`); + } if (str === "") { str = string; } else { @@ -611,7 +712,7 @@ export function joinBehaviors( } } } - return str; + return str as GraphileBuild.BehaviorString; } interface StackItem { @@ -622,7 +723,8 @@ interface StackItem { interface ResolvedBehavior { stack: ReadonlyArray; - behaviorString: string; + behaviorString: GraphileBuild.BehaviorString; + toString(): string; } function getCachedEntity( @@ -751,11 +853,18 @@ function multiplyBehavior( positive: prefEntry.positive && infEntry.positive, }); } + if (final.length === 0) { + console.warn( + `No matches for behavior '${infEntry.scope.join( + ":", + )}' - please ensure that this behavior is registered for entity type '${entityType}'`, + ); + } return final; }); const behaviorString = result .map((r) => `${r.positive ? "" : "-"}${r.scope.join(":")}`) .join(" "); - return behaviorString; + return behaviorString as GraphileBuild.BehaviorString; } diff --git a/graphile-build/graphile-build/src/index.ts b/graphile-build/graphile-build/src/index.ts index 42ef7c78c..afe307db2 100644 --- a/graphile-build/graphile-build/src/index.ts +++ b/graphile-build/graphile-build/src/index.ts @@ -703,6 +703,17 @@ export async function watchSchema( export { version } from "./version.js"; declare global { + namespace GraphileBuild { + type EntityBehaviorHook< + entityType extends keyof GraphileBuild.BehaviorEntities, + > = PluginHook< + ( + behavior: GraphileBuild.BehaviorString, + entity: GraphileBuild.BehaviorEntities[entityType], + build: GraphileBuild.Build, + ) => GraphileBuild.BehaviorString | GraphileBuild.BehaviorString[] + >; + } namespace GraphileConfig { interface Provides { default: true; @@ -880,15 +891,10 @@ declare global { [entityType in keyof GraphileBuild.BehaviorEntities]?: | GraphileBuild.BehaviorString | GraphileBuild.BehaviorString[] - | PluginHook< - ( - behavior: GraphileBuild.BehaviorString, - entity: GraphileBuild.BehaviorEntities[entityType], - build: GraphileBuild.Build, - ) => - | GraphileBuild.BehaviorString - | GraphileBuild.BehaviorString[] - >; + | { + inferred?: GraphileBuild.EntityBehaviorHook; + override?: GraphileBuild.EntityBehaviorHook; + }; }; hooks?: { diff --git a/graphile-build/graphile-build/src/plugins/CommonBehaviorsPlugin.ts b/graphile-build/graphile-build/src/plugins/CommonBehaviorsPlugin.ts index 33d6fec04..be05356ea 100644 --- a/graphile-build/graphile-build/src/plugins/CommonBehaviorsPlugin.ts +++ b/graphile-build/graphile-build/src/plugins/CommonBehaviorsPlugin.ts @@ -16,6 +16,7 @@ declare global { "interface:node": true; "type:node": true; + node: true; } } } @@ -55,6 +56,11 @@ export const CommonBehaviorsPlugin: GraphileConfig.Plugin = { description: "should this type implement the Node interface?", entities: [], }, + + node: { + description: "should this type implement the Node interface?", + entities: [], + }, }, }, }, diff --git a/postgraphile/postgraphile/src/presets/relay.ts b/postgraphile/postgraphile/src/presets/relay.ts index 333a9683b..77e92be5a 100644 --- a/postgraphile/postgraphile/src/presets/relay.ts +++ b/postgraphile/postgraphile/src/presets/relay.ts @@ -57,87 +57,89 @@ export const PgRelayPlugin: GraphileConfig.Plugin = { +nodeId:base \ `, entityBehavior: { - pgCodecAttribute(behavior, [codec, attributeName], build) { - const newBehavior = [behavior]; - const attr = codec.attributes[attributeName]; + pgCodecAttribute: { + inferred(behavior, [codec, attributeName], build) { + const newBehavior = [behavior]; + const attr = codec.attributes[attributeName]; - const resource = - codec.polymorphism?.mode === "union" - ? Object.values(build.input.pgRegistry.pgResources).find((r) => { - if (r.parameters) return false; - if (r.isVirtual) return false; - if (r.isUnique) return false; - if (r.uniques.length === 0) return false; - const name = codec.extensions?.tags?.name ?? codec.name; - const impl = r.codec.extensions?.tags?.implements; - const implArr = impl - ? Array.isArray(impl) - ? impl - : [impl] - : []; - if (!implArr.includes(name)) return false; - return true; - }) - : Object.values(build.input.pgRegistry.pgResources).find((r) => { - if (r.codec !== codec) return false; - if (r.parameters) return false; - if (r.isVirtual) return false; - if (r.isUnique) return false; - if (r.uniques.length === 0) return false; - return true; - }); - const pk = resource?.uniques.find((u) => u.isPrimary); + const resource = + codec.polymorphism?.mode === "union" + ? Object.values(build.input.pgRegistry.pgResources).find((r) => { + if (r.parameters) return false; + if (r.isVirtual) return false; + if (r.isUnique) return false; + if (r.uniques.length === 0) return false; + const name = codec.extensions?.tags?.name ?? codec.name; + const impl = r.codec.extensions?.tags?.implements; + const implArr = impl + ? Array.isArray(impl) + ? impl + : [impl] + : []; + if (!implArr.includes(name)) return false; + return true; + }) + : Object.values(build.input.pgRegistry.pgResources).find((r) => { + if (r.codec !== codec) return false; + if (r.parameters) return false; + if (r.isVirtual) return false; + if (r.isUnique) return false; + if (r.uniques.length === 0) return false; + return true; + }); + const pk = resource?.uniques.find((u) => u.isPrimary); - // If the column is a primary key, don't include it (since it will be in the NodeID instead) - if (pk?.attributes.includes(attributeName)) { - // Do not include this column in the schema (other than for create) - newBehavior.push(...RELAY_HIDDEN_COLUMN_BEHAVIORS); - } else { - // If the column is available via a singular relation, don't include the column itself - const relationsMap = build.input.pgRegistry.pgRelations[codec.name]; - const relations = relationsMap - ? (Object.values( - build.input.pgRegistry.pgRelations[codec.name] ?? {}, - ) as PgCodecRelation[]) - : []; - const singularRelationsUsingThisColumn = relations.filter((r) => { - // NOTE: We do this even if the end table is not visible, because - // otherwise making the end table visible would be a breaking schema - // change. Users should make sure these columns are hidden from the - // schema if they are also hiding the target table. - if (!r.isUnique) return false; - if (r.isReferencee) return false; - if (!r.localAttributes.includes(attributeName)) return false; - return true; - }); - if ( - singularRelationsUsingThisColumn.length > 0 && - !attr.codec.extensions?.isEnumTableEnum - ) { + // If the column is a primary key, don't include it (since it will be in the NodeID instead) + if (pk?.attributes.includes(attributeName)) { // Do not include this column in the schema (other than for create) newBehavior.push(...RELAY_HIDDEN_COLUMN_BEHAVIORS); + } else { + // If the column is available via a singular relation, don't include the column itself + const relationsMap = build.input.pgRegistry.pgRelations[codec.name]; + const relations = relationsMap + ? (Object.values( + build.input.pgRegistry.pgRelations[codec.name] ?? {}, + ) as PgCodecRelation[]) + : []; + const singularRelationsUsingThisColumn = relations.filter((r) => { + // NOTE: We do this even if the end table is not visible, because + // otherwise making the end table visible would be a breaking schema + // change. Users should make sure these columns are hidden from the + // schema if they are also hiding the target table. + if (!r.isUnique) return false; + if (r.isReferencee) return false; + if (!r.localAttributes.includes(attributeName)) return false; + return true; + }); + if ( + singularRelationsUsingThisColumn.length > 0 && + !attr.codec.extensions?.isEnumTableEnum + ) { + // Do not include this column in the schema (other than for create) + newBehavior.push(...RELAY_HIDDEN_COLUMN_BEHAVIORS); + } + } + const relations = ( + Object.values( + build.input.pgRegistry.pgRelations[codec.name] ?? {}, + ) as PgCodecRelation[] + ).filter((r) => !r.isReferencee && r.isUnique); + const isPartOfRelation = + !attr.codec.extensions?.isEnumTableEnum && + relations.some((r) => r.localAttributes.includes(attributeName)); + if (isPartOfRelation) { + // `nodeId:filterBy` handles this + newBehavior.push(`-attribute:filterBy`); + // `nodeId:insert` handles this + newBehavior.push(`-attribute:insert`); + // `nodeId:update` handles this + newBehavior.push(`-attribute:update`); + // `nodeId:base` handles this + newBehavior.push(`-attribute:base`); } - } - const relations = ( - Object.values( - build.input.pgRegistry.pgRelations[codec.name] ?? {}, - ) as PgCodecRelation[] - ).filter((r) => !r.isReferencee && r.isUnique); - const isPartOfRelation = - !attr.codec.extensions?.isEnumTableEnum && - relations.some((r) => r.localAttributes.includes(attributeName)); - if (isPartOfRelation) { - // `nodeId:filterBy` handles this - newBehavior.push(`-attribute:filterBy`); - // `nodeId:insert` handles this - newBehavior.push(`-attribute:insert`); - // `nodeId:update` handles this - newBehavior.push(`-attribute:update`); - // `nodeId:base` handles this - newBehavior.push(`-attribute:base`); - } - return newBehavior; + return newBehavior; + }, }, }, }, diff --git a/postgraphile/postgraphile/src/presets/v4.ts b/postgraphile/postgraphile/src/presets/v4.ts index fe991c0c6..1adae9b82 100644 --- a/postgraphile/postgraphile/src/presets/v4.ts +++ b/postgraphile/postgraphile/src/presets/v4.ts @@ -161,25 +161,27 @@ const makeV4Plugin = (options: V4Options): GraphileConfig.Plugin => { }, entityBehavior: { pgResource: "delete:resource:select", - pgCodecAttribute(behavior, [codec, attributeName]) { - const attribute = codec.attributes[attributeName]; - const underlyingCodec = - attribute.codec.domainOfCodec ?? attribute.codec; - const newBehavior = [behavior]; - if ( - underlyingCodec.arrayOfCodec || - underlyingCodec.isBinary || - underlyingCodec.rangeOfCodec - ) { - newBehavior.push("-attribute:orderBy"); - } - if ( - underlyingCodec.isBinary || - underlyingCodec.arrayOfCodec?.isBinary - ) { - newBehavior.push("-condition:attribute:filterBy"); - } - return newBehavior; + pgCodecAttribute: { + inferred(behavior, [codec, attributeName]) { + const attribute = codec.attributes[attributeName]; + const underlyingCodec = + attribute.codec.domainOfCodec ?? attribute.codec; + const newBehavior = [behavior]; + if ( + underlyingCodec.arrayOfCodec || + underlyingCodec.isBinary || + underlyingCodec.rangeOfCodec + ) { + newBehavior.push("-attribute:orderBy"); + } + if ( + underlyingCodec.isBinary || + underlyingCodec.arrayOfCodec?.isBinary + ) { + newBehavior.push("-condition:attribute:filterBy"); + } + return newBehavior; + }, }, }, },