graphile-connection-filter 1.1.0
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/LICENSE +23 -0
- package/README.md +107 -0
- package/augmentations.d.ts +104 -0
- package/augmentations.js +11 -0
- package/esm/augmentations.d.ts +104 -0
- package/esm/augmentations.js +9 -0
- package/esm/index.d.ts +55 -0
- package/esm/index.js +56 -0
- package/esm/plugins/ConnectionFilterArgPlugin.d.ts +13 -0
- package/esm/plugins/ConnectionFilterArgPlugin.js +96 -0
- package/esm/plugins/ConnectionFilterAttributesPlugin.d.ts +14 -0
- package/esm/plugins/ConnectionFilterAttributesPlugin.js +79 -0
- package/esm/plugins/ConnectionFilterBackwardRelationsPlugin.d.ts +33 -0
- package/esm/plugins/ConnectionFilterBackwardRelationsPlugin.js +398 -0
- package/esm/plugins/ConnectionFilterComputedAttributesPlugin.d.ts +19 -0
- package/esm/plugins/ConnectionFilterComputedAttributesPlugin.js +133 -0
- package/esm/plugins/ConnectionFilterCustomOperatorsPlugin.d.ts +35 -0
- package/esm/plugins/ConnectionFilterCustomOperatorsPlugin.js +129 -0
- package/esm/plugins/ConnectionFilterForwardRelationsPlugin.d.ts +28 -0
- package/esm/plugins/ConnectionFilterForwardRelationsPlugin.js +168 -0
- package/esm/plugins/ConnectionFilterInflectionPlugin.d.ts +11 -0
- package/esm/plugins/ConnectionFilterInflectionPlugin.js +27 -0
- package/esm/plugins/ConnectionFilterLogicalOperatorsPlugin.d.ts +15 -0
- package/esm/plugins/ConnectionFilterLogicalOperatorsPlugin.js +86 -0
- package/esm/plugins/ConnectionFilterOperatorsPlugin.d.ts +21 -0
- package/esm/plugins/ConnectionFilterOperatorsPlugin.js +677 -0
- package/esm/plugins/ConnectionFilterTypesPlugin.d.ts +12 -0
- package/esm/plugins/ConnectionFilterTypesPlugin.js +225 -0
- package/esm/plugins/index.d.ts +11 -0
- package/esm/plugins/index.js +11 -0
- package/esm/plugins/operatorApply.d.ts +11 -0
- package/esm/plugins/operatorApply.js +70 -0
- package/esm/preset.d.ts +35 -0
- package/esm/preset.js +72 -0
- package/esm/types.d.ts +146 -0
- package/esm/types.js +4 -0
- package/esm/utils.d.ts +44 -0
- package/esm/utils.js +112 -0
- package/index.d.ts +55 -0
- package/index.js +77 -0
- package/package.json +58 -0
- package/plugins/ConnectionFilterArgPlugin.d.ts +13 -0
- package/plugins/ConnectionFilterArgPlugin.js +99 -0
- package/plugins/ConnectionFilterAttributesPlugin.d.ts +14 -0
- package/plugins/ConnectionFilterAttributesPlugin.js +82 -0
- package/plugins/ConnectionFilterBackwardRelationsPlugin.d.ts +33 -0
- package/plugins/ConnectionFilterBackwardRelationsPlugin.js +401 -0
- package/plugins/ConnectionFilterComputedAttributesPlugin.d.ts +19 -0
- package/plugins/ConnectionFilterComputedAttributesPlugin.js +136 -0
- package/plugins/ConnectionFilterCustomOperatorsPlugin.d.ts +35 -0
- package/plugins/ConnectionFilterCustomOperatorsPlugin.js +132 -0
- package/plugins/ConnectionFilterForwardRelationsPlugin.d.ts +28 -0
- package/plugins/ConnectionFilterForwardRelationsPlugin.js +171 -0
- package/plugins/ConnectionFilterInflectionPlugin.d.ts +11 -0
- package/plugins/ConnectionFilterInflectionPlugin.js +30 -0
- package/plugins/ConnectionFilterLogicalOperatorsPlugin.d.ts +15 -0
- package/plugins/ConnectionFilterLogicalOperatorsPlugin.js +89 -0
- package/plugins/ConnectionFilterOperatorsPlugin.d.ts +21 -0
- package/plugins/ConnectionFilterOperatorsPlugin.js +680 -0
- package/plugins/ConnectionFilterTypesPlugin.d.ts +12 -0
- package/plugins/ConnectionFilterTypesPlugin.js +228 -0
- package/plugins/index.d.ts +11 -0
- package/plugins/index.js +25 -0
- package/plugins/operatorApply.d.ts +11 -0
- package/plugins/operatorApply.js +73 -0
- package/preset.d.ts +35 -0
- package/preset.js +75 -0
- package/types.d.ts +146 -0
- package/types.js +7 -0
- package/utils.d.ts +44 -0
- package/utils.js +119 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import '../augmentations';
|
|
2
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
3
|
+
/**
|
|
4
|
+
* ConnectionFilterTypesPlugin
|
|
5
|
+
*
|
|
6
|
+
* Registers the filter types in the schema:
|
|
7
|
+
* 1. Per-table filter types (e.g. UserFilter) - registered for every codec with attributes
|
|
8
|
+
* 2. Per-scalar operator types (e.g. StringFilter, IntFilter) - registered for each scalar type
|
|
9
|
+
*
|
|
10
|
+
* Also adds `connectionFilterOperatorsDigest` and `escapeLikeWildcards` to the build object.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ConnectionFilterTypesPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import '../augmentations';
|
|
2
|
+
const version = '1.0.0';
|
|
3
|
+
/**
|
|
4
|
+
* Determines if a codec is suitable for scalar/enum/domain/array filtering.
|
|
5
|
+
* Excludes void, composite types, anonymous types, and polymorphic types.
|
|
6
|
+
*/
|
|
7
|
+
function isSuitableForFiltering(build, codec) {
|
|
8
|
+
return (codec !== build.dataplanPg.TYPES.void &&
|
|
9
|
+
!codec.attributes &&
|
|
10
|
+
!codec.isAnonymous &&
|
|
11
|
+
!codec.polymorphism &&
|
|
12
|
+
(!codec.arrayOfCodec || isSuitableForFiltering(build, codec.arrayOfCodec)) &&
|
|
13
|
+
(!codec.domainOfCodec || isSuitableForFiltering(build, codec.domainOfCodec)));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* ConnectionFilterTypesPlugin
|
|
17
|
+
*
|
|
18
|
+
* Registers the filter types in the schema:
|
|
19
|
+
* 1. Per-table filter types (e.g. UserFilter) - registered for every codec with attributes
|
|
20
|
+
* 2. Per-scalar operator types (e.g. StringFilter, IntFilter) - registered for each scalar type
|
|
21
|
+
*
|
|
22
|
+
* Also adds `connectionFilterOperatorsDigest` and `escapeLikeWildcards` to the build object.
|
|
23
|
+
*/
|
|
24
|
+
export const ConnectionFilterTypesPlugin = {
|
|
25
|
+
name: 'ConnectionFilterTypesPlugin',
|
|
26
|
+
version,
|
|
27
|
+
description: 'Registers connection filter input types for tables and scalars',
|
|
28
|
+
schema: {
|
|
29
|
+
behaviorRegistry: {
|
|
30
|
+
add: {
|
|
31
|
+
filterProc: {
|
|
32
|
+
description: 'Can this function be filtered?',
|
|
33
|
+
entities: ['pgResource'],
|
|
34
|
+
},
|
|
35
|
+
filter: {
|
|
36
|
+
description: 'Can this table be filtered?',
|
|
37
|
+
entities: ['pgResource'],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
entityBehavior: {
|
|
42
|
+
pgCodec: 'filter',
|
|
43
|
+
pgResource: {
|
|
44
|
+
inferred(behavior, entity, build) {
|
|
45
|
+
if (entity.parameters) {
|
|
46
|
+
return [
|
|
47
|
+
behavior,
|
|
48
|
+
build.options.connectionFilterSetofFunctions
|
|
49
|
+
? 'filterProc'
|
|
50
|
+
: '-filterProc',
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return ['filter', behavior];
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
hooks: {
|
|
60
|
+
build(build) {
|
|
61
|
+
const { inflection, options: { connectionFilterAllowedFieldTypes, connectionFilterArrays, }, EXPORTABLE, } = build;
|
|
62
|
+
// Add connectionFilterOperatorsDigest to the build object
|
|
63
|
+
build.connectionFilterOperatorsDigest = (codec) => {
|
|
64
|
+
const finalBuild = build;
|
|
65
|
+
const { dataplanPg: { getInnerCodec, TYPES, isEnumCodec }, } = finalBuild;
|
|
66
|
+
if (!isSuitableForFiltering(finalBuild, codec)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
// Unwrap to the simple type
|
|
70
|
+
const pgSimpleCodec = getInnerCodec(codec);
|
|
71
|
+
if (!pgSimpleCodec)
|
|
72
|
+
return null;
|
|
73
|
+
if (pgSimpleCodec.polymorphism ||
|
|
74
|
+
pgSimpleCodec.attributes ||
|
|
75
|
+
pgSimpleCodec.isAnonymous) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
// json type has no valid operators (use jsonb instead)
|
|
79
|
+
if (pgSimpleCodec === TYPES.json) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const itemCodec = codec.arrayOfCodec ?? codec;
|
|
83
|
+
const fieldTypeName = build.getGraphQLTypeNameByPgCodec(itemCodec, 'output');
|
|
84
|
+
if (!fieldTypeName)
|
|
85
|
+
return null;
|
|
86
|
+
const fieldTypeMeta = build.getTypeMetaByName(fieldTypeName);
|
|
87
|
+
if (!fieldTypeMeta)
|
|
88
|
+
return null;
|
|
89
|
+
const fieldInputTypeName = build.getGraphQLTypeNameByPgCodec(itemCodec, 'input');
|
|
90
|
+
if (!fieldInputTypeName)
|
|
91
|
+
return null;
|
|
92
|
+
const fieldInputTypeMeta = build.getTypeMetaByName(fieldInputTypeName);
|
|
93
|
+
if (!fieldInputTypeMeta)
|
|
94
|
+
return null;
|
|
95
|
+
// Skip unrecognized types that PostGraphile maps to String
|
|
96
|
+
const namedTypeName = fieldTypeName;
|
|
97
|
+
const namedInputTypeName = fieldInputTypeName;
|
|
98
|
+
const actualStringCodecs = [
|
|
99
|
+
TYPES.bpchar,
|
|
100
|
+
TYPES.char,
|
|
101
|
+
TYPES.name,
|
|
102
|
+
TYPES.text,
|
|
103
|
+
TYPES.varchar,
|
|
104
|
+
TYPES.citext,
|
|
105
|
+
];
|
|
106
|
+
if (namedInputTypeName === 'String' &&
|
|
107
|
+
!actualStringCodecs.includes(pgSimpleCodec)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
// Respect connectionFilterAllowedFieldTypes
|
|
111
|
+
if (connectionFilterAllowedFieldTypes &&
|
|
112
|
+
!connectionFilterAllowedFieldTypes.includes(namedTypeName)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
// Respect connectionFilterArrays
|
|
116
|
+
const isArray = !!codec.arrayOfCodec;
|
|
117
|
+
if (isArray && !connectionFilterArrays) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const listType = !!(codec.arrayOfCodec ||
|
|
121
|
+
codec.domainOfCodec?.arrayOfCodec ||
|
|
122
|
+
codec.rangeOfCodec?.arrayOfCodec);
|
|
123
|
+
const operatorsTypeName = listType
|
|
124
|
+
? inflection.filterFieldListType(namedTypeName)
|
|
125
|
+
: inflection.filterFieldType(namedTypeName);
|
|
126
|
+
const rangeElementInputTypeName = codec.rangeOfCodec && !codec.rangeOfCodec.arrayOfCodec
|
|
127
|
+
? build.getGraphQLTypeNameByPgCodec(codec.rangeOfCodec, 'input')
|
|
128
|
+
: null;
|
|
129
|
+
const domainBaseTypeName = codec.domainOfCodec && !codec.domainOfCodec.arrayOfCodec
|
|
130
|
+
? build.getGraphQLTypeNameByPgCodec(codec.domainOfCodec, 'output')
|
|
131
|
+
: null;
|
|
132
|
+
return {
|
|
133
|
+
isList: listType,
|
|
134
|
+
operatorsTypeName,
|
|
135
|
+
relatedTypeName: namedTypeName,
|
|
136
|
+
inputTypeName: fieldInputTypeName,
|
|
137
|
+
rangeElementInputTypeName,
|
|
138
|
+
domainBaseTypeName,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
// Add escapeLikeWildcards helper
|
|
142
|
+
build.escapeLikeWildcards = EXPORTABLE(() => function (input) {
|
|
143
|
+
if ('string' !== typeof input) {
|
|
144
|
+
throw new Error('Non-string input was provided to escapeLikeWildcards');
|
|
145
|
+
}
|
|
146
|
+
return input.split('%').join('\\%').split('_').join('\\_');
|
|
147
|
+
}, []);
|
|
148
|
+
return build;
|
|
149
|
+
},
|
|
150
|
+
init: {
|
|
151
|
+
after: ['PgCodecs'],
|
|
152
|
+
callback(_, build) {
|
|
153
|
+
const { inflection } = build;
|
|
154
|
+
// Register per-table filter types (e.g. UserFilter)
|
|
155
|
+
for (const pgCodec of build.allPgCodecs) {
|
|
156
|
+
if (!pgCodec.attributes) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const nodeTypeName = build.getGraphQLTypeNameByPgCodec(pgCodec, 'output');
|
|
160
|
+
if (!nodeTypeName) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const filterTypeName = inflection.filterType(nodeTypeName);
|
|
164
|
+
build.registerInputObjectType(filterTypeName, {
|
|
165
|
+
pgCodec,
|
|
166
|
+
isPgConnectionFilter: true,
|
|
167
|
+
}, () => ({
|
|
168
|
+
description: `A filter to be used against \`${nodeTypeName}\` object types. All fields are combined with a logical \u2018and.\u2019`,
|
|
169
|
+
}), 'ConnectionFilterTypesPlugin');
|
|
170
|
+
}
|
|
171
|
+
// Register per-scalar operator types (e.g. StringFilter, IntFilter)
|
|
172
|
+
const codecsByFilterTypeName = {};
|
|
173
|
+
for (const codec of build.allPgCodecs) {
|
|
174
|
+
const digest = build.connectionFilterOperatorsDigest(codec);
|
|
175
|
+
if (!digest)
|
|
176
|
+
continue;
|
|
177
|
+
const { isList, operatorsTypeName, relatedTypeName, inputTypeName, rangeElementInputTypeName, domainBaseTypeName, } = digest;
|
|
178
|
+
if (!codecsByFilterTypeName[operatorsTypeName]) {
|
|
179
|
+
codecsByFilterTypeName[operatorsTypeName] = {
|
|
180
|
+
isList,
|
|
181
|
+
relatedTypeName,
|
|
182
|
+
pgCodecs: [codec],
|
|
183
|
+
inputTypeName,
|
|
184
|
+
rangeElementInputTypeName,
|
|
185
|
+
domainBaseTypeName,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// Validate consistency
|
|
190
|
+
for (const key of [
|
|
191
|
+
'isList',
|
|
192
|
+
'relatedTypeName',
|
|
193
|
+
'inputTypeName',
|
|
194
|
+
'rangeElementInputTypeName',
|
|
195
|
+
]) {
|
|
196
|
+
if (digest[key] !==
|
|
197
|
+
codecsByFilterTypeName[operatorsTypeName][key]) {
|
|
198
|
+
throw new Error(`${key} mismatch: existing codecs (${codecsByFilterTypeName[operatorsTypeName].pgCodecs
|
|
199
|
+
.map((c) => c.name)
|
|
200
|
+
.join(', ')}) had ${key} = ${codecsByFilterTypeName[operatorsTypeName][key]}, but ${codec.name} instead has ${key} = ${digest[key]}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
codecsByFilterTypeName[operatorsTypeName].pgCodecs.push(codec);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const [operatorsTypeName, { isList, relatedTypeName, pgCodecs, inputTypeName, rangeElementInputTypeName, domainBaseTypeName, },] of Object.entries(codecsByFilterTypeName)) {
|
|
207
|
+
build.registerInputObjectType(operatorsTypeName, {
|
|
208
|
+
pgConnectionFilterOperators: {
|
|
209
|
+
isList,
|
|
210
|
+
pgCodecs,
|
|
211
|
+
inputTypeName,
|
|
212
|
+
rangeElementInputTypeName,
|
|
213
|
+
domainBaseTypeName,
|
|
214
|
+
},
|
|
215
|
+
}, () => ({
|
|
216
|
+
name: operatorsTypeName,
|
|
217
|
+
description: `A filter to be used against ${relatedTypeName}${isList ? ' List' : ''} fields. All fields are combined with a logical \u2018and.\u2019`,
|
|
218
|
+
}), 'ConnectionFilterTypesPlugin');
|
|
219
|
+
}
|
|
220
|
+
return _;
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { ConnectionFilterInflectionPlugin } from './ConnectionFilterInflectionPlugin';
|
|
2
|
+
export { ConnectionFilterTypesPlugin } from './ConnectionFilterTypesPlugin';
|
|
3
|
+
export { ConnectionFilterArgPlugin } from './ConnectionFilterArgPlugin';
|
|
4
|
+
export { ConnectionFilterAttributesPlugin } from './ConnectionFilterAttributesPlugin';
|
|
5
|
+
export { ConnectionFilterOperatorsPlugin } from './ConnectionFilterOperatorsPlugin';
|
|
6
|
+
export { ConnectionFilterCustomOperatorsPlugin } from './ConnectionFilterCustomOperatorsPlugin';
|
|
7
|
+
export { ConnectionFilterLogicalOperatorsPlugin } from './ConnectionFilterLogicalOperatorsPlugin';
|
|
8
|
+
export { ConnectionFilterComputedAttributesPlugin } from './ConnectionFilterComputedAttributesPlugin';
|
|
9
|
+
export { ConnectionFilterForwardRelationsPlugin } from './ConnectionFilterForwardRelationsPlugin';
|
|
10
|
+
export { ConnectionFilterBackwardRelationsPlugin } from './ConnectionFilterBackwardRelationsPlugin';
|
|
11
|
+
export { makeApplyFromOperatorSpec } from './operatorApply';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { ConnectionFilterInflectionPlugin } from './ConnectionFilterInflectionPlugin';
|
|
2
|
+
export { ConnectionFilterTypesPlugin } from './ConnectionFilterTypesPlugin';
|
|
3
|
+
export { ConnectionFilterArgPlugin } from './ConnectionFilterArgPlugin';
|
|
4
|
+
export { ConnectionFilterAttributesPlugin } from './ConnectionFilterAttributesPlugin';
|
|
5
|
+
export { ConnectionFilterOperatorsPlugin } from './ConnectionFilterOperatorsPlugin';
|
|
6
|
+
export { ConnectionFilterCustomOperatorsPlugin } from './ConnectionFilterCustomOperatorsPlugin';
|
|
7
|
+
export { ConnectionFilterLogicalOperatorsPlugin } from './ConnectionFilterLogicalOperatorsPlugin';
|
|
8
|
+
export { ConnectionFilterComputedAttributesPlugin } from './ConnectionFilterComputedAttributesPlugin';
|
|
9
|
+
export { ConnectionFilterForwardRelationsPlugin } from './ConnectionFilterForwardRelationsPlugin';
|
|
10
|
+
export { ConnectionFilterBackwardRelationsPlugin } from './ConnectionFilterBackwardRelationsPlugin';
|
|
11
|
+
export { makeApplyFromOperatorSpec } from './operatorApply';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an `apply` function for a filter operator field.
|
|
3
|
+
*
|
|
4
|
+
* This is the core execution logic: given a PgCondition ($where) and an input value,
|
|
5
|
+
* it resolves the SQL identifier, SQL value, and calls the operator's resolve function
|
|
6
|
+
* to produce a SQL WHERE clause fragment, then applies it via `$where.where(fragment)`.
|
|
7
|
+
*
|
|
8
|
+
* The `pgFilterAttribute` extension on $where provides the context about which
|
|
9
|
+
* column is being filtered, including the attribute name, codec, and any expression.
|
|
10
|
+
*/
|
|
11
|
+
export declare function makeApplyFromOperatorSpec(build: any, _typeName: string, fieldName: string, spec: any, _type: any): any;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an `apply` function for a filter operator field.
|
|
3
|
+
*
|
|
4
|
+
* This is the core execution logic: given a PgCondition ($where) and an input value,
|
|
5
|
+
* it resolves the SQL identifier, SQL value, and calls the operator's resolve function
|
|
6
|
+
* to produce a SQL WHERE clause fragment, then applies it via `$where.where(fragment)`.
|
|
7
|
+
*
|
|
8
|
+
* The `pgFilterAttribute` extension on $where provides the context about which
|
|
9
|
+
* column is being filtered, including the attribute name, codec, and any expression.
|
|
10
|
+
*/
|
|
11
|
+
export function makeApplyFromOperatorSpec(build, _typeName, fieldName, spec, _type) {
|
|
12
|
+
const { sql, dataplanPg: { TYPES, sqlValueWithCodec }, EXPORTABLE, } = build;
|
|
13
|
+
const { resolve, resolveInput, resolveSqlIdentifier, resolveSqlValue, resolveInputCodec, } = spec;
|
|
14
|
+
const { options: { connectionFilterAllowNullInput } } = build;
|
|
15
|
+
return EXPORTABLE((connectionFilterAllowNullInput, fieldName, resolve, resolveInput, resolveInputCodec, resolveSqlIdentifier, resolveSqlValue, sql, sqlValueWithCodec) => function ($where, value) {
|
|
16
|
+
if (!$where.extensions?.pgFilterAttribute) {
|
|
17
|
+
throw new Error("Planning error: expected 'pgFilterAttribute' to be present on the $where plan's extensions; your extensions to the connection filter plugin do not implement the required interfaces.");
|
|
18
|
+
}
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const { fieldName: parentFieldName, attributeName, attribute, codec, expression, } = $where.extensions.pgFilterAttribute;
|
|
23
|
+
// Determine the SQL expression for the column
|
|
24
|
+
const sourceAlias = attribute
|
|
25
|
+
? attribute.expression
|
|
26
|
+
? attribute.expression($where.alias)
|
|
27
|
+
: sql `${$where.alias}.${sql.identifier(attributeName)}`
|
|
28
|
+
: expression
|
|
29
|
+
? expression
|
|
30
|
+
: $where.alias;
|
|
31
|
+
const sourceCodec = codec ?? attribute.codec;
|
|
32
|
+
// Optionally override the SQL identifier (e.g. cast citext to text)
|
|
33
|
+
const [sqlIdentifier, identifierCodec] = resolveSqlIdentifier
|
|
34
|
+
? resolveSqlIdentifier(sourceAlias, sourceCodec)
|
|
35
|
+
: [sourceAlias, sourceCodec];
|
|
36
|
+
// Handle null input
|
|
37
|
+
if (connectionFilterAllowNullInput && value === null) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!connectionFilterAllowNullInput && value === null) {
|
|
41
|
+
throw Object.assign(new Error('Null literals are forbidden in filter argument input.'), {});
|
|
42
|
+
}
|
|
43
|
+
// Optionally transform the input value
|
|
44
|
+
const resolvedInput = resolveInput ? resolveInput(value) : value;
|
|
45
|
+
// Determine the input codec
|
|
46
|
+
const inputCodec = resolveInputCodec
|
|
47
|
+
? resolveInputCodec(codec ?? attribute.codec)
|
|
48
|
+
: codec ?? attribute.codec;
|
|
49
|
+
// Generate the SQL value
|
|
50
|
+
const sqlValue = resolveSqlValue
|
|
51
|
+
? resolveSqlValue($where, value, inputCodec)
|
|
52
|
+
: sqlValueWithCodec(resolvedInput, inputCodec);
|
|
53
|
+
// Generate the WHERE clause fragment and apply it
|
|
54
|
+
const fragment = resolve(sqlIdentifier, sqlValue, value, $where, {
|
|
55
|
+
fieldName: parentFieldName ?? null,
|
|
56
|
+
operatorName: fieldName,
|
|
57
|
+
});
|
|
58
|
+
$where.where(fragment);
|
|
59
|
+
}, [
|
|
60
|
+
connectionFilterAllowNullInput,
|
|
61
|
+
fieldName,
|
|
62
|
+
resolve,
|
|
63
|
+
resolveInput,
|
|
64
|
+
resolveInputCodec,
|
|
65
|
+
resolveSqlIdentifier,
|
|
66
|
+
resolveSqlValue,
|
|
67
|
+
sql,
|
|
68
|
+
sqlValueWithCodec,
|
|
69
|
+
]);
|
|
70
|
+
}
|
package/esm/preset.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Filter Preset
|
|
3
|
+
*
|
|
4
|
+
* Provides a convenient preset factory for including connection filtering
|
|
5
|
+
* in PostGraphile v5.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { ConnectionFilterPreset } from 'graphile-connection-filter';
|
|
10
|
+
*
|
|
11
|
+
* const preset = {
|
|
12
|
+
* extends: [
|
|
13
|
+
* ConnectionFilterPreset(),
|
|
14
|
+
* // or with options:
|
|
15
|
+
* ConnectionFilterPreset({
|
|
16
|
+
* connectionFilterLogicalOperators: true,
|
|
17
|
+
* connectionFilterArrays: true,
|
|
18
|
+
* }),
|
|
19
|
+
* ],
|
|
20
|
+
* };
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import './augmentations';
|
|
24
|
+
import type { GraphileConfig } from 'graphile-config';
|
|
25
|
+
import type { ConnectionFilterOptions } from './types';
|
|
26
|
+
/**
|
|
27
|
+
* Creates a preset that includes the connection filter plugins with the given options.
|
|
28
|
+
*
|
|
29
|
+
* All plugins are always included. Use the schema options to control behavior:
|
|
30
|
+
* - `connectionFilterLogicalOperators: false` to exclude and/or/not
|
|
31
|
+
* - `connectionFilterArrays: false` to exclude array type filters
|
|
32
|
+
* - `connectionFilterAllowNullInput: true` to allow null literals
|
|
33
|
+
*/
|
|
34
|
+
export declare function ConnectionFilterPreset(options?: ConnectionFilterOptions): GraphileConfig.Preset;
|
|
35
|
+
export default ConnectionFilterPreset;
|
package/esm/preset.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Filter Preset
|
|
3
|
+
*
|
|
4
|
+
* Provides a convenient preset factory for including connection filtering
|
|
5
|
+
* in PostGraphile v5.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { ConnectionFilterPreset } from 'graphile-connection-filter';
|
|
10
|
+
*
|
|
11
|
+
* const preset = {
|
|
12
|
+
* extends: [
|
|
13
|
+
* ConnectionFilterPreset(),
|
|
14
|
+
* // or with options:
|
|
15
|
+
* ConnectionFilterPreset({
|
|
16
|
+
* connectionFilterLogicalOperators: true,
|
|
17
|
+
* connectionFilterArrays: true,
|
|
18
|
+
* }),
|
|
19
|
+
* ],
|
|
20
|
+
* };
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import './augmentations';
|
|
24
|
+
import { ConnectionFilterInflectionPlugin, ConnectionFilterTypesPlugin, ConnectionFilterArgPlugin, ConnectionFilterAttributesPlugin, ConnectionFilterOperatorsPlugin, ConnectionFilterCustomOperatorsPlugin, ConnectionFilterLogicalOperatorsPlugin, ConnectionFilterComputedAttributesPlugin, ConnectionFilterForwardRelationsPlugin, ConnectionFilterBackwardRelationsPlugin, } from './plugins';
|
|
25
|
+
/**
|
|
26
|
+
* Default schema options for the connection filter.
|
|
27
|
+
*/
|
|
28
|
+
const defaultSchemaOptions = {
|
|
29
|
+
connectionFilterArgumentName: 'where',
|
|
30
|
+
connectionFilterArrays: true,
|
|
31
|
+
connectionFilterLogicalOperators: true,
|
|
32
|
+
connectionFilterAllowNullInput: false,
|
|
33
|
+
connectionFilterSetofFunctions: true,
|
|
34
|
+
connectionFilterComputedColumns: true,
|
|
35
|
+
connectionFilterRelations: false,
|
|
36
|
+
connectionFilterRelationsRequireIndex: true,
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Creates a preset that includes the connection filter plugins with the given options.
|
|
40
|
+
*
|
|
41
|
+
* All plugins are always included. Use the schema options to control behavior:
|
|
42
|
+
* - `connectionFilterLogicalOperators: false` to exclude and/or/not
|
|
43
|
+
* - `connectionFilterArrays: false` to exclude array type filters
|
|
44
|
+
* - `connectionFilterAllowNullInput: true` to allow null literals
|
|
45
|
+
*/
|
|
46
|
+
export function ConnectionFilterPreset(options = {}) {
|
|
47
|
+
const mergedOptions = { ...defaultSchemaOptions, ...options };
|
|
48
|
+
const plugins = [
|
|
49
|
+
ConnectionFilterInflectionPlugin,
|
|
50
|
+
ConnectionFilterTypesPlugin,
|
|
51
|
+
ConnectionFilterArgPlugin,
|
|
52
|
+
ConnectionFilterAttributesPlugin,
|
|
53
|
+
ConnectionFilterOperatorsPlugin,
|
|
54
|
+
ConnectionFilterCustomOperatorsPlugin,
|
|
55
|
+
];
|
|
56
|
+
// Logical operators plugin always included — checks
|
|
57
|
+
// build.options.connectionFilterLogicalOperators at runtime
|
|
58
|
+
plugins.push(ConnectionFilterLogicalOperatorsPlugin);
|
|
59
|
+
// Computed columns are opt-in (disabled by default)
|
|
60
|
+
if (mergedOptions.connectionFilterComputedColumns) {
|
|
61
|
+
plugins.push(ConnectionFilterComputedAttributesPlugin);
|
|
62
|
+
}
|
|
63
|
+
// Relation filter plugins are always included but check
|
|
64
|
+
// build.options.connectionFilterRelations at runtime
|
|
65
|
+
plugins.push(ConnectionFilterForwardRelationsPlugin);
|
|
66
|
+
plugins.push(ConnectionFilterBackwardRelationsPlugin);
|
|
67
|
+
return {
|
|
68
|
+
plugins,
|
|
69
|
+
schema: mergedOptions,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export default ConnectionFilterPreset;
|
package/esm/types.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { SQL } from 'pg-sql2';
|
|
2
|
+
import type { GraphQLInputType } from 'graphql';
|
|
3
|
+
/**
|
|
4
|
+
* Symbol used to store the filter operator registry on the build object.
|
|
5
|
+
*/
|
|
6
|
+
export declare const $$filters: unique symbol;
|
|
7
|
+
/**
|
|
8
|
+
* Specification for a filter operator.
|
|
9
|
+
*
|
|
10
|
+
* This is the contract that satellite plugins (PostGIS, search, pgvector, etc.)
|
|
11
|
+
* use when calling `addConnectionFilterOperator`.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConnectionFilterOperatorSpec {
|
|
14
|
+
/** Human-readable description for the GraphQL schema. */
|
|
15
|
+
description?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Core resolve function: given the SQL identifier for the column and the SQL
|
|
18
|
+
* value for the user input, return a SQL fragment for the WHERE clause.
|
|
19
|
+
*/
|
|
20
|
+
resolve: (sqlIdentifier: SQL, sqlValue: SQL, input: unknown, $where: any, details: {
|
|
21
|
+
fieldName: string | null;
|
|
22
|
+
operatorName: string;
|
|
23
|
+
}) => SQL;
|
|
24
|
+
/**
|
|
25
|
+
* Optional: transform the user input before encoding to SQL.
|
|
26
|
+
* e.g. wrapping a LIKE pattern with `%`.
|
|
27
|
+
*/
|
|
28
|
+
resolveInput?: (input: unknown) => unknown;
|
|
29
|
+
/**
|
|
30
|
+
* Optional: override the codec used for encoding the input value to SQL.
|
|
31
|
+
* Receives the column's codec and returns the codec to use for the input.
|
|
32
|
+
*/
|
|
33
|
+
resolveInputCodec?: (codec: any) => any;
|
|
34
|
+
/**
|
|
35
|
+
* Optional: override the SQL identifier and its codec.
|
|
36
|
+
* Useful for casting columns (e.g. citext -> text for case-sensitive matching).
|
|
37
|
+
*/
|
|
38
|
+
resolveSqlIdentifier?: (sqlIdentifier: SQL, codec: any) => [SQL, any];
|
|
39
|
+
/**
|
|
40
|
+
* Optional: completely override how the SQL value is generated.
|
|
41
|
+
* Receives the $where plan, the raw input value, and the resolved input codec.
|
|
42
|
+
*/
|
|
43
|
+
resolveSqlValue?: ($where: any, value: unknown, inputCodec: any) => SQL;
|
|
44
|
+
/**
|
|
45
|
+
* Optional: override the GraphQL input type for this operator.
|
|
46
|
+
* Receives the default field input type and returns the type to use.
|
|
47
|
+
*/
|
|
48
|
+
resolveType?: (fieldType: GraphQLInputType) => GraphQLInputType;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A single operator registration returned by a factory function.
|
|
52
|
+
* Declares which type(s) the operator applies to, its name, and its spec.
|
|
53
|
+
*/
|
|
54
|
+
export interface ConnectionFilterOperatorRegistration {
|
|
55
|
+
/** The GraphQL type name(s) this operator applies to (e.g. 'String', 'Int', or an array). */
|
|
56
|
+
typeNames: string | string[];
|
|
57
|
+
/** The operator field name (e.g. 'similarTo', 'matches', 'intersects'). */
|
|
58
|
+
operatorName: string;
|
|
59
|
+
/** The operator specification (resolve function, description, etc.). */
|
|
60
|
+
spec: ConnectionFilterOperatorSpec;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A factory function that receives the build object and returns operator registrations.
|
|
64
|
+
* This is the declarative replacement for `build.addConnectionFilterOperator()`.
|
|
65
|
+
*
|
|
66
|
+
* Satellite plugins declare these in their preset's `connectionFilterOperatorFactories`
|
|
67
|
+
* option. The connection filter plugin processes them during its own init hook,
|
|
68
|
+
* eliminating timing dependencies between plugins.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const myFactory: ConnectionFilterOperatorFactory = (build) => [
|
|
73
|
+
* {
|
|
74
|
+
* typeNames: 'String',
|
|
75
|
+
* operatorName: 'similarTo',
|
|
76
|
+
* spec: {
|
|
77
|
+
* description: 'Fuzzy match',
|
|
78
|
+
* resolve: (i, v) => build.sql`similarity(${i}, ${v}) > 0.3`,
|
|
79
|
+
* },
|
|
80
|
+
* },
|
|
81
|
+
* ];
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export type ConnectionFilterOperatorFactory = (build: any) => ConnectionFilterOperatorRegistration[];
|
|
85
|
+
/**
|
|
86
|
+
* Configuration options for the connection filter preset.
|
|
87
|
+
*/
|
|
88
|
+
export interface ConnectionFilterOptions {
|
|
89
|
+
/** The GraphQL argument name for the filter. Default: 'where' */
|
|
90
|
+
connectionFilterArgumentName?: string;
|
|
91
|
+
/** Allow filtering on array columns. Default: true */
|
|
92
|
+
connectionFilterArrays?: boolean;
|
|
93
|
+
/** Include logical operators (and/or/not). Default: true */
|
|
94
|
+
connectionFilterLogicalOperators?: boolean;
|
|
95
|
+
/** Allow null literals in filter input. Default: false */
|
|
96
|
+
connectionFilterAllowNullInput?: boolean;
|
|
97
|
+
/** Restrict which field types can be filtered. Default: all types */
|
|
98
|
+
connectionFilterAllowedFieldTypes?: string[];
|
|
99
|
+
/** Restrict which operators are available. Default: all operators */
|
|
100
|
+
connectionFilterAllowedOperators?: string[];
|
|
101
|
+
/** Rename operators. Keys are default names, values are custom names. */
|
|
102
|
+
connectionFilterOperatorNames?: Record<string, string>;
|
|
103
|
+
/** Allow filtering on setof functions. Default: true */
|
|
104
|
+
connectionFilterSetofFunctions?: boolean;
|
|
105
|
+
/** Allow filtering on computed columns. Default: true */
|
|
106
|
+
connectionFilterComputedColumns?: boolean;
|
|
107
|
+
/** Allow filtering on related tables via FK relationships. Default: false */
|
|
108
|
+
connectionFilterRelations?: boolean;
|
|
109
|
+
/** Only create relation filter fields for FKs with supporting indexes.
|
|
110
|
+
* Prevents EXISTS subqueries that would cause sequential scans on large tables.
|
|
111
|
+
* Default: true */
|
|
112
|
+
connectionFilterRelationsRequireIndex?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Declarative operator factories. Each factory receives the build object and
|
|
115
|
+
* returns an array of operator registrations. This replaces the imperative
|
|
116
|
+
* `build.addConnectionFilterOperator()` API.
|
|
117
|
+
*
|
|
118
|
+
* Satellite plugins (PostGIS, search, pg_trgm, etc.) declare their operators
|
|
119
|
+
* here instead of calling `addConnectionFilterOperator` in an init hook.
|
|
120
|
+
* The connection filter plugin processes all factories during its own init,
|
|
121
|
+
* eliminating timing/ordering dependencies.
|
|
122
|
+
*/
|
|
123
|
+
connectionFilterOperatorFactories?: ConnectionFilterOperatorFactory[];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Digest of operator type information for a given codec.
|
|
127
|
+
* Used to determine which filter type (e.g. StringFilter, IntFilter) to create.
|
|
128
|
+
*/
|
|
129
|
+
export interface ConnectionFilterOperatorsDigest {
|
|
130
|
+
isList: boolean;
|
|
131
|
+
operatorsTypeName: string;
|
|
132
|
+
relatedTypeName: string;
|
|
133
|
+
inputTypeName: string;
|
|
134
|
+
rangeElementInputTypeName: string | null;
|
|
135
|
+
domainBaseTypeName: string | null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Scope metadata stored on operator filter types (e.g. StringFilter, IntFilter).
|
|
139
|
+
*/
|
|
140
|
+
export interface PgConnectionFilterOperatorsScope {
|
|
141
|
+
isList: boolean;
|
|
142
|
+
pgCodecs: any[];
|
|
143
|
+
inputTypeName: string;
|
|
144
|
+
rangeElementInputTypeName: string | null;
|
|
145
|
+
domainBaseTypeName: string | null;
|
|
146
|
+
}
|
package/esm/types.js
ADDED
package/esm/utils.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the connection filter plugin.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Check if a value is an empty object (no own enumerable keys).
|
|
6
|
+
*/
|
|
7
|
+
export declare function isEmpty(o: unknown): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Check if a pgResource is a computed scalar attribute function.
|
|
10
|
+
* A computed attribute is a function that:
|
|
11
|
+
* - has parameters
|
|
12
|
+
* - returns a scalar (no attributes on codec)
|
|
13
|
+
* - is unique (returns single row)
|
|
14
|
+
* - first parameter's codec has attributes (i.e. takes a table row)
|
|
15
|
+
*/
|
|
16
|
+
export declare function isComputedScalarAttributeResource(s: any): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Get all computed attribute resources for a given source.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getComputedAttributeResources(build: any, source: any): any[];
|
|
21
|
+
/**
|
|
22
|
+
* Walks from a PgCondition up to the PgSelectQueryBuilder.
|
|
23
|
+
* Uses the .parent property on PgCondition to traverse up the chain,
|
|
24
|
+
* following Benjie's pattern from postgraphile-plugin-fulltext-filter.
|
|
25
|
+
*
|
|
26
|
+
* This is used by satellite plugins (search, BM25, pgvector) that need
|
|
27
|
+
* to access the query builder from within a filter's apply callback
|
|
28
|
+
* to inject SELECT expressions (for ranking/scoring) and ORDER BY clauses.
|
|
29
|
+
*
|
|
30
|
+
* @param build - The Graphile Build object (needs build.dataplanPg.PgCondition)
|
|
31
|
+
* @param $condition - The PgCondition instance from the filter apply callback
|
|
32
|
+
* @returns The PgSelectQueryBuilder if found, or null
|
|
33
|
+
*/
|
|
34
|
+
export declare function getQueryBuilder(build: GraphileBuild.Build, $condition: any): any | null;
|
|
35
|
+
/**
|
|
36
|
+
* Creates an assertion function that validates filter input values.
|
|
37
|
+
*
|
|
38
|
+
* Rejects empty objects in nested contexts (logical operators, relation filters)
|
|
39
|
+
* and optionally rejects null literals based on the connectionFilterAllowNullInput option.
|
|
40
|
+
*
|
|
41
|
+
* Note: Top-level empty filter `{}` is handled in ConnectionFilterArgPlugin's
|
|
42
|
+
* applyPlan — it's treated as "no filter" and skipped, not rejected.
|
|
43
|
+
*/
|
|
44
|
+
export declare function makeAssertAllowed(build: any): (value: unknown, mode: 'object' | 'list') => void;
|