UNPKG

postgraphile-plugin-connection-filter

Version:
484 lines (469 loc) 28.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PgConnectionArgFilterBackwardRelationsPlugin = void 0; const utils_1 = require("./utils"); const version_1 = require("./version"); exports.PgConnectionArgFilterBackwardRelationsPlugin = { name: "PgConnectionArgFilterBackwardRelationsPlugin", version: version_1.version, inflection: { add: { filterManyType(preset, table, foreignTable) { return this.upperCamelCase(`${this.tableType(table)}-to-many-${this.tableType(foreignTable.codec)}-filter`); }, filterBackwardSingleRelationExistsFieldName(preset, relationFieldName) { return `${relationFieldName}Exists`; }, filterBackwardManyRelationExistsFieldName(preset, relationFieldName) { return `${relationFieldName}Exist`; }, filterSingleRelationByKeysBackwardsFieldName(preset, fieldName) { return fieldName; }, filterManyRelationByKeysFieldName(preset, fieldName) { return fieldName; }, }, }, schema: { entityBehavior: { pgCodecRelation: "filterBy", }, hooks: { init(_, build) { const { inflection } = build; for (const source of Object.values(build.input.pgRegistry.pgResources)) { if (source.parameters || !source.codec.attributes || source.isUnique) { continue; } for (const [relationName, relation] of Object.entries(source.getRelations())) { const foreignTable = relation.remoteResource; const filterManyTypeName = inflection.filterManyType(source.codec, foreignTable); const foreignTableTypeName = inflection.tableType(foreignTable.codec); if (!build.getTypeMetaByName(filterManyTypeName)) { build.recoverable(null, () => { build.registerInputObjectType(filterManyTypeName, { foreignTable, isPgConnectionFilterMany: true, }, () => ({ name: filterManyTypeName, description: `A filter to be used against many \`${foreignTableTypeName}\` object types. All fields are combined with a logical ‘and.’`, }), `PgConnectionArgFilterBackwardRelationsPlugin: Adding '${filterManyTypeName}' type for ${foreignTable.name}`); }); } } } return _; }, GraphQLInputObjectType_fields(inFields, build, context) { let fields = inFields; const { extend, inflection, sql, graphql: { GraphQLBoolean }, EXPORTABLE, } = build; const { fieldWithHooks, scope: { // fn1 pgCodec, isPgConnectionFilter, // fn2 foreignTable, isPgConnectionFilterMany, }, Self, } = context; const assertAllowed = (0, utils_1.makeAssertAllowed)(build); const source = pgCodec && Object.values(build.input.pgRegistry.pgResources).find((s) => s.codec === pgCodec && !s.parameters); if (isPgConnectionFilter && pgCodec && pgCodec.attributes && source) { const backwardsRelations = Object.entries(source.getRelations()).filter(([relationName, relation]) => { return relation.isReferencee; }); for (const [relationName, relation] of backwardsRelations) { const foreignTable = relation.remoteResource; // Deliberate shadowing // Used to use 'read' behavior too if (!build.behavior.pgCodecRelationMatches(relation, "filterBy")) { continue; } const isForeignKeyUnique = relation.isUnique; const isOneToMany = !relation.isUnique; /* const addField = ( fieldName: string, description: string, type: any, resolve: any, spec: BackwardRelationSpec, hint: string ) => { // Field fields = extend( fields, { [fieldName]: fieldWithHooks( fieldName, { description, type, }, { isPgConnectionFilterField: true, } ), }, hint ); // Relation spec for use in resolver backwardRelationSpecByFieldName = extend( backwardRelationSpecByFieldName, { [fieldName]: spec, } ); // Resolver connectionFilterRegisterResolver(Self.name, fieldName, resolve); }; const resolveSingle: ConnectionFilterResolver = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => { if (fieldValue == null) return null; const { foreignTable, foreignKeyAttributes, keyAttributes } = backwardRelationSpecByFieldName[fieldName]; const foreignTableTypeName = inflection.tableType(foreignTable); const foreignTableAlias = sql.identifier(Symbol()); const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName); const sqlIdentifier = sql.identifier( foreignTable.namespace.name, foreignTable.name ); const sqlKeysMatch = sql.query`(${sql.join( foreignKeyAttributes.map((attr, i) => { return sql.fragment`${foreignTableAlias}.${sql.identifier( attr.name )} = ${sourceAlias}.${sql.identifier(keyAttributes[i].name)}`; }), ") and (" )})`; const sqlSelectWhereKeysMatch = sql.query`select 1 from ${sqlIdentifier} as ${foreignTableAlias} where ${sqlKeysMatch}`; const sqlFragment = connectionFilterResolve( fieldValue, foreignTableAlias, foreignTableFilterTypeName, queryBuilder ); return sqlFragment == null ? null : sql.query`exists(${sqlSelectWhereKeysMatch} and (${sqlFragment}))`; }; const resolveExists: ConnectionFilterResolver = ({ sourceAlias, fieldName, fieldValue, }) => { if (fieldValue == null) return null; const { foreignTable, foreignKeyAttributes, keyAttributes } = backwardRelationSpecByFieldName[fieldName]; const foreignTableAlias = sql.identifier(Symbol()); const sqlIdentifier = sql.identifier( foreignTable.namespace.name, foreignTable.name ); const sqlKeysMatch = sql.query`(${sql.join( foreignKeyAttributes.map((attr, i) => { return sql.fragment`${foreignTableAlias}.${sql.identifier( attr.name )} = ${sourceAlias}.${sql.identifier(keyAttributes[i].name)}`; }), ") and (" )})`; const sqlSelectWhereKeysMatch = sql.query`select 1 from ${sqlIdentifier} as ${foreignTableAlias} where ${sqlKeysMatch}`; return fieldValue === true ? sql.query`exists(${sqlSelectWhereKeysMatch})` : sql.query`not exists(${sqlSelectWhereKeysMatch})`; }; const makeResolveMany = ( backwardRelationSpec: BackwardRelationSpec ) => { const resolveMany: ConnectionFilterResolver = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => { if (fieldValue == null) return null; const { foreignTable } = backwardRelationSpecByFieldName[fieldName]; const foreignTableFilterManyTypeName = inflection.filterManyType(table, foreignTable); const sqlFragment = connectionFilterResolve( fieldValue, sourceAlias, foreignTableFilterManyTypeName, queryBuilder, null, null, null, { backwardRelationSpec } ); return sqlFragment == null ? null : sqlFragment; }; return resolveMany; }; for (const spec of backwardRelationSpecs) { const { foreignTable, foreignKeyAttributes, foreignConstraint, isOneToMany, } = spec; */ const foreignTableTypeName = inflection.tableType(foreignTable.codec); const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName); const ForeignTableFilterType = build.getTypeByName(foreignTableFilterTypeName); if (!ForeignTableFilterType) continue; if (typeof foreignTable.from === "function") { continue; } const foreignTableExpression = foreignTable.from; const localAttributes = relation.localAttributes; const remoteAttributes = relation.remoteAttributes; if (isOneToMany) { if (build.behavior.pgCodecRelationMatches(relation, "list") || build.behavior.pgCodecRelationMatches(relation, "connection")) { const filterManyTypeName = inflection.filterManyType(source.codec, foreignTable); const FilterManyType = build.getTypeByName(filterManyTypeName); if (!FilterManyType) { throw new Error(`Failed to retrieve type '${filterManyTypeName}'`); } // TODO: revisit using `_` prefixed inflector const fieldName = inflection._manyRelation({ registry: source.registry, codec: source.codec, relationName, }); const filterFieldName = inflection.filterManyRelationByKeysFieldName(fieldName); fields = extend(fields, { [filterFieldName]: fieldWithHooks({ fieldName: filterFieldName, isPgConnectionFilterField: true, }, () => ({ description: `Filter by the object’s \`${fieldName}\` relation.`, type: FilterManyType, // $where.alias represents source; we need a condition that references the relational target apply: EXPORTABLE((assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes) => function ($where, value) { assertAllowed(value, "object"); const $rel = $where.andPlan(); $rel.extensions.pgFilterRelation = { tableExpression: foreignTableExpression, alias: foreignTable.name, localAttributes, remoteAttributes, }; return $rel; }, [ assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes, ]), })), }, `Adding connection filter backward relation field from ${source.name} to ${foreignTable.name}`); const existsFieldName = inflection.filterBackwardManyRelationExistsFieldName(fieldName); fields = extend(fields, { [existsFieldName]: fieldWithHooks({ fieldName: existsFieldName, isPgConnectionFilterField: true, }, () => ({ description: `Some related \`${fieldName}\` exist.`, type: GraphQLBoolean, // TODO: many of the applyPlan functions in this file // and in PgConnectionArgFilterForwardRelationsPlugin // are very very similar. We should extract them to a // helper function. apply: EXPORTABLE((assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes, sql) => function ($where, value) { assertAllowed(value, "scalar"); if (value == null) return; const $subQuery = $where.existsPlan({ tableExpression: foreignTableExpression, alias: foreignTable.name, equals: value, }); localAttributes.forEach((localAttribute, i) => { const remoteAttribute = remoteAttributes[i]; $subQuery.where(sql `${$where.alias}.${sql.identifier(localAttribute)} = ${$subQuery.alias}.${sql.identifier(remoteAttribute)}`); }); }, [ assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes, sql, ]), })), }, `Adding connection filter backward relation exists field from ${source.name} to ${foreignTable.name}`); } } else { const fieldName = inflection.singleRelationBackwards({ registry: source.registry, codec: source.codec, relationName, }); const filterFieldName = inflection.filterSingleRelationByKeysBackwardsFieldName(fieldName); fields = extend(fields, { [filterFieldName]: fieldWithHooks({ fieldName: filterFieldName, isPgConnectionFilterField: true, }, () => ({ description: `Filter by the object’s \`${fieldName}\` relation.`, type: ForeignTableFilterType, apply: EXPORTABLE((assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes, sql) => function ($where, value) { assertAllowed(value, "object"); const $subQuery = $where.existsPlan({ tableExpression: foreignTableExpression, alias: foreignTable.name, }); localAttributes.forEach((localAttribute, i) => { const remoteAttribute = remoteAttributes[i]; $subQuery.where(sql `${$where.alias}.${sql.identifier(localAttribute)} = ${$subQuery.alias}.${sql.identifier(remoteAttribute)}`); }); return $subQuery; }, [ assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes, sql, ]), })), }, `Adding connection filter backward relation field from ${source.name} to ${foreignTable.name}`); const existsFieldName = inflection.filterBackwardSingleRelationExistsFieldName(fieldName); fields = build.recoverable(fields, () => extend(fields, { [existsFieldName]: fieldWithHooks({ fieldName: existsFieldName, isPgConnectionFilterField: true, }, () => ({ description: `A related \`${fieldName}\` exists.`, type: GraphQLBoolean, apply: EXPORTABLE((assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes, sql) => function ($where, value) { assertAllowed(value, "scalar"); if (value == null) return; const $subQuery = $where.existsPlan({ tableExpression: foreignTableExpression, alias: foreignTable.name, equals: value, }); localAttributes.forEach((localAttribute, i) => { const remoteAttribute = remoteAttributes[i]; $subQuery.where(sql `${$where.alias}.${sql.identifier(localAttribute)} = ${$subQuery.alias}.${sql.identifier(remoteAttribute)}`); }); }, [ assertAllowed, foreignTable, foreignTableExpression, localAttributes, remoteAttributes, sql, ]), })), }, `Adding connection filter backward relation exists field from ${source.name} to ${foreignTable.name}`)); } } } if (isPgConnectionFilterMany && foreignTable) { const foreignTableTypeName = inflection.tableType(foreignTable.codec); const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName); const FilterType = build.getTypeByName(foreignTableFilterTypeName); if (!FilterType) { throw new Error(`Failed to load type ${foreignTableFilterTypeName}`); } const manyFields = { every: fieldWithHooks({ fieldName: "every", isPgConnectionFilterManyField: true, }, () => ({ description: `Every related \`${foreignTableTypeName}\` matches the filter criteria. All fields are combined with a logical ‘and.’`, type: FilterType, apply: EXPORTABLE((assertAllowed, sql) => function ($where, value) { assertAllowed(value, "object"); if (value == null) return; if (!$where.extensions.pgFilterRelation) { throw new Error(`Invalid use of filter, 'pgFilterRelation' expected`); } const { localAttributes, remoteAttributes, tableExpression, alias, } = $where.extensions.pgFilterRelation; const $subQuery = $where.notPlan().existsPlan({ tableExpression, alias, }); localAttributes.forEach((localAttribute, i) => { const remoteAttribute = remoteAttributes[i]; $subQuery.where(sql `${$where.alias}.${sql.identifier(localAttribute)} = ${$subQuery.alias}.${sql.identifier(remoteAttribute)}`); }); return $subQuery.notPlan().andPlan(); }, [assertAllowed, sql]), })), some: fieldWithHooks({ fieldName: "some", isPgConnectionFilterManyField: true, }, () => ({ description: `Some related \`${foreignTableTypeName}\` matches the filter criteria. All fields are combined with a logical ‘and.’`, type: FilterType, apply: EXPORTABLE((assertAllowed, sql) => function ($where, value) { assertAllowed(value, "object"); if (value == null) return; if (!$where.extensions.pgFilterRelation) { throw new Error(`Invalid use of filter, 'pgFilterRelation' expected`); } const { localAttributes, remoteAttributes, tableExpression, alias, } = $where.extensions.pgFilterRelation; const $subQuery = $where.existsPlan({ tableExpression, alias, }); localAttributes.forEach((localAttribute, i) => { const remoteAttribute = remoteAttributes[i]; $subQuery.where(sql `${$where.alias}.${sql.identifier(localAttribute)} = ${$subQuery.alias}.${sql.identifier(remoteAttribute)}`); }); $subQuery.ignoreUnlessAmended(); return $subQuery; }, [assertAllowed, sql]), })), none: fieldWithHooks({ fieldName: "none", isPgConnectionFilterManyField: true, }, () => ({ description: `No related \`${foreignTableTypeName}\` matches the filter criteria. All fields are combined with a logical ‘and.’`, type: FilterType, apply: EXPORTABLE((assertAllowed, sql) => function ($where, value) { assertAllowed(value, "object"); if (value == null) return; if (!$where.extensions.pgFilterRelation) { throw new Error(`Invalid use of filter, 'pgFilterRelation' expected`); } const { localAttributes, remoteAttributes, tableExpression, alias, } = $where.extensions.pgFilterRelation; const $subQuery = $where.notPlan().existsPlan({ tableExpression, alias, }); localAttributes.forEach((localAttribute, i) => { const remoteAttribute = remoteAttributes[i]; $subQuery.where(sql `${$where.alias}.${sql.identifier(localAttribute)} = ${$subQuery.alias}.${sql.identifier(remoteAttribute)}`); }); $subQuery.ignoreUnlessAmended(); return $subQuery; }, [assertAllowed, sql]), })), }; fields = extend(fields, manyFields, ""); } return fields; }, }, }, }; //# sourceMappingURL=PgConnectionArgFilterBackwardRelationsPlugin.js.map