graphile-pg-aggregates 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/AddAggregateTypesPlugin.d.ts +38 -0
- package/AddAggregateTypesPlugin.js +268 -0
- package/AddConnectionAggregatesPlugin.d.ts +2 -0
- package/AddConnectionAggregatesPlugin.js +56 -0
- package/AddConnectionGroupedAggregatesPlugin.d.ts +10 -0
- package/AddConnectionGroupedAggregatesPlugin.js +118 -0
- package/AddGroupByAggregateEnumValuesForAttributesPlugin.d.ts +10 -0
- package/AddGroupByAggregateEnumValuesForAttributesPlugin.js +116 -0
- package/AddGroupByAggregateEnumsPlugin.d.ts +2 -0
- package/AddGroupByAggregateEnumsPlugin.js +51 -0
- package/AddHavingAggregateTypesPlugin.d.ts +22 -0
- package/AddHavingAggregateTypesPlugin.js +449 -0
- package/AggregateSpecsPlugin.d.ts +1 -0
- package/AggregateSpecsPlugin.js +206 -0
- package/AggregatesSmartTagsPlugin.d.ts +8 -0
- package/AggregatesSmartTagsPlugin.js +65 -0
- package/EXPORTABLE.d.ts +1 -0
- package/EXPORTABLE.js +15 -0
- package/FilterRelationalAggregatesPlugin.d.ts +74 -0
- package/FilterRelationalAggregatesPlugin.js +443 -0
- package/InflectionPlugin.d.ts +67 -0
- package/InflectionPlugin.js +71 -0
- package/LICENSE +23 -0
- package/OrderByAggregatesPlugin.d.ts +31 -0
- package/OrderByAggregatesPlugin.js +205 -0
- package/README.md +128 -0
- package/esm/AddAggregateTypesPlugin.d.ts +38 -0
- package/esm/AddAggregateTypesPlugin.js +265 -0
- package/esm/AddConnectionAggregatesPlugin.d.ts +2 -0
- package/esm/AddConnectionAggregatesPlugin.js +53 -0
- package/esm/AddConnectionGroupedAggregatesPlugin.d.ts +10 -0
- package/esm/AddConnectionGroupedAggregatesPlugin.js +115 -0
- package/esm/AddGroupByAggregateEnumValuesForAttributesPlugin.d.ts +10 -0
- package/esm/AddGroupByAggregateEnumValuesForAttributesPlugin.js +113 -0
- package/esm/AddGroupByAggregateEnumsPlugin.d.ts +2 -0
- package/esm/AddGroupByAggregateEnumsPlugin.js +48 -0
- package/esm/AddHavingAggregateTypesPlugin.d.ts +22 -0
- package/esm/AddHavingAggregateTypesPlugin.js +446 -0
- package/esm/AggregateSpecsPlugin.d.ts +1 -0
- package/esm/AggregateSpecsPlugin.js +203 -0
- package/esm/AggregatesSmartTagsPlugin.d.ts +8 -0
- package/esm/AggregatesSmartTagsPlugin.js +62 -0
- package/esm/EXPORTABLE.d.ts +1 -0
- package/esm/EXPORTABLE.js +12 -0
- package/esm/FilterRelationalAggregatesPlugin.d.ts +74 -0
- package/esm/FilterRelationalAggregatesPlugin.js +440 -0
- package/esm/InflectionPlugin.d.ts +67 -0
- package/esm/InflectionPlugin.js +68 -0
- package/esm/OrderByAggregatesPlugin.d.ts +31 -0
- package/esm/OrderByAggregatesPlugin.js +202 -0
- package/esm/index.d.ts +42 -0
- package/esm/index.js +37 -0
- package/esm/interfaces.d.ts +145 -0
- package/esm/interfaces.js +44 -0
- package/esm/utils.d.ts +23 -0
- package/esm/utils.js +22 -0
- package/index.d.ts +42 -0
- package/index.js +40 -0
- package/interfaces.d.ts +145 -0
- package/interfaces.js +47 -0
- package/package.json +61 -0
- package/utils.d.ts +23 -0
- package/utils.js +25 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const version = '1.0.0';
|
|
2
|
+
const EMPTY_OBJECT = Object.freeze({});
|
|
3
|
+
export const PgAggregatesSmartTagsPlugin = {
|
|
4
|
+
name: 'PgAggregatesSmartTagsPlugin',
|
|
5
|
+
description: 'For compatibility with PostGraphile v4 schemas, this plugin attempts to convert `@aggregates` V4 smart tags to V5 behaviors',
|
|
6
|
+
version,
|
|
7
|
+
before: [
|
|
8
|
+
'PgAggregatesAddAggregateTypesPlugin',
|
|
9
|
+
'PgAggregatesFilterRelationalAggregatesPlugin',
|
|
10
|
+
'PgAggregatesOrderByAggregatesPlugin'
|
|
11
|
+
],
|
|
12
|
+
provides: ['smart-tags'],
|
|
13
|
+
gather: {
|
|
14
|
+
namespace: 'pgV4AggregatesSmartTags',
|
|
15
|
+
initialCache() {
|
|
16
|
+
return EMPTY_OBJECT;
|
|
17
|
+
},
|
|
18
|
+
initialState() {
|
|
19
|
+
return EMPTY_OBJECT;
|
|
20
|
+
},
|
|
21
|
+
helpers: {},
|
|
22
|
+
hooks: {
|
|
23
|
+
// Run in the 'introspection' phase before anything uses the tags
|
|
24
|
+
pgIntrospection_introspection(info, event) {
|
|
25
|
+
for (const pgClass of event.introspection.classes) {
|
|
26
|
+
processTags(pgClass.getTags());
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
function processTags(tags) {
|
|
33
|
+
switch (tags?.aggregates) {
|
|
34
|
+
case 'on':
|
|
35
|
+
addBehaviorToTags(tags, '+aggregates +aggregates:filterBy +aggregates:orderBy +aggregate +aggregate:filterBy +aggregate:orderBy');
|
|
36
|
+
break;
|
|
37
|
+
case 'off':
|
|
38
|
+
addBehaviorToTags(tags, '-aggregates -aggregates:filterBy -aggregates:orderBy -aggregate -aggregate:filterBy -aggregate:orderBy');
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function addBehaviorToTags(tags, behavior, prepend = false) {
|
|
43
|
+
if (Array.isArray(tags.behavior)) {
|
|
44
|
+
if (prepend) {
|
|
45
|
+
tags.behavior = [behavior, ...tags.behavior];
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
tags.behavior = [...tags.behavior, behavior];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (typeof tags.behavior === 'string') {
|
|
52
|
+
tags.behavior = prepend
|
|
53
|
+
? [behavior, tags.behavior]
|
|
54
|
+
: [tags.behavior, behavior];
|
|
55
|
+
}
|
|
56
|
+
else if (!tags.behavior) {
|
|
57
|
+
tags.behavior = [behavior];
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw new Error(`Did not understand tags.behavior - it wasn't an array or a string`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function EXPORTABLE<T, TScope extends any[]>(factory: (...args: TScope) => T, args: [...TScope], nameHint?: string): T;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function EXPORTABLE(factory, args, nameHint) {
|
|
2
|
+
const fn = factory(...args);
|
|
3
|
+
if (((typeof fn === 'object' && fn !== null) || typeof fn === 'function') &&
|
|
4
|
+
!('$exporter$factory' in fn)) {
|
|
5
|
+
Object.defineProperties(fn, {
|
|
6
|
+
$exporter$args: { value: args },
|
|
7
|
+
$exporter$factory: { value: factory },
|
|
8
|
+
$exporter$name: { writable: true, value: nameHint }
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
return fn;
|
|
12
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { PgCodec, PgConditionCapableParent, PgWhereConditionSpec, sql } from '@dataplan/pg';
|
|
2
|
+
import type { Modifier } from 'grafast';
|
|
3
|
+
import type { AggregateSpec } from './interfaces';
|
|
4
|
+
type PgSQL = typeof sql;
|
|
5
|
+
type SQL = ReturnType<PgSQL>;
|
|
6
|
+
declare global {
|
|
7
|
+
namespace DataplanPg {
|
|
8
|
+
interface PgConditionExtensions {
|
|
9
|
+
pgFilterAttribute?: {
|
|
10
|
+
fieldName?: string;
|
|
11
|
+
codec: PgCodec;
|
|
12
|
+
expression?: SQL;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
namespace GraphileBuild {
|
|
17
|
+
interface Build {
|
|
18
|
+
PgAggregateCondition: PgAggregateConditionClass;
|
|
19
|
+
PgAggregateConditionExpression: PgAggregateConditionExpressionClass;
|
|
20
|
+
}
|
|
21
|
+
interface BehaviorStrings {
|
|
22
|
+
'resource:aggregates:filterBy': true;
|
|
23
|
+
'aggregates:filterBy': true;
|
|
24
|
+
'aggregate:filterBy': true;
|
|
25
|
+
'sum:resource:aggregates:filterBy': true;
|
|
26
|
+
'distinctCount:resource:aggregates:filterBy': true;
|
|
27
|
+
'min:resource:aggregates:filterBy': true;
|
|
28
|
+
'max:resource:aggregates:filterBy': true;
|
|
29
|
+
'average:resource:aggregates:filterBy': true;
|
|
30
|
+
'stddevSample:resource:aggregates:filterBy': true;
|
|
31
|
+
'stddevPopulation:resource:aggregates:filterBy': true;
|
|
32
|
+
'varianceSample:resource:aggregates:filterBy': true;
|
|
33
|
+
'variancePopulation:resource:aggregates:filterBy': true;
|
|
34
|
+
'sum:attribute:aggregate:filterBy': true;
|
|
35
|
+
'distinctCount:attribute:aggregate:filterBy': true;
|
|
36
|
+
'min:attribute:aggregate:filterBy': true;
|
|
37
|
+
'max:attribute:aggregate:filterBy': true;
|
|
38
|
+
'average:attribute:aggregate:filterBy': true;
|
|
39
|
+
'stddevSample:attribute:aggregate:filterBy': true;
|
|
40
|
+
'stddevPopulation:attribute:aggregate:filterBy': true;
|
|
41
|
+
'varianceSample:attribute:aggregate:filterBy': true;
|
|
42
|
+
'variancePopulation:attribute:aggregate:filterBy': true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export declare const Plugin: GraphileConfig.Plugin;
|
|
47
|
+
export { Plugin as PgAggregatesFilterRelationalAggregatesPlugin };
|
|
48
|
+
interface PgAggregateCondition<TParentStep extends PgConditionCapableParent> extends Modifier<TParentStep> {
|
|
49
|
+
sql: PgSQL;
|
|
50
|
+
tableExpression: SQL;
|
|
51
|
+
alias: SQL;
|
|
52
|
+
conditions: PgWhereConditionSpec<any>[];
|
|
53
|
+
expressions: SQL[];
|
|
54
|
+
where(condition: PgWhereConditionSpec<any>): void;
|
|
55
|
+
expression(expression: SQL): void;
|
|
56
|
+
forAggregate(spec: AggregateSpec): PgAggregateConditionExpression;
|
|
57
|
+
apply(): void;
|
|
58
|
+
}
|
|
59
|
+
interface PgAggregateConditionClass {
|
|
60
|
+
new <TParentStep extends PgConditionCapableParent>($parent: TParentStep, options: {
|
|
61
|
+
sql: PgSQL;
|
|
62
|
+
tableExpression: SQL;
|
|
63
|
+
alias?: string;
|
|
64
|
+
}, pgWhereConditionSpecListToSQL: GraphileBuild.Build['dataplanPg']['pgWhereConditionSpecListToSQL']): PgAggregateCondition<TParentStep>;
|
|
65
|
+
}
|
|
66
|
+
interface PgAggregateConditionExpression extends Modifier<PgAggregateCondition<any>> {
|
|
67
|
+
alias: SQL;
|
|
68
|
+
conditions: PgWhereConditionSpec<any>[];
|
|
69
|
+
where(condition: PgWhereConditionSpec<any>): void;
|
|
70
|
+
apply(): void;
|
|
71
|
+
}
|
|
72
|
+
interface PgAggregateConditionExpressionClass {
|
|
73
|
+
new ($parent: PgAggregateCondition<any>, spec: AggregateSpec, pgWhereConditionSpecListToSQL: GraphileBuild.Build['dataplanPg']['pgWhereConditionSpecListToSQL']): PgAggregateConditionExpression;
|
|
74
|
+
}
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { EXPORTABLE } from './EXPORTABLE';
|
|
2
|
+
const version = '1.0.0';
|
|
3
|
+
const pgAggregateApplyAttributeOrder = EXPORTABLE(() => (PgCondition, sql, spec, attributeName, attrCodec, rawAttrCodec, $parent, input) => {
|
|
4
|
+
if (input == null)
|
|
5
|
+
return;
|
|
6
|
+
const $col = new PgCondition($parent);
|
|
7
|
+
$col.extensions.pgFilterAttribute = {
|
|
8
|
+
codec: attrCodec,
|
|
9
|
+
expression: spec.sqlAggregateWrap(sql `${$col.alias}.${sql.identifier(attributeName)}`, rawAttrCodec)
|
|
10
|
+
};
|
|
11
|
+
return $col;
|
|
12
|
+
}, [], 'pgAggregateApplyAttributeOrder');
|
|
13
|
+
const pgAggregateApplyComputedAttributeOrder = EXPORTABLE(() => (PgCondition, sql, spec, proc, attrCodec, $parent, input) => {
|
|
14
|
+
if (input == null)
|
|
15
|
+
return;
|
|
16
|
+
const $col = new PgCondition($parent);
|
|
17
|
+
const sqlComputedAttributeCall = sql.query `${typeof proc.from === 'function'
|
|
18
|
+
? proc.from({ placeholder: $col.alias })
|
|
19
|
+
: proc.from}`;
|
|
20
|
+
$col.extensions.pgFilterAttribute = {
|
|
21
|
+
codec: attrCodec,
|
|
22
|
+
expression: spec.sqlAggregateWrap(sqlComputedAttributeCall, proc.codec)
|
|
23
|
+
};
|
|
24
|
+
return $col;
|
|
25
|
+
}, [], 'pgAggregateApplyComputedAttributeOrder');
|
|
26
|
+
const pgAggregateApplyForeignCondition = EXPORTABLE(() => function (PgCondition, $subquery, input) {
|
|
27
|
+
if (input == null)
|
|
28
|
+
return;
|
|
29
|
+
// Enable all the helpers
|
|
30
|
+
const $condition = new PgCondition($subquery, false, 'AND');
|
|
31
|
+
return $condition;
|
|
32
|
+
}, [], 'pgAggregateApplyForeignCondition');
|
|
33
|
+
const pgAggregatesApply = EXPORTABLE(() => (PgAggregateCondition, pgWhereConditionSpecListToSQL, sql, $where, input) => {
|
|
34
|
+
if (input == null)
|
|
35
|
+
return;
|
|
36
|
+
// Constructive fork: read from _manyRelation (set by our
|
|
37
|
+
// ConnectionFilterBackwardRelationsPlugin) instead of upstream's
|
|
38
|
+
// $where.extensions.pgFilterRelation.
|
|
39
|
+
const rel = $where._manyRelation;
|
|
40
|
+
if (!rel) {
|
|
41
|
+
throw new Error(`Invalid use of aggregates filter: '_manyRelation' expected on $where. ` +
|
|
42
|
+
`Ensure graphile-connection-filter backward relations plugin has tagged $where.`);
|
|
43
|
+
}
|
|
44
|
+
const { localAttributes, remoteAttributes, foreignTableExpression, foreignTable } = rel;
|
|
45
|
+
const $subQuery = new PgAggregateCondition($where, {
|
|
46
|
+
sql,
|
|
47
|
+
tableExpression: foreignTableExpression,
|
|
48
|
+
alias: foreignTable.name
|
|
49
|
+
}, pgWhereConditionSpecListToSQL);
|
|
50
|
+
localAttributes.forEach((localAttribute, i) => {
|
|
51
|
+
const remoteAttribute = remoteAttributes[i];
|
|
52
|
+
$subQuery.where(sql `${$where.alias}.${sql.identifier(localAttribute)} = ${$subQuery.alias}.${sql.identifier(remoteAttribute)}`);
|
|
53
|
+
});
|
|
54
|
+
return $subQuery;
|
|
55
|
+
}, [], 'pgAggregatesApply');
|
|
56
|
+
export const Plugin = {
|
|
57
|
+
name: 'PgAggregatesFilterRelationalAggregatesPlugin',
|
|
58
|
+
description: `\
|
|
59
|
+
Adds the ability to filter a collection by aggregates on relationships via \
|
|
60
|
+
graphile-connection-filter, e.g. filtering all players based on \
|
|
61
|
+
the sum of their points scored.`,
|
|
62
|
+
version,
|
|
63
|
+
// This has to run AFTER any plugins that provide `build.pgAggregateSpecs`
|
|
64
|
+
// otherwise we might add codecs to build.allPgCodecs before all the relevant
|
|
65
|
+
// codecs/specs are available.
|
|
66
|
+
after: ['PgBasicsPlugin', 'PgCodecsPlugin', 'aggregates'],
|
|
67
|
+
provides: ['codecs'],
|
|
68
|
+
before: ['ConnectionFilterArgPlugin'],
|
|
69
|
+
inflection: {
|
|
70
|
+
add: {
|
|
71
|
+
// TODO: rename this!
|
|
72
|
+
filterTableAggregateType(_preset, foreignTable, spec) {
|
|
73
|
+
const foreignTableTypeName = this.tableType(foreignTable.codec);
|
|
74
|
+
return this.filterType(foreignTableTypeName + this.upperCamelCase(spec.id) + 'Aggregate');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
schema: {
|
|
79
|
+
behaviorRegistry: {
|
|
80
|
+
add: {
|
|
81
|
+
'aggregates:filterBy': {
|
|
82
|
+
description: 'Can we filter by the details of this resource whilst aggregating the resource?',
|
|
83
|
+
entities: ['pgResource']
|
|
84
|
+
},
|
|
85
|
+
'aggregate:filterBy': {
|
|
86
|
+
description: 'Can we filter using the value of this attribute',
|
|
87
|
+
entities: ['pgCodecAttribute']
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
entityBehavior: {
|
|
92
|
+
pgResource: ['aggregates:filterBy', 'aggregate:filterBy'],
|
|
93
|
+
pgCodecAttribute: ['aggregate:filterBy']
|
|
94
|
+
},
|
|
95
|
+
hooks: {
|
|
96
|
+
build(build) {
|
|
97
|
+
const { EXPORTABLE, grafast: { Modifier } } = build;
|
|
98
|
+
if (!build.allPgCodecs) {
|
|
99
|
+
throw new Error('PgAggregatesFilterRelationalAggregatesPlugin must run after build.allPgCodecs has been established');
|
|
100
|
+
}
|
|
101
|
+
if (!build.pgAggregateSpecs) {
|
|
102
|
+
throw new Error('PgAggregatesFilterRelationalAggregatesPlugin must run after build.pgAggregateSpecs has been established');
|
|
103
|
+
}
|
|
104
|
+
// Add aggregate derivative codecs to `allPgCodecs`
|
|
105
|
+
for (const spec of build.pgAggregateSpecs) {
|
|
106
|
+
if (!spec.pgTypeCodecModifier) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
for (const existingCodec of build.allPgCodecs) {
|
|
110
|
+
if (spec.isSuitableType(existingCodec)) {
|
|
111
|
+
const codec = spec.pgTypeCodecModifier(existingCodec);
|
|
112
|
+
if (!build.allPgCodecs.has(codec)) {
|
|
113
|
+
build.allPgCodecs.add(codec);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const PgAggregateConditionExpression = EXPORTABLE((Modifier) => class PgAggregateConditionExpression extends Modifier {
|
|
119
|
+
spec;
|
|
120
|
+
pgWhereConditionSpecListToSQL;
|
|
121
|
+
alias;
|
|
122
|
+
conditions = [];
|
|
123
|
+
constructor(parent, spec, pgWhereConditionSpecListToSQL) {
|
|
124
|
+
super(parent);
|
|
125
|
+
this.spec = spec;
|
|
126
|
+
this.pgWhereConditionSpecListToSQL = pgWhereConditionSpecListToSQL;
|
|
127
|
+
this.alias = parent.alias;
|
|
128
|
+
}
|
|
129
|
+
where(condition) {
|
|
130
|
+
this.conditions.push(condition);
|
|
131
|
+
}
|
|
132
|
+
apply() {
|
|
133
|
+
const sqlCondition = this.pgWhereConditionSpecListToSQL(this.alias, this.conditions);
|
|
134
|
+
if (sqlCondition) {
|
|
135
|
+
this.parent.expression(sqlCondition);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}, [Modifier]);
|
|
139
|
+
const PgAggregateCondition = EXPORTABLE((Modifier, PgAggregateConditionExpression) => class PgAggregateCondition extends Modifier {
|
|
140
|
+
pgWhereConditionSpecListToSQL;
|
|
141
|
+
sql;
|
|
142
|
+
tableExpression;
|
|
143
|
+
alias;
|
|
144
|
+
conditions = [];
|
|
145
|
+
expressions = [];
|
|
146
|
+
constructor(parent, options, pgWhereConditionSpecListToSQL) {
|
|
147
|
+
super(parent);
|
|
148
|
+
this.pgWhereConditionSpecListToSQL = pgWhereConditionSpecListToSQL;
|
|
149
|
+
const { sql, tableExpression, alias } = options;
|
|
150
|
+
this.sql = sql;
|
|
151
|
+
this.alias = sql.identifier(Symbol(alias ?? 'aggregate'));
|
|
152
|
+
this.tableExpression = tableExpression;
|
|
153
|
+
}
|
|
154
|
+
where(condition) {
|
|
155
|
+
this.conditions.push(condition);
|
|
156
|
+
}
|
|
157
|
+
expression(expression) {
|
|
158
|
+
this.expressions.push(expression);
|
|
159
|
+
}
|
|
160
|
+
forAggregate(spec) {
|
|
161
|
+
return new PgAggregateConditionExpression(this, spec, this.pgWhereConditionSpecListToSQL);
|
|
162
|
+
}
|
|
163
|
+
apply() {
|
|
164
|
+
const { sql } = this;
|
|
165
|
+
const sqlCondition = this.pgWhereConditionSpecListToSQL(this.alias, this.conditions);
|
|
166
|
+
const where = sqlCondition
|
|
167
|
+
? sql `where ${sqlCondition}`
|
|
168
|
+
: sql.blank;
|
|
169
|
+
const boolExpr = this.expressions.length === 0
|
|
170
|
+
? sql.true
|
|
171
|
+
: sql.parens(sql.join(this.expressions.map((expr) => sql.parens(expr)), '\nand\n'));
|
|
172
|
+
const subquery = sql `(${sql.indent `\
|
|
173
|
+
select ${boolExpr}
|
|
174
|
+
from ${this.tableExpression} as ${this.alias}
|
|
175
|
+
${where}`}
|
|
176
|
+
group by ())`;
|
|
177
|
+
return this.parent.where(subquery);
|
|
178
|
+
}
|
|
179
|
+
}, [Modifier, PgAggregateConditionExpression]);
|
|
180
|
+
return build.extend(build, {
|
|
181
|
+
PgAggregateCondition,
|
|
182
|
+
PgAggregateConditionExpression
|
|
183
|
+
}, 'Adding step classes from postgraphile-plugin-connection-filter');
|
|
184
|
+
},
|
|
185
|
+
init(_, build) {
|
|
186
|
+
const { inflection, dataplanPg: { PgCondition }, EXPORTABLE } = build;
|
|
187
|
+
if (!inflection.filterType) {
|
|
188
|
+
// Filter plugin is not enabled
|
|
189
|
+
return _;
|
|
190
|
+
}
|
|
191
|
+
// Register the aggregate filter type for each table
|
|
192
|
+
for (const foreignTable of Object.values(build.input.pgRegistry.pgResources)) {
|
|
193
|
+
if (foreignTable.parameters || !foreignTable.codec.attributes) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!build.behavior.pgResourceMatches(foreignTable, 'resource:aggregates:filterBy')) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const foreignTableTypeName = inflection.tableType(foreignTable.codec);
|
|
200
|
+
const foreignTableFilterTypeName = inflection.filterType(foreignTableTypeName);
|
|
201
|
+
const foreignTableAggregateFilterTypeName = inflection.filterType(foreignTableTypeName + 'Aggregates');
|
|
202
|
+
build.recoverable(null, () => {
|
|
203
|
+
const filterFieldName = 'where';
|
|
204
|
+
build.registerInputObjectType(foreignTableAggregateFilterTypeName, {
|
|
205
|
+
pgResource: foreignTable,
|
|
206
|
+
isPgConnectionAggregateFilter: true
|
|
207
|
+
}, () => {
|
|
208
|
+
return {
|
|
209
|
+
description: `A filter to be used against aggregates of \`${foreignTableTypeName}\` object types.`,
|
|
210
|
+
fields: () => {
|
|
211
|
+
const type = build.getTypeByName(foreignTableFilterTypeName);
|
|
212
|
+
if (!type) {
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
[filterFieldName]: {
|
|
217
|
+
description: `A filter that must pass for the relevant \`${foreignTableTypeName}\` object to be included within the aggregate.`,
|
|
218
|
+
type,
|
|
219
|
+
apply: EXPORTABLE((PgCondition, pgAggregateApplyForeignCondition) => ($subquery, input) => pgAggregateApplyForeignCondition(PgCondition, $subquery, input), [PgCondition, pgAggregateApplyForeignCondition], `${filterFieldName}Apply`)
|
|
220
|
+
// No need to auto-apply since we're applied manually via `fieldArgs.apply($subQuery)` below.
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
}, 'Adding aggregate filter input type');
|
|
226
|
+
});
|
|
227
|
+
// Register the aggregate spec filter type for each aggreage spec for each source
|
|
228
|
+
for (const spec of build.pgAggregateSpecs) {
|
|
229
|
+
if (!build.behavior.pgResourceMatches(foreignTable, `${spec.id}:resource:aggregates:filterBy`)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const filterTypeName = inflection.filterTableAggregateType(foreignTable, spec);
|
|
233
|
+
build.registerInputObjectType(filterTypeName, {
|
|
234
|
+
isPgConnectionAggregateAggregateFilter: true,
|
|
235
|
+
pgConnectionAggregateFilterAggregateSpec: spec,
|
|
236
|
+
pgTypeResource: foreignTable
|
|
237
|
+
}, () => ({}), `Add '${spec.id}' aggregate filter type for '${foreignTableTypeName}'`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return _;
|
|
241
|
+
},
|
|
242
|
+
// This hook adds 'aggregates' under a "backwards" relation, siblings of
|
|
243
|
+
// every, some, none.
|
|
244
|
+
// See https://github.com/graphile-contrib/postgraphile-plugin-connection-filter/blob/6223cdb1d2ac5723aecdf55f735a18f8e2b98683/src/PgConnectionArgFilterBackwardRelationsPlugin.ts#L374
|
|
245
|
+
GraphQLInputObjectType_fields(inFields, build, context) {
|
|
246
|
+
let fields = inFields;
|
|
247
|
+
const { extend, inflection, sql, pgAggregateSpecs, dataplanPg: { PgCondition, pgWhereConditionSpecListToSQL }, PgAggregateCondition, EXPORTABLE } = build;
|
|
248
|
+
if (!inflection.filterType) {
|
|
249
|
+
// Filter plugin is not enabled
|
|
250
|
+
return inFields;
|
|
251
|
+
}
|
|
252
|
+
const { fieldWithHooks, scope: { foreignTable, isPgConnectionFilterMany, isPgConnectionAggregateFilter, pgResource, isPgConnectionAggregateAggregateFilter, pgConnectionAggregateFilterAggregateSpec: spec, pgTypeResource } } = context;
|
|
253
|
+
// Add 'aggregates' field to relation filters, next to `every`/`some`/`none`
|
|
254
|
+
fields = (() => {
|
|
255
|
+
if (!isPgConnectionFilterMany || !foreignTable)
|
|
256
|
+
return fields;
|
|
257
|
+
const foreignTableTypeName = inflection.tableType(foreignTable.codec);
|
|
258
|
+
const foreignTableAggregateFilterTypeName = inflection.filterType(foreignTableTypeName + 'Aggregates');
|
|
259
|
+
const fieldName = 'aggregates';
|
|
260
|
+
const AggregateType = build.getTypeByName(foreignTableAggregateFilterTypeName);
|
|
261
|
+
if (!AggregateType) {
|
|
262
|
+
return fields;
|
|
263
|
+
}
|
|
264
|
+
return build.extend(fields, {
|
|
265
|
+
[fieldName]: fieldWithHooks({
|
|
266
|
+
fieldName,
|
|
267
|
+
isPgConnectionFilterAggregatesField: true
|
|
268
|
+
}, {
|
|
269
|
+
description: `Aggregates across related \`${foreignTableTypeName}\` match the filter criteria.`,
|
|
270
|
+
type: AggregateType,
|
|
271
|
+
apply: EXPORTABLE((PgAggregateCondition, pgAggregatesApply, pgWhereConditionSpecListToSQL, sql) => function ($where, input) {
|
|
272
|
+
return pgAggregatesApply(PgAggregateCondition, pgWhereConditionSpecListToSQL, sql, $where, input);
|
|
273
|
+
}, [
|
|
274
|
+
PgAggregateCondition,
|
|
275
|
+
pgAggregatesApply,
|
|
276
|
+
pgWhereConditionSpecListToSQL,
|
|
277
|
+
sql
|
|
278
|
+
])
|
|
279
|
+
// No need to auto-apply, postgraphile-plugin-connection-filter explicitly calls fieldArgs.apply()
|
|
280
|
+
})
|
|
281
|
+
}, "Adding 'aggregates' filter field on relation");
|
|
282
|
+
})();
|
|
283
|
+
// This hook adds our various aggregates to the 'aggregates' input defined in `AggregateType` above
|
|
284
|
+
fields = (() => {
|
|
285
|
+
if (!isPgConnectionAggregateFilter ||
|
|
286
|
+
!pgResource ||
|
|
287
|
+
pgResource.parameters ||
|
|
288
|
+
!pgResource.codec.attributes) {
|
|
289
|
+
return fields;
|
|
290
|
+
}
|
|
291
|
+
const foreignTable = pgResource;
|
|
292
|
+
const foreignTableTypeName = inflection.tableType(foreignTable.codec);
|
|
293
|
+
return pgAggregateSpecs.reduce((memo, spec) => {
|
|
294
|
+
const filterTypeName = inflection.filterTableAggregateType(foreignTable, spec);
|
|
295
|
+
const fieldName = inflection.camelCase(spec.id);
|
|
296
|
+
const type = build.getTypeByName(filterTypeName);
|
|
297
|
+
if (!type) {
|
|
298
|
+
return memo;
|
|
299
|
+
}
|
|
300
|
+
return extend(memo, {
|
|
301
|
+
[fieldName]: fieldWithHooks({ fieldName }, () => ({
|
|
302
|
+
type,
|
|
303
|
+
description: `${spec.HumanLabel} aggregate over matching \`${foreignTableTypeName}\` objects.`,
|
|
304
|
+
apply: EXPORTABLE((spec) => function ($subquery, input) {
|
|
305
|
+
if (input == null)
|
|
306
|
+
return;
|
|
307
|
+
return $subquery.forAggregate(spec);
|
|
308
|
+
}, [spec])
|
|
309
|
+
}))
|
|
310
|
+
}, `Adding aggregate '${spec.id}' filter input for '${pgResource.name}'. `);
|
|
311
|
+
}, fields);
|
|
312
|
+
})();
|
|
313
|
+
// This hook adds matching attributes to the relevant aggregate types.
|
|
314
|
+
fields = (() => {
|
|
315
|
+
if (!isPgConnectionAggregateAggregateFilter ||
|
|
316
|
+
!spec ||
|
|
317
|
+
!pgTypeResource ||
|
|
318
|
+
pgTypeResource.parameters ||
|
|
319
|
+
!pgTypeResource.codec.attributes) {
|
|
320
|
+
return fields;
|
|
321
|
+
}
|
|
322
|
+
const table = pgTypeResource;
|
|
323
|
+
const attributes = table.codec.attributes;
|
|
324
|
+
fields = extend(fields, {
|
|
325
|
+
...Object.entries(attributes).reduce((memo, [attributeName, attribute]) => {
|
|
326
|
+
if (!build.behavior.pgCodecAttributeMatches([table.codec, attributeName], `${spec.id}:attribute:aggregate:filterBy`)) {
|
|
327
|
+
return memo;
|
|
328
|
+
}
|
|
329
|
+
if ((spec.shouldApplyToEntity &&
|
|
330
|
+
!spec.shouldApplyToEntity({
|
|
331
|
+
type: 'attribute',
|
|
332
|
+
codec: table.codec,
|
|
333
|
+
attributeName: attributeName
|
|
334
|
+
})) ||
|
|
335
|
+
!spec.isSuitableType(attribute.codec)) {
|
|
336
|
+
return memo;
|
|
337
|
+
}
|
|
338
|
+
const rawAttrCodec = attribute.codec;
|
|
339
|
+
const attrCodec = spec.pgTypeCodecModifier
|
|
340
|
+
? spec.pgTypeCodecModifier(attribute.codec)
|
|
341
|
+
: attribute.codec;
|
|
342
|
+
const fieldName = inflection.attribute({
|
|
343
|
+
codec: table.codec,
|
|
344
|
+
attributeName
|
|
345
|
+
});
|
|
346
|
+
const digest = build.connectionFilterOperatorsDigest(attrCodec);
|
|
347
|
+
if (!digest) {
|
|
348
|
+
return memo;
|
|
349
|
+
}
|
|
350
|
+
const OperatorsType = build.getTypeByName(digest.operatorsTypeName);
|
|
351
|
+
if (!OperatorsType) {
|
|
352
|
+
return memo;
|
|
353
|
+
}
|
|
354
|
+
return build.extend(memo, {
|
|
355
|
+
[fieldName]: {
|
|
356
|
+
type: OperatorsType,
|
|
357
|
+
apply: EXPORTABLE((PgCondition, attrCodec, attributeName, pgAggregateApplyAttributeOrder, rawAttrCodec, spec, sql) => function ($parent, input) {
|
|
358
|
+
return pgAggregateApplyAttributeOrder(PgCondition, sql, spec, attributeName, attrCodec, rawAttrCodec, $parent, input);
|
|
359
|
+
}, [
|
|
360
|
+
PgCondition,
|
|
361
|
+
attrCodec,
|
|
362
|
+
attributeName,
|
|
363
|
+
pgAggregateApplyAttributeOrder,
|
|
364
|
+
rawAttrCodec,
|
|
365
|
+
spec,
|
|
366
|
+
sql
|
|
367
|
+
])
|
|
368
|
+
// No need to auto-apply since we're called via `fieldArgs.apply($subquery.forAggregate(spec))` above
|
|
369
|
+
}
|
|
370
|
+
}, `Add aggregate '${attributeName}' filter for source '${table.name}' for spec '${spec.id}'`);
|
|
371
|
+
}, Object.create(null))
|
|
372
|
+
}, `Adding per-attribute '${spec.id}' aggregate filters for '${pgTypeResource.name}'`);
|
|
373
|
+
const computedAttributeResources = Object.values(build.input.pgRegistry.pgResources).filter((s) => isComputedScalarAttributeResource(s) &&
|
|
374
|
+
s.parameters[0].codec === table.codec &&
|
|
375
|
+
s.parameters.slice(1).every((p) => p.optional));
|
|
376
|
+
fields = extend(fields, {
|
|
377
|
+
...computedAttributeResources.reduce((memo, proc) => {
|
|
378
|
+
if ((spec.shouldApplyToEntity &&
|
|
379
|
+
!spec.shouldApplyToEntity({
|
|
380
|
+
type: 'computedAttribute',
|
|
381
|
+
resource: proc
|
|
382
|
+
})) ||
|
|
383
|
+
!spec.isSuitableType(proc.codec)) {
|
|
384
|
+
return memo;
|
|
385
|
+
}
|
|
386
|
+
const fieldName = inflection.computedAttributeField({
|
|
387
|
+
resource: proc
|
|
388
|
+
});
|
|
389
|
+
const attrCodec = spec.pgTypeCodecModifier
|
|
390
|
+
? spec.pgTypeCodecModifier(proc.codec)
|
|
391
|
+
: proc.codec;
|
|
392
|
+
const digest = build.connectionFilterOperatorsDigest(attrCodec);
|
|
393
|
+
if (!digest) {
|
|
394
|
+
return memo;
|
|
395
|
+
}
|
|
396
|
+
const OperatorsType = build.getTypeByName(digest.operatorsTypeName);
|
|
397
|
+
if (!OperatorsType) {
|
|
398
|
+
return memo;
|
|
399
|
+
}
|
|
400
|
+
return build.extend(memo, {
|
|
401
|
+
[fieldName]: {
|
|
402
|
+
type: OperatorsType,
|
|
403
|
+
apply: EXPORTABLE((PgCondition, attrCodec, pgAggregateApplyComputedAttributeOrder, proc, spec, sql) => function apply($parent, input) {
|
|
404
|
+
return pgAggregateApplyComputedAttributeOrder(PgCondition, sql, spec, proc, attrCodec, $parent, input);
|
|
405
|
+
}, [
|
|
406
|
+
PgCondition,
|
|
407
|
+
attrCodec,
|
|
408
|
+
pgAggregateApplyComputedAttributeOrder,
|
|
409
|
+
proc,
|
|
410
|
+
spec,
|
|
411
|
+
sql
|
|
412
|
+
])
|
|
413
|
+
}
|
|
414
|
+
}, `Add computed aggregate '${fieldName}' filter for source '${table.name}' for spec '${spec.id}'`);
|
|
415
|
+
}, Object.create(null))
|
|
416
|
+
}, `Adding per-computed-column '${spec.id}' aggregate filters for '${pgTypeResource.name}'`);
|
|
417
|
+
return fields;
|
|
418
|
+
})();
|
|
419
|
+
return fields;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
export { Plugin as PgAggregatesFilterRelationalAggregatesPlugin };
|
|
425
|
+
function isComputedScalarAttributeResource(s) {
|
|
426
|
+
if (!s.parameters || s.parameters.length < 1) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
if (s.codec.attributes) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if (!s.isUnique) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
const firstParameter = s.parameters[0];
|
|
436
|
+
if (!firstParameter?.codec.attributes) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { PgCodecWithAttributes, PgRegistry, PgResource } from '@dataplan/pg';
|
|
2
|
+
import type { AggregateGroupBySpec, AggregateSpec, CORE_HAVING_FILTER_SPECS } from './interfaces';
|
|
3
|
+
declare global {
|
|
4
|
+
namespace GraphileBuild {
|
|
5
|
+
interface Inflection {
|
|
6
|
+
aggregateContainerType(this: Inflection, details: {
|
|
7
|
+
resource: PgResource<any, any, any, any, any>;
|
|
8
|
+
}): string;
|
|
9
|
+
aggregateType(this: Inflection, details: {
|
|
10
|
+
resource: PgResource<any, any, any, any, any>;
|
|
11
|
+
aggregateSpec: AggregateSpec;
|
|
12
|
+
}): string;
|
|
13
|
+
aggregatesContainerField(this: Inflection, details: {
|
|
14
|
+
resource: PgResource<any, any, any, any, any>;
|
|
15
|
+
}): string;
|
|
16
|
+
groupedAggregatesContainerField(this: Inflection, details: {
|
|
17
|
+
resource: PgResource<any, any, any, any, any>;
|
|
18
|
+
}): string;
|
|
19
|
+
aggregatesField(this: Inflection, details: {
|
|
20
|
+
aggregateSpec: AggregateSpec;
|
|
21
|
+
}): string;
|
|
22
|
+
aggregateGroupByType(this: Inflection, details: {
|
|
23
|
+
resource: PgResource<any, any, any, any, any>;
|
|
24
|
+
}): string;
|
|
25
|
+
aggregateGroupByAttributeEnum(this: Inflection, details: {
|
|
26
|
+
resource: PgResource<any, any, any, any, any>;
|
|
27
|
+
attributeName: string;
|
|
28
|
+
}): string;
|
|
29
|
+
aggregateHavingInputType(this: Inflection, details: {
|
|
30
|
+
resource: PgResource<any, any, any, any, any>;
|
|
31
|
+
}): string;
|
|
32
|
+
aggregateHavingAggregateInputType(this: Inflection, details: {
|
|
33
|
+
resource: PgResource<any, any, any, any, any>;
|
|
34
|
+
aggregateSpec: AggregateSpec;
|
|
35
|
+
}): string;
|
|
36
|
+
aggregateHavingAggregateComputedAttributeInputType(this: Inflection, details: {
|
|
37
|
+
resource: PgResource<any, any, any, any, any>;
|
|
38
|
+
aggregateSpec: AggregateSpec;
|
|
39
|
+
computedAttributeResource: PgResource<any, any, any, any, any>;
|
|
40
|
+
}): string;
|
|
41
|
+
aggregateHavingAggregateComputedAttributeArgsInputType(this: Inflection, details: {
|
|
42
|
+
resource: PgResource<any, any, any, any, any>;
|
|
43
|
+
aggregateSpec: AggregateSpec;
|
|
44
|
+
computedAttributeResource: PgResource<any, any, any, any, any>;
|
|
45
|
+
}): string;
|
|
46
|
+
aggregateGroupByAttributeDerivativeEnum(this: Inflection, details: {
|
|
47
|
+
resource: PgResource<any, any, any, any, any>;
|
|
48
|
+
attributeName: string;
|
|
49
|
+
aggregateGroupBySpec: AggregateGroupBySpec;
|
|
50
|
+
}): string;
|
|
51
|
+
orderByCountOfManyRelationByKeys(this: Inflection, details: {
|
|
52
|
+
registry: PgRegistry;
|
|
53
|
+
codec: PgCodecWithAttributes;
|
|
54
|
+
relationName: string;
|
|
55
|
+
}): string;
|
|
56
|
+
orderByAttributeAggregateOfManyRelationByKeys(this: Inflection, details: {
|
|
57
|
+
registry: PgRegistry;
|
|
58
|
+
codec: PgCodecWithAttributes;
|
|
59
|
+
relationName: string;
|
|
60
|
+
aggregateSpec: AggregateSpec;
|
|
61
|
+
attributeName: string;
|
|
62
|
+
}): string;
|
|
63
|
+
aggregateHavingFilterInputType(this: Inflection, spec: (typeof CORE_HAVING_FILTER_SPECS)[number]): string;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export declare const PgAggregatesInflectorsPlugin: GraphileConfig.Plugin;
|