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.
Files changed (39) hide show
  1. package/ConnectionArgFilterPlugin.d.ts +3 -0
  2. package/ConnectionArgFilterPlugin.js +18 -0
  3. package/LICENSE +23 -0
  4. package/PgConnectionArgFilterBackwardRelationsPlugin.d.ts +12 -0
  5. package/PgConnectionArgFilterBackwardRelationsPlugin.js +244 -0
  6. package/PgConnectionArgFilterColumnsPlugin.d.ts +3 -0
  7. package/PgConnectionArgFilterColumnsPlugin.js +51 -0
  8. package/PgConnectionArgFilterCompositeTypeColumnsPlugin.d.ts +3 -0
  9. package/PgConnectionArgFilterCompositeTypeColumnsPlugin.js +67 -0
  10. package/PgConnectionArgFilterComputedColumnsPlugin.d.ts +3 -0
  11. package/PgConnectionArgFilterComputedColumnsPlugin.js +114 -0
  12. package/PgConnectionArgFilterForwardRelationsPlugin.d.ts +11 -0
  13. package/PgConnectionArgFilterForwardRelationsPlugin.js +130 -0
  14. package/PgConnectionArgFilterLogicalOperatorsPlugin.d.ts +3 -0
  15. package/PgConnectionArgFilterLogicalOperatorsPlugin.js +67 -0
  16. package/PgConnectionArgFilterOperatorsPlugin.d.ts +15 -0
  17. package/PgConnectionArgFilterOperatorsPlugin.js +551 -0
  18. package/PgConnectionArgFilterPlugin.d.ts +27 -0
  19. package/PgConnectionArgFilterPlugin.js +305 -0
  20. package/PgConnectionArgFilterRecordFunctionsPlugin.d.ts +3 -0
  21. package/PgConnectionArgFilterRecordFunctionsPlugin.js +75 -0
  22. package/README.md +364 -0
  23. package/esm/ConnectionArgFilterPlugin.js +16 -0
  24. package/esm/PgConnectionArgFilterBackwardRelationsPlugin.js +242 -0
  25. package/esm/PgConnectionArgFilterColumnsPlugin.js +49 -0
  26. package/esm/PgConnectionArgFilterCompositeTypeColumnsPlugin.js +65 -0
  27. package/esm/PgConnectionArgFilterComputedColumnsPlugin.js +112 -0
  28. package/esm/PgConnectionArgFilterForwardRelationsPlugin.js +128 -0
  29. package/esm/PgConnectionArgFilterLogicalOperatorsPlugin.js +65 -0
  30. package/esm/PgConnectionArgFilterOperatorsPlugin.js +549 -0
  31. package/esm/PgConnectionArgFilterPlugin.js +303 -0
  32. package/esm/PgConnectionArgFilterRecordFunctionsPlugin.js +73 -0
  33. package/esm/index.js +58 -0
  34. package/esm/types.js +1 -0
  35. package/index.d.ts +6 -0
  36. package/index.js +64 -0
  37. package/package.json +58 -0
  38. package/types.d.ts +25 -0
  39. package/types.js +2 -0
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const PgConnectionArgFilterPlugin = (builder, rawOptions) => {
4
+ const { connectionFilterAllowedFieldTypes, connectionFilterArrays, connectionFilterSetofFunctions, connectionFilterAllowNullInput, connectionFilterAllowEmptyObjectInput, } = rawOptions;
5
+ // Add `filter` input argument to connection and simple collection types
6
+ builder.hook('GraphQLObjectType:fields:field:args', (args, build, context) => {
7
+ const { extend, newWithHooks, getTypeByName, inflection, pgGetGqlTypeByTypeIdAndModifier, pgOmit: omit, connectionFilterResolve, connectionFilterType, } = build;
8
+ const { scope: { isPgFieldConnection, isPgFieldSimpleCollection, pgFieldIntrospection: source, }, addArgDataGenerator, field, Self, } = context;
9
+ const shouldAddFilter = isPgFieldConnection || isPgFieldSimpleCollection;
10
+ if (!shouldAddFilter)
11
+ return args;
12
+ if (!source)
13
+ return args;
14
+ if (omit(source, 'filter'))
15
+ return args;
16
+ if (source.kind === 'procedure') {
17
+ if (!(source.tags.filterable || connectionFilterSetofFunctions)) {
18
+ return args;
19
+ }
20
+ }
21
+ const returnTypeId = source.kind === 'class' ? source.type.id : source.returnTypeId;
22
+ const returnType = source.kind === 'class'
23
+ ? source.type
24
+ : build.pgIntrospectionResultsByKind.type.find((t) => t.id === returnTypeId);
25
+ if (!returnType) {
26
+ return args;
27
+ }
28
+ const isRecordLike = returnTypeId === '2249';
29
+ const nodeTypeName = isRecordLike
30
+ ? inflection.recordFunctionReturnType(source)
31
+ : pgGetGqlTypeByTypeIdAndModifier(returnTypeId, null).name;
32
+ const filterTypeName = inflection.filterType(nodeTypeName);
33
+ const nodeType = getTypeByName(nodeTypeName);
34
+ if (!nodeType) {
35
+ return args;
36
+ }
37
+ const nodeSource = source.kind === 'procedure' && returnType.class
38
+ ? returnType.class
39
+ : source;
40
+ const FilterType = connectionFilterType(newWithHooks, filterTypeName, nodeSource, nodeTypeName);
41
+ if (!FilterType) {
42
+ return args;
43
+ }
44
+ // Generate SQL where clause from filter argument
45
+ addArgDataGenerator(function connectionFilter(args) {
46
+ return {
47
+ pgQuery: (queryBuilder) => {
48
+ if (Object.prototype.hasOwnProperty.call(args, 'filter')) {
49
+ const sqlFragment = connectionFilterResolve(args.filter, queryBuilder.getTableAlias(), filterTypeName, queryBuilder, returnType, null);
50
+ if (sqlFragment != null) {
51
+ queryBuilder.where(sqlFragment);
52
+ }
53
+ }
54
+ },
55
+ };
56
+ });
57
+ return extend(args, {
58
+ filter: {
59
+ description: 'A filter to be used in determining which values should be returned by the collection.',
60
+ type: FilterType,
61
+ },
62
+ }, `Adding connection filter arg to field '${field.name}' of '${Self.name}'`);
63
+ });
64
+ builder.hook('build', (build) => {
65
+ const { extend, graphql: { getNamedType, GraphQLInputObjectType, GraphQLList }, inflection, pgIntrospectionResultsByKind: introspectionResultsByKind, pgGetGqlInputTypeByTypeIdAndModifier, pgGetGqlTypeByTypeIdAndModifier, pgSql: sql, } = build;
66
+ const connectionFilterResolvers = {};
67
+ const connectionFilterTypesByTypeName = {};
68
+ const handleNullInput = () => {
69
+ if (!connectionFilterAllowNullInput) {
70
+ throw new Error('Null literals are forbidden in filter argument input.');
71
+ }
72
+ return null;
73
+ };
74
+ const handleEmptyObjectInput = () => {
75
+ if (!connectionFilterAllowEmptyObjectInput) {
76
+ throw new Error('Empty objects are forbidden in filter argument input.');
77
+ }
78
+ return null;
79
+ };
80
+ const isEmptyObject = (obj) => typeof obj === 'object' &&
81
+ obj !== null &&
82
+ !Array.isArray(obj) &&
83
+ Object.keys(obj).length === 0;
84
+ const connectionFilterRegisterResolver = (typeName, fieldName, resolve) => {
85
+ const existingResolvers = connectionFilterResolvers[typeName] || {};
86
+ if (existingResolvers[fieldName]) {
87
+ return;
88
+ }
89
+ connectionFilterResolvers[typeName] = extend(existingResolvers, {
90
+ [fieldName]: resolve,
91
+ });
92
+ };
93
+ const connectionFilterResolve = (obj, sourceAlias, typeName, queryBuilder, pgType, pgTypeModifier, parentFieldName, parentFieldInfo) => {
94
+ if (obj == null)
95
+ return handleNullInput();
96
+ if (isEmptyObject(obj))
97
+ return handleEmptyObjectInput();
98
+ const sqlFragments = Object.entries(obj)
99
+ .map(([key, value]) => {
100
+ if (value == null)
101
+ return handleNullInput();
102
+ if (isEmptyObject(value))
103
+ return handleEmptyObjectInput();
104
+ const resolversByFieldName = connectionFilterResolvers[typeName];
105
+ if (resolversByFieldName && resolversByFieldName[key]) {
106
+ return resolversByFieldName[key]({
107
+ sourceAlias,
108
+ fieldName: key,
109
+ fieldValue: value,
110
+ queryBuilder,
111
+ pgType,
112
+ pgTypeModifier,
113
+ parentFieldName,
114
+ parentFieldInfo,
115
+ });
116
+ }
117
+ throw new Error(`Unable to resolve filter field '${key}'`);
118
+ })
119
+ .filter((x) => x != null);
120
+ return sqlFragments.length === 0
121
+ ? null
122
+ : sql.query `(${sql.join(sqlFragments, ') and (')})`;
123
+ };
124
+ // Get or create types like IntFilter, StringFilter, etc.
125
+ const connectionFilterOperatorsType = (newWithHooks, pgTypeId, pgTypeModifier) => {
126
+ const pgType = introspectionResultsByKind.typeById[pgTypeId];
127
+ const allowedPgTypeTypes = ['b', 'd', 'e', 'r'];
128
+ if (!allowedPgTypeTypes.includes(pgType.type)) {
129
+ // Not a base, domain, enum, or range type? Skip.
130
+ return null;
131
+ }
132
+ // Perform some checks on the simple type (after removing array/range/domain wrappers)
133
+ const pgGetNonArrayType = (pgType) => pgType.isPgArray && pgType.arrayItemType
134
+ ? pgType.arrayItemType
135
+ : pgType;
136
+ const pgGetNonRangeType = (pgType) => pgType.rangeSubTypeId
137
+ ? introspectionResultsByKind.typeById[pgType.rangeSubTypeId]
138
+ : pgType;
139
+ const pgGetNonDomainType = (pgType) => pgType.type === 'd' && pgType.domainBaseTypeId
140
+ ? introspectionResultsByKind.typeById[pgType.domainBaseTypeId]
141
+ : pgType;
142
+ const pgGetSimpleType = (pgType) => pgGetNonDomainType(pgGetNonRangeType(pgGetNonArrayType(pgType)));
143
+ const pgSimpleType = pgGetSimpleType(pgType);
144
+ if (!pgSimpleType)
145
+ return null;
146
+ if (!(pgSimpleType.type === 'e' ||
147
+ (pgSimpleType.type === 'b' && !pgSimpleType.isPgArray))) {
148
+ // Haven't found an enum type or a non-array base type? Skip.
149
+ return null;
150
+ }
151
+ if (pgSimpleType.name === 'json') {
152
+ // The PG `json` type has no valid operators.
153
+ // Skip filter type creation to allow the proper
154
+ // operators to be exposed for PG `jsonb` types.
155
+ return null;
156
+ }
157
+ // Establish field type and field input type
158
+ const fieldType = pgGetGqlTypeByTypeIdAndModifier(pgTypeId, pgTypeModifier);
159
+ if (!fieldType)
160
+ return null;
161
+ const fieldInputType = pgGetGqlInputTypeByTypeIdAndModifier(pgTypeId, pgTypeModifier);
162
+ if (!fieldInputType)
163
+ return null;
164
+ // Avoid exposing filter operators on unrecognized types that PostGraphile handles as Strings
165
+ const namedType = getNamedType(fieldType);
166
+ const namedInputType = getNamedType(fieldInputType);
167
+ const actualStringPgTypeIds = [
168
+ '1042', // bpchar
169
+ '18', // char
170
+ '19', // name
171
+ '25', // text
172
+ '1043', // varchar
173
+ ];
174
+ // Include citext as recognized String type
175
+ const citextPgType = introspectionResultsByKind.type.find((t) => t.name === 'citext');
176
+ if (citextPgType) {
177
+ actualStringPgTypeIds.push(citextPgType.id);
178
+ }
179
+ if (namedInputType &&
180
+ namedInputType.name === 'String' &&
181
+ !actualStringPgTypeIds.includes(pgSimpleType.id)) {
182
+ // Not a real string type? Skip.
183
+ return null;
184
+ }
185
+ // Respect `connectionFilterAllowedFieldTypes` config option
186
+ if (connectionFilterAllowedFieldTypes &&
187
+ !connectionFilterAllowedFieldTypes.includes(namedType.name)) {
188
+ return null;
189
+ }
190
+ const pgConnectionFilterOperatorsCategory = pgType.isPgArray
191
+ ? 'Array'
192
+ : pgType.rangeSubTypeId
193
+ ? 'Range'
194
+ : pgType.type === 'e'
195
+ ? 'Enum'
196
+ : pgType.type === 'd'
197
+ ? 'Domain'
198
+ : 'Scalar';
199
+ // Respect `connectionFilterArrays` config option
200
+ if (pgConnectionFilterOperatorsCategory === 'Array' &&
201
+ !connectionFilterArrays) {
202
+ return null;
203
+ }
204
+ const rangeElementInputType = pgType.rangeSubTypeId
205
+ ? pgGetGqlInputTypeByTypeIdAndModifier(pgType.rangeSubTypeId, pgTypeModifier)
206
+ : null;
207
+ const domainBaseType = pgType.type === 'd'
208
+ ? pgGetGqlTypeByTypeIdAndModifier(pgType.domainBaseTypeId, pgType.domainTypeModifier)
209
+ : null;
210
+ const isListType = fieldType instanceof GraphQLList;
211
+ const operatorsTypeName = isListType
212
+ ? inflection.filterFieldListType(namedType.name)
213
+ : inflection.filterFieldType(namedType.name);
214
+ const existingType = connectionFilterTypesByTypeName[operatorsTypeName];
215
+ if (existingType) {
216
+ return existingType;
217
+ }
218
+ return newWithHooks(GraphQLInputObjectType, {
219
+ name: operatorsTypeName,
220
+ description: `A filter to be used against ${namedType.name}${isListType ? ' List' : ''} fields. All fields are combined with a logical ‘and.’`,
221
+ }, {
222
+ isPgConnectionFilterOperators: true,
223
+ pgConnectionFilterOperatorsCategory,
224
+ fieldType,
225
+ fieldInputType,
226
+ rangeElementInputType,
227
+ domainBaseType,
228
+ }, true);
229
+ };
230
+ const connectionFilterType = (newWithHooks, filterTypeName, source, nodeTypeName) => {
231
+ const existingType = connectionFilterTypesByTypeName[filterTypeName];
232
+ if (existingType) {
233
+ return existingType;
234
+ }
235
+ const placeholder = new GraphQLInputObjectType({
236
+ name: filterTypeName,
237
+ fields: {},
238
+ });
239
+ connectionFilterTypesByTypeName[filterTypeName] = placeholder;
240
+ const FilterType = newWithHooks(GraphQLInputObjectType, {
241
+ description: `A filter to be used against \`${nodeTypeName}\` object types. All fields are combined with a logical ‘and.’`,
242
+ name: filterTypeName,
243
+ }, {
244
+ pgIntrospection: source,
245
+ isPgConnectionFilter: true,
246
+ }, true);
247
+ connectionFilterTypesByTypeName[filterTypeName] = FilterType;
248
+ return FilterType;
249
+ };
250
+ const escapeLikeWildcards = (input) => {
251
+ if ('string' !== typeof input) {
252
+ throw new Error('Non-string input was provided to escapeLikeWildcards');
253
+ }
254
+ else {
255
+ return input.split('%').join('\\%').split('_').join('\\_');
256
+ }
257
+ };
258
+ const addConnectionFilterOperator = (typeNames, operatorName, description, resolveType, resolve, options = {}) => {
259
+ if (!typeNames) {
260
+ const msg = `Missing first argument 'typeNames' in call to 'addConnectionFilterOperator' for operator '${operatorName}'`;
261
+ throw new Error(msg);
262
+ }
263
+ if (!operatorName) {
264
+ const msg = `Missing second argument 'operatorName' in call to 'addConnectionFilterOperator' for operator '${operatorName}'`;
265
+ throw new Error(msg);
266
+ }
267
+ if (!resolveType) {
268
+ const msg = `Missing fourth argument 'resolveType' in call to 'addConnectionFilterOperator' for operator '${operatorName}'`;
269
+ throw new Error(msg);
270
+ }
271
+ if (!resolve) {
272
+ const msg = `Missing fifth argument 'resolve' in call to 'addConnectionFilterOperator' for operator '${operatorName}'`;
273
+ throw new Error(msg);
274
+ }
275
+ const { connectionFilterScalarOperators } = build;
276
+ const gqlTypeNames = Array.isArray(typeNames) ? typeNames : [typeNames];
277
+ for (const gqlTypeName of gqlTypeNames) {
278
+ if (!connectionFilterScalarOperators[gqlTypeName]) {
279
+ connectionFilterScalarOperators[gqlTypeName] = {};
280
+ }
281
+ if (connectionFilterScalarOperators[gqlTypeName][operatorName]) {
282
+ const msg = `Operator '${operatorName}' already exists for type '${gqlTypeName}'.`;
283
+ throw new Error(msg);
284
+ }
285
+ connectionFilterScalarOperators[gqlTypeName][operatorName] = {
286
+ description,
287
+ resolveType,
288
+ resolve,
289
+ // These functions may exist on `options`: resolveSqlIdentifier, resolveSqlValue, resolveInput
290
+ ...options,
291
+ };
292
+ }
293
+ };
294
+ return extend(build, {
295
+ connectionFilterTypesByTypeName,
296
+ connectionFilterRegisterResolver,
297
+ connectionFilterResolve,
298
+ connectionFilterOperatorsType,
299
+ connectionFilterType,
300
+ escapeLikeWildcards,
301
+ addConnectionFilterOperator,
302
+ });
303
+ });
304
+ };
305
+ exports.default = PgConnectionArgFilterPlugin;
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'graphile-build';
2
+ declare const PgConnectionArgFilterRecordFunctionsPlugin: Plugin;
3
+ export default PgConnectionArgFilterRecordFunctionsPlugin;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const PgConnectionArgFilterRecordFunctionsPlugin = (builder, rawOptions) => {
4
+ const { connectionFilterSetofFunctions } = rawOptions;
5
+ builder.hook('GraphQLInputObjectType:fields', (fields, build, context) => {
6
+ const { extend, newWithHooks, pgSql: sql, pgIntrospectionResultsByKind: introspectionResultsByKind, pgGetGqlTypeByTypeIdAndModifier, inflection, describePgEntity, connectionFilterOperatorsType, connectionFilterRegisterResolver, connectionFilterResolve, connectionFilterTypesByTypeName, } = build;
7
+ const { fieldWithHooks, scope: { pgIntrospection: proc, isPgConnectionFilter }, Self, } = context;
8
+ if (!isPgConnectionFilter || proc.kind !== 'procedure')
9
+ return fields;
10
+ connectionFilterTypesByTypeName[Self.name] = Self;
11
+ // Must return a `RECORD` type
12
+ const isRecordLike = proc.returnTypeId === '2249';
13
+ if (!isRecordLike)
14
+ return fields;
15
+ // Must be marked @filterable OR enabled via plugin option
16
+ if (!(proc.tags.filterable || connectionFilterSetofFunctions))
17
+ return fields;
18
+ const argModesWithOutput = [
19
+ 'o', // OUT,
20
+ 'b', // INOUT
21
+ 't', // TABLE
22
+ ];
23
+ const outputArgNames = proc.argTypeIds.reduce((prev, _, idx) => {
24
+ if (argModesWithOutput.includes(proc.argModes[idx])) {
25
+ prev.push(proc.argNames[idx] || '');
26
+ }
27
+ return prev;
28
+ }, []);
29
+ const outputArgTypes = proc.argTypeIds.reduce((prev, typeId, idx) => {
30
+ if (argModesWithOutput.includes(proc.argModes[idx])) {
31
+ prev.push(introspectionResultsByKind.typeById[typeId]);
32
+ }
33
+ return prev;
34
+ }, []);
35
+ const outputArgByFieldName = outputArgNames.reduce((memo, outputArgName, idx) => {
36
+ const fieldName = inflection.functionOutputFieldName(proc, outputArgName, idx + 1);
37
+ if (memo[fieldName]) {
38
+ throw new Error(`Tried to register field name '${fieldName}' twice in '${describePgEntity(proc)}'; the argument names are too similar.`);
39
+ }
40
+ memo[fieldName] = {
41
+ name: outputArgName,
42
+ type: outputArgTypes[idx],
43
+ };
44
+ return memo;
45
+ }, {});
46
+ const outputArgFields = Object.entries(outputArgByFieldName).reduce((memo, [fieldName, outputArg]) => {
47
+ const OperatorsType = connectionFilterOperatorsType(newWithHooks, outputArg.type.id, null);
48
+ if (!OperatorsType) {
49
+ return memo;
50
+ }
51
+ return extend(memo, {
52
+ [fieldName]: fieldWithHooks(fieldName, {
53
+ description: `Filter by the object’s \`${fieldName}\` field.`,
54
+ type: OperatorsType,
55
+ }, {
56
+ isPgConnectionFilterField: true,
57
+ }),
58
+ });
59
+ }, {});
60
+ const resolve = ({ sourceAlias, fieldName, fieldValue, queryBuilder, }) => {
61
+ if (fieldValue == null)
62
+ return null;
63
+ const outputArg = outputArgByFieldName[fieldName];
64
+ const sqlIdentifier = sql.query `${sourceAlias}.${sql.identifier(outputArg.name)}`;
65
+ const typeName = pgGetGqlTypeByTypeIdAndModifier(outputArg.type.id, null).name;
66
+ const filterTypeName = inflection.filterType(typeName);
67
+ return connectionFilterResolve(fieldValue, sqlIdentifier, filterTypeName, queryBuilder, outputArg.type, null, fieldName);
68
+ };
69
+ for (const fieldName of Object.keys(outputArgFields)) {
70
+ connectionFilterRegisterResolver(Self.name, fieldName, resolve);
71
+ }
72
+ return extend(fields, outputArgFields);
73
+ });
74
+ };
75
+ exports.default = PgConnectionArgFilterRecordFunctionsPlugin;