postgraphile-plugin-connection-filter
Version:
Filtering on PostGraphile connections
484 lines (469 loc) • 28.7 kB
JavaScript
;
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