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,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ConnectionArgFilterPlugin = (builder) => {
|
|
4
|
+
builder.hook('inflection', (inflection) => {
|
|
5
|
+
return Object.assign(inflection, {
|
|
6
|
+
filterType(typeName) {
|
|
7
|
+
return `${typeName}Filter`;
|
|
8
|
+
},
|
|
9
|
+
filterFieldType(typeName) {
|
|
10
|
+
return `${typeName}Filter`;
|
|
11
|
+
},
|
|
12
|
+
filterFieldListType(typeName) {
|
|
13
|
+
return `${typeName}ListFilter`;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
exports.default = ConnectionArgFilterPlugin;
|
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dan Lynch <pyramation@gmail.com>
|
|
4
|
+
Copyright (c) 2025 Hyperweb <developers@hyperweb.io>
|
|
5
|
+
Copyright (c) 2020-present, Interweb, Inc.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Plugin } from 'graphile-build';
|
|
2
|
+
import type { PgAttribute, PgClass, PgConstraint } from 'graphile-build-pg';
|
|
3
|
+
declare const PgConnectionArgFilterBackwardRelationsPlugin: Plugin;
|
|
4
|
+
export interface BackwardRelationSpec {
|
|
5
|
+
table: PgClass;
|
|
6
|
+
keyAttributes: PgAttribute[];
|
|
7
|
+
foreignTable: PgClass;
|
|
8
|
+
foreignKeyAttributes: PgAttribute[];
|
|
9
|
+
foreignConstraint: PgConstraint;
|
|
10
|
+
isOneToMany: boolean;
|
|
11
|
+
}
|
|
12
|
+
export default PgConnectionArgFilterBackwardRelationsPlugin;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const PgConnectionArgFilterBackwardRelationsPlugin = (builder, rawOptions) => {
|
|
4
|
+
const { pgSimpleCollections, pgOmitListSuffix, connectionFilterUseListInflectors, } = rawOptions;
|
|
5
|
+
const hasConnections = pgSimpleCollections !== 'only';
|
|
6
|
+
const simpleInflectorsAreShorter = pgOmitListSuffix === true;
|
|
7
|
+
if (simpleInflectorsAreShorter &&
|
|
8
|
+
connectionFilterUseListInflectors === undefined) {
|
|
9
|
+
// TODO: in V3 consider doing this for the user automatically (doing it in V2 would be a breaking change)
|
|
10
|
+
console.warn(`We recommend you set the 'connectionFilterUseListInflectors' option to 'true' since you've set the 'pgOmitListSuffix' option`);
|
|
11
|
+
}
|
|
12
|
+
const useConnectionInflectors = connectionFilterUseListInflectors === undefined
|
|
13
|
+
? hasConnections
|
|
14
|
+
: !connectionFilterUseListInflectors;
|
|
15
|
+
builder.hook('inflection', (inflection) => {
|
|
16
|
+
return Object.assign(inflection, {
|
|
17
|
+
filterManyType(table, foreignTable) {
|
|
18
|
+
return this.upperCamelCase(`${this.tableType(table)}-to-many-${this.tableType(foreignTable)}-filter`);
|
|
19
|
+
},
|
|
20
|
+
filterBackwardSingleRelationExistsFieldName(relationFieldName) {
|
|
21
|
+
return `${relationFieldName}Exists`;
|
|
22
|
+
},
|
|
23
|
+
filterBackwardManyRelationExistsFieldName(relationFieldName) {
|
|
24
|
+
return `${relationFieldName}Exist`;
|
|
25
|
+
},
|
|
26
|
+
filterSingleRelationByKeysBackwardsFieldName(fieldName) {
|
|
27
|
+
return fieldName;
|
|
28
|
+
},
|
|
29
|
+
filterManyRelationByKeysFieldName(fieldName) {
|
|
30
|
+
return fieldName;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
|
|
35
|
+
const { describePgEntity, extend, newWithHooks, inflection, pgOmit: omit, pgSql: sql, pgIntrospectionResultsByKind: introspectionResultsByKind, graphql: { GraphQLInputObjectType, GraphQLBoolean }, connectionFilterResolve, connectionFilterRegisterResolver, connectionFilterTypesByTypeName, connectionFilterType, } = build;
|
|
36
|
+
const { fieldWithHooks, scope: { pgIntrospection: table, isPgConnectionFilter }, Self, } = context;
|
|
37
|
+
if (!isPgConnectionFilter || table.kind !== 'class')
|
|
38
|
+
return fields;
|
|
39
|
+
connectionFilterTypesByTypeName[Self.name] = Self;
|
|
40
|
+
const backwardRelationSpecs = introspectionResultsByKind.constraint
|
|
41
|
+
.filter((con) => con.type === 'f')
|
|
42
|
+
.filter((con) => con.foreignClassId === table.id)
|
|
43
|
+
.reduce((memo, foreignConstraint) => {
|
|
44
|
+
if (omit(foreignConstraint, 'read') ||
|
|
45
|
+
omit(foreignConstraint, 'filter')) {
|
|
46
|
+
return memo;
|
|
47
|
+
}
|
|
48
|
+
const foreignTable = introspectionResultsByKind.classById[foreignConstraint.classId];
|
|
49
|
+
if (!foreignTable) {
|
|
50
|
+
throw new Error(`Could not find the foreign table (constraint: ${foreignConstraint.name})`);
|
|
51
|
+
}
|
|
52
|
+
if (omit(foreignTable, 'read') || omit(foreignTable, 'filter')) {
|
|
53
|
+
return memo;
|
|
54
|
+
}
|
|
55
|
+
const attributes = introspectionResultsByKind.attribute
|
|
56
|
+
.filter((attr) => attr.classId === table.id)
|
|
57
|
+
.sort((a, b) => a.num - b.num);
|
|
58
|
+
const foreignAttributes = introspectionResultsByKind.attribute
|
|
59
|
+
.filter((attr) => attr.classId === foreignTable.id)
|
|
60
|
+
.sort((a, b) => a.num - b.num);
|
|
61
|
+
const keyAttributes = foreignConstraint.foreignKeyAttributeNums.map((num) => attributes.filter((attr) => attr.num === num)[0]);
|
|
62
|
+
const foreignKeyAttributes = foreignConstraint.keyAttributeNums.map((num) => foreignAttributes.filter((attr) => attr.num === num)[0]);
|
|
63
|
+
if (keyAttributes.some((attr) => omit(attr, 'read'))) {
|
|
64
|
+
return memo;
|
|
65
|
+
}
|
|
66
|
+
if (foreignKeyAttributes.some((attr) => omit(attr, 'read'))) {
|
|
67
|
+
return memo;
|
|
68
|
+
}
|
|
69
|
+
const isForeignKeyUnique = !!introspectionResultsByKind.constraint.find((c) => c.classId === foreignTable.id &&
|
|
70
|
+
(c.type === 'p' || c.type === 'u') &&
|
|
71
|
+
c.keyAttributeNums.length === foreignKeyAttributes.length &&
|
|
72
|
+
c.keyAttributeNums.every((n, i) => foreignKeyAttributes[i].num === n));
|
|
73
|
+
memo.push({
|
|
74
|
+
table,
|
|
75
|
+
keyAttributes,
|
|
76
|
+
foreignTable,
|
|
77
|
+
foreignKeyAttributes,
|
|
78
|
+
foreignConstraint,
|
|
79
|
+
isOneToMany: !isForeignKeyUnique,
|
|
80
|
+
});
|
|
81
|
+
return memo;
|
|
82
|
+
}, []);
|
|
83
|
+
let backwardRelationSpecByFieldName = {};
|
|
84
|
+
const addField = (fieldName, description, type, resolve, spec, hint) => {
|
|
85
|
+
// Field
|
|
86
|
+
fields = extend(fields, {
|
|
87
|
+
[fieldName]: fieldWithHooks(fieldName, {
|
|
88
|
+
description,
|
|
89
|
+
type,
|
|
90
|
+
}, {
|
|
91
|
+
isPgConnectionFilterField: true,
|
|
92
|
+
}),
|
|
93
|
+
}, hint);
|
|
94
|
+
// Relation spec for use in resolver
|
|
95
|
+
backwardRelationSpecByFieldName = extend(backwardRelationSpecByFieldName, {
|
|
96
|
+
[fieldName]: spec,
|
|
97
|
+
});
|
|
98
|
+
// Resolver
|
|
99
|
+
connectionFilterRegisterResolver(Self.name, fieldName, resolve);
|
|
100
|
+
};
|
|
101
|
+
const resolveSingle = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
|
|
102
|
+
if (fieldValue == null)
|
|
103
|
+
return null;
|
|
104
|
+
const { foreignTable, foreignKeyAttributes, keyAttributes } = backwardRelationSpecByFieldName[fieldName];
|
|
105
|
+
const foreignTableTypeName = inflection.tableType(foreignTable);
|
|
106
|
+
const foreignTableAlias = sql.identifier(Symbol());
|
|
107
|
+
const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName);
|
|
108
|
+
const sqlIdentifier = sql.identifier(foreignTable.namespace.name, foreignTable.name);
|
|
109
|
+
const sqlKeysMatch = sql.query `(${sql.join(foreignKeyAttributes.map((attr, i) => {
|
|
110
|
+
return sql.fragment `${foreignTableAlias}.${sql.identifier(attr.name)} = ${sourceAlias}.${sql.identifier(keyAttributes[i].name)}`;
|
|
111
|
+
}), ') and (')})`;
|
|
112
|
+
const sqlSelectWhereKeysMatch = sql.query `select 1 from ${sqlIdentifier} as ${foreignTableAlias} where ${sqlKeysMatch}`;
|
|
113
|
+
const sqlFragment = connectionFilterResolve(fieldValue, foreignTableAlias, foreignTableFilterTypeName, queryBuilder);
|
|
114
|
+
return sqlFragment == null
|
|
115
|
+
? null
|
|
116
|
+
: sql.query `exists(${sqlSelectWhereKeysMatch} and (${sqlFragment}))`;
|
|
117
|
+
};
|
|
118
|
+
const resolveExists = ({ sourceAlias, fieldName, fieldValue, }) => {
|
|
119
|
+
if (fieldValue == null)
|
|
120
|
+
return null;
|
|
121
|
+
const { foreignTable, foreignKeyAttributes, keyAttributes } = backwardRelationSpecByFieldName[fieldName];
|
|
122
|
+
const foreignTableAlias = sql.identifier(Symbol());
|
|
123
|
+
const sqlIdentifier = sql.identifier(foreignTable.namespace.name, foreignTable.name);
|
|
124
|
+
const sqlKeysMatch = sql.query `(${sql.join(foreignKeyAttributes.map((attr, i) => {
|
|
125
|
+
return sql.fragment `${foreignTableAlias}.${sql.identifier(attr.name)} = ${sourceAlias}.${sql.identifier(keyAttributes[i].name)}`;
|
|
126
|
+
}), ') and (')})`;
|
|
127
|
+
const sqlSelectWhereKeysMatch = sql.query `select 1 from ${sqlIdentifier} as ${foreignTableAlias} where ${sqlKeysMatch}`;
|
|
128
|
+
return fieldValue === true
|
|
129
|
+
? sql.query `exists(${sqlSelectWhereKeysMatch})`
|
|
130
|
+
: sql.query `not exists(${sqlSelectWhereKeysMatch})`;
|
|
131
|
+
};
|
|
132
|
+
const makeResolveMany = (backwardRelationSpec) => {
|
|
133
|
+
const resolveMany = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
|
|
134
|
+
if (fieldValue == null)
|
|
135
|
+
return null;
|
|
136
|
+
const { foreignTable } = backwardRelationSpecByFieldName[fieldName];
|
|
137
|
+
const foreignTableFilterManyTypeName = inflection.filterManyType(table, foreignTable);
|
|
138
|
+
const sqlFragment = connectionFilterResolve(fieldValue, sourceAlias, foreignTableFilterManyTypeName, queryBuilder, null, null, null, { backwardRelationSpec });
|
|
139
|
+
return sqlFragment == null ? null : sqlFragment;
|
|
140
|
+
};
|
|
141
|
+
return resolveMany;
|
|
142
|
+
};
|
|
143
|
+
for (const spec of backwardRelationSpecs) {
|
|
144
|
+
const { foreignTable, foreignKeyAttributes, foreignConstraint, isOneToMany, } = spec;
|
|
145
|
+
const foreignTableTypeName = inflection.tableType(foreignTable);
|
|
146
|
+
const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName);
|
|
147
|
+
const ForeignTableFilterType = connectionFilterType(newWithHooks, foreignTableFilterTypeName, foreignTable, foreignTableTypeName);
|
|
148
|
+
if (!ForeignTableFilterType)
|
|
149
|
+
continue;
|
|
150
|
+
if (isOneToMany) {
|
|
151
|
+
if (!omit(foreignTable, 'many')) {
|
|
152
|
+
const filterManyTypeName = inflection.filterManyType(table, foreignTable);
|
|
153
|
+
if (!connectionFilterTypesByTypeName[filterManyTypeName]) {
|
|
154
|
+
connectionFilterTypesByTypeName[filterManyTypeName] = newWithHooks(GraphQLInputObjectType, {
|
|
155
|
+
name: filterManyTypeName,
|
|
156
|
+
description: `A filter to be used against many \`${foreignTableTypeName}\` object types. All fields are combined with a logical ‘and.’`,
|
|
157
|
+
}, {
|
|
158
|
+
foreignTable,
|
|
159
|
+
isPgConnectionFilterMany: true,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const FilterManyType = connectionFilterTypesByTypeName[filterManyTypeName];
|
|
163
|
+
const fieldName = useConnectionInflectors
|
|
164
|
+
? inflection.manyRelationByKeys(foreignKeyAttributes, foreignTable, table, foreignConstraint)
|
|
165
|
+
: inflection.manyRelationByKeysSimple(foreignKeyAttributes, foreignTable, table, foreignConstraint);
|
|
166
|
+
const filterFieldName = inflection.filterManyRelationByKeysFieldName(fieldName);
|
|
167
|
+
addField(filterFieldName, `Filter by the object’s \`${fieldName}\` relation.`, FilterManyType, makeResolveMany(spec), spec, `Adding connection filter backward relation field from ${describePgEntity(table)} to ${describePgEntity(foreignTable)}`);
|
|
168
|
+
const existsFieldName = inflection.filterBackwardManyRelationExistsFieldName(fieldName);
|
|
169
|
+
addField(existsFieldName, `Some related \`${fieldName}\` exist.`, GraphQLBoolean, resolveExists, spec, `Adding connection filter backward relation exists field from ${describePgEntity(table)} to ${describePgEntity(foreignTable)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const fieldName = inflection.singleRelationByKeysBackwards(foreignKeyAttributes, foreignTable, table, foreignConstraint);
|
|
174
|
+
const filterFieldName = inflection.filterSingleRelationByKeysBackwardsFieldName(fieldName);
|
|
175
|
+
addField(filterFieldName, `Filter by the object’s \`${fieldName}\` relation.`, ForeignTableFilterType, resolveSingle, spec, `Adding connection filter backward relation field from ${describePgEntity(table)} to ${describePgEntity(foreignTable)}`);
|
|
176
|
+
const existsFieldName = inflection.filterBackwardSingleRelationExistsFieldName(fieldName);
|
|
177
|
+
addField(existsFieldName, `A related \`${fieldName}\` exists.`, GraphQLBoolean, resolveExists, spec, `Adding connection filter backward relation exists field from ${describePgEntity(table)} to ${describePgEntity(foreignTable)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return fields;
|
|
181
|
+
});
|
|
182
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
|
|
183
|
+
const { extend, newWithHooks, inflection, pgSql: sql, connectionFilterResolve, connectionFilterRegisterResolver, connectionFilterTypesByTypeName, connectionFilterType, } = build;
|
|
184
|
+
const { fieldWithHooks, scope: { foreignTable, isPgConnectionFilterMany }, Self, } = context;
|
|
185
|
+
if (!isPgConnectionFilterMany || !foreignTable)
|
|
186
|
+
return fields;
|
|
187
|
+
connectionFilterTypesByTypeName[Self.name] = Self;
|
|
188
|
+
const foreignTableTypeName = inflection.tableType(foreignTable);
|
|
189
|
+
const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName);
|
|
190
|
+
const FilterType = connectionFilterType(newWithHooks, foreignTableFilterTypeName, foreignTable, foreignTableTypeName);
|
|
191
|
+
const manyFields = {
|
|
192
|
+
every: fieldWithHooks('every', {
|
|
193
|
+
description: `Every related \`${foreignTableTypeName}\` matches the filter criteria. All fields are combined with a logical ‘and.’`,
|
|
194
|
+
type: FilterType,
|
|
195
|
+
}, {
|
|
196
|
+
isPgConnectionFilterManyField: true,
|
|
197
|
+
}),
|
|
198
|
+
some: fieldWithHooks('some', {
|
|
199
|
+
description: `Some related \`${foreignTableTypeName}\` matches the filter criteria. All fields are combined with a logical ‘and.’`,
|
|
200
|
+
type: FilterType,
|
|
201
|
+
}, {
|
|
202
|
+
isPgConnectionFilterManyField: true,
|
|
203
|
+
}),
|
|
204
|
+
none: fieldWithHooks('none', {
|
|
205
|
+
description: `No related \`${foreignTableTypeName}\` matches the filter criteria. All fields are combined with a logical ‘and.’`,
|
|
206
|
+
type: FilterType,
|
|
207
|
+
}, {
|
|
208
|
+
isPgConnectionFilterManyField: true,
|
|
209
|
+
}),
|
|
210
|
+
};
|
|
211
|
+
const resolve = ({ sourceAlias, fieldName, fieldValue, queryBuilder, parentFieldInfo, }) => {
|
|
212
|
+
if (fieldValue == null)
|
|
213
|
+
return null;
|
|
214
|
+
if (!parentFieldInfo || !parentFieldInfo.backwardRelationSpec)
|
|
215
|
+
throw new Error('Did not receive backward relation spec');
|
|
216
|
+
const { keyAttributes, foreignKeyAttributes } = parentFieldInfo.backwardRelationSpec;
|
|
217
|
+
const foreignTableAlias = sql.identifier(Symbol());
|
|
218
|
+
const sqlIdentifier = sql.identifier(foreignTable.namespace.name, foreignTable.name);
|
|
219
|
+
const sqlKeysMatch = sql.query `(${sql.join(foreignKeyAttributes.map((attr, i) => {
|
|
220
|
+
return sql.fragment `${foreignTableAlias}.${sql.identifier(attr.name)} = ${sourceAlias}.${sql.identifier(keyAttributes[i].name)}`;
|
|
221
|
+
}), ') and (')})`;
|
|
222
|
+
const sqlSelectWhereKeysMatch = sql.query `select 1 from ${sqlIdentifier} as ${foreignTableAlias} where ${sqlKeysMatch}`;
|
|
223
|
+
const sqlFragment = connectionFilterResolve(fieldValue, foreignTableAlias, foreignTableFilterTypeName, queryBuilder);
|
|
224
|
+
if (sqlFragment == null) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
else if (fieldName === 'every') {
|
|
228
|
+
return sql.query `not exists(${sqlSelectWhereKeysMatch} and not (${sqlFragment}))`;
|
|
229
|
+
}
|
|
230
|
+
else if (fieldName === 'some') {
|
|
231
|
+
return sql.query `exists(${sqlSelectWhereKeysMatch} and (${sqlFragment}))`;
|
|
232
|
+
}
|
|
233
|
+
else if (fieldName === 'none') {
|
|
234
|
+
return sql.query `not exists(${sqlSelectWhereKeysMatch} and (${sqlFragment}))`;
|
|
235
|
+
}
|
|
236
|
+
throw new Error(`Unknown field name: ${fieldName}`);
|
|
237
|
+
};
|
|
238
|
+
for (const fieldName of Object.keys(manyFields)) {
|
|
239
|
+
connectionFilterRegisterResolver(Self.name, fieldName, resolve);
|
|
240
|
+
}
|
|
241
|
+
return extend(fields, manyFields);
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
exports.default = PgConnectionArgFilterBackwardRelationsPlugin;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const PgConnectionArgFilterColumnsPlugin = (builder) => {
|
|
4
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
|
|
5
|
+
const { extend, newWithHooks, pgSql: sql, pgIntrospectionResultsByKind: introspectionResultsByKind, pgColumnFilter, pgOmit: omit, inflection, connectionFilterOperatorsType, connectionFilterRegisterResolver, connectionFilterResolve, connectionFilterTypesByTypeName, } = build;
|
|
6
|
+
const { fieldWithHooks, scope: { pgIntrospection: table, isPgConnectionFilter }, Self, } = context;
|
|
7
|
+
if (!isPgConnectionFilter || table.kind !== 'class')
|
|
8
|
+
return fields;
|
|
9
|
+
connectionFilterTypesByTypeName[Self.name] = Self;
|
|
10
|
+
const attrByFieldName = introspectionResultsByKind.attribute
|
|
11
|
+
.filter((attr) => attr.classId === table.id)
|
|
12
|
+
.filter((attr) => pgColumnFilter(attr, build, context))
|
|
13
|
+
.filter((attr) => !omit(attr, 'filter'))
|
|
14
|
+
.reduce((memo, attr) => {
|
|
15
|
+
const fieldName = inflection.column(attr);
|
|
16
|
+
memo[fieldName] = attr;
|
|
17
|
+
return memo;
|
|
18
|
+
}, {});
|
|
19
|
+
const operatorsTypeNameByFieldName = {};
|
|
20
|
+
const attrFields = Object.entries(attrByFieldName).reduce((memo, [fieldName, attr]) => {
|
|
21
|
+
const OperatorsType = connectionFilterOperatorsType(newWithHooks, attr.typeId, attr.typeModifier);
|
|
22
|
+
if (!OperatorsType) {
|
|
23
|
+
return memo;
|
|
24
|
+
}
|
|
25
|
+
operatorsTypeNameByFieldName[fieldName] = OperatorsType.name;
|
|
26
|
+
return extend(memo, {
|
|
27
|
+
[fieldName]: fieldWithHooks(fieldName, {
|
|
28
|
+
description: `Filter by the object’s \`${fieldName}\` field.`,
|
|
29
|
+
type: OperatorsType,
|
|
30
|
+
}, {
|
|
31
|
+
isPgConnectionFilterField: true,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
}, {});
|
|
35
|
+
const resolve = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
|
|
36
|
+
if (fieldValue == null)
|
|
37
|
+
return null;
|
|
38
|
+
const attr = attrByFieldName[fieldName];
|
|
39
|
+
const sqlIdentifier = sql.query `${sourceAlias}.${sql.identifier(attr.name)}`;
|
|
40
|
+
const pgType = attr.type;
|
|
41
|
+
const pgTypeModifier = attr.typeModifier;
|
|
42
|
+
const filterTypeName = operatorsTypeNameByFieldName[fieldName];
|
|
43
|
+
return connectionFilterResolve(fieldValue, sqlIdentifier, filterTypeName, queryBuilder, pgType, pgTypeModifier, fieldName);
|
|
44
|
+
};
|
|
45
|
+
for (const fieldName of Object.keys(attrFields)) {
|
|
46
|
+
connectionFilterRegisterResolver(Self.name, fieldName, resolve);
|
|
47
|
+
}
|
|
48
|
+
return extend(fields, attrFields);
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
exports.default = PgConnectionArgFilterColumnsPlugin;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const PgConnectionArgFilterCompositeTypeColumnsPlugin = (builder, rawOptions) => {
|
|
4
|
+
const { connectionFilterAllowedFieldTypes } = rawOptions;
|
|
5
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
|
|
6
|
+
const { extend, newWithHooks, pgSql: sql, pgIntrospectionResultsByKind: introspectionResultsByKind, pgGetGqlTypeByTypeIdAndModifier, pgColumnFilter, pgOmit: omit, inflection, connectionFilterRegisterResolver, connectionFilterResolve, connectionFilterType, connectionFilterTypesByTypeName, } = build;
|
|
7
|
+
const { fieldWithHooks, scope: { pgIntrospection: table, isPgConnectionFilter }, Self, } = context;
|
|
8
|
+
if (!isPgConnectionFilter || table.kind !== 'class')
|
|
9
|
+
return fields;
|
|
10
|
+
connectionFilterTypesByTypeName[Self.name] = Self;
|
|
11
|
+
const attrByFieldName = introspectionResultsByKind.attribute
|
|
12
|
+
.filter((attr) => attr.classId === table.id)
|
|
13
|
+
.filter((attr) => pgColumnFilter(attr, build, context))
|
|
14
|
+
.filter((attr) => !omit(attr, 'filter'))
|
|
15
|
+
.filter((attr) => attr.type &&
|
|
16
|
+
attr.type.type === 'c' &&
|
|
17
|
+
attr.type.class &&
|
|
18
|
+
!attr.type.class.isSelectable) // keep only the composite type columns
|
|
19
|
+
.reduce((memo, attr) => {
|
|
20
|
+
const fieldName = inflection.column(attr);
|
|
21
|
+
memo[fieldName] = attr;
|
|
22
|
+
return memo;
|
|
23
|
+
}, {});
|
|
24
|
+
const filterTypeNameByFieldName = {};
|
|
25
|
+
const attrFields = Object.entries(attrByFieldName).reduce((memo, [fieldName, attr]) => {
|
|
26
|
+
const NodeType = pgGetGqlTypeByTypeIdAndModifier(attr.typeId, attr.typeModifier);
|
|
27
|
+
if (!NodeType) {
|
|
28
|
+
return memo;
|
|
29
|
+
}
|
|
30
|
+
const nodeTypeName = NodeType.name;
|
|
31
|
+
// Respect `connectionFilterAllowedFieldTypes` config option
|
|
32
|
+
if (connectionFilterAllowedFieldTypes &&
|
|
33
|
+
!connectionFilterAllowedFieldTypes.includes(nodeTypeName)) {
|
|
34
|
+
return memo;
|
|
35
|
+
}
|
|
36
|
+
const filterTypeName = inflection.filterType(nodeTypeName);
|
|
37
|
+
const CompositeFilterType = connectionFilterType(newWithHooks, filterTypeName, attr.type.class, nodeTypeName);
|
|
38
|
+
if (!CompositeFilterType) {
|
|
39
|
+
return memo;
|
|
40
|
+
}
|
|
41
|
+
filterTypeNameByFieldName[fieldName] = filterTypeName;
|
|
42
|
+
return extend(memo, {
|
|
43
|
+
[fieldName]: fieldWithHooks(fieldName, {
|
|
44
|
+
description: `Filter by the object’s \`${fieldName}\` field.`,
|
|
45
|
+
type: CompositeFilterType,
|
|
46
|
+
}, {
|
|
47
|
+
isPgConnectionFilterField: true,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
}, {});
|
|
51
|
+
const resolve = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
|
|
52
|
+
if (fieldValue == null)
|
|
53
|
+
return null;
|
|
54
|
+
const attr = attrByFieldName[fieldName];
|
|
55
|
+
const sqlIdentifier = sql.query `(${sourceAlias}.${sql.identifier(attr.name)})`; // parentheses are required to avoid confusing the parser
|
|
56
|
+
const pgType = attr.type;
|
|
57
|
+
const pgTypeModifier = attr.typeModifier;
|
|
58
|
+
const filterTypeName = filterTypeNameByFieldName[fieldName];
|
|
59
|
+
return connectionFilterResolve(fieldValue, sqlIdentifier, filterTypeName, queryBuilder, pgType, pgTypeModifier, fieldName);
|
|
60
|
+
};
|
|
61
|
+
for (const fieldName of Object.keys(attrFields)) {
|
|
62
|
+
connectionFilterRegisterResolver(Self.name, fieldName, resolve);
|
|
63
|
+
}
|
|
64
|
+
return extend(fields, attrFields);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
exports.default = PgConnectionArgFilterCompositeTypeColumnsPlugin;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const PgConnectionArgFilterComputedColumnsPlugin = (builder, rawOptions) => {
|
|
4
|
+
const { connectionFilterComputedColumns } = rawOptions;
|
|
5
|
+
builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
|
|
6
|
+
const { extend, newWithHooks, pgIntrospectionResultsByKind: introspectionResultsByKind, pgOmit: omit, pgSql: sql, inflection, connectionFilterOperatorsType, connectionFilterRegisterResolver, connectionFilterResolve, connectionFilterTypesByTypeName, } = build;
|
|
7
|
+
const { scope: { isPgConnectionFilter, pgIntrospection: table }, fieldWithHooks, Self, } = context;
|
|
8
|
+
if (!isPgConnectionFilter || !table || table.kind !== 'class') {
|
|
9
|
+
return fields;
|
|
10
|
+
}
|
|
11
|
+
connectionFilterTypesByTypeName[Self.name] = Self;
|
|
12
|
+
const procByFieldName = introspectionResultsByKind.procedure.reduce((memo, proc) => {
|
|
13
|
+
// Must be marked @filterable OR enabled via plugin option
|
|
14
|
+
if (!(proc.tags.filterable || connectionFilterComputedColumns))
|
|
15
|
+
return memo;
|
|
16
|
+
// Must not be omitted
|
|
17
|
+
if (omit(proc, 'execute'))
|
|
18
|
+
return memo;
|
|
19
|
+
if (omit(proc, 'filter'))
|
|
20
|
+
return memo;
|
|
21
|
+
// Must be a computed column
|
|
22
|
+
const computedColumnDetails = getComputedColumnDetails(build, table, proc);
|
|
23
|
+
if (!computedColumnDetails)
|
|
24
|
+
return memo;
|
|
25
|
+
const { pseudoColumnName } = computedColumnDetails;
|
|
26
|
+
// Must have only one required argument
|
|
27
|
+
const inputArgsCount = proc.argTypeIds.filter((_typeId, idx) => proc.argModes.length === 0 || // all args are `in`
|
|
28
|
+
proc.argModes[idx] === 'i' || // this arg is `in`
|
|
29
|
+
proc.argModes[idx] === 'b' // this arg is `inout`
|
|
30
|
+
).length;
|
|
31
|
+
const nonOptionalArgumentsCount = inputArgsCount - proc.argDefaultsNum;
|
|
32
|
+
if (nonOptionalArgumentsCount > 1) {
|
|
33
|
+
return memo;
|
|
34
|
+
}
|
|
35
|
+
// Must return a scalar or an array
|
|
36
|
+
if (proc.returnsSet)
|
|
37
|
+
return memo;
|
|
38
|
+
const returnType = introspectionResultsByKind.typeById[proc.returnTypeId];
|
|
39
|
+
const returnTypeTable = introspectionResultsByKind.classById[returnType.classId];
|
|
40
|
+
if (returnTypeTable)
|
|
41
|
+
return memo;
|
|
42
|
+
const isRecordLike = returnType.id === '2249';
|
|
43
|
+
if (isRecordLike)
|
|
44
|
+
return memo;
|
|
45
|
+
const isVoid = String(returnType.id) === '2278';
|
|
46
|
+
if (isVoid)
|
|
47
|
+
return memo;
|
|
48
|
+
// Looks good
|
|
49
|
+
const fieldName = inflection.computedColumn(pseudoColumnName, proc, table);
|
|
50
|
+
memo = build.extend(memo, { [fieldName]: proc });
|
|
51
|
+
return memo;
|
|
52
|
+
}, {});
|
|
53
|
+
const operatorsTypeNameByFieldName = {};
|
|
54
|
+
const procFields = Object.entries(procByFieldName).reduce((memo, [fieldName, proc]) => {
|
|
55
|
+
const OperatorsType = connectionFilterOperatorsType(newWithHooks, proc.returnTypeId, null);
|
|
56
|
+
if (!OperatorsType) {
|
|
57
|
+
return memo;
|
|
58
|
+
}
|
|
59
|
+
operatorsTypeNameByFieldName[fieldName] = OperatorsType.name;
|
|
60
|
+
return extend(memo, {
|
|
61
|
+
[fieldName]: fieldWithHooks(fieldName, {
|
|
62
|
+
description: `Filter by the object’s \`${fieldName}\` field.`,
|
|
63
|
+
type: OperatorsType,
|
|
64
|
+
}, {
|
|
65
|
+
isPgConnectionFilterField: true,
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
}, {});
|
|
69
|
+
const resolve = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
|
|
70
|
+
if (fieldValue == null)
|
|
71
|
+
return null;
|
|
72
|
+
const proc = procByFieldName[fieldName];
|
|
73
|
+
const sqlIdentifier = sql.query `${sql.identifier(proc.namespace.name)}.${sql.identifier(proc.name)}(${sourceAlias})`;
|
|
74
|
+
const pgType = introspectionResultsByKind.typeById[proc.returnTypeId];
|
|
75
|
+
const pgTypeModifier = null;
|
|
76
|
+
const filterTypeName = operatorsTypeNameByFieldName[fieldName];
|
|
77
|
+
return connectionFilterResolve(fieldValue, sqlIdentifier, filterTypeName, queryBuilder, pgType, pgTypeModifier, fieldName);
|
|
78
|
+
};
|
|
79
|
+
for (const fieldName of Object.keys(procFields)) {
|
|
80
|
+
connectionFilterRegisterResolver(Self.name, fieldName, resolve);
|
|
81
|
+
}
|
|
82
|
+
return extend(fields, procFields);
|
|
83
|
+
});
|
|
84
|
+
function getComputedColumnDetails(build, table, proc) {
|
|
85
|
+
if (!proc.isStable)
|
|
86
|
+
return null;
|
|
87
|
+
if (proc.namespaceId !== table.namespaceId)
|
|
88
|
+
return null;
|
|
89
|
+
if (!proc.name.startsWith(`${table.name}_`))
|
|
90
|
+
return null;
|
|
91
|
+
if (proc.argTypeIds.length < 1)
|
|
92
|
+
return null;
|
|
93
|
+
if (proc.argTypeIds[0] !== table.type.id)
|
|
94
|
+
return null;
|
|
95
|
+
const argTypes = proc.argTypeIds.reduce((prev, typeId, idx) => {
|
|
96
|
+
if (proc.argModes.length === 0 || // all args are `in`
|
|
97
|
+
proc.argModes[idx] === 'i' || // this arg is `in`
|
|
98
|
+
proc.argModes[idx] === 'b' // this arg is `inout`
|
|
99
|
+
) {
|
|
100
|
+
prev.push(build.pgIntrospectionResultsByKind.typeById[typeId]);
|
|
101
|
+
}
|
|
102
|
+
return prev;
|
|
103
|
+
}, []);
|
|
104
|
+
if (argTypes
|
|
105
|
+
.slice(1)
|
|
106
|
+
.some((type) => type.type === 'c' && type.class && type.class.isSelectable)) {
|
|
107
|
+
// Accepts two input tables? Skip.
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const pseudoColumnName = proc.name.substr(table.name.length + 1);
|
|
111
|
+
return { argTypes, pseudoColumnName };
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
exports.default = PgConnectionArgFilterComputedColumnsPlugin;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin } from 'graphile-build';
|
|
2
|
+
import type { PgAttribute, PgClass, PgConstraint } from 'graphile-build-pg';
|
|
3
|
+
declare const PgConnectionArgFilterForwardRelationsPlugin: Plugin;
|
|
4
|
+
export interface ForwardRelationSpec {
|
|
5
|
+
table: PgClass;
|
|
6
|
+
keyAttributes: PgAttribute[];
|
|
7
|
+
foreignTable: PgClass;
|
|
8
|
+
foreignKeyAttributes: PgAttribute[];
|
|
9
|
+
constraint: PgConstraint;
|
|
10
|
+
}
|
|
11
|
+
export default PgConnectionArgFilterForwardRelationsPlugin;
|