UNPKG

postgraphile-plugin-connection-filter

Version:
399 lines (398 loc) 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PgConnectionArgFilterPlugin = void 0; const utils_1 = require("./utils"); const version_1 = require("./version"); const isSuitableForFiltering = (build, codec) => codec !== build.dataplanPg.TYPES.void && !codec.attributes && !codec.isAnonymous && !codec.polymorphism && (!codec.arrayOfCodec || isSuitableForFiltering(build, codec.arrayOfCodec)) && (!codec.domainOfCodec || isSuitableForFiltering(build, codec.domainOfCodec)); exports.PgConnectionArgFilterPlugin = { name: "PgConnectionArgFilterPlugin", version: version_1.version, // Sometimes we want to order by things we filter by (e.g. if we're doing // fulltext search), so we should ensure that the filters are applied before // ordering. before: ["PgConnectionArgOrderByPlugin"], /* gather: { hooks: { pgProcedures_functionSource_options(info, event) { if (info.resolvedPreset.schema) { const { connectionFilterComputedColumns, connectionFilterSetofFunctions, } = info.resolvedPreset.schema; if ( event.pgProc.provolatile === "i" || event.pgProc.provolatile === "s" ) { const args = event.pgProc.getArguments(); if (args[0]?.type.getClass()) { if (connectionFilterComputedColumns) { if (!event.options.extensions) { event.options.extensions = { tags: Object.create(null) }; } // TODO: only do this if they've not added `-advanced:filter`? addBehaviorToTags( event.options.extensions.tags, "advanced:filter" ); } } else { if (connectionFilterSetofFunctions) { ... } } } } }, }, }, */ schema: { behaviorRegistry: { add: { filterProc: { description: "Can this function be filtered?", entities: ["pgResource"], }, filter: { description: "Can this table be filtered?", entities: ["pgResource"], }, }, }, entityBehavior: { pgCodec: "filter", pgResource: { inferred(behavior, entity, build) { if (entity.parameters) { return [ behavior, // procedure sources aren't filterable by default (unless // connectionFilterSetofFunctions is set), but can be made filterable // by adding the `+filterProc` behavior. build.options.connectionFilterSetofFunctions ? "filterProc" : "-filterProc", ]; } else { return ["filter", behavior]; } }, }, }, hooks: { build(build) { const { inflection, options: { connectionFilterAllowedFieldTypes, connectionFilterArrays, }, EXPORTABLE, } = build; build.connectionFilterOperatorsDigest = (codec) => { const finalBuild = build; const { dataplanPg: { getInnerCodec, TYPES, isEnumCodec }, } = finalBuild; if (!isSuitableForFiltering(finalBuild, codec)) { // Not a base, domain, enum, or range type? Skip. return null; } // Perform some checks on the simple type (after removing array/range/domain wrappers) const pgSimpleCodec = getInnerCodec(codec); if (!pgSimpleCodec) return null; if (pgSimpleCodec.polymorphism || pgSimpleCodec.attributes || pgSimpleCodec.isAnonymous) { // Haven't found an enum type or a non-array base type? Skip. return null; } if (pgSimpleCodec === TYPES.json) { // The PG `json` type has no valid operators. // Skip filter type creation to allow the proper // operators to be exposed for PG `jsonb` types. return null; } // TODO:v5: I'm unsure if this will work as before, e.g. it might not wrap with GraphQLList/GraphQLNonNull/etc // Establish field type and field input type const itemCodec = codec.arrayOfCodec ?? codec; const fieldTypeName = build.getGraphQLTypeNameByPgCodec(itemCodec, "output"); if (!fieldTypeName) { return null; } const fieldTypeMeta = build.getTypeMetaByName(fieldTypeName); if (!fieldTypeMeta) { return null; } const fieldInputTypeName = build.getGraphQLTypeNameByPgCodec(itemCodec, "input"); if (!fieldInputTypeName) return null; const fieldInputTypeMeta = build.getTypeMetaByName(fieldInputTypeName); if (!fieldInputTypeMeta) return null; // Avoid exposing filter operators on unrecognized types that PostGraphile handles as Strings const namedTypeName = fieldTypeName; const namedInputTypeName = fieldInputTypeName; const actualStringCodecs = [ TYPES.bpchar, TYPES.char, TYPES.name, TYPES.text, TYPES.varchar, TYPES.citext, ]; if (namedInputTypeName === "String" && !actualStringCodecs.includes(pgSimpleCodec)) { // Not a real string type? Skip. return null; } // Respect `connectionFilterAllowedFieldTypes` config option if (connectionFilterAllowedFieldTypes && !connectionFilterAllowedFieldTypes.includes(namedTypeName)) { return null; } const pgConnectionFilterOperatorsCategory = codec.arrayOfCodec ? "Array" : codec.rangeOfCodec ? "Range" : isEnumCodec(codec) ? "Enum" : codec.domainOfCodec ? "Domain" : "Scalar"; // Respect `connectionFilterArrays` config option if (pgConnectionFilterOperatorsCategory === "Array" && !connectionFilterArrays) { return null; } const rangeElementInputTypeName = codec.rangeOfCodec && !codec.rangeOfCodec.arrayOfCodec ? build.getGraphQLTypeNameByPgCodec(codec.rangeOfCodec, "input") : null; const domainBaseTypeName = codec.domainOfCodec && !codec.domainOfCodec.arrayOfCodec ? build.getGraphQLTypeNameByPgCodec(codec.domainOfCodec, "output") : null; const listType = !!(codec.arrayOfCodec || codec.domainOfCodec?.arrayOfCodec || codec.rangeOfCodec?.arrayOfCodec); const operatorsTypeName = listType ? inflection.filterFieldListType(namedTypeName) : inflection.filterFieldType(namedTypeName); return { isList: listType, operatorsTypeName, relatedTypeName: namedTypeName, inputTypeName: fieldInputTypeName, rangeElementInputTypeName, domainBaseTypeName, }; }; build.escapeLikeWildcards = EXPORTABLE(() => function (input) { if ("string" !== typeof input) { throw new Error("Non-string input was provided to escapeLikeWildcards"); } else { return input.split("%").join("\\%").split("_").join("\\_"); } }, []); return build; }, init: { after: ["PgCodecs"], callback(_, build) { const { inflection } = build; // Create filter type for all column-having codecs for (const pgCodec of build.allPgCodecs) { if (!pgCodec.attributes) { continue; } const nodeTypeName = build.getGraphQLTypeNameByPgCodec(pgCodec, "output"); if (!nodeTypeName) { //console.log(`No node type name ${pgCodec.name}`); continue; } const filterTypeName = inflection.filterType(nodeTypeName); build.registerInputObjectType(filterTypeName, { pgCodec, isPgConnectionFilter: true, }, () => ({ description: `A filter to be used against \`${nodeTypeName}\` object types. All fields are combined with a logical ‘and.’`, }), "PgConnectionArgFilterPlugin"); } // Get or create types like IntFilter, StringFilter, etc. const codecsByFilterTypeName = {}; for (const codec of build.allPgCodecs) { const digest = build.connectionFilterOperatorsDigest(codec); if (!digest) { continue; } const { isList, operatorsTypeName, relatedTypeName, inputTypeName, rangeElementInputTypeName, domainBaseTypeName, } = digest; if (!codecsByFilterTypeName[operatorsTypeName]) { codecsByFilterTypeName[operatorsTypeName] = { isList, relatedTypeName, pgCodecs: [codec], inputTypeName, rangeElementInputTypeName, domainBaseTypeName, }; } else { for (const key of [ "isList", "relatedTypeName", "inputTypeName", "rangeElementInputTypeName", ]) { if (digest[key] !== codecsByFilterTypeName[operatorsTypeName][key]) { throw new Error(`${key} mismatch: existing codecs (${codecsByFilterTypeName[operatorsTypeName].pgCodecs .map((c) => c.name) .join(", ")}) had ${key} = ${codecsByFilterTypeName[operatorsTypeName][key]}, but ${codec.name} instead has ${key} = ${digest[key]}`); } } codecsByFilterTypeName[operatorsTypeName].pgCodecs.push(codec); } } for (const [operatorsTypeName, { isList, relatedTypeName, pgCodecs, inputTypeName, rangeElementInputTypeName, domainBaseTypeName, },] of Object.entries(codecsByFilterTypeName)) { build.registerInputObjectType(operatorsTypeName, { pgConnectionFilterOperators: { isList, pgCodecs, inputTypeName, rangeElementInputTypeName, domainBaseTypeName, }, /* pgConnectionFilterOperatorsCategory, fieldType, fieldInputType, rangeElementInputType, domainBaseType, */ }, () => ({ name: operatorsTypeName, description: `A filter to be used against ${relatedTypeName}${isList ? " List" : ""} fields. All fields are combined with a logical ‘and.’`, }), "PgConnectionArgFilterPlugin"); } return _; }, }, // Add `filter` input argument to connection and simple collection types GraphQLObjectType_fields_field_args(args, build, context) { const { extend, inflection, EXPORTABLE, dataplanPg: { PgCondition }, } = build; const { scope: { isPgFieldConnection, isPgFieldSimpleCollection, pgFieldResource: resource, pgFieldCodec, fieldName, }, Self, } = context; const shouldAddFilter = isPgFieldConnection || isPgFieldSimpleCollection; if (!shouldAddFilter) return args; const codec = (pgFieldCodec ?? resource?.codec); if (!codec) return args; // Procedures get their own special behavior const desiredBehavior = resource?.parameters ? "filterProc" : "filter"; // TODO: should factor in connectionFilterComputedColumns different. // 'queryField:list' and 'queryField:connection' behaviours are for setof functions. // 'typeField:list' and 'typeField:connection' behaviours are for computed attributes functions. if (resource ? !build.behavior.pgResourceMatches(resource, desiredBehavior) : !build.behavior.pgCodecMatches(codec, desiredBehavior)) { /* console.log(`NO FILTER: ${source.name}`, { behavior, defaultBehavior, }); */ return args; } const returnCodec = codec; const nodeType = build.getGraphQLTypeByPgCodec(returnCodec, "output"); if (!nodeType) { return args; } const nodeTypeName = nodeType.name; const filterTypeName = inflection.filterType(nodeTypeName); const FilterType = build.getTypeByName(filterTypeName); if (!FilterType) { return args; } const assertAllowed = (0, utils_1.makeAssertAllowed)(build); const attributeCodec = resource?.parameters && !resource?.codec.attributes ? resource.codec : null; return extend(args, { filter: { description: "A filter to be used in determining which values should be returned by the collection.", type: FilterType, ...(isPgFieldConnection ? { applyPlan: EXPORTABLE((PgCondition, assertAllowed, attributeCodec) => function (_, $connection, fieldArg) { const $pgSelect = $connection.getSubplan(); fieldArg.apply($pgSelect, (queryBuilder, value) => { assertAllowed(value, "object"); if (value == null) return; const condition = new PgCondition(queryBuilder); if (attributeCodec) { condition.extensions.pgFilterAttribute = { codec: attributeCodec, }; } return condition; }); }, [PgCondition, assertAllowed, attributeCodec]), } : { applyPlan: EXPORTABLE((PgCondition, assertAllowed, attributeCodec) => function (_, $pgSelect, fieldArg) { fieldArg.apply($pgSelect, (queryBuilder, value) => { assertAllowed(value, "object"); if (value == null) return; const condition = new PgCondition(queryBuilder); if (attributeCodec) { condition.extensions.pgFilterAttribute = { codec: attributeCodec, }; } return condition; }); }, [PgCondition, assertAllowed, attributeCodec]), }), }, }, `Adding connection filter arg to field '${fieldName}' of '${Self.name}'`); }, }, }, }; /* export interface AddConnectionFilterOperator { ( typeNames: string | string[], operatorName: string, description: string | null, resolveType: ( fieldInputType: GraphQLInputType, rangeElementInputType: GraphQLInputType ) => GraphQLType, resolve: ( sqlIdentifier: SQL, sqlValue: SQL, input: unknown, parentFieldName: string, queryBuilder: QueryBuilder ) => SQL | null, options?: { resolveInput?: (input: unknown) => unknown; resolveSqlIdentifier?: ( sqlIdentifier: SQL, pgType: PgType, pgTypeModifier: number | null ) => SQL; resolveSqlValue?: ( input: unknown, pgType: PgType, pgTypeModifier: number | null, resolveListItemSqlValue?: any ) => SQL | null; } ): void; } export default PgConnectionArgFilterPlugin; */ //# sourceMappingURL=PgConnectionArgFilterPlugin.js.map