From 6e8e7a3873d7f897e422ee2921db34866ce48496 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 4 Mar 2025 15:59:09 +0000 Subject: [PATCH 1/6] Start addressing issues for V5 beta2 --- src/AddConnectionFilterOperatorPlugin.ts | 4 +- src/PgConnectionArgFilterAttributesPlugin.ts | 47 +++++---- ...nectionArgFilterBackwardRelationsPlugin.ts | 98 ++++++++++++------- ...nArgFilterCompositeTypeAttributesPlugin.ts | 5 +- ...ectionArgFilterComputedAttributesPlugin.ts | 35 ++++--- ...nnectionArgFilterForwardRelationsPlugin.ts | 26 +++-- ...nnectionArgFilterLogicalOperatorsPlugin.ts | 44 ++++++--- src/PgConnectionArgFilterOperatorsPlugin.ts | 86 +++++++--------- src/PgConnectionArgFilterPlugin.ts | 63 ++++++------ src/index.ts | 6 +- src/utils.ts | 30 +++--- 11 files changed, 253 insertions(+), 191 deletions(-) diff --git a/src/AddConnectionFilterOperatorPlugin.ts b/src/AddConnectionFilterOperatorPlugin.ts index c664750..242fa80 100644 --- a/src/AddConnectionFilterOperatorPlugin.ts +++ b/src/AddConnectionFilterOperatorPlugin.ts @@ -1,6 +1,6 @@ import type { GraphQLInputType } from "graphql"; import { $$filters } from "./interfaces"; -import { makeApplyPlanFromOperatorSpec } from "./PgConnectionArgFilterOperatorsPlugin"; +import { makeApplyFromOperatorSpec } from "./PgConnectionArgFilterOperatorsPlugin"; const { version } = require("../package.json"); @@ -103,7 +103,7 @@ export const AddConnectionFilterOperatorPlugin: GraphileConfig.Plugin = { { description, type, - applyPlan: makeApplyPlanFromOperatorSpec( + apply: makeApplyFromOperatorSpec( build, Self.name, filterName, diff --git a/src/PgConnectionArgFilterAttributesPlugin.ts b/src/PgConnectionArgFilterAttributesPlugin.ts index 03c0a79..5f27a5d 100644 --- a/src/PgConnectionArgFilterAttributesPlugin.ts +++ b/src/PgConnectionArgFilterAttributesPlugin.ts @@ -1,4 +1,8 @@ -import type { PgConditionStep, PgCodecWithAttributes } from "@dataplan/pg"; +import type { + PgCodecWithAttributes, + PgConditionCapableParent, +} from "@dataplan/pg"; +import { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); @@ -17,7 +21,7 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { const { inflection, connectionFilterOperatorsDigest, - dataplanPg: { PgConditionStep }, + dataplanPg: { PgCondition }, EXPORTABLE, } = build; const { @@ -47,7 +51,9 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { if (!digest) { continue; } - const OperatorsType = build.getTypeByName(digest.operatorsTypeName); + const OperatorsType = build.getTypeByName( + digest.operatorsTypeName + ) as GraphQLInputObjectType; if (!OperatorsType) { continue; } @@ -66,26 +72,24 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { () => ({ description: `Filter by the object’s \`${fieldName}\` field.`, type: OperatorsType, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( ( - PgConditionStep, + PgCondition, colSpec, connectionFilterAllowEmptyObjectInput, connectionFilterAllowNullInput ) => - function ($where: PgConditionStep, fieldArgs: any) { - const $raw = fieldArgs.getRaw(); - if ($raw.evalIs(undefined)) { + function ( + queryBuilder: PgConditionCapableParent, + value: unknown + ) { + if (value === undefined) { return; } - if ( - !connectionFilterAllowEmptyObjectInput && - "evalIsEmpty" in $raw && - $raw.evalIsEmpty() - ) { + if (!connectionFilterAllowNullInput && value === null) { throw Object.assign( new Error( - "Empty objects are forbidden in filter argument input." + "Null literals are forbidden in filter argument input." ), { //TODO: mark this error as safe @@ -93,24 +97,25 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { ); } if ( - !connectionFilterAllowNullInput && - $raw.evalIs(null) + !connectionFilterAllowEmptyObjectInput && + value != null && + Object.keys(value).length === 0 ) { throw Object.assign( new Error( - "Null literals are forbidden in filter argument input." + "Empty objects are forbidden in filter argument input." ), { //TODO: mark this error as safe } ); } - const $col = new PgConditionStep($where); - $col.extensions.pgFilterAttribute = colSpec; - fieldArgs.apply($col); + const condition = new PgCondition(queryBuilder); + condition.extensions.pgFilterAttribute = colSpec; + return condition; }, [ - PgConditionStep, + PgCondition, colSpec, connectionFilterAllowEmptyObjectInput, connectionFilterAllowNullInput, diff --git a/src/PgConnectionArgFilterBackwardRelationsPlugin.ts b/src/PgConnectionArgFilterBackwardRelationsPlugin.ts index c603bae..a010166 100644 --- a/src/PgConnectionArgFilterBackwardRelationsPlugin.ts +++ b/src/PgConnectionArgFilterBackwardRelationsPlugin.ts @@ -1,11 +1,13 @@ import type { - PgConditionStep, + PgCondition, PgCodecRelation, PgCodecWithAttributes, PgRegistry, PgResource, + PgConditionCapableParent, } from "@dataplan/pg"; import { makeAssertAllowed } from "./utils"; +import { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); @@ -319,7 +321,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin inflection.filterType(foreignTableTypeName); const ForeignTableFilterType = build.getTypeByName( foreignTableFilterTypeName - ); + ) as GraphQLInputObjectType; if (!ForeignTableFilterType) continue; if (typeof foreignTable.from === "function") { @@ -338,8 +340,14 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin source.codec, foreignTable ); - const FilterManyType = - build.getTypeByName(filterManyTypeName); + const FilterManyType = build.getTypeByName( + filterManyTypeName + ) as GraphQLInputObjectType; + if (!FilterManyType) { + throw new Error( + `Failed to retrieve type '${filterManyTypeName}'` + ); + } // TODO: revisit using `_` prefixed inflector const fieldName = inflection._manyRelation({ registry: source.registry, @@ -360,7 +368,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `Filter by the object’s \`${fieldName}\` relation.`, type: FilterManyType, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( ( assertAllowed, foreignTable, @@ -369,10 +377,10 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin remoteAttributes ) => function ( - $where: PgConditionStep, - fieldArgs + $where: PgCondition, + value: object | null ) { - assertAllowed(fieldArgs, "object"); + assertAllowed(value, "object"); // $where.alias represents source; we need a condition that references the relational target const $rel = $where.andPlan(); $rel.extensions.pgFilterRelation = { @@ -381,7 +389,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin localAttributes, remoteAttributes, }; - fieldArgs.apply($rel); + return $rel; }, [ assertAllowed, @@ -417,7 +425,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin // and in PgConnectionArgFilterForwardRelationsPlugin // are very very similar. We should extract them to a // helper function. - applyPlan: EXPORTABLE( + apply: EXPORTABLE( ( assertAllowed, foreignTable, @@ -427,14 +435,15 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin sql ) => function ( - $where: PgConditionStep, - fieldArgs + $where: PgCondition, + value: boolean | null ) { - assertAllowed(fieldArgs, "scalar"); + assertAllowed(value, "scalar"); + if (value == null) return; const $subQuery = $where.existsPlan({ tableExpression: foreignTableExpression, alias: foreignTable.name, - $equals: fieldArgs.get(), + equals: value as boolean, }); localAttributes.forEach((localAttribute, i) => { const remoteAttribute = remoteAttributes[i]; @@ -483,7 +492,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `Filter by the object’s \`${fieldName}\` relation.`, type: ForeignTableFilterType, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( ( assertAllowed, foreignTable, @@ -492,8 +501,11 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin remoteAttributes, sql ) => - function ($where: PgConditionStep, fieldArgs) { - assertAllowed(fieldArgs, "object"); + function ( + $where: PgCondition, + value: object | null + ) { + assertAllowed(value, "object"); const $subQuery = $where.existsPlan({ tableExpression: foreignTableExpression, alias: foreignTable.name, @@ -508,7 +520,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin )}` ); }); - fieldArgs.apply($subQuery); + return $subQuery; }, [ assertAllowed, @@ -541,7 +553,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `A related \`${fieldName}\` exists.`, type: GraphQLBoolean, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( ( assertAllowed, foreignTable, @@ -551,14 +563,15 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin sql ) => function ( - $where: PgConditionStep, - fieldArgs + $where: PgCondition, + value: boolean | null ) { - assertAllowed(fieldArgs, "scalar"); + assertAllowed(value, "scalar"); + if (value == null) return; const $subQuery = $where.existsPlan({ tableExpression: foreignTableExpression, alias: foreignTable.name, - $equals: fieldArgs.get(), + equals: value, }); localAttributes.forEach((localAttribute, i) => { const remoteAttribute = remoteAttributes[i]; @@ -596,7 +609,14 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin ); const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName); - const FilterType = build.getTypeByName(foreignTableFilterTypeName); + const FilterType = build.getTypeByName( + foreignTableFilterTypeName + ) as GraphQLInputObjectType; + if (!FilterType) { + throw new Error( + `Failed to load type ${foreignTableFilterTypeName}` + ); + } const manyFields = { every: fieldWithHooks( @@ -607,10 +627,14 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `Every related \`${foreignTableTypeName}\` matches the filter criteria. All fields are combined with a logical ‘and.’`, type: FilterType, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( (assertAllowed, sql) => - function ($where: PgConditionStep, fieldArgs) { - assertAllowed(fieldArgs, "object"); + function ( + $where: PgCondition, + value: object | null + ) { + assertAllowed(value, "object"); + if (value == null) return; if (!$where.extensions.pgFilterRelation) { throw new Error( `Invalid use of filter, 'pgFilterRelation' expected` @@ -636,7 +660,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin )}` ); }); - fieldArgs.apply($subQuery.notPlan().andPlan()); + return $subQuery.notPlan().andPlan(); }, [assertAllowed, sql] ), @@ -650,10 +674,11 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `Some related \`${foreignTableTypeName}\` matches the filter criteria. All fields are combined with a logical ‘and.’`, type: FilterType, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( (assertAllowed, sql) => - function ($where: PgConditionStep, fieldArgs) { - assertAllowed(fieldArgs, "object"); + function ($where: PgCondition, value: object | null) { + assertAllowed(value, "object"); + if (value == null) return; if (!$where.extensions.pgFilterRelation) { throw new Error( `Invalid use of filter, 'pgFilterRelation' expected` @@ -679,7 +704,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin )}` ); }); - fieldArgs.apply($subQuery); + return $subQuery; }, [assertAllowed, sql] ), @@ -693,10 +718,11 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `No related \`${foreignTableTypeName}\` matches the filter criteria. All fields are combined with a logical ‘and.’`, type: FilterType, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( (assertAllowed, sql) => - function ($where: PgConditionStep, fieldArgs) { - assertAllowed(fieldArgs, "object"); + function ($where: PgCondition, value: object | null) { + assertAllowed(value, "object"); + if (value == null) return; if (!$where.extensions.pgFilterRelation) { throw new Error( `Invalid use of filter, 'pgFilterRelation' expected` @@ -722,7 +748,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin )}` ); }); - fieldArgs.apply($subQuery); + return $subQuery; }, [assertAllowed, sql] ), diff --git a/src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts b/src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts index c06d80a..e46a8fe 100644 --- a/src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts +++ b/src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts @@ -1,4 +1,5 @@ import type { PgCodecAttributes, PgCodecWithAttributes } from "@dataplan/pg"; +import { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); @@ -72,7 +73,9 @@ export const PgConnectionArgFilterCompositeTypeAttributesPlugin: GraphileConfig. } const filterTypeName = inflection.filterType(nodeTypeName); - const CompositeFilterType = build.getTypeByName(filterTypeName); + const CompositeFilterType = build.getTypeByName( + filterTypeName + ) as GraphQLInputObjectType; if (!CompositeFilterType) { continue; } diff --git a/src/PgConnectionArgFilterComputedAttributesPlugin.ts b/src/PgConnectionArgFilterComputedAttributesPlugin.ts index e401610..00c0a72 100644 --- a/src/PgConnectionArgFilterComputedAttributesPlugin.ts +++ b/src/PgConnectionArgFilterComputedAttributesPlugin.ts @@ -1,9 +1,10 @@ -import type { PgConditionStep } from "@dataplan/pg"; +import type { PgCondition } from "@dataplan/pg"; import { getComputedAttributeResources, isComputedScalarAttributeResource, } from "./utils"; import type { FieldArgs } from "grafast"; +import { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); @@ -51,7 +52,7 @@ export const PgConnectionArgFilterComputedAttributesPlugin: GraphileConfig.Plugi const { inflection, connectionFilterOperatorsDigest, - dataplanPg: { TYPES, PgConditionStep }, + dataplanPg: { TYPES, PgCondition }, EXPORTABLE, } = build; const { @@ -99,7 +100,9 @@ export const PgConnectionArgFilterComputedAttributesPlugin: GraphileConfig.Plugi if (!digest) { continue; } - const OperatorsType = build.getTypeByName(digest.operatorsTypeName); + const OperatorsType = build.getTypeByName( + digest.operatorsTypeName + ) as GraphQLInputObjectType; if (!OperatorsType) { continue; } @@ -142,28 +145,38 @@ export const PgConnectionArgFilterComputedAttributesPlugin: GraphileConfig.Plugi { description: `Filter by the object’s \`${fieldName}\` field.`, type: OperatorsType, - applyPlan: EXPORTABLE( - (PgConditionStep, computedAttributeResource, fieldName, functionResultCodec) => function ( - $where: PgConditionStep, - fieldArgs: FieldArgs - ) { + apply: EXPORTABLE( + ( + PgCondition, + computedAttributeResource, + fieldName, + functionResultCodec + ) => + function ($where: PgCondition, value: object | null) { if ( typeof computedAttributeResource.from !== "function" ) { throw new Error(`Unexpected...`); } + // TODO: assertAllowed? + if (value == null) return; const expression = computedAttributeResource.from({ placeholder: $where.alias, }); - const $col = new PgConditionStep($where); + const $col = new PgCondition($where); $col.extensions.pgFilterAttribute = { fieldName, codec: functionResultCodec, expression, }; - fieldArgs.apply($col); + return $col; }, - [PgConditionStep, computedAttributeResource, fieldName, functionResultCodec] + [ + PgCondition, + computedAttributeResource, + fieldName, + functionResultCodec, + ] ), } ), diff --git a/src/PgConnectionArgFilterForwardRelationsPlugin.ts b/src/PgConnectionArgFilterForwardRelationsPlugin.ts index 7a112dc..41fdc23 100644 --- a/src/PgConnectionArgFilterForwardRelationsPlugin.ts +++ b/src/PgConnectionArgFilterForwardRelationsPlugin.ts @@ -1,11 +1,12 @@ import type { - PgConditionStep, + PgCondition, PgCodecRelation, PgCodecAttribute, PgCodecWithAttributes, PgResource, } from "@dataplan/pg"; import { makeAssertAllowed } from "./utils"; +import { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); @@ -100,7 +101,7 @@ export const PgConnectionArgFilterForwardRelationsPlugin: GraphileConfig.Plugin inflection.filterType(foreignTableTypeName); const ForeignTableFilterType = build.getTypeByName( foreignTableFilterTypeName - ); + ) as GraphQLInputObjectType; if (!ForeignTableFilterType) continue; if (typeof foreignTable.from === "function") { @@ -121,7 +122,7 @@ export const PgConnectionArgFilterForwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `Filter by the object’s \`${fieldName}\` relation.`, type: ForeignTableFilterType, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( ( assertAllowed, foreignTable, @@ -130,8 +131,9 @@ export const PgConnectionArgFilterForwardRelationsPlugin: GraphileConfig.Plugin remoteAttributes, sql ) => - function ($where: PgConditionStep, fieldArgs) { - assertAllowed(fieldArgs, "object"); + function ($where: PgCondition, value: object | null) { + assertAllowed(value, "object"); + if (value == null) return; const $subQuery = $where.existsPlan({ tableExpression: foreignTableExpression, alias: foreignTable.name, @@ -146,7 +148,7 @@ export const PgConnectionArgFilterForwardRelationsPlugin: GraphileConfig.Plugin )}` ); }); - fieldArgs.apply($subQuery); + return $subQuery; }, [ assertAllowed, @@ -181,7 +183,7 @@ export const PgConnectionArgFilterForwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `A related \`${fieldName}\` exists.`, type: GraphQLBoolean, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( ( assertAllowed, foreignTable, @@ -190,12 +192,16 @@ export const PgConnectionArgFilterForwardRelationsPlugin: GraphileConfig.Plugin remoteAttributes, sql ) => - function ($where: PgConditionStep, fieldArgs) { - assertAllowed(fieldArgs, "scalar"); + function ( + $where: PgCondition, + value: boolean | null + ) { + assertAllowed(value, "scalar"); + if (value == null) return; const $subQuery = $where.existsPlan({ tableExpression: foreignTableExpression, alias: foreignTable.name, - $equals: fieldArgs.get(), + equals: value, }); localAttributes.forEach((localAttribute, i) => { const remoteAttribute = remoteAttributes[i]; diff --git a/src/PgConnectionArgFilterLogicalOperatorsPlugin.ts b/src/PgConnectionArgFilterLogicalOperatorsPlugin.ts index a303dcb..bf6d168 100644 --- a/src/PgConnectionArgFilterLogicalOperatorsPlugin.ts +++ b/src/PgConnectionArgFilterLogicalOperatorsPlugin.ts @@ -1,8 +1,14 @@ -import type { PgConditionStep } from "@dataplan/pg"; +import type { PgCondition } from "@dataplan/pg"; import { makeAssertAllowed } from "./utils"; const { version } = require("../package.json"); +type LogicalOperatorInput = { + and?: null | ReadonlyArray; + or?: null | ReadonlyArray; + not?: null | LogicalOperatorInput; +}; + export const PgConnectionArgFilterLogicalOperatorsPlugin: GraphileConfig.Plugin = { name: "PgConnectionArgFilterLogicalOperatorsPlugin", @@ -40,14 +46,18 @@ export const PgConnectionArgFilterLogicalOperatorsPlugin: GraphileConfig.Plugin { description: `Checks for all expressions in this list.`, type: new GraphQLList(new GraphQLNonNull(Self)), - applyPlan: EXPORTABLE( + apply: EXPORTABLE( (assertAllowed) => - function ($where: PgConditionStep, fieldArgs) { - assertAllowed(fieldArgs, "list"); + function ( + $where: PgCondition, + value: ReadonlyArray | null + ) { + assertAllowed(value, "list"); + if (value == null) return; const $and = $where.andPlan(); // No need for this more correct form, easier to read if it's flatter. // fieldArgs.apply(() => $and.andPlan()); - fieldArgs.apply($and); + return $and; }, [assertAllowed] ), @@ -61,13 +71,17 @@ export const PgConnectionArgFilterLogicalOperatorsPlugin: GraphileConfig.Plugin { description: `Checks for any expressions in this list.`, type: new GraphQLList(new GraphQLNonNull(Self)), - applyPlan: EXPORTABLE( + apply: EXPORTABLE( (assertAllowed) => - function ($where: PgConditionStep, fieldArgs) { - assertAllowed(fieldArgs, "list"); + function ( + $where: PgCondition, + value: ReadonlyArray | null + ) { + assertAllowed(value, "list"); + if (value == null) return; const $or = $where.orPlan(); // Every entry is added to the `$or`, but the entries themselves should use an `and`. - fieldArgs.apply(() => $or.andPlan()); + return () => $or.andPlan(); }, [assertAllowed] ), @@ -81,13 +95,17 @@ export const PgConnectionArgFilterLogicalOperatorsPlugin: GraphileConfig.Plugin { description: `Negates the expression.`, type: Self, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( (assertAllowed) => - function ($where: PgConditionStep, fieldArgs) { - assertAllowed(fieldArgs, "object"); + function ( + $where: PgCondition, + value: LogicalOperatorInput | null + ) { + assertAllowed(value, "object"); + if (value == null) return; const $not = $where.notPlan(); const $and = $not.andPlan(); - fieldArgs.apply($and); + return $and; }, [assertAllowed] ), diff --git a/src/PgConnectionArgFilterOperatorsPlugin.ts b/src/PgConnectionArgFilterOperatorsPlugin.ts index 7fed7e2..e5fc4ee 100644 --- a/src/PgConnectionArgFilterOperatorsPlugin.ts +++ b/src/PgConnectionArgFilterOperatorsPlugin.ts @@ -1,9 +1,13 @@ -import type { PgConditionStep, PgCodec } from "@dataplan/pg"; import type { - ExecutableStep, + PgCondition, + PgCodec, + PgConditionCapableParent, +} from "@dataplan/pg"; +import { sqlValueWithCodec } from "@dataplan/pg"; +import type { GrafastInputFieldConfigMap, - InputObjectFieldApplyPlanResolver, - InputStep, + InputObjectFieldApplyResolver, + Step, } from "grafast"; import type { GraphQLInputType, GraphQLNamedType } from "graphql"; import type { SQL } from "pg-sql2"; @@ -253,8 +257,8 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { ), resolveSqlValue: EXPORTABLE((sql) => () => sql.null, [sql]), // do not parse resolve: EXPORTABLE( - (sql) => (i, _v, $input) => - sql`${i} ${$input.eval() ? sql`IS NULL` : sql`IS NOT NULL`}`, + (sql) => (i, _v, input) => + sql`${i} ${input ? sql`IS NULL` : sql`IS NOT NULL`}`, [sql] ), }, @@ -694,17 +698,14 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { [TYPES, resolveDomains, sql] ); const resolveSqlValue = EXPORTABLE( - (TYPES, name, sql) => + (TYPES, name, sql, sqlValueWithCodec) => function ( - $placeholderable: PlaceholderableStep, - $input: InputStep, + _unused: unknown, + input: any, inputCodec: PgCodec ) { if (name === "in" || name === "notIn") { - const sqlList = $placeholderable.placeholder( - $input, - inputCodec - ); + const sqlList = sqlValueWithCodec(input, inputCodec); if (inputCodec.arrayOfCodec === TYPES.citext) { // already case-insensitive, so no need to call `lower()` return sqlList; @@ -715,10 +716,7 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { return sql`(select lower(t) from unnest(${sqlList}) t)`; } } else { - const sqlValue = $placeholderable.placeholder( - $input, - inputCodec - ); + const sqlValue = sqlValueWithCodec(input, inputCodec); if (inputCodec === TYPES.citext) { // already case-insensitive, so no need to call `lower()` return sqlValue; @@ -727,7 +725,7 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { } } }, - [TYPES, name, sql] + [TYPES, name, sql, sqlValueWithCodec] ); const resolveInputCodec = EXPORTABLE( @@ -1163,7 +1161,7 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { { description, type, - applyPlan: makeApplyPlanFromOperatorSpec( + apply: makeApplyFromOperatorSpec( build, Self.name, operatorName, @@ -1174,7 +1172,7 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { ); return memo; }, - Object.create(null) as GrafastInputFieldConfigMap + Object.create(null) as GrafastInputFieldConfigMap ); return extend(fields, operatorFields, ""); @@ -1183,13 +1181,6 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { }, }; -type PlaceholderableStep = { - placeholder( - step: ExecutableStep, - codec: PgCodec - ): SQL; -}; - export interface OperatorSpec { name?: string; description: string; @@ -1204,16 +1195,18 @@ export interface OperatorSpec { ) => PgCodec; resolveSql?: any; resolveSqlValue?: ( - $placeholderable: PlaceholderableStep, - $input: InputStep, + //was: $placeholderable: PlaceholderableStep, + _unused: PgConditionCapableParent, + value: any, codec: PgCodec // UNUSED? resolveListItemSqlValue?: any ) => SQL; resolve: ( sqlIdentifier: SQL, sqlValue: SQL, - $input: InputStep, - $placeholderable: PlaceholderableStep, + $input: Step, + // was: $placeholderable: PlaceholderableStep, + _unused: PgConditionCapableParent, details: { // TODO: move $input and $placeholderable here too fieldName: string | null; @@ -1223,16 +1216,15 @@ export interface OperatorSpec { resolveType?: (type: GraphQLInputType) => GraphQLInputType; } -export function makeApplyPlanFromOperatorSpec( +export function makeApplyFromOperatorSpec( build: GraphileBuild.Build, typeName: string, fieldName: string, spec: OperatorSpec, type: GraphQLInputType -): InputObjectFieldApplyPlanResolver> { +): InputObjectFieldApplyResolver { const { sql, - grafast: { lambda }, dataplanPg: { TYPES }, EXPORTABLE, } = build; @@ -1271,22 +1263,21 @@ export function makeApplyPlanFromOperatorSpec( ( connectionFilterAllowNullInput, fieldName, - lambda, resolve, resolveInput, resolveInputCodec, resolveSqlIdentifier, resolveSqlValue, - sql + sql, + sqlValueWithCodec ) => - function ($where, fieldArgs) { + function ($where, value) { if (!$where.extensions?.pgFilterAttribute) { throw new Error( `Planning error: expected 'pgFilterAttribute' to be present on the $where plan's extensions; your extensions to \`postgraphile-plugin-connection-filter\` does not implement the required interfaces.` ); } - const $input = fieldArgs.getRaw(); - if ($input.evalIs(undefined)) { + if (value === undefined) { return; } const { @@ -1316,11 +1307,11 @@ export function makeApplyPlanFromOperatorSpec( */ [sourceAlias, sourceCodec]; - if (connectionFilterAllowNullInput && $input.evalIs(null)) { + if (connectionFilterAllowNullInput && value === null) { // Don't add a filter return; } - if (!connectionFilterAllowNullInput && $input.evalIs(null)) { + if (!connectionFilterAllowNullInput && value === null) { // Forbidden throw Object.assign( new Error("Null literals are forbidden in filter argument input."), @@ -1329,24 +1320,21 @@ export function makeApplyPlanFromOperatorSpec( } ); } - const $resolvedInput = resolveInput - ? lambda($input, resolveInput) - : $input; + const resolvedInput = resolveInput ? resolveInput(value) : value; const inputCodec = resolveInputCodec ? resolveInputCodec(codec ?? attribute.codec) : codec ?? attribute.codec; const sqlValue = resolveSqlValue - ? resolveSqlValue($where, $input, inputCodec) + ? resolveSqlValue($where, value, inputCodec) : /* : attribute.codec === TYPES.citext ? $where.placeholder($resolvedInput, TYPES.text) // cast input to text : attribute.codec.arrayOfCodec === TYPES.citext ? $where.placeholder($resolvedInput, listOfCodec(TYPES.citext as any)) // cast input to text[] */ - $where.placeholder($resolvedInput, inputCodec); - - const fragment = resolve(sqlIdentifier, sqlValue, $input, $where, { + sqlValueWithCodec(resolvedInput, inputCodec); + const fragment = resolve(sqlIdentifier, sqlValue, value, $where, { fieldName: parentFieldName ?? null, operatorName: fieldName, }); @@ -1355,13 +1343,13 @@ export function makeApplyPlanFromOperatorSpec( [ connectionFilterAllowNullInput, fieldName, - lambda, resolve, resolveInput, resolveInputCodec, resolveSqlIdentifier, resolveSqlValue, sql, + sqlValueWithCodec, ] ); } diff --git a/src/PgConnectionArgFilterPlugin.ts b/src/PgConnectionArgFilterPlugin.ts index 711e3a2..872bdb6 100644 --- a/src/PgConnectionArgFilterPlugin.ts +++ b/src/PgConnectionArgFilterPlugin.ts @@ -1,5 +1,5 @@ -import type { PgSelectStep, PgCodec } from "@dataplan/pg"; -import type { ConnectionStep, FieldArgs } from "grafast"; +import type { PgSelectStep, PgCodec, PgSelectQueryBuilder } from "@dataplan/pg"; +import type { ConnectionStep, FieldArg } from "grafast"; import type { GraphQLInputType, GraphQLOutputType, @@ -403,15 +403,7 @@ export const PgConnectionArgFilterPlugin: GraphileConfig.Plugin = { // Add `filter` input argument to connection and simple collection types GraphQLObjectType_fields_field_args(args, build, context) { - const { - extend, - inflection, - options: { - connectionFilterAllowNullInput, - connectionFilterAllowEmptyObjectInput, - }, - EXPORTABLE, - } = build; + const { extend, inflection, EXPORTABLE } = build; const { scope: { isPgFieldConnection, @@ -483,7 +475,6 @@ export const PgConnectionArgFilterPlugin: GraphileConfig.Plugin = { description: "A filter to be used in determining which values should be returned by the collection.", type: FilterType, - autoApplyAfterParentPlan: true, ...(isPgFieldConnection ? { applyPlan: EXPORTABLE( @@ -496,17 +487,23 @@ export const PgConnectionArgFilterPlugin: GraphileConfig.Plugin = { any, PgSelectStep >, - fieldArgs: FieldArgs + fieldArg: FieldArg ) { - assertAllowed(fieldArgs, "object"); + assertAllowed(fieldArg, "object"); const $pgSelect = $connection.getSubplan(); - const $where = $pgSelect.wherePlan(); - if (attributeCodec) { - $where.extensions.pgFilterAttribute = { - codec: attributeCodec, - }; - } - fieldArgs.apply($where); + const myExtensions = attributeCodec + ? { pgFilterAttribute: { codec: attributeCodec } } + : null; + fieldArg.apply( + $pgSelect, + (queryBuilder: PgSelectQueryBuilder) => ({ + ...queryBuilder, + extensions: { + ...queryBuilder.extensions, + ...myExtensions, + }, + }) + ); }, [assertAllowed, attributeCodec] ), @@ -517,16 +514,22 @@ export const PgConnectionArgFilterPlugin: GraphileConfig.Plugin = { function ( _: any, $pgSelect: PgSelectStep, - fieldArgs: any + fieldArg: FieldArg ) { - assertAllowed(fieldArgs, "object"); - const $where = $pgSelect.wherePlan(); - if (attributeCodec) { - $where.extensions.pgFilterAttribute = { - codec: attributeCodec, - }; - } - fieldArgs.apply($where); + assertAllowed(fieldArg, "object"); + const myExtensions = attributeCodec + ? { pgFilterAttribute: { codec: attributeCodec } } + : null; + fieldArg.apply( + $pgSelect, + (queryBuilder: PgSelectQueryBuilder) => ({ + ...queryBuilder, + extensions: { + ...queryBuilder.extensions, + ...myExtensions, + }, + }) + ); }, [assertAllowed, attributeCodec] ), diff --git a/src/index.ts b/src/index.ts index dc0767e..0105ee3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import { PgConnectionArgFilterLogicalOperatorsPlugin } from "./PgConnectionArgFi import { OperatorSpec, PgConnectionArgFilterOperatorsPlugin, - makeApplyPlanFromOperatorSpec, + makeApplyFromOperatorSpec, } from "./PgConnectionArgFilterOperatorsPlugin"; import { $$filters, OperatorsCategory } from "./interfaces"; import type { GraphQLInputType, GraphQLOutputType } from "graphql"; @@ -21,11 +21,11 @@ import type {} from "postgraphile/presets/v4"; import { AddConnectionFilterOperatorPlugin } from "./AddConnectionFilterOperatorPlugin"; import type { SQL } from "pg-sql2"; -export { makeApplyPlanFromOperatorSpec }; +export { makeApplyFromOperatorSpec }; declare global { namespace DataplanPg { - interface PgConditionStepExtensions { + interface PgConditionExtensions { pgFilterAttribute?: /** Filtering a column */ | { fieldName: string; diff --git a/src/utils.ts b/src/utils.ts index 1ae5a6a..508cead 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,7 +4,6 @@ import type { PgResource, PgResourceParameter, } from "@dataplan/pg"; -import type { FieldArgs } from "grafast"; import type { GraphileBuild } from "graphile-build"; import type {} from "graphile-build-pg"; @@ -64,13 +63,13 @@ export function makeAssertAllowed(build: GraphileBuild.Build) { } = options; const assertAllowed = EXPORTABLE( (connectionFilterAllowEmptyObjectInput, connectionFilterAllowNullInput) => - function (fieldArgs: FieldArgs, mode: "list" | "object" | "scalar") { - const $raw = fieldArgs.getRaw(); + function (value: unknown, mode: "list" | "object" | "scalar") { if ( mode === "object" && !connectionFilterAllowEmptyObjectInput && - "evalIsEmpty" in $raw && - $raw.evalIsEmpty() + typeof value === "object" && + value !== null && + Object.keys(value).length === 0 ) { throw Object.assign( new Error("Empty objects are forbidden in filter argument input."), @@ -79,16 +78,17 @@ export function makeAssertAllowed(build: GraphileBuild.Build) { } ); } - if ( - mode === "list" && - !connectionFilterAllowEmptyObjectInput && - "evalLength" in $raw - ) { - const l = $raw.evalLength(); - if (l != null) { + if (mode === "list" && !connectionFilterAllowEmptyObjectInput) { + const arr = value as any[] | null | undefined; + if (arr) { + const l = arr.length; for (let i = 0; i < l; i++) { - const $entry = $raw.at(i); - if ("evalIsEmpty" in $entry && $entry.evalIsEmpty()) { + const entry = arr[i]; + if ( + typeof entry === "object" && + entry !== null && + Object.keys(entry).length === 0 + ) { throw Object.assign( new Error( "Empty objects are forbidden in filter argument input." @@ -102,7 +102,7 @@ export function makeAssertAllowed(build: GraphileBuild.Build) { } } // For all modes, check null - if (!connectionFilterAllowNullInput && $raw.evalIs(null)) { + if (!connectionFilterAllowNullInput && value === null) { throw Object.assign( new Error("Null literals are forbidden in filter argument input."), { From 087fcefe7c8c5231d65b467cfc65526571b80b4e Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 4 Mar 2025 16:48:24 +0000 Subject: [PATCH 2/6] Fix logic in filter arg --- ...nectionArgFilterBackwardRelationsPlugin.ts | 1 - src/PgConnectionArgFilterPlugin.ts | 65 +++++++++++-------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/PgConnectionArgFilterBackwardRelationsPlugin.ts b/src/PgConnectionArgFilterBackwardRelationsPlugin.ts index a010166..19bd65f 100644 --- a/src/PgConnectionArgFilterBackwardRelationsPlugin.ts +++ b/src/PgConnectionArgFilterBackwardRelationsPlugin.ts @@ -4,7 +4,6 @@ import type { PgCodecWithAttributes, PgRegistry, PgResource, - PgConditionCapableParent, } from "@dataplan/pg"; import { makeAssertAllowed } from "./utils"; import { GraphQLInputObjectType } from "graphql"; diff --git a/src/PgConnectionArgFilterPlugin.ts b/src/PgConnectionArgFilterPlugin.ts index 872bdb6..2b577d5 100644 --- a/src/PgConnectionArgFilterPlugin.ts +++ b/src/PgConnectionArgFilterPlugin.ts @@ -403,7 +403,12 @@ export const PgConnectionArgFilterPlugin: GraphileConfig.Plugin = { // Add `filter` input argument to connection and simple collection types GraphQLObjectType_fields_field_args(args, build, context) { - const { extend, inflection, EXPORTABLE } = build; + const { + extend, + inflection, + EXPORTABLE, + dataplanPg: { PgCondition }, + } = build; const { scope: { isPgFieldConnection, @@ -478,7 +483,7 @@ export const PgConnectionArgFilterPlugin: GraphileConfig.Plugin = { ...(isPgFieldConnection ? { applyPlan: EXPORTABLE( - (assertAllowed, attributeCodec) => + (PgCondition, assertAllowed, attributeCodec) => function ( _: any, $connection: ConnectionStep< @@ -489,49 +494,55 @@ export const PgConnectionArgFilterPlugin: GraphileConfig.Plugin = { >, fieldArg: FieldArg ) { - assertAllowed(fieldArg, "object"); const $pgSelect = $connection.getSubplan(); - const myExtensions = attributeCodec - ? { pgFilterAttribute: { codec: attributeCodec } } - : null; fieldArg.apply( $pgSelect, - (queryBuilder: PgSelectQueryBuilder) => ({ - ...queryBuilder, - extensions: { - ...queryBuilder.extensions, - ...myExtensions, - }, - }) + ( + queryBuilder: PgSelectQueryBuilder, + value: object | null + ) => { + assertAllowed(value, "object"); + if (value == null) return; + const condition = new PgCondition(queryBuilder); + if (attributeCodec) { + condition.extensions.pgFilterAttribute = { + codec: attributeCodec, + }; + } + return condition; + } ); }, - [assertAllowed, attributeCodec] + [PgCondition, assertAllowed, attributeCodec] ), } : { applyPlan: EXPORTABLE( - (assertAllowed, attributeCodec) => + (PgCondition, assertAllowed, attributeCodec) => function ( _: any, $pgSelect: PgSelectStep, fieldArg: FieldArg ) { - assertAllowed(fieldArg, "object"); - const myExtensions = attributeCodec - ? { pgFilterAttribute: { codec: attributeCodec } } - : null; fieldArg.apply( $pgSelect, - (queryBuilder: PgSelectQueryBuilder) => ({ - ...queryBuilder, - extensions: { - ...queryBuilder.extensions, - ...myExtensions, - }, - }) + ( + queryBuilder: PgSelectQueryBuilder, + value: object | null + ) => { + assertAllowed(value, "object"); + if (value == null) return; + const condition = new PgCondition(queryBuilder); + if (attributeCodec) { + condition.extensions.pgFilterAttribute = { + codec: attributeCodec, + }; + } + return condition; + } ); }, - [assertAllowed, attributeCodec] + [PgCondition, assertAllowed, attributeCodec] ), }), }, From 24f5b817582578a95644aea309d54174313fed0c Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 4 Mar 2025 16:55:29 +0000 Subject: [PATCH 3/6] Get values from allowed positions --- src/PgConnectionArgFilterOperatorsPlugin.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PgConnectionArgFilterOperatorsPlugin.ts b/src/PgConnectionArgFilterOperatorsPlugin.ts index e5fc4ee..f837f47 100644 --- a/src/PgConnectionArgFilterOperatorsPlugin.ts +++ b/src/PgConnectionArgFilterOperatorsPlugin.ts @@ -3,7 +3,6 @@ import type { PgCodec, PgConditionCapableParent, } from "@dataplan/pg"; -import { sqlValueWithCodec } from "@dataplan/pg"; import type { GrafastInputFieldConfigMap, InputObjectFieldApplyResolver, @@ -26,7 +25,7 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { const { extend, graphql: { GraphQLNonNull, GraphQLList, isListType, isNonNullType }, - dataplanPg: { isEnumCodec, listOfCodec, TYPES }, + dataplanPg: { isEnumCodec, listOfCodec, TYPES, sqlValueWithCodec }, sql, escapeLikeWildcards, options: { @@ -1225,7 +1224,7 @@ export function makeApplyFromOperatorSpec( ): InputObjectFieldApplyResolver { const { sql, - dataplanPg: { TYPES }, + dataplanPg: { TYPES, sqlValueWithCodec }, EXPORTABLE, } = build; const { From c570a668840a57861cf8dba74ecd40e47188f98f Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 4 Mar 2025 16:57:39 +0000 Subject: [PATCH 4/6] Use type imports --- src/PgConnectionArgFilterAttributesPlugin.ts | 2 +- src/PgConnectionArgFilterBackwardRelationsPlugin.ts | 2 +- src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts | 2 +- src/PgConnectionArgFilterComputedAttributesPlugin.ts | 3 +-- src/PgConnectionArgFilterForwardRelationsPlugin.ts | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/PgConnectionArgFilterAttributesPlugin.ts b/src/PgConnectionArgFilterAttributesPlugin.ts index 5f27a5d..f4c5257 100644 --- a/src/PgConnectionArgFilterAttributesPlugin.ts +++ b/src/PgConnectionArgFilterAttributesPlugin.ts @@ -2,7 +2,7 @@ import type { PgCodecWithAttributes, PgConditionCapableParent, } from "@dataplan/pg"; -import { GraphQLInputObjectType } from "graphql"; +import type { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); diff --git a/src/PgConnectionArgFilterBackwardRelationsPlugin.ts b/src/PgConnectionArgFilterBackwardRelationsPlugin.ts index 19bd65f..7f81179 100644 --- a/src/PgConnectionArgFilterBackwardRelationsPlugin.ts +++ b/src/PgConnectionArgFilterBackwardRelationsPlugin.ts @@ -6,7 +6,7 @@ import type { PgResource, } from "@dataplan/pg"; import { makeAssertAllowed } from "./utils"; -import { GraphQLInputObjectType } from "graphql"; +import type { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); diff --git a/src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts b/src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts index e46a8fe..5bd2607 100644 --- a/src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts +++ b/src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts @@ -1,5 +1,5 @@ import type { PgCodecAttributes, PgCodecWithAttributes } from "@dataplan/pg"; -import { GraphQLInputObjectType } from "graphql"; +import type { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); diff --git a/src/PgConnectionArgFilterComputedAttributesPlugin.ts b/src/PgConnectionArgFilterComputedAttributesPlugin.ts index 00c0a72..8e3b12a 100644 --- a/src/PgConnectionArgFilterComputedAttributesPlugin.ts +++ b/src/PgConnectionArgFilterComputedAttributesPlugin.ts @@ -3,8 +3,7 @@ import { getComputedAttributeResources, isComputedScalarAttributeResource, } from "./utils"; -import type { FieldArgs } from "grafast"; -import { GraphQLInputObjectType } from "graphql"; +import type { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); diff --git a/src/PgConnectionArgFilterForwardRelationsPlugin.ts b/src/PgConnectionArgFilterForwardRelationsPlugin.ts index 41fdc23..9971a41 100644 --- a/src/PgConnectionArgFilterForwardRelationsPlugin.ts +++ b/src/PgConnectionArgFilterForwardRelationsPlugin.ts @@ -6,7 +6,7 @@ import type { PgResource, } from "@dataplan/pg"; import { makeAssertAllowed } from "./utils"; -import { GraphQLInputObjectType } from "graphql"; +import type { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); From 79488b018eea3f66d10ed754a8ebce6623637ecf Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 4 Mar 2025 17:02:11 +0000 Subject: [PATCH 5/6] Tidy up isEmpty check --- src/PgConnectionArgFilterAttributesPlugin.ts | 19 +++----------- src/utils.ts | 27 ++++++++++++-------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/PgConnectionArgFilterAttributesPlugin.ts b/src/PgConnectionArgFilterAttributesPlugin.ts index f4c5257..d987037 100644 --- a/src/PgConnectionArgFilterAttributesPlugin.ts +++ b/src/PgConnectionArgFilterAttributesPlugin.ts @@ -3,6 +3,7 @@ import type { PgConditionCapableParent, } from "@dataplan/pg"; import type { GraphQLInputObjectType } from "graphql"; +import { isEmpty } from "./utils"; const { version } = require("../package.json"); @@ -73,13 +74,7 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { description: `Filter by the object’s \`${fieldName}\` field.`, type: OperatorsType, apply: EXPORTABLE( - ( - PgCondition, - colSpec, - connectionFilterAllowEmptyObjectInput, - connectionFilterAllowNullInput - ) => - function ( + (PgCondition, colSpec, connectionFilterAllowEmptyObjectInput, connectionFilterAllowNullInput, isEmpty) => function ( queryBuilder: PgConditionCapableParent, value: unknown ) { @@ -98,8 +93,7 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { } if ( !connectionFilterAllowEmptyObjectInput && - value != null && - Object.keys(value).length === 0 + isEmpty(value) ) { throw Object.assign( new Error( @@ -114,12 +108,7 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { condition.extensions.pgFilterAttribute = colSpec; return condition; }, - [ - PgCondition, - colSpec, - connectionFilterAllowEmptyObjectInput, - connectionFilterAllowNullInput, - ] + [PgCondition, colSpec, connectionFilterAllowEmptyObjectInput, connectionFilterAllowNullInput, isEmpty] ), }) ), diff --git a/src/utils.ts b/src/utils.ts index 508cead..51935fd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -62,14 +62,16 @@ export function makeAssertAllowed(build: GraphileBuild.Build) { connectionFilterAllowEmptyObjectInput, } = options; const assertAllowed = EXPORTABLE( - (connectionFilterAllowEmptyObjectInput, connectionFilterAllowNullInput) => + ( + connectionFilterAllowEmptyObjectInput, + connectionFilterAllowNullInput, + isEmpty + ) => function (value: unknown, mode: "list" | "object" | "scalar") { if ( mode === "object" && !connectionFilterAllowEmptyObjectInput && - typeof value === "object" && - value !== null && - Object.keys(value).length === 0 + isEmpty(value) ) { throw Object.assign( new Error("Empty objects are forbidden in filter argument input."), @@ -83,12 +85,7 @@ export function makeAssertAllowed(build: GraphileBuild.Build) { if (arr) { const l = arr.length; for (let i = 0; i < l; i++) { - const entry = arr[i]; - if ( - typeof entry === "object" && - entry !== null && - Object.keys(entry).length === 0 - ) { + if (isEmpty(arr[i])) { throw Object.assign( new Error( "Empty objects are forbidden in filter argument input." @@ -111,7 +108,15 @@ export function makeAssertAllowed(build: GraphileBuild.Build) { ); } }, - [connectionFilterAllowEmptyObjectInput, connectionFilterAllowNullInput] + [ + connectionFilterAllowEmptyObjectInput, + connectionFilterAllowNullInput, + isEmpty, + ] ); return assertAllowed; } + +export function isEmpty(o: unknown): o is Record { + return typeof o === "object" && o !== null && Object.keys(o).length === 0; +} From de48aa8ede3763487fb7ddd3b80586e6d5df544a Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Tue, 4 Mar 2025 17:03:02 +0000 Subject: [PATCH 6/6] Restore old code order --- src/PgConnectionArgFilterAttributesPlugin.ts | 31 ++++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/PgConnectionArgFilterAttributesPlugin.ts b/src/PgConnectionArgFilterAttributesPlugin.ts index d987037..31a1d2f 100644 --- a/src/PgConnectionArgFilterAttributesPlugin.ts +++ b/src/PgConnectionArgFilterAttributesPlugin.ts @@ -74,30 +74,37 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { description: `Filter by the object’s \`${fieldName}\` field.`, type: OperatorsType, apply: EXPORTABLE( - (PgCondition, colSpec, connectionFilterAllowEmptyObjectInput, connectionFilterAllowNullInput, isEmpty) => function ( + ( + PgCondition, + colSpec, + connectionFilterAllowEmptyObjectInput, + connectionFilterAllowNullInput, + isEmpty + ) => + function ( queryBuilder: PgConditionCapableParent, value: unknown ) { if (value === undefined) { return; } - if (!connectionFilterAllowNullInput && value === null) { + if ( + !connectionFilterAllowEmptyObjectInput && + isEmpty(value) + ) { throw Object.assign( new Error( - "Null literals are forbidden in filter argument input." + "Empty objects are forbidden in filter argument input." ), { //TODO: mark this error as safe } ); } - if ( - !connectionFilterAllowEmptyObjectInput && - isEmpty(value) - ) { + if (!connectionFilterAllowNullInput && value === null) { throw Object.assign( new Error( - "Empty objects are forbidden in filter argument input." + "Null literals are forbidden in filter argument input." ), { //TODO: mark this error as safe @@ -108,7 +115,13 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { condition.extensions.pgFilterAttribute = colSpec; return condition; }, - [PgCondition, colSpec, connectionFilterAllowEmptyObjectInput, connectionFilterAllowNullInput, isEmpty] + [ + PgCondition, + colSpec, + connectionFilterAllowEmptyObjectInput, + connectionFilterAllowNullInput, + isEmpty, + ] ), }) ),