graphile-plugin-connection-filter 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ConnectionArgFilterPlugin.d.ts +3 -0
- package/ConnectionArgFilterPlugin.js +18 -0
- package/LICENSE +23 -0
- package/PgConnectionArgFilterBackwardRelationsPlugin.d.ts +12 -0
- package/PgConnectionArgFilterBackwardRelationsPlugin.js +244 -0
- package/PgConnectionArgFilterColumnsPlugin.d.ts +3 -0
- package/PgConnectionArgFilterColumnsPlugin.js +51 -0
- package/PgConnectionArgFilterCompositeTypeColumnsPlugin.d.ts +3 -0
- package/PgConnectionArgFilterCompositeTypeColumnsPlugin.js +67 -0
- package/PgConnectionArgFilterComputedColumnsPlugin.d.ts +3 -0
- package/PgConnectionArgFilterComputedColumnsPlugin.js +114 -0
- package/PgConnectionArgFilterForwardRelationsPlugin.d.ts +11 -0
- package/PgConnectionArgFilterForwardRelationsPlugin.js +130 -0
- package/PgConnectionArgFilterLogicalOperatorsPlugin.d.ts +3 -0
- package/PgConnectionArgFilterLogicalOperatorsPlugin.js +67 -0
- package/PgConnectionArgFilterOperatorsPlugin.d.ts +15 -0
- package/PgConnectionArgFilterOperatorsPlugin.js +551 -0
- package/PgConnectionArgFilterPlugin.d.ts +27 -0
- package/PgConnectionArgFilterPlugin.js +305 -0
- package/PgConnectionArgFilterRecordFunctionsPlugin.d.ts +3 -0
- package/PgConnectionArgFilterRecordFunctionsPlugin.js +75 -0
- package/README.md +364 -0
- package/esm/ConnectionArgFilterPlugin.js +16 -0
- package/esm/PgConnectionArgFilterBackwardRelationsPlugin.js +242 -0
- package/esm/PgConnectionArgFilterColumnsPlugin.js +49 -0
- package/esm/PgConnectionArgFilterCompositeTypeColumnsPlugin.js +65 -0
- package/esm/PgConnectionArgFilterComputedColumnsPlugin.js +112 -0
- package/esm/PgConnectionArgFilterForwardRelationsPlugin.js +128 -0
- package/esm/PgConnectionArgFilterLogicalOperatorsPlugin.js +65 -0
- package/esm/PgConnectionArgFilterOperatorsPlugin.js +549 -0
- package/esm/PgConnectionArgFilterPlugin.js +303 -0
- package/esm/PgConnectionArgFilterRecordFunctionsPlugin.js +73 -0
- package/esm/index.js +58 -0
- package/esm/types.js +1 -0
- package/index.d.ts +6 -0
- package/index.js +64 -0
- package/package.json +58 -0
- package/types.d.ts +25 -0
- package/types.js +2 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const PgConnectionArgFilterColumnsPlugin = (builder) => {
|
|
2
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
|
|
3
|
+
const { extend, newWithHooks, pgSql: sql, pgIntrospectionResultsByKind: introspectionResultsByKind, pgColumnFilter, pgOmit: omit, inflection, connectionFilterOperatorsType, connectionFilterRegisterResolver, connectionFilterResolve, connectionFilterTypesByTypeName, } = build;
|
|
4
|
+
const { fieldWithHooks, scope: { pgIntrospection: table, isPgConnectionFilter }, Self, } = context;
|
|
5
|
+
if (!isPgConnectionFilter || table.kind !== 'class')
|
|
6
|
+
return fields;
|
|
7
|
+
connectionFilterTypesByTypeName[Self.name] = Self;
|
|
8
|
+
const attrByFieldName = introspectionResultsByKind.attribute
|
|
9
|
+
.filter((attr) => attr.classId === table.id)
|
|
10
|
+
.filter((attr) => pgColumnFilter(attr, build, context))
|
|
11
|
+
.filter((attr) => !omit(attr, 'filter'))
|
|
12
|
+
.reduce((memo, attr) => {
|
|
13
|
+
const fieldName = inflection.column(attr);
|
|
14
|
+
memo[fieldName] = attr;
|
|
15
|
+
return memo;
|
|
16
|
+
}, {});
|
|
17
|
+
const operatorsTypeNameByFieldName = {};
|
|
18
|
+
const attrFields = Object.entries(attrByFieldName).reduce((memo, [fieldName, attr]) => {
|
|
19
|
+
const OperatorsType = connectionFilterOperatorsType(newWithHooks, attr.typeId, attr.typeModifier);
|
|
20
|
+
if (!OperatorsType) {
|
|
21
|
+
return memo;
|
|
22
|
+
}
|
|
23
|
+
operatorsTypeNameByFieldName[fieldName] = OperatorsType.name;
|
|
24
|
+
return extend(memo, {
|
|
25
|
+
[fieldName]: fieldWithHooks(fieldName, {
|
|
26
|
+
description: `Filter by the object’s \`${fieldName}\` field.`,
|
|
27
|
+
type: OperatorsType,
|
|
28
|
+
}, {
|
|
29
|
+
isPgConnectionFilterField: true,
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
}, {});
|
|
33
|
+
const resolve = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
|
|
34
|
+
if (fieldValue == null)
|
|
35
|
+
return null;
|
|
36
|
+
const attr = attrByFieldName[fieldName];
|
|
37
|
+
const sqlIdentifier = sql.query `${sourceAlias}.${sql.identifier(attr.name)}`;
|
|
38
|
+
const pgType = attr.type;
|
|
39
|
+
const pgTypeModifier = attr.typeModifier;
|
|
40
|
+
const filterTypeName = operatorsTypeNameByFieldName[fieldName];
|
|
41
|
+
return connectionFilterResolve(fieldValue, sqlIdentifier, filterTypeName, queryBuilder, pgType, pgTypeModifier, fieldName);
|
|
42
|
+
};
|
|
43
|
+
for (const fieldName of Object.keys(attrFields)) {
|
|
44
|
+
connectionFilterRegisterResolver(Self.name, fieldName, resolve);
|
|
45
|
+
}
|
|
46
|
+
return extend(fields, attrFields);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
export default PgConnectionArgFilterColumnsPlugin;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const PgConnectionArgFilterCompositeTypeColumnsPlugin = (builder, rawOptions) => {
|
|
2
|
+
const { connectionFilterAllowedFieldTypes } = rawOptions;
|
|
3
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
|
|
4
|
+
const { extend, newWithHooks, pgSql: sql, pgIntrospectionResultsByKind: introspectionResultsByKind, pgGetGqlTypeByTypeIdAndModifier, pgColumnFilter, pgOmit: omit, inflection, connectionFilterRegisterResolver, connectionFilterResolve, connectionFilterType, connectionFilterTypesByTypeName, } = build;
|
|
5
|
+
const { fieldWithHooks, scope: { pgIntrospection: table, isPgConnectionFilter }, Self, } = context;
|
|
6
|
+
if (!isPgConnectionFilter || table.kind !== 'class')
|
|
7
|
+
return fields;
|
|
8
|
+
connectionFilterTypesByTypeName[Self.name] = Self;
|
|
9
|
+
const attrByFieldName = introspectionResultsByKind.attribute
|
|
10
|
+
.filter((attr) => attr.classId === table.id)
|
|
11
|
+
.filter((attr) => pgColumnFilter(attr, build, context))
|
|
12
|
+
.filter((attr) => !omit(attr, 'filter'))
|
|
13
|
+
.filter((attr) => attr.type &&
|
|
14
|
+
attr.type.type === 'c' &&
|
|
15
|
+
attr.type.class &&
|
|
16
|
+
!attr.type.class.isSelectable) // keep only the composite type columns
|
|
17
|
+
.reduce((memo, attr) => {
|
|
18
|
+
const fieldName = inflection.column(attr);
|
|
19
|
+
memo[fieldName] = attr;
|
|
20
|
+
return memo;
|
|
21
|
+
}, {});
|
|
22
|
+
const filterTypeNameByFieldName = {};
|
|
23
|
+
const attrFields = Object.entries(attrByFieldName).reduce((memo, [fieldName, attr]) => {
|
|
24
|
+
const NodeType = pgGetGqlTypeByTypeIdAndModifier(attr.typeId, attr.typeModifier);
|
|
25
|
+
if (!NodeType) {
|
|
26
|
+
return memo;
|
|
27
|
+
}
|
|
28
|
+
const nodeTypeName = NodeType.name;
|
|
29
|
+
// Respect `connectionFilterAllowedFieldTypes` config option
|
|
30
|
+
if (connectionFilterAllowedFieldTypes &&
|
|
31
|
+
!connectionFilterAllowedFieldTypes.includes(nodeTypeName)) {
|
|
32
|
+
return memo;
|
|
33
|
+
}
|
|
34
|
+
const filterTypeName = inflection.filterType(nodeTypeName);
|
|
35
|
+
const CompositeFilterType = connectionFilterType(newWithHooks, filterTypeName, attr.type.class, nodeTypeName);
|
|
36
|
+
if (!CompositeFilterType) {
|
|
37
|
+
return memo;
|
|
38
|
+
}
|
|
39
|
+
filterTypeNameByFieldName[fieldName] = filterTypeName;
|
|
40
|
+
return extend(memo, {
|
|
41
|
+
[fieldName]: fieldWithHooks(fieldName, {
|
|
42
|
+
description: `Filter by the object’s \`${fieldName}\` field.`,
|
|
43
|
+
type: CompositeFilterType,
|
|
44
|
+
}, {
|
|
45
|
+
isPgConnectionFilterField: true,
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
}, {});
|
|
49
|
+
const resolve = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
|
|
50
|
+
if (fieldValue == null)
|
|
51
|
+
return null;
|
|
52
|
+
const attr = attrByFieldName[fieldName];
|
|
53
|
+
const sqlIdentifier = sql.query `(${sourceAlias}.${sql.identifier(attr.name)})`; // parentheses are required to avoid confusing the parser
|
|
54
|
+
const pgType = attr.type;
|
|
55
|
+
const pgTypeModifier = attr.typeModifier;
|
|
56
|
+
const filterTypeName = filterTypeNameByFieldName[fieldName];
|
|
57
|
+
return connectionFilterResolve(fieldValue, sqlIdentifier, filterTypeName, queryBuilder, pgType, pgTypeModifier, fieldName);
|
|
58
|
+
};
|
|
59
|
+
for (const fieldName of Object.keys(attrFields)) {
|
|
60
|
+
connectionFilterRegisterResolver(Self.name, fieldName, resolve);
|
|
61
|
+
}
|
|
62
|
+
return extend(fields, attrFields);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
export default PgConnectionArgFilterCompositeTypeColumnsPlugin;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const PgConnectionArgFilterComputedColumnsPlugin = (builder, rawOptions) => {
|
|
2
|
+
const { connectionFilterComputedColumns } = rawOptions;
|
|
3
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
|
|
4
|
+
const { extend, newWithHooks, pgIntrospectionResultsByKind: introspectionResultsByKind, pgOmit: omit, pgSql: sql, inflection, connectionFilterOperatorsType, connectionFilterRegisterResolver, connectionFilterResolve, connectionFilterTypesByTypeName, } = build;
|
|
5
|
+
const { scope: { isPgConnectionFilter, pgIntrospection: table }, fieldWithHooks, Self, } = context;
|
|
6
|
+
if (!isPgConnectionFilter || !table || table.kind !== 'class') {
|
|
7
|
+
return fields;
|
|
8
|
+
}
|
|
9
|
+
connectionFilterTypesByTypeName[Self.name] = Self;
|
|
10
|
+
const procByFieldName = introspectionResultsByKind.procedure.reduce((memo, proc) => {
|
|
11
|
+
// Must be marked @filterable OR enabled via plugin option
|
|
12
|
+
if (!(proc.tags.filterable || connectionFilterComputedColumns))
|
|
13
|
+
return memo;
|
|
14
|
+
// Must not be omitted
|
|
15
|
+
if (omit(proc, 'execute'))
|
|
16
|
+
return memo;
|
|
17
|
+
if (omit(proc, 'filter'))
|
|
18
|
+
return memo;
|
|
19
|
+
// Must be a computed column
|
|
20
|
+
const computedColumnDetails = getComputedColumnDetails(build, table, proc);
|
|
21
|
+
if (!computedColumnDetails)
|
|
22
|
+
return memo;
|
|
23
|
+
const { pseudoColumnName } = computedColumnDetails;
|
|
24
|
+
// Must have only one required argument
|
|
25
|
+
const inputArgsCount = proc.argTypeIds.filter((_typeId, idx) => proc.argModes.length === 0 || // all args are `in`
|
|
26
|
+
proc.argModes[idx] === 'i' || // this arg is `in`
|
|
27
|
+
proc.argModes[idx] === 'b' // this arg is `inout`
|
|
28
|
+
).length;
|
|
29
|
+
const nonOptionalArgumentsCount = inputArgsCount - proc.argDefaultsNum;
|
|
30
|
+
if (nonOptionalArgumentsCount > 1) {
|
|
31
|
+
return memo;
|
|
32
|
+
}
|
|
33
|
+
// Must return a scalar or an array
|
|
34
|
+
if (proc.returnsSet)
|
|
35
|
+
return memo;
|
|
36
|
+
const returnType = introspectionResultsByKind.typeById[proc.returnTypeId];
|
|
37
|
+
const returnTypeTable = introspectionResultsByKind.classById[returnType.classId];
|
|
38
|
+
if (returnTypeTable)
|
|
39
|
+
return memo;
|
|
40
|
+
const isRecordLike = returnType.id === '2249';
|
|
41
|
+
if (isRecordLike)
|
|
42
|
+
return memo;
|
|
43
|
+
const isVoid = String(returnType.id) === '2278';
|
|
44
|
+
if (isVoid)
|
|
45
|
+
return memo;
|
|
46
|
+
// Looks good
|
|
47
|
+
const fieldName = inflection.computedColumn(pseudoColumnName, proc, table);
|
|
48
|
+
memo = build.extend(memo, { [fieldName]: proc });
|
|
49
|
+
return memo;
|
|
50
|
+
}, {});
|
|
51
|
+
const operatorsTypeNameByFieldName = {};
|
|
52
|
+
const procFields = Object.entries(procByFieldName).reduce((memo, [fieldName, proc]) => {
|
|
53
|
+
const OperatorsType = connectionFilterOperatorsType(newWithHooks, proc.returnTypeId, null);
|
|
54
|
+
if (!OperatorsType) {
|
|
55
|
+
return memo;
|
|
56
|
+
}
|
|
57
|
+
operatorsTypeNameByFieldName[fieldName] = OperatorsType.name;
|
|
58
|
+
return extend(memo, {
|
|
59
|
+
[fieldName]: fieldWithHooks(fieldName, {
|
|
60
|
+
description: `Filter by the object’s \`${fieldName}\` field.`,
|
|
61
|
+
type: OperatorsType,
|
|
62
|
+
}, {
|
|
63
|
+
isPgConnectionFilterField: true,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
}, {});
|
|
67
|
+
const resolve = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
|
|
68
|
+
if (fieldValue == null)
|
|
69
|
+
return null;
|
|
70
|
+
const proc = procByFieldName[fieldName];
|
|
71
|
+
const sqlIdentifier = sql.query `${sql.identifier(proc.namespace.name)}.${sql.identifier(proc.name)}(${sourceAlias})`;
|
|
72
|
+
const pgType = introspectionResultsByKind.typeById[proc.returnTypeId];
|
|
73
|
+
const pgTypeModifier = null;
|
|
74
|
+
const filterTypeName = operatorsTypeNameByFieldName[fieldName];
|
|
75
|
+
return connectionFilterResolve(fieldValue, sqlIdentifier, filterTypeName, queryBuilder, pgType, pgTypeModifier, fieldName);
|
|
76
|
+
};
|
|
77
|
+
for (const fieldName of Object.keys(procFields)) {
|
|
78
|
+
connectionFilterRegisterResolver(Self.name, fieldName, resolve);
|
|
79
|
+
}
|
|
80
|
+
return extend(fields, procFields);
|
|
81
|
+
});
|
|
82
|
+
function getComputedColumnDetails(build, table, proc) {
|
|
83
|
+
if (!proc.isStable)
|
|
84
|
+
return null;
|
|
85
|
+
if (proc.namespaceId !== table.namespaceId)
|
|
86
|
+
return null;
|
|
87
|
+
if (!proc.name.startsWith(`${table.name}_`))
|
|
88
|
+
return null;
|
|
89
|
+
if (proc.argTypeIds.length < 1)
|
|
90
|
+
return null;
|
|
91
|
+
if (proc.argTypeIds[0] !== table.type.id)
|
|
92
|
+
return null;
|
|
93
|
+
const argTypes = proc.argTypeIds.reduce((prev, typeId, idx) => {
|
|
94
|
+
if (proc.argModes.length === 0 || // all args are `in`
|
|
95
|
+
proc.argModes[idx] === 'i' || // this arg is `in`
|
|
96
|
+
proc.argModes[idx] === 'b' // this arg is `inout`
|
|
97
|
+
) {
|
|
98
|
+
prev.push(build.pgIntrospectionResultsByKind.typeById[typeId]);
|
|
99
|
+
}
|
|
100
|
+
return prev;
|
|
101
|
+
}, []);
|
|
102
|
+
if (argTypes
|
|
103
|
+
.slice(1)
|
|
104
|
+
.some((type) => type.type === 'c' && type.class && type.class.isSelectable)) {
|
|
105
|
+
// Accepts two input tables? Skip.
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const pseudoColumnName = proc.name.substr(table.name.length + 1);
|
|
109
|
+
return { argTypes, pseudoColumnName };
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
export default PgConnectionArgFilterComputedColumnsPlugin;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const PgConnectionArgFilterForwardRelationsPlugin = (builder) => {
|
|
2
|
+
builder.hook('inflection', (inflection) => ({
|
|
3
|
+
...inflection,
|
|
4
|
+
filterForwardRelationExistsFieldName(relationFieldName) {
|
|
5
|
+
return `${relationFieldName}Exists`;
|
|
6
|
+
},
|
|
7
|
+
filterSingleRelationFieldName(fieldName) {
|
|
8
|
+
return fieldName;
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
|
|
12
|
+
const { describePgEntity, extend, newWithHooks, inflection, graphql: { GraphQLBoolean }, pgOmit: omit, pgSql: sql, pgIntrospectionResultsByKind: introspectionResultsByKind, connectionFilterResolve, connectionFilterRegisterResolver, connectionFilterTypesByTypeName, connectionFilterType, } = build;
|
|
13
|
+
const { fieldWithHooks, scope: { pgIntrospection: table, isPgConnectionFilter }, Self, } = context;
|
|
14
|
+
if (!isPgConnectionFilter || table.kind !== 'class')
|
|
15
|
+
return fields;
|
|
16
|
+
connectionFilterTypesByTypeName[Self.name] = Self;
|
|
17
|
+
const forwardRelationSpecs = introspectionResultsByKind.constraint
|
|
18
|
+
.filter((con) => con.type === 'f')
|
|
19
|
+
.filter((con) => con.classId === table.id)
|
|
20
|
+
.reduce((memo, constraint) => {
|
|
21
|
+
if (omit(constraint, 'read') || omit(constraint, 'filter')) {
|
|
22
|
+
return memo;
|
|
23
|
+
}
|
|
24
|
+
const foreignTable = constraint.foreignClassId
|
|
25
|
+
? introspectionResultsByKind.classById[constraint.foreignClassId]
|
|
26
|
+
: null;
|
|
27
|
+
if (!foreignTable) {
|
|
28
|
+
throw new Error(`Could not find the foreign table (constraint: ${constraint.name})`);
|
|
29
|
+
}
|
|
30
|
+
if (omit(foreignTable, 'read') || omit(foreignTable, 'filter')) {
|
|
31
|
+
return memo;
|
|
32
|
+
}
|
|
33
|
+
const attributes = introspectionResultsByKind.attribute
|
|
34
|
+
.filter((attr) => attr.classId === table.id)
|
|
35
|
+
.sort((a, b) => a.num - b.num);
|
|
36
|
+
const foreignAttributes = introspectionResultsByKind.attribute
|
|
37
|
+
.filter((attr) => attr.classId === foreignTable.id)
|
|
38
|
+
.sort((a, b) => a.num - b.num);
|
|
39
|
+
const keyAttributes = constraint.keyAttributeNums.map((num) => attributes.filter((attr) => attr.num === num)[0]);
|
|
40
|
+
const foreignKeyAttributes = constraint.foreignKeyAttributeNums.map((num) => foreignAttributes.filter((attr) => attr.num === num)[0]);
|
|
41
|
+
if (keyAttributes.some((attr) => omit(attr, 'read'))) {
|
|
42
|
+
return memo;
|
|
43
|
+
}
|
|
44
|
+
if (foreignKeyAttributes.some((attr) => omit(attr, 'read'))) {
|
|
45
|
+
return memo;
|
|
46
|
+
}
|
|
47
|
+
memo.push({
|
|
48
|
+
table,
|
|
49
|
+
keyAttributes,
|
|
50
|
+
foreignTable,
|
|
51
|
+
foreignKeyAttributes,
|
|
52
|
+
constraint,
|
|
53
|
+
});
|
|
54
|
+
return memo;
|
|
55
|
+
}, []);
|
|
56
|
+
let forwardRelationSpecByFieldName = {};
|
|
57
|
+
const addField = (fieldName, description, type, resolve, spec, hint) => {
|
|
58
|
+
// Field
|
|
59
|
+
fields = extend(fields, {
|
|
60
|
+
[fieldName]: fieldWithHooks(fieldName, {
|
|
61
|
+
description,
|
|
62
|
+
type,
|
|
63
|
+
}, {
|
|
64
|
+
isPgConnectionFilterField: true,
|
|
65
|
+
}),
|
|
66
|
+
}, hint);
|
|
67
|
+
// Spec for use in resolver
|
|
68
|
+
forwardRelationSpecByFieldName = extend(forwardRelationSpecByFieldName, {
|
|
69
|
+
[fieldName]: spec,
|
|
70
|
+
});
|
|
71
|
+
// Resolver
|
|
72
|
+
connectionFilterRegisterResolver(Self.name, fieldName, resolve);
|
|
73
|
+
};
|
|
74
|
+
const resolve = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
|
|
75
|
+
if (fieldValue == null)
|
|
76
|
+
return null;
|
|
77
|
+
const { foreignTable, foreignKeyAttributes, keyAttributes } = forwardRelationSpecByFieldName[fieldName];
|
|
78
|
+
const foreignTableAlias = sql.identifier(Symbol());
|
|
79
|
+
const sqlIdentifier = sql.identifier(foreignTable.namespace.name, foreignTable.name);
|
|
80
|
+
const sqlKeysMatch = sql.query `(${sql.join(keyAttributes.map((key, i) => {
|
|
81
|
+
return sql.fragment `${sourceAlias}.${sql.identifier(key.name)} = ${foreignTableAlias}.${sql.identifier(foreignKeyAttributes[i].name)}`;
|
|
82
|
+
}), ') and (')})`;
|
|
83
|
+
const foreignTableTypeName = inflection.tableType(foreignTable);
|
|
84
|
+
const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName);
|
|
85
|
+
const sqlFragment = connectionFilterResolve(fieldValue, foreignTableAlias, foreignTableFilterTypeName, queryBuilder);
|
|
86
|
+
return sqlFragment == null
|
|
87
|
+
? null
|
|
88
|
+
: sql.query `\
|
|
89
|
+
exists(
|
|
90
|
+
select 1 from ${sqlIdentifier} as ${foreignTableAlias}
|
|
91
|
+
where ${sqlKeysMatch} and
|
|
92
|
+
(${sqlFragment})
|
|
93
|
+
)`;
|
|
94
|
+
};
|
|
95
|
+
const resolveExists = ({ sourceAlias, fieldName, fieldValue, }) => {
|
|
96
|
+
if (fieldValue == null)
|
|
97
|
+
return null;
|
|
98
|
+
const { foreignTable, foreignKeyAttributes, keyAttributes } = forwardRelationSpecByFieldName[fieldName];
|
|
99
|
+
const foreignTableAlias = sql.identifier(Symbol());
|
|
100
|
+
const sqlIdentifier = sql.identifier(foreignTable.namespace.name, foreignTable.name);
|
|
101
|
+
const sqlKeysMatch = sql.query `(${sql.join(keyAttributes.map((key, i) => {
|
|
102
|
+
return sql.fragment `${sourceAlias}.${sql.identifier(key.name)} = ${foreignTableAlias}.${sql.identifier(foreignKeyAttributes[i].name)}`;
|
|
103
|
+
}), ') and (')})`;
|
|
104
|
+
const sqlSelectWhereKeysMatch = sql.query `select 1 from ${sqlIdentifier} as ${foreignTableAlias} where ${sqlKeysMatch}`;
|
|
105
|
+
return fieldValue === true
|
|
106
|
+
? sql.query `exists(${sqlSelectWhereKeysMatch})`
|
|
107
|
+
: sql.query `not exists(${sqlSelectWhereKeysMatch})`;
|
|
108
|
+
};
|
|
109
|
+
for (const spec of forwardRelationSpecs) {
|
|
110
|
+
const { constraint, foreignTable, keyAttributes } = spec;
|
|
111
|
+
const fieldName = inflection.singleRelationByKeys(keyAttributes, foreignTable, table, constraint);
|
|
112
|
+
const filterFieldName = inflection.filterSingleRelationFieldName(fieldName);
|
|
113
|
+
const foreignTableTypeName = inflection.tableType(foreignTable);
|
|
114
|
+
const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName);
|
|
115
|
+
const ForeignTableFilterType = connectionFilterType(newWithHooks, foreignTableFilterTypeName, foreignTable, foreignTableTypeName);
|
|
116
|
+
if (!ForeignTableFilterType)
|
|
117
|
+
continue;
|
|
118
|
+
addField(filterFieldName, `Filter by the object’s \`${fieldName}\` relation.`, ForeignTableFilterType, resolve, spec, `Adding connection filter forward relation field from ${describePgEntity(table)} to ${describePgEntity(foreignTable)}`);
|
|
119
|
+
const keyIsNullable = !keyAttributes.every((attr) => attr.isNotNull);
|
|
120
|
+
if (keyIsNullable) {
|
|
121
|
+
const existsFieldName = inflection.filterForwardRelationExistsFieldName(fieldName);
|
|
122
|
+
addField(existsFieldName, `A related \`${fieldName}\` exists.`, GraphQLBoolean, resolveExists, spec, `Adding connection filter forward relation exists field from ${describePgEntity(table)} to ${describePgEntity(foreignTable)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return fields;
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
export default PgConnectionArgFilterForwardRelationsPlugin;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const PgConnectionArgFilterLogicalOperatorsPlugin = (builder) => {
|
|
2
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
|
|
3
|
+
const { extend, graphql: { GraphQLList, GraphQLNonNull }, pgSql: sql, connectionFilterTypesByTypeName, connectionFilterResolve, connectionFilterRegisterResolver, } = build;
|
|
4
|
+
const { fieldWithHooks, scope: { isPgConnectionFilter }, Self, } = context;
|
|
5
|
+
if (!isPgConnectionFilter)
|
|
6
|
+
return fields;
|
|
7
|
+
connectionFilterTypesByTypeName[Self.name] = Self;
|
|
8
|
+
if (Object.keys(fields).length === 0) {
|
|
9
|
+
// Skip adding these operators if they would be the only fields
|
|
10
|
+
return fields;
|
|
11
|
+
}
|
|
12
|
+
const logicResolversByFieldName = {
|
|
13
|
+
and: (arr, sourceAlias, queryBuilder) => {
|
|
14
|
+
const sqlFragments = arr
|
|
15
|
+
.map((o) => connectionFilterResolve(o, sourceAlias, Self.name, queryBuilder))
|
|
16
|
+
.filter((x) => x != null);
|
|
17
|
+
return sqlFragments.length === 0
|
|
18
|
+
? null
|
|
19
|
+
: sql.query `(${sql.join(sqlFragments, ') and (')})`;
|
|
20
|
+
},
|
|
21
|
+
or: (arr, sourceAlias, queryBuilder) => {
|
|
22
|
+
const sqlFragments = arr
|
|
23
|
+
.map((o) => connectionFilterResolve(o, sourceAlias, Self.name, queryBuilder))
|
|
24
|
+
.filter((x) => x != null);
|
|
25
|
+
return sqlFragments.length === 0
|
|
26
|
+
? null
|
|
27
|
+
: sql.query `(${sql.join(sqlFragments, ') or (')})`;
|
|
28
|
+
},
|
|
29
|
+
not: (obj, sourceAlias, queryBuilder) => {
|
|
30
|
+
const sqlFragment = connectionFilterResolve(obj, sourceAlias, Self.name, queryBuilder);
|
|
31
|
+
return sqlFragment == null ? null : sql.query `not (${sqlFragment})`;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const logicalOperatorFields = {
|
|
35
|
+
and: fieldWithHooks('and', {
|
|
36
|
+
description: `Checks for all expressions in this list.`,
|
|
37
|
+
type: new GraphQLList(new GraphQLNonNull(Self)),
|
|
38
|
+
}, {
|
|
39
|
+
isPgConnectionFilterOperatorLogical: true,
|
|
40
|
+
}),
|
|
41
|
+
or: fieldWithHooks('or', {
|
|
42
|
+
description: `Checks for any expressions in this list.`,
|
|
43
|
+
type: new GraphQLList(new GraphQLNonNull(Self)),
|
|
44
|
+
}, {
|
|
45
|
+
isPgConnectionFilterOperatorLogical: true,
|
|
46
|
+
}),
|
|
47
|
+
not: fieldWithHooks('not', {
|
|
48
|
+
description: `Negates the expression.`,
|
|
49
|
+
type: Self,
|
|
50
|
+
}, {
|
|
51
|
+
isPgConnectionFilterOperatorLogical: true,
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
const resolve = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
|
|
55
|
+
if (fieldValue == null)
|
|
56
|
+
return null;
|
|
57
|
+
return logicResolversByFieldName[fieldName](fieldValue, sourceAlias, queryBuilder);
|
|
58
|
+
};
|
|
59
|
+
for (const fieldName of Object.keys(logicResolversByFieldName)) {
|
|
60
|
+
connectionFilterRegisterResolver(Self.name, fieldName, resolve);
|
|
61
|
+
}
|
|
62
|
+
return extend(fields, logicalOperatorFields);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
export default PgConnectionArgFilterLogicalOperatorsPlugin;
|