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..31a1d2f 100644 --- a/src/PgConnectionArgFilterAttributesPlugin.ts +++ b/src/PgConnectionArgFilterAttributesPlugin.ts @@ -1,4 +1,9 @@ -import type { PgConditionStep, PgCodecWithAttributes } from "@dataplan/pg"; +import type { + PgCodecWithAttributes, + PgConditionCapableParent, +} from "@dataplan/pg"; +import type { GraphQLInputObjectType } from "graphql"; +import { isEmpty } from "./utils"; const { version } = require("../package.json"); @@ -17,7 +22,7 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { const { inflection, connectionFilterOperatorsDigest, - dataplanPg: { PgConditionStep }, + dataplanPg: { PgCondition }, EXPORTABLE, } = build; const { @@ -47,7 +52,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,22 +73,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 + connectionFilterAllowNullInput, + isEmpty ) => - 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() + isEmpty(value) ) { throw Object.assign( new Error( @@ -92,10 +101,7 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { } ); } - if ( - !connectionFilterAllowNullInput && - $raw.evalIs(null) - ) { + if (!connectionFilterAllowNullInput && value === null) { throw Object.assign( new Error( "Null literals are forbidden in filter argument input." @@ -105,15 +111,16 @@ export const PgConnectionArgFilterAttributesPlugin: GraphileConfig.Plugin = { } ); } - 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, + isEmpty, ] ), }) diff --git a/src/PgConnectionArgFilterBackwardRelationsPlugin.ts b/src/PgConnectionArgFilterBackwardRelationsPlugin.ts index c603bae..7f81179 100644 --- a/src/PgConnectionArgFilterBackwardRelationsPlugin.ts +++ b/src/PgConnectionArgFilterBackwardRelationsPlugin.ts @@ -1,11 +1,12 @@ import type { - PgConditionStep, + PgCondition, PgCodecRelation, PgCodecWithAttributes, PgRegistry, PgResource, } from "@dataplan/pg"; import { makeAssertAllowed } from "./utils"; +import type { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); @@ -319,7 +320,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 +339,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 +367,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `Filter by the object’s \`${fieldName}\` relation.`, type: FilterManyType, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( ( assertAllowed, foreignTable, @@ -369,10 +376,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 +388,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin localAttributes, remoteAttributes, }; - fieldArgs.apply($rel); + return $rel; }, [ assertAllowed, @@ -417,7 +424,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 +434,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 +491,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `Filter by the object’s \`${fieldName}\` relation.`, type: ForeignTableFilterType, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( ( assertAllowed, foreignTable, @@ -492,8 +500,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 +519,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin )}` ); }); - fieldArgs.apply($subQuery); + return $subQuery; }, [ assertAllowed, @@ -541,7 +552,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin () => ({ description: `A related \`${fieldName}\` exists.`, type: GraphQLBoolean, - applyPlan: EXPORTABLE( + apply: EXPORTABLE( ( assertAllowed, foreignTable, @@ -551,14 +562,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 +608,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 +626,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 +659,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin )}` ); }); - fieldArgs.apply($subQuery.notPlan().andPlan()); + return $subQuery.notPlan().andPlan(); }, [assertAllowed, sql] ), @@ -650,10 +673,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 +703,7 @@ export const PgConnectionArgFilterBackwardRelationsPlugin: GraphileConfig.Plugin )}` ); }); - fieldArgs.apply($subQuery); + return $subQuery; }, [assertAllowed, sql] ), @@ -693,10 +717,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 +747,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..5bd2607 100644 --- a/src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts +++ b/src/PgConnectionArgFilterCompositeTypeAttributesPlugin.ts @@ -1,4 +1,5 @@ import type { PgCodecAttributes, PgCodecWithAttributes } from "@dataplan/pg"; +import type { 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..8e3b12a 100644 --- a/src/PgConnectionArgFilterComputedAttributesPlugin.ts +++ b/src/PgConnectionArgFilterComputedAttributesPlugin.ts @@ -1,9 +1,9 @@ -import type { PgConditionStep } from "@dataplan/pg"; +import type { PgCondition } from "@dataplan/pg"; import { getComputedAttributeResources, isComputedScalarAttributeResource, } from "./utils"; -import type { FieldArgs } from "grafast"; +import type { GraphQLInputObjectType } from "graphql"; const { version } = require("../package.json"); @@ -51,7 +51,7 @@ export const PgConnectionArgFilterComputedAttributesPlugin: GraphileConfig.Plugi const { inflection, connectionFilterOperatorsDigest, - dataplanPg: { TYPES, PgConditionStep }, + dataplanPg: { TYPES, PgCondition }, EXPORTABLE, } = build; const { @@ -99,7 +99,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 +144,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..9971a41 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 type { 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..f837f47 100644 --- a/src/PgConnectionArgFilterOperatorsPlugin.ts +++ b/src/PgConnectionArgFilterOperatorsPlugin.ts @@ -1,9 +1,12 @@ -import type { PgConditionStep, PgCodec } from "@dataplan/pg"; import type { - ExecutableStep, + PgCondition, + PgCodec, + PgConditionCapableParent, +} from "@dataplan/pg"; +import type { GrafastInputFieldConfigMap, - InputObjectFieldApplyPlanResolver, - InputStep, + InputObjectFieldApplyResolver, + Step, } from "grafast"; import type { GraphQLInputType, GraphQLNamedType } from "graphql"; import type { SQL } from "pg-sql2"; @@ -22,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: { @@ -253,8 +256,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 +697,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 +715,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 +724,7 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { } } }, - [TYPES, name, sql] + [TYPES, name, sql, sqlValueWithCodec] ); const resolveInputCodec = EXPORTABLE( @@ -1163,7 +1160,7 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { { description, type, - applyPlan: makeApplyPlanFromOperatorSpec( + apply: makeApplyFromOperatorSpec( build, Self.name, operatorName, @@ -1174,7 +1171,7 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { ); return memo; }, - Object.create(null) as GrafastInputFieldConfigMap + Object.create(null) as GrafastInputFieldConfigMap ); return extend(fields, operatorFields, ""); @@ -1183,13 +1180,6 @@ export const PgConnectionArgFilterOperatorsPlugin: GraphileConfig.Plugin = { }, }; -type PlaceholderableStep = { - placeholder( - step: ExecutableStep, - codec: PgCodec - ): SQL; -}; - export interface OperatorSpec { name?: string; description: string; @@ -1204,16 +1194,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,17 +1215,16 @@ 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 }, + dataplanPg: { TYPES, sqlValueWithCodec }, EXPORTABLE, } = build; const { @@ -1271,22 +1262,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 +1306,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 +1319,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 +1342,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..2b577d5 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, @@ -406,11 +406,8 @@ export const PgConnectionArgFilterPlugin: GraphileConfig.Plugin = { const { extend, inflection, - options: { - connectionFilterAllowNullInput, - connectionFilterAllowEmptyObjectInput, - }, EXPORTABLE, + dataplanPg: { PgCondition }, } = build; const { scope: { @@ -483,11 +480,10 @@ 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( - (assertAllowed, attributeCodec) => + (PgCondition, assertAllowed, attributeCodec) => function ( _: any, $connection: ConnectionStep< @@ -496,39 +492,57 @@ export const PgConnectionArgFilterPlugin: GraphileConfig.Plugin = { any, PgSelectStep >, - fieldArgs: FieldArgs + fieldArg: FieldArg ) { - assertAllowed(fieldArgs, "object"); const $pgSelect = $connection.getSubplan(); - const $where = $pgSelect.wherePlan(); - if (attributeCodec) { - $where.extensions.pgFilterAttribute = { - codec: attributeCodec, - }; - } - fieldArgs.apply($where); + fieldArg.apply( + $pgSelect, + ( + 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, - fieldArgs: any + fieldArg: FieldArg ) { - assertAllowed(fieldArgs, "object"); - const $where = $pgSelect.wherePlan(); - if (attributeCodec) { - $where.extensions.pgFilterAttribute = { - codec: attributeCodec, - }; - } - fieldArgs.apply($where); + fieldArg.apply( + $pgSelect, + ( + 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] ), }), }, 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..51935fd 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"; @@ -63,14 +62,16 @@ export function makeAssertAllowed(build: GraphileBuild.Build) { connectionFilterAllowEmptyObjectInput, } = options; const assertAllowed = EXPORTABLE( - (connectionFilterAllowEmptyObjectInput, connectionFilterAllowNullInput) => - function (fieldArgs: FieldArgs, mode: "list" | "object" | "scalar") { - const $raw = fieldArgs.getRaw(); + ( + connectionFilterAllowEmptyObjectInput, + connectionFilterAllowNullInput, + isEmpty + ) => + function (value: unknown, mode: "list" | "object" | "scalar") { if ( mode === "object" && !connectionFilterAllowEmptyObjectInput && - "evalIsEmpty" in $raw && - $raw.evalIsEmpty() + isEmpty(value) ) { throw Object.assign( new Error("Empty objects are forbidden in filter argument input."), @@ -79,16 +80,12 @@ 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()) { + if (isEmpty(arr[i])) { throw Object.assign( new Error( "Empty objects are forbidden in filter argument input." @@ -102,7 +99,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."), { @@ -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; +}