diff --git a/.changeset/beige-toes-tease.md b/.changeset/beige-toes-tease.md new file mode 100644 index 0000000000..f10e87b484 --- /dev/null +++ b/.changeset/beige-toes-tease.md @@ -0,0 +1,8 @@ +--- +"@dataplan/pg": patch +--- + +Add new `RECORD_EXPRESSION` mode to PgCondition so +postgraphile-plugin-connection-filter doesn't need to rely on a hack to enable +filtering on composite columns. Also changes `PgCondition` signature to accept a +configuration object. diff --git a/grafast/dataplan-pg/src/steps/pgCondition.ts b/grafast/dataplan-pg/src/steps/pgCondition.ts index 3a40ea744b..7024d76317 100644 --- a/grafast/dataplan-pg/src/steps/pgCondition.ts +++ b/grafast/dataplan-pg/src/steps/pgCondition.ts @@ -1,4 +1,4 @@ -import { Modifier } from "grafast"; +import { inspect, Modifier } from "grafast"; import type { SQL } from "pg-sql2"; import { $$toSQL, sql } from "pg-sql2"; @@ -39,12 +39,24 @@ type PgConditionModeExists = { equals?: boolean; }; +type PgConditionModeRecordExpression = { + mode: "RECORD_EXPRESSION"; + /** + * BEWARE: this expression may be _repeated_ multiple times in the generated + * query, so it should be inexpensive and have no side effects. Intended for + * use with columns of a composite type, so expression should typically be + * wrapped in parenthesis, enabling `(my_table.my_column).my_attribute` + */ + expression: SQL; +}; + export type PgConditionResolvedMode = | { mode: "PASS_THRU" } | { mode: "AND" } | { mode: "OR" } | { mode: "NOT" } - | PgConditionModeExists; + | PgConditionModeExists + | PgConditionModeRecordExpression; export type PgConditionMode = | "PASS_THRU" @@ -53,6 +65,30 @@ export type PgConditionMode = | "NOT" | PgConditionResolvedMode; +interface PgConditionOptions { + /** @defaultValue `false` */ + isHaving?: boolean; + /** @defaultValue `"PASS_THRU"` */ + mode?: PgConditionMode; +} + +function resolveOptions( + isHavingOrOptions: PgConditionOptions | boolean | undefined, + maybeMode: PgConditionMode | undefined, +): PgConditionOptions { + if (typeof isHavingOrOptions === "boolean") { + return { isHaving: isHavingOrOptions, mode: maybeMode }; + } else if (isHavingOrOptions == null) { + return { mode: maybeMode }; + } else if (maybeMode !== undefined) { + throw new Error( + `Invalid call signature to PgCondition constructor, use \`new PgCondition(parent, options)\``, + ); + } else { + return isHavingOrOptions; + } +} + export class PgCondition< TParent extends PgConditionCapableParent = PgConditionCapableParent, > @@ -73,12 +109,16 @@ export class PgCondition< public readonly resolvedMode: PgConditionResolvedMode; private isHaving: boolean; + constructor(parent: TParent, options: PgConditionOptions); + constructor(parent: TParent, isHaving?: boolean, mode?: PgConditionMode); constructor( parent: TParent, - isHaving = false, - mode: PgConditionMode = "PASS_THRU", + isHavingOrOptions?: PgConditionOptions | boolean, + maybeMode?: PgConditionMode, ) { super(parent); + const options = resolveOptions(isHavingOrOptions, maybeMode); + const { isHaving = false, mode = "PASS_THRU" } = options; this.isHaving = isHaving; if (typeof mode === "string") { this.resolvedMode = { mode }; @@ -99,6 +139,14 @@ export class PgCondition< ); break; } + case "RECORD_EXPRESSION": { + this.alias = this.resolvedMode.expression; + break; + } + default: { + const never: never = this.resolvedMode; + throw new Error(`Unexpected mode ${inspect(never)}`); + } } } @@ -190,6 +238,9 @@ where ${sqlCondition}`})`; return sqlExists; } } + case "RECORD_EXPRESSION": { + return sqlCondition; + } default: { const never: never = this.resolvedMode; throw new Error(`Unhandled mode: ${(never as any).mode}`);