UNPKG

postgraphile-plugin-connection-filter

Version:
843 lines (834 loc) 55.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PgConnectionArgFilterOperatorsPlugin = void 0; exports.makeApplyFromOperatorSpec = makeApplyFromOperatorSpec; const EXPORTABLE_1 = require("./EXPORTABLE"); const version_1 = require("./version"); exports.PgConnectionArgFilterOperatorsPlugin = { name: "PgConnectionArgFilterOperatorsPlugin", version: version_1.version, after: ["PgBasicsPlugin"], schema: { hooks: { build(build) { if (!build.dataplanPg) { throw new Error("Must be loaded after dataplanPg is added to build"); } const { TYPES } = build.dataplanPg; return build.extend(build, { pgAggregatesForceTextTypesInsensitive: [TYPES.char, TYPES.bpchar], pgAggregatesForceTextTypesSensitive: [ TYPES.citext, TYPES.char, TYPES.bpchar, ], }, "Adding text types that need to be forced to be case sensitive"); }, GraphQLInputObjectType_fields(fields, build, context) { const { extend, graphql: { GraphQLNonNull, GraphQLList, isListType, isNonNullType }, dataplanPg: { isEnumCodec, listOfCodec, TYPES, sqlValueWithCodec }, sql, escapeLikeWildcards, options: { connectionFilterAllowedOperators, connectionFilterOperatorNames, }, EXPORTABLE, pgAggregatesForceTextTypesSensitive: forceTextTypesSensitive, pgAggregatesForceTextTypesInsensitive: forceTextTypesInsensitive, } = build; const { scope: { pgConnectionFilterOperators, /* pgConnectionFilterOperatorsCategory, fieldType, fieldInputType, rangeElementInputType, domainBaseType, */ }, fieldWithHooks, Self, } = context; if (!pgConnectionFilterOperators /* || !pgConnectionFilterOperatorsCategory || !fieldType || !isNamedType(fieldType) || !fieldInputType */ ) { return fields; } /** Turn `[Foo]` into `[Foo!]` */ const resolveTypeToListOfNonNullable = EXPORTABLE((GraphQLList, GraphQLNonNull, isListType, isNonNullType) => function (type) { if (isListType(type) && !isNonNullType(type.ofType)) { return new GraphQLList(new GraphQLNonNull(type.ofType)); } else { return type; } }, [GraphQLList, GraphQLNonNull, isListType, isNonNullType], "resolveTypeToListOfNonNullable"); const resolveDomains = EXPORTABLE(() => function (c) { let current = c; while (current.domainOfCodec) { current = current.domainOfCodec; } return current; }, [], "resolveDomains"); const resolveArrayInputCodecSensitive = EXPORTABLE((TYPES, forceTextTypesSensitive, listOfCodec, resolveDomains) => function (c) { if (forceTextTypesSensitive.includes(resolveDomains(c))) { return listOfCodec(TYPES.text, { extensions: { listItemNonNull: true }, }); } else { return listOfCodec(c, { extensions: { listItemNonNull: true }, }); } }, [TYPES, forceTextTypesSensitive, listOfCodec, resolveDomains], "resolveArrayInputCodecSensitive"); const resolveArrayItemInputCodecSensitive = EXPORTABLE((TYPES, forceTextTypesSensitive, resolveDomains) => function (c) { if (c.arrayOfCodec) { if (forceTextTypesSensitive.includes(resolveDomains(c.arrayOfCodec))) { return TYPES.text; } return c.arrayOfCodec; } else { throw new Error(`Expected array codec`); } }, [TYPES, forceTextTypesSensitive, resolveDomains], "resolveArrayItemInputCodecSensitive"); const resolveInputCodecSensitive = EXPORTABLE((TYPES, forceTextTypesSensitive, listOfCodec, resolveDomains) => function (c) { if (c.arrayOfCodec) { if (forceTextTypesSensitive.includes(resolveDomains(c.arrayOfCodec))) { return listOfCodec(TYPES.text, { extensions: { listItemNonNull: c.extensions?.listItemNonNull, }, }); } return c; } else { if (forceTextTypesSensitive.includes(resolveDomains(c))) { return TYPES.text; } return c; } }, [TYPES, forceTextTypesSensitive, listOfCodec, resolveDomains], "resolveInputCodecSensitive"); const resolveSqlIdentifierSensitive = EXPORTABLE((TYPES, forceTextTypesSensitive, listOfCodec, resolveDomains, sql) => function (identifier, c) { if (c.arrayOfCodec && forceTextTypesSensitive.includes(resolveDomains(c.arrayOfCodec))) { return [ sql `(${identifier})::text[]`, listOfCodec(TYPES.text, { extensions: { listItemNonNull: c.extensions?.listItemNonNull, }, }), ]; } else if (forceTextTypesSensitive.includes(resolveDomains(c))) { return [sql `(${identifier})::text`, TYPES.text]; } else { return [identifier, c]; } }, [TYPES, forceTextTypesSensitive, listOfCodec, resolveDomains, sql], "resolveSqlIdentifierSensitive"); const resolveInputCodecInsensitive = EXPORTABLE((TYPES, forceTextTypesInsensitive, listOfCodec, resolveDomains) => function (c) { if (c.arrayOfCodec) { if (forceTextTypesInsensitive.includes(resolveDomains(c.arrayOfCodec))) { return listOfCodec(TYPES.text, { extensions: { listItemNonNull: c.extensions?.listItemNonNull, }, }); } return c; } else { if (forceTextTypesInsensitive.includes(resolveDomains(c))) { return TYPES.text; } return c; } }, [TYPES, forceTextTypesInsensitive, listOfCodec, resolveDomains], "resolveInputCodecInsensitive"); const resolveSqlIdentifierInsensitive = EXPORTABLE((TYPES, forceTextTypesInsensitive, listOfCodec, resolveDomains, sql) => function (identifier, c) { if (c.arrayOfCodec && forceTextTypesInsensitive.includes(resolveDomains(c.arrayOfCodec))) { return [ sql `(${identifier})::text[]`, listOfCodec(TYPES.text, { extensions: { listItemNonNull: c.extensions?.listItemNonNull, }, }), ]; } else if (forceTextTypesInsensitive.includes(resolveDomains(c))) { return [sql `(${identifier})::text`, TYPES.text]; } else { return [identifier, c]; } }, [TYPES, forceTextTypesInsensitive, listOfCodec, resolveDomains, sql], "resolveSqlIdentifierInsensitive"); const standardOperators = { isNull: { description: "Is null (if `true` is specified) or is not null (if `false` is specified).", resolveInputCodec: EXPORTABLE((TYPES) => () => TYPES.boolean, [TYPES], "resolveBoolean"), resolveSqlValue: EXPORTABLE((sql) => () => sql.null, [sql], "resolveSqlValue_null"), // do not parse resolve: EXPORTABLE((sql) => (i, _v, input) => sql `${i} ${input ? sql `IS NULL` : sql `IS NOT NULL`}`, [sql], "resolveIsNull"), }, equalTo: { description: "Equal to the specified value.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} = ${v}`, [sql], "resolveEquality"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, }, notEqualTo: { description: "Not equal to the specified value.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} <> ${v}`, [sql], "resolveInequality"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, }, distinctFrom: { description: "Not equal to the specified value, treating null like an ordinary value.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} IS DISTINCT FROM ${v}`, [sql], "resolveDistinct"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, }, notDistinctFrom: { description: "Equal to the specified value, treating null like an ordinary value.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} IS NOT DISTINCT FROM ${v}`, [sql], "resolveNotDistinct"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, }, in: { description: "Included in the specified list.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} = ANY(${v})`, [sql], "resolveEqualsAny"), resolveInputCodec: resolveArrayInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolveType: resolveTypeToListOfNonNullable, }, notIn: { description: "Not included in the specified list.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} <> ALL(${v})`, [sql], "resolveInequalAll"), resolveInputCodec: resolveArrayInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolveType: resolveTypeToListOfNonNullable, }, }; for (const key in standardOperators) { standardOperators[key].name ??= key; } const sortOperators = { lessThan: { description: "Less than the specified value.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} < ${v}`, [sql], "resolveLessThan"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, }, lessThanOrEqualTo: { description: "Less than or equal to the specified value.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} <= ${v}`, [sql], "resolveLessThanOrEqualTo"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, }, greaterThan: { description: "Greater than the specified value.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} > ${v}`, [sql], "resolveGreaterThan"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, }, greaterThanOrEqualTo: { description: "Greater than or equal to the specified value.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} >= ${v}`, [sql], "resolveGreaterThanOrEqualTo"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, }, }; for (const key in sortOperators) { sortOperators[key].name ??= key; } const patternMatchingOperators = { includes: { description: "Contains the specified string (case-sensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `%${escapeLikeWildcards(input)}%`, [escapeLikeWildcards], "resolveInputContains"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} LIKE ${v}`, [sql], "resolveLike"), }, notIncludes: { description: "Does not contain the specified string (case-sensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `%${escapeLikeWildcards(input)}%`, [escapeLikeWildcards], "resolveInputContains"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} NOT LIKE ${v}`, [sql], "resolveNotLike"), }, includesInsensitive: { description: "Contains the specified string (case-insensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `%${escapeLikeWildcards(input)}%`, [escapeLikeWildcards], "resolveInputContains"), resolve: EXPORTABLE((sql) => (i, v) => sql `${i} ILIKE ${v}`, [sql], "resolveILike"), resolveInputCodec: resolveInputCodecInsensitive, resolveSqlIdentifier: resolveSqlIdentifierInsensitive, }, notIncludesInsensitive: { description: "Does not contain the specified string (case-insensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `%${escapeLikeWildcards(input)}%`, [escapeLikeWildcards], "resolveInputContains"), resolve: EXPORTABLE((sql) => (i, v) => sql `${i} NOT ILIKE ${v}`, [sql], "resolveNotILike"), resolveInputCodec: resolveInputCodecInsensitive, resolveSqlIdentifier: resolveSqlIdentifierInsensitive, }, startsWith: { description: "Starts with the specified string (case-sensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `${escapeLikeWildcards(input)}%`, [escapeLikeWildcards], "resolveInputStartsWith"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} LIKE ${v}`, [sql], "resolveLike"), }, notStartsWith: { description: "Does not start with the specified string (case-sensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `${escapeLikeWildcards(input)}%`, [escapeLikeWildcards], "resolveInputStartsWith"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} NOT LIKE ${v}`, [sql], "resolveNotLike"), }, startsWithInsensitive: { description: "Starts with the specified string (case-insensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `${escapeLikeWildcards(input)}%`, [escapeLikeWildcards], "resolveInputStartsWith"), resolve: EXPORTABLE((sql) => (i, v) => sql `${i} ILIKE ${v}`, [sql], "resolveILike"), resolveInputCodec: resolveInputCodecInsensitive, resolveSqlIdentifier: resolveSqlIdentifierInsensitive, }, notStartsWithInsensitive: { description: "Does not start with the specified string (case-insensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `${escapeLikeWildcards(input)}%`, [escapeLikeWildcards], "resolveInputStartsWith"), resolve: EXPORTABLE((sql) => (i, v) => sql `${i} NOT ILIKE ${v}`, [sql], "resolveNotILike"), resolveInputCodec: resolveInputCodecInsensitive, resolveSqlIdentifier: resolveSqlIdentifierInsensitive, }, endsWith: { description: "Ends with the specified string (case-sensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `%${escapeLikeWildcards(input)}`, [escapeLikeWildcards], "resolveInputEndsWith"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} LIKE ${v}`, [sql], "resolveLike"), }, notEndsWith: { description: "Does not end with the specified string (case-sensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `%${escapeLikeWildcards(input)}`, [escapeLikeWildcards], "resolveInputEndsWith"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} NOT LIKE ${v}`, [sql], "resolveNotLike"), }, endsWithInsensitive: { description: "Ends with the specified string (case-insensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `%${escapeLikeWildcards(input)}`, [escapeLikeWildcards], "resolveInputEndsWith"), resolve: EXPORTABLE((sql) => (i, v) => sql `${i} ILIKE ${v}`, [sql], "resolveILike"), resolveInputCodec: resolveInputCodecInsensitive, resolveSqlIdentifier: resolveSqlIdentifierInsensitive, }, notEndsWithInsensitive: { description: "Does not end with the specified string (case-insensitive).", resolveInput: EXPORTABLE((escapeLikeWildcards) => (input) => `%${escapeLikeWildcards(input)}`, [escapeLikeWildcards], "resolveInputEndsWith"), resolve: EXPORTABLE((sql) => (i, v) => sql `${i} NOT ILIKE ${v}`, [sql], "resolveNotILike"), resolveInputCodec: resolveInputCodecInsensitive, resolveSqlIdentifier: resolveSqlIdentifierInsensitive, }, like: { description: "Matches the specified pattern (case-sensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} LIKE ${v}`, [sql], "resolveLike"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, }, notLike: { description: "Does not match the specified pattern (case-sensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} NOT LIKE ${v}`, [sql], "resolveNotLike"), resolveInputCodec: resolveInputCodecSensitive, resolveSqlIdentifier: resolveSqlIdentifierSensitive, }, likeInsensitive: { description: "Matches the specified pattern (case-insensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} ILIKE ${v}`, [sql], "resolveILike"), resolveInputCodec: resolveInputCodecInsensitive, resolveSqlIdentifier: resolveSqlIdentifierInsensitive, }, notLikeInsensitive: { description: "Does not match the specified pattern (case-insensitive). An underscore (_) matches any single character; a percent sign (%) matches any sequence of zero or more characters.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} NOT ILIKE ${v}`, [sql], "resolveNotILike"), resolveInputCodec: resolveInputCodecInsensitive, resolveSqlIdentifier: resolveSqlIdentifierInsensitive, }, }; for (const key in patternMatchingOperators) { patternMatchingOperators[key].name ??= key; } const resolveTextArrayInputCodec = EXPORTABLE((TYPES, listOfCodec) => () => listOfCodec(TYPES.text, { extensions: { listItemNonNull: true } }), [TYPES, listOfCodec], "resolveTextArrayInputCodec"); const hstoreOperators = { contains: { description: "Contains the specified KeyValueHash.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} @> ${v}`, [sql], "resolveContains"), }, containsKey: { description: "Contains the specified key.", resolveInputCodec: EXPORTABLE((TYPES) => () => TYPES.text, [TYPES], "resolveInputCodecText"), resolve: EXPORTABLE((sql) => (i, v) => sql `${i} ? ${v}`, [sql], "resolveContainsKey"), }, containsAllKeys: { name: "containsAllKeys", description: "Contains all of the specified keys.", resolveInputCodec: resolveTextArrayInputCodec, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} ?& ${v}`, [sql], "resolveContainsAllKeys"), resolveType: resolveTypeToListOfNonNullable, }, containsAnyKeys: { name: "containsAnyKeys", description: "Contains any of the specified keys.", resolveInputCodec: resolveTextArrayInputCodec, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} ?| ${v}`, [sql], "resolveContainsAnyKeys"), resolveType: resolveTypeToListOfNonNullable, }, containedBy: { description: "Contained by the specified KeyValueHash.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} <@ ${v}`, [sql], "resolveContainedBy"), }, }; for (const key in hstoreOperators) { hstoreOperators[key].name ??= `hstore${key}`; } const jsonbOperators = { contains: { description: "Contains the specified JSON.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} @> ${v}`, [sql], "resolveContains"), }, containsKey: { description: "Contains the specified key.", resolveInputCodec: EXPORTABLE((TYPES) => () => TYPES.text, [TYPES], "resolveInputCodecText"), resolve: EXPORTABLE((sql) => (i, v) => sql `${i} ? ${v}`, [sql], "resolveContainsKey"), }, containsAllKeys: { name: "containsAllKeys", description: "Contains all of the specified keys.", resolveInputCodec: resolveTextArrayInputCodec, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} ?& ${v}`, [sql], "resolveContainsAllKeys"), }, containsAnyKeys: { name: "containsAnyKeys", description: "Contains any of the specified keys.", resolveInputCodec: resolveTextArrayInputCodec, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} ?| ${v}`, [sql], "resolveContainsAnyKeys"), }, containedBy: { description: "Contained by the specified JSON.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} <@ ${v}`, [sql], "resolveContainedBy"), }, }; for (const key in jsonbOperators) { jsonbOperators[key].name ??= `jsonb${key}`; } const inetOperators = { contains: { description: "Contains the specified internet address.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} >> ${v}`, [sql], "resolveContains"), }, containsOrEqualTo: { description: "Contains or equal to the specified internet address.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} >>= ${v}`, [sql], "resolveContainsOrEqualTo"), }, containedBy: { description: "Contained by the specified internet address.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} << ${v}`, [sql], "resolveContainedBy"), }, containedByOrEqualTo: { description: "Contained by or equal to the specified internet address.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} <<= ${v}`, [sql], "resolveContainedByOrEqualTo"), }, containsOrContainedBy: { description: "Contains or contained by the specified internet address.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} && ${v}`, [sql], "resolveContainsOrContainedBy"), }, }; for (const key in inetOperators) { inetOperators[key].name ??= `inet${key}`; } const insensitiveOperators = {}; /** * This block adds the following operators: * - distinctFromInsensitive * - equalToInsensitive * - greaterThanInsensitive * - greaterThanOrEqualToInsensitive * - inInsensitive * - lessThanInsensitive * - lessThanOrEqualToInsensitive * - notDistinctFromInsensitive * - notEqualToInsensitive * - notInInsensitive * * The compiled SQL depends on the underlying PostgreSQL column type. * Using case-insensitive operators with `text`/`varchar`/`char` columns * will result in calling `lower()` on the operands. Using case-sensitive * operators with `citext` columns will result in casting the operands to `text`. * * For example, here is how the `equalTo`/`equalToInsensitive` operators compile to SQL: * | GraphQL operator | PostgreSQL column type | Compiled SQL | * | ------------------ | ----------------------- | -------------------------- | * | equalTo | `text`/`varchar`/`char` | `<col> = $1` | * | equalTo | `citext` | `<col>::text = $1::text` | * | equalToInsensitive | `text`/`varchar`/`char` | `lower(<col>) = lower($1)` | * | equalToInsensitive | `citext` | `<col> = $1` | */ for (const [name, spec] of [ ...Object.entries(standardOperators), ...Object.entries(sortOperators), ]) { if (name == "isNull") continue; const description = `${spec.description.substring(0, spec.description.length - 1)} (case-insensitive).`; const resolveSqlIdentifier = EXPORTABLE((TYPES, resolveDomains, sql) => function (sourceAlias, codec) { return resolveDomains(codec) === TYPES.citext ? [sourceAlias, codec] // already case-insensitive, so no need to call `lower()` : [sql `lower(${sourceAlias}::text)`, TYPES.text]; }, [TYPES, resolveDomains, sql], "resolveSqlIdentifierInsensitiveOperator"); const inOrNotIn = name === "in" || name === "notIn"; const resolveSqlValue = EXPORTABLE((TYPES, inOrNotIn, sql, sqlValueWithCodec) => function (_unused, input, inputCodec) { if (inOrNotIn) { const sqlList = sqlValueWithCodec(input, inputCodec); if (inputCodec.arrayOfCodec === TYPES.citext) { // already case-insensitive, so no need to call `lower()` return sqlList; } else { // This is being used in an `= ANY(subquery)` syntax, so no // need to array_agg it. See // https://www.postgresql.org/docs/current/functions-subquery.html#FUNCTIONS-SUBQUERY-ANY-SOME return sql `(select lower(t) from unnest(${sqlList}) t)`; } } else { const sqlValue = sqlValueWithCodec(input, inputCodec); if (inputCodec === TYPES.citext) { // already case-insensitive, so no need to call `lower()` return sqlValue; } else { return sql `lower(${sqlValue})`; } } }, [TYPES, inOrNotIn, sql, sqlValueWithCodec], `resolveSqlValueInsensitiveOperator${inOrNotIn ? "_list" : ""}`); const resolveInputCodec = EXPORTABLE((TYPES, inOrNotIn, listOfCodec, resolveDomains) => function (inputCodec) { if (inOrNotIn) { const t = resolveDomains(inputCodec) === TYPES.citext ? inputCodec : TYPES.text; return listOfCodec(t, { extensions: { listItemNonNull: true }, }); } else { const t = resolveDomains(inputCodec) === TYPES.citext ? inputCodec : TYPES.text; return t; } }, [TYPES, inOrNotIn, listOfCodec, resolveDomains], `resolveInputCodecInsensitiveOperator${inOrNotIn ? "_list" : ""}`); insensitiveOperators[`${name}Insensitive`] = { ...spec, name: `${name}Insensitive`, description, resolveInputCodec, resolveSqlIdentifier, resolveSqlValue, }; } const connectionFilterEnumOperators = { ...standardOperators, ...sortOperators, }; const connectionFilterRangeOperators = { ...standardOperators, ...sortOperators, contains: { description: "Contains the specified range.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} @> ${v}`, [sql], "resolveContains"), }, containsElement: { description: "Contains the specified value.", resolveInputCodec: EXPORTABLE(() => function (c) { if (c.rangeOfCodec) { return c.rangeOfCodec; } else { throw new Error(`Couldn't determine the range element type to use`); } }, [], "resolveInputCodecContainsElement"), resolve: EXPORTABLE((sql) => (i, v) => sql `${i} @> ${v}`, [sql], "resolveContainsElement"), }, containedBy: { description: "Contained by the specified range.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} <@ ${v}`, [sql], "resolveContainedBy"), }, overlaps: { description: "Overlaps the specified range.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} && ${v}`, [sql], "resolveOverlaps"), }, strictlyLeftOf: { description: "Strictly left of the specified range.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} << ${v}`, [sql], "resolveStrictlyLeftOf"), }, strictlyRightOf: { description: "Strictly right of the specified range.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} >> ${v}`, [sql], "resolveStrictlyRightOf"), }, notExtendsRightOf: { description: "Does not extend right of the specified range.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} &< ${v}`, [sql], "resolveNotExtendsRightOf"), }, notExtendsLeftOf: { description: "Does not extend left of the specified range.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} &> ${v}`, [sql], "resolveNotExtendsLeftOf"), }, adjacentTo: { description: "Adjacent to the specified range.", resolve: EXPORTABLE((sql) => (i, v) => sql `${i} -|- ${v}`, [sql], "resolveAdjacentTo"), }, }; for (const key in connectionFilterRangeOperators) { connectionFilterRangeOperators[key].name ??= `range${key}`; } const connectionFilterArrayOperators = { isNull: standardOperators.isNull, equalTo: standardOperators.equalTo, notEqualTo: standardOperators.notEqualTo, distinctFrom: standardOperators.distinctFrom, notDistinctFrom: standardOperators.notDistinctFrom, ...sortOperators, contains: { description: "Contains the specified list of values.", resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolveInputCodec: resolveInputCodecSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} @> ${v}`, [sql], "resolveContains"), }, containedBy: { description: "Contained by the specified list of values.", resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolveInputCodec: resolveInputCodecSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} <@ ${v}`, [sql], "resolveContainedBy"), }, overlaps: { description: "Overlaps the specified list of values.", resolveSqlIdentifier: resolveSqlIdentifierSensitive, resolveInputCodec: resolveInputCodecSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${i} && ${v}`, [sql], "resolveOverlaps"), }, anyEqualTo: { description: "Any array item is equal to the specified value.", resolveInputCodec: resolveArrayItemInputCodecSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${v} = ANY (${i})`, [sql], "resolveAnyEqualTo"), }, anyNotEqualTo: { description: "Any array item is not equal to the specified value.", resolveInputCodec: resolveArrayItemInputCodecSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${v} <> ANY (${i})`, [sql], "resolveAnyNotEqualTo"), }, anyLessThan: { description: "Any array item is less than the specified value.", resolveInputCodec: resolveArrayItemInputCodecSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${v} > ANY (${i})`, [sql], "resolveAnyLessThan"), }, anyLessThanOrEqualTo: { description: "Any array item is less than or equal to the specified value.", resolveInputCodec: resolveArrayItemInputCodecSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${v} >= ANY (${i})`, [sql], "resolveAnyLessThanOrEqualTo"), }, anyGreaterThan: { description: "Any array item is greater than the specified value.", resolveInputCodec: resolveArrayItemInputCodecSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${v} < ANY (${i})`, [sql], "resolveAnyGreaterThan"), }, anyGreaterThanOrEqualTo: { description: "Any array item is greater than or equal to the specified value.", resolveInputCodec: resolveArrayItemInputCodecSensitive, resolve: EXPORTABLE((sql) => (i, v) => sql `${v} <= ANY (${i})`, [sql], "resolveAnyGreaterThanOrEqualTo"), }, }; for (const key in connectionFilterArrayOperators) { connectionFilterArrayOperators[key].name ??= `array${key}`; } const { //inputTypeName, //rangeElementInputTypeName, //domainBaseTypeName, pgCodecs, } = pgConnectionFilterOperators; // We know all these pgCodecs will produce the same GraphQL input type, // so we only need to grab the type of one of them const someCodec = pgCodecs[0]; const fieldInputType = build.getGraphQLTypeByPgCodec(someCodec, "input"); const rangeElementInputType = someCodec.rangeOfCodec ? build.getGraphQLTypeByPgCodec(someCodec.rangeOfCodec, "input") : null; let textLike = true; let sortable = true; let inetLike = true; let jsonLike = true; let hstoreLike = true; let arrayLike = true; let rangeLike = true; let enumLike = true; for (const codec of pgCodecs) { const underlyingType = codec.domainOfCodec ?? codec; if (!underlyingType.arrayOfCodec) { arrayLike = false; } if (!underlyingType.rangeOfCodec) { rangeLike = false; } if (!isEnumCodec(underlyingType)) { enumLike = false; } switch (underlyingType) { case TYPES.numeric: case TYPES.money: case TYPES.float: case TYPES.float4: case TYPES.bigint: case TYPES.int: case TYPES.int2: case TYPES.boolean: case TYPES.varbit: case TYPES.bit: case TYPES.date: case TYPES.timestamp: case TYPES.timestamptz: case TYPES.time: case TYPES.timetz: case TYPES.interval: case TYPES.json: case TYPES.jsonb: case TYPES.cidr: case TYPES.inet: case TYPES.macaddr: case TYPES.macaddr8: case TYPES.text: case TYPES.name: case TYPES.citext: case TYPES.varchar: case TYPES.char: case TYPES.bpchar: case TYPES.uuid: { // Sort is fine break; } default: { // NOT SORTABLE! if (Self.name === "FloatFilter" /* TODO: || ... */) { // TODO: solve this! console.log(`The postgraphile-plugin-connection-filter unsupported codec ${underlyingType.name} is preventing ${Self.name} being detected as sortable!`); } sortable = false; } } switch (underlyingType) { case TYPES.cidr: case TYPES.inet: case TYPES.macaddr: case TYPES.macaddr8: { // Inet is fine break; } default: { // NOT INET! if (Self.name === "InternetAddressFilter") { // TODO: solve this! console.log(`The postgraphile-plugin-connection-filter unsupported codec ${underlyingType.name} is preventing ${Self.name} being detected as inet-like!`); } inetLike = false; } } switch (underlyingType) { case TYPES.text: case TYPES.name: case TYPES.citext: case TYPES.varchar: case TYPES.char: case TYPES.bpchar: { // Text break; } default: { // NOT TEXT! if (Self.name === "StringFilter") { // TODO: solve this! console.log(`The postgraphile-plugin-connection-filter unsupported codec ${underlyingType.name} is preventing ${Self.name} being detected as text-like!`); } textLike = false; } } switch (underlyingType) { case TYPES.json: case TYPES.jsonb: { // JSON break; } default: { // NOT JSON! jsonLike = false; } } switch (underlyingType) { case TYPES.hstore: { // HSTORE break; } default: { // NOT HSTORE! hstoreLike = false; } } /* switch (underlyingType) { case TYPES.numeric: case TYPES.money: case TYPES.float: case TYPES.float4: case TYPES.bigint: case TYPES.int: case TYPES.int2: case TYPES.boolean: case TYPES.varbit: case TYPES.bit: case TYPES.date: case TYPES.timestamp: case TYPES.timestamptz: case TYPES.time: case TYPES.timetz: case TYPES.interval: case TYPES.json: case TYPES.jsonb: case TYPES.hstore: case TYPES.cidr: case TYPES.inet: case TYPES.macaddr: case TYPES.macaddr8: case TYPES.text: case TYPES.name: case TYPES.citext: case TYPES.varchar: case TYPES.char: case TYPES.bpchar: case TYPES.uuid: }*/ } const operatorSpecs = arrayLike ? connectionFilterArrayOperators : rangeLike ? connectionFilterRangeOperators : enumLike ? connectionFilterEnumOperators : { ...standardOperators, ...(sortable ? sortOperators : null), ...(inetLike ? inetOperators : null), ...(jsonLike ? jsonbOperators : null), ...(hstoreLike ? hstoreOperators : null), ...(textLike ? patternMatchingOperators : null), ...(textLike ? insensitiveOperators : null), }; const operatorFields = Object.entries(operatorSpecs).reduce((memo, [name, spec]) => { const { description, resolveInputCodec, resolveType } = spec; if (connectionFilterAllowedOperators && !connectionFilterAllowedOperators.includes(name)) { return memo; } if (!fieldInputType) { return memo; } const firstCodec = pgCodecs[0]; const inputCodec = resolveInputCodec