@haathie/postgraphile-targeted-conditions 0.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/lib/filter-implementations/declaration.d.ts +24 -0
- package/lib/filter-implementations/declaration.js +83 -0
- package/lib/filter-implementations/index.d.ts +3 -0
- package/lib/filter-implementations/index.js +3 -0
- package/lib/filter-implementations/paradedb.d.ts +8 -0
- package/lib/filter-implementations/paradedb.js +54 -0
- package/lib/filter-implementations/plain-sql.d.ts +8 -0
- package/lib/filter-implementations/plain-sql.js +49 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +46 -0
- package/lib/inflection.d.ts +1 -0
- package/lib/inflection.js +14 -0
- package/lib/schema-fields.d.ts +3 -0
- package/lib/schema-fields.js +79 -0
- package/lib/schema-init.d.ts +3 -0
- package/lib/schema-init.js +140 -0
- package/lib/types.d.ts +85 -0
- package/lib/types.js +1 -0
- package/lib/utils.d.ts +5 -0
- package/lib/utils.js +31 -0
- package/package.json +24 -0
- package/readme.md +210 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FilterApply, FilterImplementation, FilterMethod, FilterMethodConfig, FilterType } from '../types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Store configuration for each filter method.
|
|
4
|
+
*/
|
|
5
|
+
export declare const FILTER_METHODS_CONFIG: {
|
|
6
|
+
[K in FilterMethod]?: FilterMethodConfig;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Store implementations for each filter type.
|
|
10
|
+
*/
|
|
11
|
+
export declare const FILTER_TYPES_MAP: {
|
|
12
|
+
[K in FilterType]?: FilterImplementation;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Register implementations for the given filter types.
|
|
16
|
+
* If a filter type is already registered, and you attempt to re-register it,
|
|
17
|
+
* it will throw an error.
|
|
18
|
+
*/
|
|
19
|
+
export declare function registerFilterImplementations(impls: {
|
|
20
|
+
[K in FilterType]?: FilterImplementation;
|
|
21
|
+
}): void;
|
|
22
|
+
export declare function registerFilterMethod<T = unknown>(method: FilterMethod, config: FilterMethodConfig, applys: {
|
|
23
|
+
[K in FilterType]?: FilterApply<T>;
|
|
24
|
+
}): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store configuration for each filter method.
|
|
3
|
+
*/
|
|
4
|
+
export const FILTER_METHODS_CONFIG = {};
|
|
5
|
+
/**
|
|
6
|
+
* Store implementations for each filter type.
|
|
7
|
+
*/
|
|
8
|
+
export const FILTER_TYPES_MAP = {};
|
|
9
|
+
/**
|
|
10
|
+
* Register implementations for the given filter types.
|
|
11
|
+
* If a filter type is already registered, and you attempt to re-register it,
|
|
12
|
+
* it will throw an error.
|
|
13
|
+
*/
|
|
14
|
+
export function registerFilterImplementations(impls) {
|
|
15
|
+
for (const [type, impl] of Object.entries(impls)) {
|
|
16
|
+
if (FILTER_TYPES_MAP[type]) {
|
|
17
|
+
throw new Error(`Filter type ${type} is already registered.`);
|
|
18
|
+
}
|
|
19
|
+
FILTER_TYPES_MAP[type] = impl;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function registerFilterMethod(method, config, applys) {
|
|
23
|
+
if (FILTER_METHODS_CONFIG[method]) {
|
|
24
|
+
throw new Error(`Filter method ${method} is already registered.`);
|
|
25
|
+
}
|
|
26
|
+
FILTER_METHODS_CONFIG[method] = config;
|
|
27
|
+
for (const [type, apply] of Object.entries(applys)) {
|
|
28
|
+
const impl = FILTER_TYPES_MAP[type];
|
|
29
|
+
if (!impl) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
impl.applys ||= {};
|
|
33
|
+
impl.applys[method] = apply;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
registerFilterImplementations({
|
|
37
|
+
'eq': {
|
|
38
|
+
getType(codec, getGraphQlType) {
|
|
39
|
+
// for eq -- we just return the field type
|
|
40
|
+
return getGraphQlType();
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
'eqIn': {
|
|
44
|
+
getType(codec, getGraphQlType, { graphql: { GraphQLList } }) {
|
|
45
|
+
return new GraphQLList(getGraphQlType());
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
'range': {
|
|
49
|
+
getRegisterTypeInfo(fieldCodec, getGraphQlType, { inflection }) {
|
|
50
|
+
return {
|
|
51
|
+
name: inflection.rangeConditionTypeName(fieldCodec),
|
|
52
|
+
spec: () => ({
|
|
53
|
+
description: 'Filter values falling in an inclusive range',
|
|
54
|
+
fields() {
|
|
55
|
+
const fieldType = getGraphQlType();
|
|
56
|
+
if (!('name' in fieldType)) {
|
|
57
|
+
throw new Error('Cannot build range condition on a non-named type');
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
from: { type: fieldType },
|
|
61
|
+
to: { type: fieldType }
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
getType(fieldCodec, _, { inflection, getInputTypeByName }) {
|
|
68
|
+
return getInputTypeByName(inflection.rangeConditionTypeName(fieldCodec));
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
'icontains': {
|
|
72
|
+
getType(fieldCodec, getGraphQlType, { graphql: { GraphQLNonNull, GraphQLScalarType } }) {
|
|
73
|
+
let fieldType = getGraphQlType();
|
|
74
|
+
fieldType = fieldType instanceof GraphQLNonNull
|
|
75
|
+
? fieldType.ofType
|
|
76
|
+
: fieldType;
|
|
77
|
+
if (!(fieldType instanceof GraphQLScalarType)) {
|
|
78
|
+
throw new Error('Cannot build contains condition on a non-scalar type');
|
|
79
|
+
}
|
|
80
|
+
return fieldType;
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { sql } from 'postgraphile/pg-sql2';
|
|
2
|
+
import { registerFilterMethod } from "./declaration.js";
|
|
3
|
+
registerFilterMethod('paradedb', { supportedOnSubscription: false }, {
|
|
4
|
+
'eq': (cond, input, { scope: { attrName, attr } }) => {
|
|
5
|
+
const codec = attr.codec.arrayOfCodec || attr.codec;
|
|
6
|
+
const id = sql `${cond.alias}.${sql.identifier(attrName)}`;
|
|
7
|
+
if (input === null) {
|
|
8
|
+
return cond.where(sql `NOT ${id} @@@ paradedb.exists(${sql.literal(attrName)})`);
|
|
9
|
+
}
|
|
10
|
+
return cond.where(sql `${id} @@@ paradedb.term(${sql.literal(attrName)}, ${sql.value(input)}::${codec.sqlType})`);
|
|
11
|
+
},
|
|
12
|
+
'eqIn': (cond, input, { scope: { attr, attrName } }) => {
|
|
13
|
+
const codec = attr.codec.arrayOfCodec || attr.codec;
|
|
14
|
+
const id = sql `${cond.alias}.${sql.identifier(attrName)}`;
|
|
15
|
+
const whereClause = sql `${id} @@@ paradedb.term_set(
|
|
16
|
+
(SELECT ARRAY_AGG(paradedb.term(${sql.literal(attrName)}, value))
|
|
17
|
+
FROM unnest(${sql.value(input)}::${codec.sqlType}[]) value)
|
|
18
|
+
)`;
|
|
19
|
+
const hasNull = input.includes(null);
|
|
20
|
+
if (!hasNull) {
|
|
21
|
+
return cond.where(whereClause);
|
|
22
|
+
}
|
|
23
|
+
return cond.where(sql `(
|
|
24
|
+
${whereClause}
|
|
25
|
+
OR NOT ${id} @@@ paradedb.exists(${sql.literal(attrName)})
|
|
26
|
+
)`);
|
|
27
|
+
},
|
|
28
|
+
'range': (cond, { from, to }, { scope: { attr, attrName, config } }) => {
|
|
29
|
+
const id = sql `${cond.alias}.${sql.identifier(attrName)}`;
|
|
30
|
+
const { sqlType } = attr.codec.arrayOfCodec || attr.codec;
|
|
31
|
+
const fieldName = config?.fieldName || attrName;
|
|
32
|
+
const fromSql = from
|
|
33
|
+
? sql `jsonb_build_object('included', ${sql.value(from)}::${sqlType})`
|
|
34
|
+
: sql.null;
|
|
35
|
+
const toSql = to
|
|
36
|
+
? sql `jsonb_build_object('included', ${sql.value(to)}::${sqlType})`
|
|
37
|
+
: sql.null;
|
|
38
|
+
cond.where(sql `${id} @@@
|
|
39
|
+
jsonb_build_object(
|
|
40
|
+
'range',
|
|
41
|
+
jsonb_build_object(
|
|
42
|
+
'field', ${sql.literal(fieldName)},
|
|
43
|
+
'lower_bound', ${fromSql},
|
|
44
|
+
'upper_bound', ${toSql}
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
`);
|
|
48
|
+
},
|
|
49
|
+
'icontains': (cond, input, { scope: { attrName, config } }) => {
|
|
50
|
+
const fieldName = config?.fieldName || attrName;
|
|
51
|
+
const id = sql `${cond.alias}.${sql.identifier(attrName)}`;
|
|
52
|
+
return cond.where(sql `${id} @@@ paradedb.parse(${sql.value(`${fieldName}:"${input}"`)})`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { sql } from 'postgraphile/pg-sql2';
|
|
2
|
+
import { registerFilterMethod } from "./declaration.js";
|
|
3
|
+
registerFilterMethod('plainSql', { supportedOnSubscription: true }, {
|
|
4
|
+
eq: (cond, input, { scope: { attrName, attr, serialiseToSql } }) => {
|
|
5
|
+
const id = sql `${cond.alias}.${sql.identifier(attrName)}`;
|
|
6
|
+
const codec = attr.codec.arrayOfCodec || attr.codec;
|
|
7
|
+
if (input === null) {
|
|
8
|
+
return cond.where(sql `${id} IS NULL`);
|
|
9
|
+
}
|
|
10
|
+
if (attr.codec.arrayOfCodec) {
|
|
11
|
+
// If the attribute is an array, we need to check for equality
|
|
12
|
+
return cond
|
|
13
|
+
.where(sql `${serialiseToSql()}::${codec.sqlType} = ANY(${id})`);
|
|
14
|
+
}
|
|
15
|
+
return cond.where(sql `${id} = ${serialiseToSql()}::${codec.sqlType}`);
|
|
16
|
+
},
|
|
17
|
+
eqIn: (cond, input, { scope: { attrName, attr, serialiseToSql } }) => {
|
|
18
|
+
const id = sql `${cond.alias}.${sql.identifier(attrName)}`;
|
|
19
|
+
const codec = attr.codec.arrayOfCodec || attr.codec;
|
|
20
|
+
const whereClause = attr.codec.arrayOfCodec
|
|
21
|
+
? sql `${id} && ${serialiseToSql()}::${codec.sqlType}[] -- TRUE`
|
|
22
|
+
: sql `${id} IN (
|
|
23
|
+
SELECT arr FROM unnest(${serialiseToSql()}::${codec.sqlType}[]) arr
|
|
24
|
+
)`;
|
|
25
|
+
const hasNull = Array.isArray(input) && input.includes(null);
|
|
26
|
+
if (!hasNull) {
|
|
27
|
+
return cond.where(whereClause);
|
|
28
|
+
}
|
|
29
|
+
return cond.where(sql `(${whereClause} OR ${id} IS NULL)`);
|
|
30
|
+
},
|
|
31
|
+
range: (cond, { from, to }, { scope: { attrName, attr } }) => {
|
|
32
|
+
if (from !== undefined) {
|
|
33
|
+
cond.where(sql `${cond.alias}.${sql.identifier(attrName)} >= ${sql.value(from)}::${attr.codec.sqlType}`);
|
|
34
|
+
}
|
|
35
|
+
if (to !== undefined) {
|
|
36
|
+
cond.where(sql `${cond.alias}.${sql.identifier(attrName)} <= ${sql.value(to)}::${attr.codec.sqlType}`);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
'icontains': (cond, input, { scope: { attr, attrName } }) => {
|
|
40
|
+
const id = sql `${cond.alias}.${sql.identifier(attrName)}`;
|
|
41
|
+
if (attr.codec.arrayOfCodec) {
|
|
42
|
+
return cond.where(sql `EXISTS (
|
|
43
|
+
SELECT 1 FROM unnest(${id}) AS elem
|
|
44
|
+
WHERE elem ILIKE ${sql.value(`%${input}%`)}
|
|
45
|
+
)`);
|
|
46
|
+
}
|
|
47
|
+
return cond.where(sql `${id} ILIKE ${sql.value(`%${input}%`)}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export * from "./filter-implementations/declaration.js";
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
import { FILTER_METHODS_CONFIG, FILTER_TYPES_MAP } from "./filter-implementations/index.js";
|
|
4
|
+
import { inflection } from "./inflection.js";
|
|
5
|
+
import { fields } from "./schema-fields.js";
|
|
6
|
+
import { init } from "./schema-init.js";
|
|
7
|
+
export const TargetedConditionsPlugin = {
|
|
8
|
+
name: 'TargetedConditionsPlugin',
|
|
9
|
+
version: '0.0.1',
|
|
10
|
+
inflection,
|
|
11
|
+
schema: {
|
|
12
|
+
behaviorRegistry: {
|
|
13
|
+
add: {
|
|
14
|
+
'filterable': {
|
|
15
|
+
description: 'Allow filtering using by fields on this relation',
|
|
16
|
+
entities: ['pgCodecRef', 'pgRefDefinition']
|
|
17
|
+
},
|
|
18
|
+
...Object.entries(FILTER_TYPES_MAP).reduce((acc, [filterType, { description }]) => {
|
|
19
|
+
const behaviourName = `filterType:${filterType}`;
|
|
20
|
+
acc[behaviourName] = {
|
|
21
|
+
description: description || `Add ${filterType} filter type`,
|
|
22
|
+
entities: ['pgCodecAttribute'],
|
|
23
|
+
};
|
|
24
|
+
return acc;
|
|
25
|
+
}, {}),
|
|
26
|
+
...Object.entries(FILTER_METHODS_CONFIG).reduce((acc, [filterMethod, { description }]) => {
|
|
27
|
+
const name = `filterMethod:${filterMethod}`;
|
|
28
|
+
acc[name] = {
|
|
29
|
+
description: description
|
|
30
|
+
|| `Allow filtering this field using ${filterMethod} operators`,
|
|
31
|
+
entities: ['pgCodecAttribute'],
|
|
32
|
+
};
|
|
33
|
+
return acc;
|
|
34
|
+
}, {})
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
hooks: {
|
|
38
|
+
build(build) {
|
|
39
|
+
return build
|
|
40
|
+
.extend(build, { inputConditionTypes: {} }, 'TargetedConditionsPlugin');
|
|
41
|
+
},
|
|
42
|
+
init: init,
|
|
43
|
+
'GraphQLInputObjectType_fields': fields,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const inflection: GraphileConfig.PluginInflectionConfig;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const inflection = {
|
|
2
|
+
add: {
|
|
3
|
+
conditionContainerTypeName(options, resource, attrName) {
|
|
4
|
+
const attrFieldName = this._attributeName({
|
|
5
|
+
codec: resource.codec,
|
|
6
|
+
attributeName: attrName,
|
|
7
|
+
});
|
|
8
|
+
return this.upperCamelCase(`${this._resourceName(resource)}_${attrFieldName}_condition`);
|
|
9
|
+
},
|
|
10
|
+
rangeConditionTypeName(options, codec) {
|
|
11
|
+
return this.upperCamelCase(`${this._codecName(codec)}_range_condition`);
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { getInputConditionForResource } from '@haathie/postgraphile-common-utils';
|
|
2
|
+
import { sql } from 'postgraphile/pg-sql2';
|
|
3
|
+
import { getFilterTypesForAttribute } from "./utils.js";
|
|
4
|
+
export const fields = (fieldMap, build, ctx) => {
|
|
5
|
+
const { behavior, inflection, getTypeByName } = build;
|
|
6
|
+
const { scope: { pgCodec: _codec, isPgCondition }, fieldWithHooks } = ctx;
|
|
7
|
+
if (!isPgCondition || !_codec?.extensions?.isTableLike) {
|
|
8
|
+
return fieldMap;
|
|
9
|
+
}
|
|
10
|
+
const pgCodec = _codec;
|
|
11
|
+
const pgResource = build.pgTableResource(pgCodec);
|
|
12
|
+
for (const attrName in pgCodec.attributes) {
|
|
13
|
+
const hasFilter = getFilterTypesForAttribute(pgCodec, attrName, build)
|
|
14
|
+
.next()
|
|
15
|
+
.value;
|
|
16
|
+
if (!hasFilter) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const typeName = inflection.conditionContainerTypeName(pgResource, attrName);
|
|
20
|
+
const type = getTypeByName(typeName);
|
|
21
|
+
if (!type) {
|
|
22
|
+
throw new Error(`Condition type ${typeName} for attribute "${attrName}" `
|
|
23
|
+
+ `not found in codec "${pgCodec.name}".`);
|
|
24
|
+
}
|
|
25
|
+
const fieldName = inflection
|
|
26
|
+
.attribute({ attributeName: attrName, codec: pgCodec });
|
|
27
|
+
fieldMap[fieldName] = fieldWithHooks({ fieldName, isConditionContainer: true }, () => ({ extensions: { grafast: { apply: passThroughApply } }, type }));
|
|
28
|
+
}
|
|
29
|
+
// add queries via refs
|
|
30
|
+
for (const [refName, { paths }] of Object.entries(pgCodec.refs || {})) {
|
|
31
|
+
if (!behavior.pgCodecRefMatches([pgCodec, refName], 'filterable')) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!paths.length) {
|
|
35
|
+
throw new Error(`Ref ${refName} on codec ${pgCodec.name} has no paths defined.`);
|
|
36
|
+
}
|
|
37
|
+
if (paths.length > 1) {
|
|
38
|
+
throw new Error('Refs w multiple paths are not supported yet.');
|
|
39
|
+
}
|
|
40
|
+
const { relationName } = paths[0][0];
|
|
41
|
+
const field = buildRelationSearch(relationName);
|
|
42
|
+
if (!field) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const fieldName = inflection.camelCase(refName);
|
|
46
|
+
fieldMap[fieldName] = field;
|
|
47
|
+
}
|
|
48
|
+
return fieldMap;
|
|
49
|
+
function buildRelationSearch(relationName) {
|
|
50
|
+
const relation = pgResource?.getRelation(relationName);
|
|
51
|
+
if (!relation) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const rmtRrsc = relation.remoteResource;
|
|
55
|
+
const rmtRrscFrom = rmtRrsc.from;
|
|
56
|
+
const remoteResourceCond = getInputConditionForResource(
|
|
57
|
+
// @ts-expect-error
|
|
58
|
+
rmtRrsc, build);
|
|
59
|
+
if (!remoteResourceCond) {
|
|
60
|
+
throw new Error('The remote resource does not have a condition type defined.');
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
type: remoteResourceCond,
|
|
64
|
+
extensions: {
|
|
65
|
+
grafast: {
|
|
66
|
+
apply(target) {
|
|
67
|
+
const wherePlan = target
|
|
68
|
+
.existsPlan({ alias: 't', tableExpression: rmtRrscFrom });
|
|
69
|
+
const localAttrsJoined = sql.join(relation.localAttributes.map(attr => (sql `${target.alias}.${sql.identifier(attr)}`)), ',');
|
|
70
|
+
const remoteAttrsJoined = sql.join(relation.remoteAttributes.map(attr => (sql `${wherePlan.alias}.${sql.identifier(attr)}`)), ',');
|
|
71
|
+
wherePlan.where(sql `(${localAttrsJoined}) = (${remoteAttrsJoined})`);
|
|
72
|
+
return wherePlan;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const passThroughApply = p => p;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { buildFieldNameToAttrNameMap, isSubscriptionPlan, mapFieldsToAttrs } from '@haathie/postgraphile-common-utils';
|
|
2
|
+
import { FILTER_METHODS_CONFIG, FILTER_TYPES_MAP } from "./filter-implementations/index.js";
|
|
3
|
+
import { getBuildGraphQlTypeByCodec, getFilterMethodsForAttribute, getFilterTypesForAttribute } from "./utils.js";
|
|
4
|
+
const DEFAULT_FILTER_METHOD = 'plainSql';
|
|
5
|
+
export const init = (_, build) => {
|
|
6
|
+
const { input: { pgRegistry: { pgResources } }, inflection, registerInputObjectType, sql, } = build;
|
|
7
|
+
const registeredTypes = new Set();
|
|
8
|
+
for (const resource of Object.values(pgResources)) {
|
|
9
|
+
const codec = resource.codec;
|
|
10
|
+
if (!codec.extensions?.isTableLike) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
for (const [attrName, attr] of Object.entries(resource.codec.attributes)) {
|
|
14
|
+
const info = {
|
|
15
|
+
types: [],
|
|
16
|
+
method: getFilterMethodsForAttribute(codec, attrName, build)
|
|
17
|
+
.next().value || undefined
|
|
18
|
+
};
|
|
19
|
+
for (const filterType of getFilterTypesForAttribute(codec, attrName, build)) {
|
|
20
|
+
info.types.push(filterType);
|
|
21
|
+
const filterInfo = FILTER_TYPES_MAP[filterType];
|
|
22
|
+
if (!filterInfo) {
|
|
23
|
+
throw new Error(`INTERNAL: Filter type "${filterType}" is not registered.`
|
|
24
|
+
+ ' Please register it before using `registerFilterImplementations`');
|
|
25
|
+
}
|
|
26
|
+
if (!filterInfo.getRegisterTypeInfo) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const { name, spec } = filterInfo.getRegisterTypeInfo(attr.codec, getBuildGraphQlTypeByCodec(attr.codec, build), build);
|
|
30
|
+
if (registeredTypes.has(name)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
registerInputObjectType(name, { conditionFilterType: filterType, pgCodec: attr.codec }, spec, `${attr.codec.name}_${filterType}_condition`);
|
|
34
|
+
registeredTypes.add(name);
|
|
35
|
+
}
|
|
36
|
+
registerConditionType(resource, attrName, info);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return _;
|
|
40
|
+
function registerConditionType(pgResource, attrName, { types: filterTypes, method }) {
|
|
41
|
+
const attr = pgResource.codec.attributes[attrName];
|
|
42
|
+
const filterConfigs = getFilterConfigs(attr.extensions?.tags);
|
|
43
|
+
const typeName = inflection._resourceName(pgResource);
|
|
44
|
+
registerInputObjectType(inflection.conditionContainerTypeName(pgResource, attrName), {
|
|
45
|
+
isConditionContainer: true,
|
|
46
|
+
pgResource,
|
|
47
|
+
pgAttribute: attr,
|
|
48
|
+
}, () => ({
|
|
49
|
+
description: `Conditions for filtering by ${typeName}'s ${attrName}`,
|
|
50
|
+
isOneOf: true,
|
|
51
|
+
fields() {
|
|
52
|
+
return filterTypes.reduce((fields, filterType) => {
|
|
53
|
+
const condType = buildConditionField(attrName, attr, filterType, method, filterConfigs[filterType]);
|
|
54
|
+
fields[filterType] = condType;
|
|
55
|
+
return fields;
|
|
56
|
+
}, {});
|
|
57
|
+
}
|
|
58
|
+
}), `${pgResource.name}_${attrName}_condition_container`);
|
|
59
|
+
}
|
|
60
|
+
function buildConditionField(attrName, attr, filter, method = DEFAULT_FILTER_METHOD, config = {}) {
|
|
61
|
+
const { getType, applys } = FILTER_TYPES_MAP[filter];
|
|
62
|
+
const builtType = getType(attr.codec, getBuildGraphQlTypeByCodec(attr.codec, build), build);
|
|
63
|
+
const fieldMap = buildFieldNameToAttrNameMap(attr.codec, inflection);
|
|
64
|
+
const applyMethod = applys?.[method];
|
|
65
|
+
if (!applyMethod) {
|
|
66
|
+
throw new Error(`No apply fn available for filter type ${filter} and method ${method}.`);
|
|
67
|
+
}
|
|
68
|
+
const applyDefault = applys?.[DEFAULT_FILTER_METHOD];
|
|
69
|
+
return {
|
|
70
|
+
type: builtType,
|
|
71
|
+
extensions: {
|
|
72
|
+
grafast: {
|
|
73
|
+
apply: buildMethodApply(method)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
function buildMethodApply(method) {
|
|
78
|
+
return (plan, args, info) => {
|
|
79
|
+
const newInfo = {
|
|
80
|
+
...info,
|
|
81
|
+
scope: {
|
|
82
|
+
...info.scope,
|
|
83
|
+
attrName: attrName,
|
|
84
|
+
attr: attr,
|
|
85
|
+
config,
|
|
86
|
+
serialiseToSql: () => serialiseToSql(args),
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const isSubscription = isSubscriptionPlan(plan);
|
|
90
|
+
if (isSubscription
|
|
91
|
+
&& !FILTER_METHODS_CONFIG[method]?.supportedOnSubscription) {
|
|
92
|
+
if (!applyDefault) {
|
|
93
|
+
throw new Error(`Filter method "${method}" is not supported on subscriptions.`);
|
|
94
|
+
}
|
|
95
|
+
return applyDefault(plan, args, newInfo);
|
|
96
|
+
}
|
|
97
|
+
return applyMethod(plan, args, newInfo);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function serialiseToSql(input) {
|
|
101
|
+
if (input === null || input === undefined) {
|
|
102
|
+
return sql.null;
|
|
103
|
+
}
|
|
104
|
+
// so if the input isn't a compound type, we can just return it
|
|
105
|
+
// we'll assume it's a scalar value or an array of scalars
|
|
106
|
+
if (!fieldMap) {
|
|
107
|
+
return sql.value(input);
|
|
108
|
+
}
|
|
109
|
+
const mapped = mapFieldsToAttrs(input, fieldMap);
|
|
110
|
+
const mainCodec = attr.codec.arrayOfCodec || attr.codec;
|
|
111
|
+
if (Array.isArray(mapped)) {
|
|
112
|
+
return sql.value(mapped.map(v => mainCodec.toPg(v)));
|
|
113
|
+
}
|
|
114
|
+
return sql.value(mainCodec.toPg(mapped));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function getFilterConfigs(tags) {
|
|
119
|
+
const filterConfigs = {};
|
|
120
|
+
const configTag = typeof tags?.filterConfig === 'string' ? [tags.filterConfig] : tags?.filterConfig;
|
|
121
|
+
if (!configTag) {
|
|
122
|
+
return filterConfigs;
|
|
123
|
+
}
|
|
124
|
+
for (const configStr of configTag) {
|
|
125
|
+
const colonIdx = configStr.indexOf(':');
|
|
126
|
+
if (colonIdx === -1) {
|
|
127
|
+
throw new Error(`Invalid filter config tag: ${configStr}`);
|
|
128
|
+
}
|
|
129
|
+
const filterType = configStr.slice(0, colonIdx);
|
|
130
|
+
const configJsonStr = configStr.slice(colonIdx + 1);
|
|
131
|
+
try {
|
|
132
|
+
const configJson = JSON.parse(configJsonStr);
|
|
133
|
+
filterConfigs[filterType] = configJson;
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
throw new Error(`Invalid JSON in filter config tag: ${configStr}, ${e.message}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return filterConfigs;
|
|
140
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { PgCodec, PgCodecAttribute, PgCondition, PgResource } from 'postgraphile/@dataplan/pg';
|
|
2
|
+
import type { InputObjectFieldApplyResolver } from 'postgraphile/grafast';
|
|
3
|
+
import type { GraphQLInputType } from 'postgraphile/graphql';
|
|
4
|
+
import type { SQL } from 'postgraphile/pg-sql2';
|
|
5
|
+
export type FilterType = keyof GraphileBuild.FilterTypeMap;
|
|
6
|
+
export type FilterMethod = keyof GraphileBuild.FilterMethodMap;
|
|
7
|
+
export type FilterApply<T = unknown> = InputObjectFieldApplyResolver<PgCondition, any, {
|
|
8
|
+
attrName: string;
|
|
9
|
+
attr: PgCodecAttribute;
|
|
10
|
+
serialiseToSql: () => SQL;
|
|
11
|
+
config?: T;
|
|
12
|
+
}>;
|
|
13
|
+
export type FilterImplementation = {
|
|
14
|
+
description?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Register the type used to filter the attribute.
|
|
17
|
+
* @param fieldCodec The codec of the field being filtered.
|
|
18
|
+
* @param getGraphQlType A function that returns the GraphQL type of the
|
|
19
|
+
* field. Only use in `registerInputObjectType` method or you'll get an
|
|
20
|
+
* error from Graphile
|
|
21
|
+
* @param build The build object.
|
|
22
|
+
*/
|
|
23
|
+
getRegisterTypeInfo?(fieldCodec: PgCodec, getGraphQlType: () => GraphQLInputType, build: GraphileBuild.Build): {
|
|
24
|
+
name: string;
|
|
25
|
+
spec: () => Omit<GraphileBuild.GrafastInputObjectTypeConfig, 'name'>;
|
|
26
|
+
};
|
|
27
|
+
getType(fieldCodec: PgCodec, getGraphQlType: () => GraphQLInputType, build: GraphileBuild.Build): GraphQLInputType;
|
|
28
|
+
applys?: {
|
|
29
|
+
[M in FilterMethod]?: FilterApply;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export type FilterMethodConfig = {
|
|
33
|
+
/**
|
|
34
|
+
* Optionally add a human-readable description for the filter method.
|
|
35
|
+
* Used in the behaviour registry
|
|
36
|
+
*/
|
|
37
|
+
description?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Should this filter method be used on subscriptions?
|
|
40
|
+
* If false, the `plainSql` method will be used instead.
|
|
41
|
+
*/
|
|
42
|
+
supportedOnSubscription: boolean;
|
|
43
|
+
};
|
|
44
|
+
interface FilterBehaviours extends Record<`filterType:${FilterType}`, true>, Record<`filterMethod:${FilterMethod}`, true> {
|
|
45
|
+
'filterable': true;
|
|
46
|
+
}
|
|
47
|
+
declare global {
|
|
48
|
+
namespace GraphileBuild {
|
|
49
|
+
interface FilterTypeMap {
|
|
50
|
+
eq: true;
|
|
51
|
+
eqIn: true;
|
|
52
|
+
range: true;
|
|
53
|
+
icontains: true;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Method used to apply filters -- useful for different index types like
|
|
57
|
+
* GIN, paradedb, zombodb, etc.
|
|
58
|
+
*/
|
|
59
|
+
interface FilterMethodMap {
|
|
60
|
+
}
|
|
61
|
+
interface FilterTypeMap {
|
|
62
|
+
eq: true;
|
|
63
|
+
eqIn: true;
|
|
64
|
+
range: true;
|
|
65
|
+
icontains: true;
|
|
66
|
+
}
|
|
67
|
+
interface BehaviorStrings extends FilterBehaviours {
|
|
68
|
+
}
|
|
69
|
+
interface Inflection {
|
|
70
|
+
conditionContainerTypeName(resource: PgResource, attrName: string): string;
|
|
71
|
+
rangeConditionTypeName(codec: PgCodec): string;
|
|
72
|
+
}
|
|
73
|
+
interface ScopeInputObject {
|
|
74
|
+
conditionFilterType?: FilterType;
|
|
75
|
+
isConditionContainer?: boolean;
|
|
76
|
+
}
|
|
77
|
+
interface ScopeInputObjectFieldsField {
|
|
78
|
+
isConditionContainer?: boolean;
|
|
79
|
+
}
|
|
80
|
+
interface PgCodecAttributeTags {
|
|
81
|
+
filterConfig?: string[];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export {};
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PgCodec, PgCodecWithAttributes } from 'postgraphile/@dataplan/pg';
|
|
2
|
+
import type { GraphQLInputType } from 'postgraphile/graphql';
|
|
3
|
+
export declare function getBuildGraphQlTypeByCodec(codec: PgCodec, build: GraphileBuild.Build): () => GraphQLInputType;
|
|
4
|
+
export declare function getFilterTypesForAttribute(pgCodec: PgCodecWithAttributes, attrName: string, { behavior }: GraphileBuild.Build): Generator<keyof GraphileBuild.FilterTypeMap, void, unknown>;
|
|
5
|
+
export declare function getFilterMethodsForAttribute(pgCodec: PgCodecWithAttributes, attrName: string, { behavior }: GraphileBuild.Build): Generator<keyof GraphileBuild.FilterMethodMap, void, unknown>;
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { FILTER_METHODS_CONFIG, FILTER_TYPES_MAP } from "./filter-implementations/index.js";
|
|
2
|
+
export function getBuildGraphQlTypeByCodec(codec, build) {
|
|
3
|
+
codec = codec.arrayOfCodec || codec;
|
|
4
|
+
return () => {
|
|
5
|
+
const type = build.getGraphQLTypeByPgCodec(codec, 'input');
|
|
6
|
+
if (!type) {
|
|
7
|
+
throw new Error(`No input type found for codec ${codec.name}`);
|
|
8
|
+
}
|
|
9
|
+
return type;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function* getFilterTypesForAttribute(pgCodec, attrName, { behavior }) {
|
|
13
|
+
for (const _filterType in FILTER_TYPES_MAP) {
|
|
14
|
+
const filterType = _filterType;
|
|
15
|
+
if (!behavior
|
|
16
|
+
.pgCodecAttributeMatches([pgCodec, attrName], `filterType:${filterType}`)) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
yield filterType;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function* getFilterMethodsForAttribute(pgCodec, attrName, { behavior }) {
|
|
23
|
+
for (const _method in FILTER_METHODS_CONFIG) {
|
|
24
|
+
const method = _method;
|
|
25
|
+
if (!behavior
|
|
26
|
+
.pgCodecAttributeMatches([pgCodec, attrName], `filterMethod:${method}`)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
yield method;
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@haathie/postgraphile-targeted-conditions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": ["lib"],
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/haathie/graphile-tools.git"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@haathie/postgraphile-common-utils": "*"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"postgraphile": "*"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Postgraphile Targeted Conditions
|
|
2
|
+
|
|
3
|
+
Opt-in & configurable conditions plugin for Postgraphile. This plugin is designed to allow you to quickly enable conditions on fields for your Postgraphile queries without having to write custom SQL or GraphQL plans. It also supports adding conditions on fields in related tables.
|
|
4
|
+
|
|
5
|
+
Please read through [the Caveats section](#caveats) before using this plugin.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
Install:
|
|
10
|
+
``` bash
|
|
11
|
+
npm i @haathie/postgraphile-targeted-conditions
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Add the plugin to your Postgraphile configuration:
|
|
15
|
+
``` ts
|
|
16
|
+
import { TargetedConditionsPlugin } from '@haathie/postgraphile-targeted-conditions'
|
|
17
|
+
|
|
18
|
+
export const config: GraphileBuild.Preset = {
|
|
19
|
+
...otherOptions,
|
|
20
|
+
plugins: [
|
|
21
|
+
...otherPlugins,
|
|
22
|
+
TargetedConditionsPlugin,
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
By default, the plugin will not add any conditions to any connection queries. You can enable conditions for a specific field by adding a "filterType" behaviour to the field in your schema.
|
|
30
|
+
|
|
31
|
+
``` sql
|
|
32
|
+
-- will add a case-insensitive "icontains" filter, and an "eq" filter
|
|
33
|
+
-- to the "name" column of the "contacts" table
|
|
34
|
+
comment on column app.contacts.name is $$
|
|
35
|
+
@behaviour filterType:icontains filterType:eq
|
|
36
|
+
$$;
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This will allow you to filter the "contacts" table by the "name" column using the following GraphQL query:
|
|
40
|
+
|
|
41
|
+
``` graphql
|
|
42
|
+
query GetContacts {
|
|
43
|
+
allContacts(condition: { name: { icontains: "john" } }) {
|
|
44
|
+
nodes {
|
|
45
|
+
id
|
|
46
|
+
name
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The plugin will always create a `oneOf` Input Object type for the filter type of each field. Eg.
|
|
53
|
+
``` graphql
|
|
54
|
+
input ContactNameCondition @oneOf {
|
|
55
|
+
icontains: String
|
|
56
|
+
eq: String
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This allows for adding/removing filter types without breaking existing queries. For example, if you add a new filter type for "equals in" to the "name" column, and the above query will still work without any changes.
|
|
61
|
+
|
|
62
|
+
## Relational Conditions
|
|
63
|
+
|
|
64
|
+
Let's say we have a `contacts` table and a `tags` table, with each contact having multiple tags. We can add a filter to the `contacts` table to filter by tags. We'll create a ref and add `filterable` behaviour to it.
|
|
65
|
+
This will give the contacts relation the ability to filter by all filterable fields in the `tags` table.
|
|
66
|
+
|
|
67
|
+
``` sql
|
|
68
|
+
-- will add a "tags" filter to the "contacts" table
|
|
69
|
+
-- for more info on refs, see: https://postgraphile.org/postgraphile/5/refs/#ref-and-refvia
|
|
70
|
+
comment on table "conditions_test"."authors" is $$
|
|
71
|
+
@ref tags via:(id)->tags(contact_id) behavior:filterable
|
|
72
|
+
$$;
|
|
73
|
+
|
|
74
|
+
-- we'll also add an "eq" filter to the "name" column of the "tags" table
|
|
75
|
+
comment on column app.tags.name is $$
|
|
76
|
+
@behaviour filterType:eq
|
|
77
|
+
$$;
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This now enables us to query `contacts` by `tags` in the following way:
|
|
81
|
+
``` graphql
|
|
82
|
+
query GetContacts {
|
|
83
|
+
allContacts(condition: { tags: { name: { eq: "important" } } }) {
|
|
84
|
+
nodes {
|
|
85
|
+
id
|
|
86
|
+
name
|
|
87
|
+
# also return the tags for each contact
|
|
88
|
+
# (not related to the conditions plugin, but useful)
|
|
89
|
+
tags {
|
|
90
|
+
nodes {
|
|
91
|
+
id
|
|
92
|
+
name
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Available Filter Types
|
|
101
|
+
|
|
102
|
+
- `eq`: Exact match, with null handling
|
|
103
|
+
- `eqIn`: Check if the value is in a list of values, with null handling
|
|
104
|
+
- `icontains`: Case-insensitive contains
|
|
105
|
+
- `range`: Check if a value is within an inclusive range
|
|
106
|
+
|
|
107
|
+
## Filter Methods
|
|
108
|
+
|
|
109
|
+
Postgres has a bunch of popular extensions that implement a different syntax for filtering -- eg. GIN indices, ZomboDB, ParadeDB, etc. This plugin is extensible to support these extensions.
|
|
110
|
+
|
|
111
|
+
Presently, the plugin supports the following filter methods:
|
|
112
|
+
- [paradedb](https://github.com/paradedb/paradedb): ParadeDB is a PostgreSQL extension that allows you to have ES-level query capabilities in your Postgres database. This plugin supports using ParadeDB's query syntax to filter your queries.
|
|
113
|
+
|
|
114
|
+
## Adding a Custom Filter Type
|
|
115
|
+
|
|
116
|
+
Let's implement a `startsWith` filter type that allows filtering strings that start with a given value.
|
|
117
|
+
|
|
118
|
+
``` ts
|
|
119
|
+
import { registerFilterImplementations } from '@haathie/postgraphile-targeted-conditions'
|
|
120
|
+
|
|
121
|
+
// extend the "FilterTypeMap" interface to add the new filter type
|
|
122
|
+
declare global {
|
|
123
|
+
namespace GraphileBuild {
|
|
124
|
+
interface FilterTypeMap {
|
|
125
|
+
startsWith: true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// write the filter implementation
|
|
131
|
+
registerFilterImplementations({
|
|
132
|
+
'startsWith': {
|
|
133
|
+
// the getType method is used to get the GraphQL type for the filter
|
|
134
|
+
// in this case, it'll just be the same type as the field being filtered.
|
|
135
|
+
// Also since startsWith only makes sense for string fields -- we can throw
|
|
136
|
+
// an error if the field is not a string.
|
|
137
|
+
getType(codec, getGraphQlType, { graphql: { GraphQLNonNull, GraphQLString } }) {
|
|
138
|
+
const type = getGraphQlType()
|
|
139
|
+
if(type !== GraphQLString) {
|
|
140
|
+
throw new Error(`The "startsWith" filter type can only be used on string fields, but the field "${codec.name}" is of type "${type.name}".`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return new GraphQLNonNull(type)
|
|
144
|
+
},
|
|
145
|
+
// in the applys method, we define how the filter gets converted to SQL
|
|
146
|
+
// using a specified method. By default, "plainSql" is used, but you can
|
|
147
|
+
// implement other methods like "paradedb" or "zombodb" to use their
|
|
148
|
+
// query syntax.
|
|
149
|
+
applys: {
|
|
150
|
+
plainSql: (cond, input, { scope: { attrName, attr } }) => {
|
|
151
|
+
const id = sql`${cond.alias}.${sql.identifier(attrName)}`
|
|
152
|
+
// handle postgres array types
|
|
153
|
+
if(attr.codec.arrayOfCodec) {
|
|
154
|
+
return cond.where(
|
|
155
|
+
sql`EXISTS (
|
|
156
|
+
SELECT 1 FROM unnest(${id}) AS elem
|
|
157
|
+
WHERE elem LIKE ${sql.value(`${input}%`)}
|
|
158
|
+
)`
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return cond.where(sql`${id} LIKE ${sql.value(`${input}%`)}`)
|
|
163
|
+
},
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
See how other filter types are implemented [here](src/filter-implementations/declaration.ts#L51).
|
|
170
|
+
|
|
171
|
+
## Adding a Custom Filter Method
|
|
172
|
+
|
|
173
|
+
To add a custom filter method, you can use the `registerFilterMethod` function. This allows you to define how the filter type should be applied in SQL.
|
|
174
|
+
|
|
175
|
+
``` ts
|
|
176
|
+
import { registerFilterMethod } from '@haathie/postgraphile-targeted-conditions'
|
|
177
|
+
|
|
178
|
+
// extend the "FilterMethodMap" interface to add the new filter method
|
|
179
|
+
declare global {
|
|
180
|
+
namespace GraphileBuild {
|
|
181
|
+
interface FilterMethodMap {
|
|
182
|
+
zombodb: true
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
registerFilterMethod(
|
|
188
|
+
'zombodb',
|
|
189
|
+
// can this be used in subscriptions? This flag is present as some filter
|
|
190
|
+
// methods may not be well suited or even supported in realtime scenarios.
|
|
191
|
+
{ supportedOnSubscription: false },
|
|
192
|
+
{
|
|
193
|
+
eq: (cond, input, { scope: { attrName, attr } }) => {
|
|
194
|
+
// implement
|
|
195
|
+
},
|
|
196
|
+
eqIn: (cond, input, { scope: { attrName, attr } }) => {
|
|
197
|
+
// implement
|
|
198
|
+
},
|
|
199
|
+
// ... other filter types
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
See how [paradedb method](src/filter-implementations/paradedb.ts) is implemented for a more detailed example.
|
|
205
|
+
|
|
206
|
+
## Caveats
|
|
207
|
+
|
|
208
|
+
- Adding relational conditions can lead to performance issues, especially if the related table has a large number of rows. Use with caution and please ensure you have the necessary indices in place.
|
|
209
|
+
- Apart from relations, adding arbitrary conditions on fields for vibes only is not a good idea. It can cause unexpected performance issues, and it is recommended to only add conditions that are necessary for your application. The plugin is meant for you to quickly add targeted conditions to your queries, and only the ones you want -- so we spend more time writing mission-critical code rather than boilerplate SQL or GraphQL plans.
|
|
210
|
+
- This plugin does not work with Postgres compound types at the moment. Only the `eq` and `eqIn` filter types are supported for compound types.
|