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,446 @@
|
|
|
1
|
+
import { EXPORTABLE } from './EXPORTABLE';
|
|
2
|
+
import { CORE_HAVING_FILTER_SPECS } from './interfaces';
|
|
3
|
+
import { getComputedAttributeResources } from './utils';
|
|
4
|
+
const version = '1.0.0';
|
|
5
|
+
const pgAggregatesApplyAnd = EXPORTABLE(() => function apply($where) {
|
|
6
|
+
return $where;
|
|
7
|
+
}, [], 'pgAggregatesApplyAnd');
|
|
8
|
+
const pgAggregatesApplyHaving = EXPORTABLE(() => function apply($filter) {
|
|
9
|
+
return $filter;
|
|
10
|
+
}, [], 'pgAggregatesApplyHaving');
|
|
11
|
+
const pgAggregatesApplyAttributeFilter = EXPORTABLE(() => (PgBooleanFilter, aggregateSpec, attribute, attributeName, sql, $having) => {
|
|
12
|
+
const attributeExpression = sql.fragment `${$having.alias}.${sql.identifier(attributeName)}`;
|
|
13
|
+
const aggregateExpression = aggregateSpec.sqlAggregateWrap(attributeExpression, attribute.codec);
|
|
14
|
+
return new PgBooleanFilter($having, aggregateExpression);
|
|
15
|
+
}, [], 'pgAggregatesApplyAttributeFilter');
|
|
16
|
+
const pgAggregatesApplyComputedColumnFilter = EXPORTABLE(() => (PgBooleanFilter, aggregateSpec, computedAttributeResource, from, getNullableType, isInputObjectType, makeArgsRuntime, parameterAnalysis, parameters, pgFromExpressionRuntime, $having, input, { field, schema }) => {
|
|
17
|
+
if (input == null) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const fieldType = getNullableType(field.type);
|
|
21
|
+
if (!isInputObjectType(fieldType)) {
|
|
22
|
+
throw new Error(`Expected an input object type (${field.type})`);
|
|
23
|
+
}
|
|
24
|
+
const argsField = fieldType.getFields().args;
|
|
25
|
+
const firstArg = {
|
|
26
|
+
placeholder: $having.alias,
|
|
27
|
+
position: 0
|
|
28
|
+
};
|
|
29
|
+
let src;
|
|
30
|
+
if (argsField && input.args != null) {
|
|
31
|
+
const argsType = getNullableType(fieldType.getFields().args.type);
|
|
32
|
+
if (!isInputObjectType(argsType)) {
|
|
33
|
+
throw new Error(`Expected an input object type (${argsType})`);
|
|
34
|
+
}
|
|
35
|
+
const fields = argsType.getFields();
|
|
36
|
+
// Because we require that the computed attribute is
|
|
37
|
+
// evaluated inline, we have to convert it to an
|
|
38
|
+
// expression here; this is only needed because of the
|
|
39
|
+
// aggregation.
|
|
40
|
+
src = pgFromExpressionRuntime(from, parameters, [firstArg, ...makeArgsRuntime(schema, fields, input.args)], parameterAnalysis);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
src = pgFromExpressionRuntime(from, parameters, [firstArg], parameterAnalysis);
|
|
44
|
+
}
|
|
45
|
+
const aggregateExpression = aggregateSpec.sqlAggregateWrap(src, computedAttributeResource.codec);
|
|
46
|
+
const $filter = new PgBooleanFilter($having, aggregateExpression);
|
|
47
|
+
// fieldArgs.apply($filter, "filter");
|
|
48
|
+
return $filter;
|
|
49
|
+
}, [], 'pgAggregatesApplyComputedColumnFilter');
|
|
50
|
+
const pgAggregatesPlanAggregatesField = EXPORTABLE(() => function apply($having) {
|
|
51
|
+
return $having;
|
|
52
|
+
}, [], 'pgAggregatesPlanAggregatesField');
|
|
53
|
+
const pgAggregatesApplyHavingBinaryOperation = EXPORTABLE(() => (codec, infix, sql, sqlValueWithCodec, $booleanFilter, input) => {
|
|
54
|
+
if (input == null)
|
|
55
|
+
return;
|
|
56
|
+
$booleanFilter.having(sql `(${sql.parens($booleanFilter.expression)} ${infix()} ${sqlValueWithCodec(input, codec)})`);
|
|
57
|
+
}, [], 'pgAggregatesApplyHavingBinaryOperation');
|
|
58
|
+
const Plugin = {
|
|
59
|
+
name: 'PgAggregatesAddHavingAggregateTypesPlugin',
|
|
60
|
+
description: `\
|
|
61
|
+
Creates the types for the 'having' clause in groupedAggregates, the fields \
|
|
62
|
+
below that for each aggregate spec, and adds fields for attributes and computed \
|
|
63
|
+
columns.`,
|
|
64
|
+
version,
|
|
65
|
+
provides: ['aggregates'],
|
|
66
|
+
after: ['PgBasicsPlugin'],
|
|
67
|
+
schema: {
|
|
68
|
+
behaviorRegistry: {
|
|
69
|
+
add: {
|
|
70
|
+
having: {
|
|
71
|
+
description: "Should we add a 'having' option to aggregates on this resource?",
|
|
72
|
+
entities: ['pgResource']
|
|
73
|
+
},
|
|
74
|
+
havingBy: {
|
|
75
|
+
description: "Should this function-like resource, or attribute, be usable in the 'having' clause when aggregating?",
|
|
76
|
+
entities: ['pgResource', 'pgCodecAttribute']
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
entityBehavior: {
|
|
81
|
+
// having - for connections' groupedAggregates field
|
|
82
|
+
// havingBy - for computed column functions acting like attributes
|
|
83
|
+
pgResource: ['order', 'having', 'havingBy'],
|
|
84
|
+
pgCodecAttribute: ['havingBy']
|
|
85
|
+
},
|
|
86
|
+
hooks: {
|
|
87
|
+
build(build) {
|
|
88
|
+
if (!build.dataplanPg) {
|
|
89
|
+
throw new Error(`PgBasicsPlugin must be loaded first`);
|
|
90
|
+
}
|
|
91
|
+
const { dataplanPg: { TYPES } } = build;
|
|
92
|
+
return build.extend(build, {
|
|
93
|
+
pgHavingFilterTypeNameForCodec(codec) {
|
|
94
|
+
switch (codec) {
|
|
95
|
+
case TYPES.int2:
|
|
96
|
+
case TYPES.int: {
|
|
97
|
+
return build.inflection.aggregateHavingFilterInputType('int');
|
|
98
|
+
}
|
|
99
|
+
case TYPES.bigint: {
|
|
100
|
+
return build.inflection.aggregateHavingFilterInputType('bigint');
|
|
101
|
+
}
|
|
102
|
+
case TYPES.float4:
|
|
103
|
+
case TYPES.float: {
|
|
104
|
+
return build.inflection.aggregateHavingFilterInputType('float');
|
|
105
|
+
}
|
|
106
|
+
case TYPES.money:
|
|
107
|
+
case TYPES.numeric: {
|
|
108
|
+
return build.inflection.aggregateHavingFilterInputType('bigfloat');
|
|
109
|
+
}
|
|
110
|
+
case TYPES.date:
|
|
111
|
+
case TYPES.timestamp:
|
|
112
|
+
case TYPES.timestamptz: {
|
|
113
|
+
return build.inflection.aggregateHavingFilterInputType('datetime');
|
|
114
|
+
}
|
|
115
|
+
case TYPES.char:
|
|
116
|
+
case TYPES.text:
|
|
117
|
+
case TYPES.varchar: {
|
|
118
|
+
return build.inflection.aggregateHavingFilterInputType('string');
|
|
119
|
+
}
|
|
120
|
+
default: {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}, 'adding pgHavingFilterTypeForTypeAndModifier');
|
|
126
|
+
},
|
|
127
|
+
init(_, build, _context) {
|
|
128
|
+
const { graphql: { GraphQLList, GraphQLNonNull, getNullableType, isInputObjectType }, inflection, sql, dataplanPg: { PgOrFilter, PgBooleanFilter, pgFromExpressionRuntime, generatePgParameterAnalysis }, EXPORTABLE } = build;
|
|
129
|
+
for (const spec of CORE_HAVING_FILTER_SPECS) {
|
|
130
|
+
const name = build.inflection.aggregateHavingFilterInputType(spec);
|
|
131
|
+
build.registerInputObjectType(name, {
|
|
132
|
+
isPgHavingFilterInputType: true,
|
|
133
|
+
pgHavingFilterSpec: spec
|
|
134
|
+
}, () => ({
|
|
135
|
+
name,
|
|
136
|
+
fields: Object.create(null)
|
|
137
|
+
}), '');
|
|
138
|
+
}
|
|
139
|
+
for (const resource of Object.values(build.input.pgRegistry.pgResources)) {
|
|
140
|
+
if (resource.parameters ||
|
|
141
|
+
!resource.codec.attributes ||
|
|
142
|
+
resource.isUnique) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!build.behavior.pgResourceMatches(resource, 'order')) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!build.behavior.pgResourceMatches(resource, 'resource:groupedAggregates:having')) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const tableTypeName = inflection.tableType(resource.codec);
|
|
152
|
+
const tableHavingInputTypeName = inflection.aggregateHavingInputType({
|
|
153
|
+
resource: resource
|
|
154
|
+
});
|
|
155
|
+
build.registerInputObjectType(tableHavingInputTypeName, {
|
|
156
|
+
pgTypeResource: resource,
|
|
157
|
+
isPgAggregateHavingInputType: true
|
|
158
|
+
}, () => ({
|
|
159
|
+
description: build.wrapDescription(`Conditions for \`${tableTypeName}\` aggregates.`, 'type'),
|
|
160
|
+
fields: () => {
|
|
161
|
+
return {
|
|
162
|
+
AND: {
|
|
163
|
+
type: new GraphQLList(new GraphQLNonNull(build.getInputTypeByName(tableHavingInputTypeName))),
|
|
164
|
+
apply: pgAggregatesApplyAnd
|
|
165
|
+
// No need to auto-apply, the having field calls `fieldArgs.apply(...)`
|
|
166
|
+
},
|
|
167
|
+
OR: {
|
|
168
|
+
type: new GraphQLList(new GraphQLNonNull(build.getInputTypeByName(tableHavingInputTypeName))),
|
|
169
|
+
apply: EXPORTABLE((PgOrFilter) => ($where) => new PgOrFilter($where), [PgOrFilter])
|
|
170
|
+
// No need to auto-apply, the having field calls `fieldArgs.apply(...)`
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}), `Adding connection "groupBy" having input type for ${resource.name}.`);
|
|
175
|
+
const computedAttributeResources = getComputedAttributeResources(build, resource);
|
|
176
|
+
for (const aggregateSpec of build.pgAggregateSpecs) {
|
|
177
|
+
for (const computedAttributeResource of computedAttributeResources) {
|
|
178
|
+
const argsTypeName = inflection.aggregateHavingAggregateComputedAttributeArgsInputType({
|
|
179
|
+
resource: resource,
|
|
180
|
+
aggregateSpec,
|
|
181
|
+
computedAttributeResource: computedAttributeResource
|
|
182
|
+
});
|
|
183
|
+
build.registerInputObjectType(argsTypeName, {}, () => {
|
|
184
|
+
return {
|
|
185
|
+
fields: () => {
|
|
186
|
+
const { argDetails } = build.pgGetArgDetailsFromParameters(computedAttributeResource, computedAttributeResource.parameters, 1);
|
|
187
|
+
return argDetails.reduce((memo, { inputType, graphqlArgName }) => {
|
|
188
|
+
memo[graphqlArgName] = {
|
|
189
|
+
type: inputType
|
|
190
|
+
// NO PLAN NEEDED!
|
|
191
|
+
};
|
|
192
|
+
return memo;
|
|
193
|
+
}, Object.create(null));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}, '');
|
|
197
|
+
const computedHavingInputName = inflection.aggregateHavingAggregateComputedAttributeInputType({
|
|
198
|
+
resource: resource,
|
|
199
|
+
aggregateSpec,
|
|
200
|
+
computedAttributeResource: computedAttributeResource
|
|
201
|
+
});
|
|
202
|
+
/*const ComputedHavingInput =*/
|
|
203
|
+
build.registerInputObjectType(computedHavingInputName, {}, () => {
|
|
204
|
+
return {
|
|
205
|
+
fields: () => {
|
|
206
|
+
const havingFilterTypeName = build.pgHavingFilterTypeNameForCodec(computedAttributeResource.codec);
|
|
207
|
+
const HavingFilterType = havingFilterTypeName
|
|
208
|
+
? build.getTypeByName(havingFilterTypeName)
|
|
209
|
+
: undefined;
|
|
210
|
+
const ArgsType = build.getTypeByName(argsTypeName);
|
|
211
|
+
if (!HavingFilterType || !ArgsType) {
|
|
212
|
+
return { fields: Object.create(null) };
|
|
213
|
+
}
|
|
214
|
+
const requiresAtLeastOneArg = computedAttributeResource.parameters
|
|
215
|
+
.slice(1)
|
|
216
|
+
.some((p) => !p.optional);
|
|
217
|
+
return {
|
|
218
|
+
...(ArgsType
|
|
219
|
+
? {
|
|
220
|
+
args: {
|
|
221
|
+
type: build.nullableIf(!requiresAtLeastOneArg, ArgsType)
|
|
222
|
+
// NO PLAN NEEDED
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
: null),
|
|
226
|
+
filter: {
|
|
227
|
+
type: new GraphQLNonNull(HavingFilterType),
|
|
228
|
+
apply: pgAggregatesApplyHaving
|
|
229
|
+
// No need to auto-apply, parent calls `fieldArgs.apply($filter, "filter")` below
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}, '');
|
|
235
|
+
}
|
|
236
|
+
const typeName = inflection.aggregateHavingAggregateInputType({
|
|
237
|
+
resource: resource,
|
|
238
|
+
aggregateSpec
|
|
239
|
+
});
|
|
240
|
+
build.registerInputObjectType(typeName, {}, () => ({
|
|
241
|
+
name: typeName,
|
|
242
|
+
fields: ({ fieldWithHooks }) => {
|
|
243
|
+
let fields = Object.create(null);
|
|
244
|
+
fields = build.extend(fields, Object.entries(resource.codec.attributes).reduce((newFields, [attributeName, attribute]) => {
|
|
245
|
+
if (!build.behavior.pgCodecAttributeMatches([resource.codec, attributeName], 'attribute:havingBy')) {
|
|
246
|
+
return newFields;
|
|
247
|
+
}
|
|
248
|
+
const fieldName = inflection.attribute({
|
|
249
|
+
codec: resource.codec,
|
|
250
|
+
attributeName: attributeName
|
|
251
|
+
});
|
|
252
|
+
const havingFilterTypeName = build.pgHavingFilterTypeNameForCodec(attribute.codec);
|
|
253
|
+
const HavingFilterType = havingFilterTypeName
|
|
254
|
+
? build.getTypeByName(havingFilterTypeName)
|
|
255
|
+
: undefined;
|
|
256
|
+
if (!HavingFilterType) {
|
|
257
|
+
return newFields;
|
|
258
|
+
}
|
|
259
|
+
const newField = fieldWithHooks({ fieldName }, () => ({
|
|
260
|
+
type: HavingFilterType,
|
|
261
|
+
apply: EXPORTABLE((PgBooleanFilter, aggregateSpec, attribute, attributeName, pgAggregatesApplyAttributeFilter, sql) => ($having) => {
|
|
262
|
+
return pgAggregatesApplyAttributeFilter(PgBooleanFilter, aggregateSpec, attribute, attributeName, sql, $having);
|
|
263
|
+
}, [
|
|
264
|
+
PgBooleanFilter,
|
|
265
|
+
aggregateSpec,
|
|
266
|
+
attribute,
|
|
267
|
+
attributeName,
|
|
268
|
+
pgAggregatesApplyAttributeFilter,
|
|
269
|
+
sql
|
|
270
|
+
])
|
|
271
|
+
}));
|
|
272
|
+
return build.extend(newFields, { [fieldName]: newField }, `Adding attribute '${attributeName}' to having filter type for '${resource.name}'`);
|
|
273
|
+
}, Object.create(null)), '');
|
|
274
|
+
fields = build.extend(fields, computedAttributeResources.reduce((memo, computedAttributeResource) => {
|
|
275
|
+
if (!build.behavior.pgResourceMatches(computedAttributeResource, 'resource:havingBy')) {
|
|
276
|
+
return memo;
|
|
277
|
+
}
|
|
278
|
+
const codec = computedAttributeResource.codec;
|
|
279
|
+
if ((aggregateSpec.shouldApplyToEntity &&
|
|
280
|
+
!aggregateSpec.shouldApplyToEntity({
|
|
281
|
+
type: 'computedAttribute',
|
|
282
|
+
resource: computedAttributeResource
|
|
283
|
+
})) ||
|
|
284
|
+
!aggregateSpec.isSuitableType(codec)) {
|
|
285
|
+
return memo;
|
|
286
|
+
}
|
|
287
|
+
const argsTypeName = inflection.aggregateHavingAggregateComputedAttributeArgsInputType({
|
|
288
|
+
resource: resource,
|
|
289
|
+
aggregateSpec,
|
|
290
|
+
computedAttributeResource: computedAttributeResource
|
|
291
|
+
});
|
|
292
|
+
const havingFilterTypeName = build.pgHavingFilterTypeNameForCodec(computedAttributeResource.codec);
|
|
293
|
+
const HavingFilterType = havingFilterTypeName
|
|
294
|
+
? build.getTypeByName(havingFilterTypeName)
|
|
295
|
+
: undefined;
|
|
296
|
+
const ArgsType = build.getTypeByName(argsTypeName);
|
|
297
|
+
const computedHavingInputName = inflection.aggregateHavingAggregateComputedAttributeInputType({
|
|
298
|
+
resource: resource,
|
|
299
|
+
aggregateSpec,
|
|
300
|
+
computedAttributeResource: computedAttributeResource
|
|
301
|
+
});
|
|
302
|
+
const ComputedHavingInput = build.getTypeByName(computedHavingInputName);
|
|
303
|
+
if (!HavingFilterType ||
|
|
304
|
+
!ArgsType ||
|
|
305
|
+
!ComputedHavingInput) {
|
|
306
|
+
return memo;
|
|
307
|
+
}
|
|
308
|
+
const fieldName = inflection.computedAttributeField({
|
|
309
|
+
resource: computedAttributeResource
|
|
310
|
+
});
|
|
311
|
+
const { from: rawFrom, parameters } = computedAttributeResource;
|
|
312
|
+
const { makeArgsRuntime } = build.pgGetArgDetailsFromParameters(computedAttributeResource, parameters, 1);
|
|
313
|
+
const parameterAnalysis = EXPORTABLE((generatePgParameterAnalysis, parameters) => generatePgParameterAnalysis(parameters), [generatePgParameterAnalysis, parameters], `${computedAttributeResource.name}ParameterAnalysis`);
|
|
314
|
+
const from = typeof rawFrom === 'function'
|
|
315
|
+
? rawFrom
|
|
316
|
+
: () => rawFrom;
|
|
317
|
+
const newField = fieldWithHooks({ fieldName }, {
|
|
318
|
+
type: ComputedHavingInput,
|
|
319
|
+
apply: EXPORTABLE((PgBooleanFilter, aggregateSpec, computedAttributeResource, from, getNullableType, isInputObjectType, makeArgsRuntime, parameterAnalysis, parameters, pgAggregatesApplyComputedColumnFilter, pgFromExpressionRuntime) => ($having, input, info) => {
|
|
320
|
+
return pgAggregatesApplyComputedColumnFilter(PgBooleanFilter, aggregateSpec, computedAttributeResource, from, getNullableType, isInputObjectType, makeArgsRuntime, parameterAnalysis, parameters, pgFromExpressionRuntime, $having, input, info);
|
|
321
|
+
}, [
|
|
322
|
+
PgBooleanFilter,
|
|
323
|
+
aggregateSpec,
|
|
324
|
+
computedAttributeResource,
|
|
325
|
+
from,
|
|
326
|
+
getNullableType,
|
|
327
|
+
isInputObjectType,
|
|
328
|
+
makeArgsRuntime,
|
|
329
|
+
parameterAnalysis,
|
|
330
|
+
parameters,
|
|
331
|
+
pgAggregatesApplyComputedColumnFilter,
|
|
332
|
+
pgFromExpressionRuntime
|
|
333
|
+
])
|
|
334
|
+
// No need to auto-apply, parent does `return $having;`
|
|
335
|
+
});
|
|
336
|
+
return build.extend(memo, { [fieldName]: newField }, `Adding computed attribute function '${computedAttributeResource.name}' to having filter type for '${resource.name}'`);
|
|
337
|
+
}, Object.create(null)), '');
|
|
338
|
+
return fields;
|
|
339
|
+
}
|
|
340
|
+
}), '');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return _;
|
|
344
|
+
},
|
|
345
|
+
GraphQLInputObjectType_fields(inFields, build, context) {
|
|
346
|
+
const { sql, inflection, dataplanPg: { TYPES, sqlValueWithCodec }, EXPORTABLE } = build;
|
|
347
|
+
const { scope: { isPgHavingFilterInputType, pgHavingFilterSpec, isPgAggregateHavingInputType, pgTypeResource: table }, fieldWithHooks } = context;
|
|
348
|
+
let fields = inFields;
|
|
349
|
+
fields = (() => {
|
|
350
|
+
if (!isPgAggregateHavingInputType ||
|
|
351
|
+
!table ||
|
|
352
|
+
table.parameters ||
|
|
353
|
+
!table.codec.attributes) {
|
|
354
|
+
return fields;
|
|
355
|
+
}
|
|
356
|
+
return build.extend(fields, build.pgAggregateSpecs.reduce((aggregateFields, aggregateSpec) => {
|
|
357
|
+
if (!build.behavior.pgResourceMatches(table, `${aggregateSpec.id}:resource:groupedAggregates:having`)) {
|
|
358
|
+
return aggregateFields;
|
|
359
|
+
}
|
|
360
|
+
const typeName = inflection.aggregateHavingAggregateInputType({
|
|
361
|
+
resource: table,
|
|
362
|
+
aggregateSpec
|
|
363
|
+
});
|
|
364
|
+
const SpecInput = build.getTypeByName(typeName);
|
|
365
|
+
if (!SpecInput) {
|
|
366
|
+
return aggregateFields;
|
|
367
|
+
}
|
|
368
|
+
const fieldName = inflection.aggregatesField({ aggregateSpec });
|
|
369
|
+
return build.extend(aggregateFields, {
|
|
370
|
+
[fieldName]: fieldWithHooks({ fieldName }, // e.g. 'average' or 'stddevPopulation'
|
|
371
|
+
{
|
|
372
|
+
type: SpecInput,
|
|
373
|
+
apply: pgAggregatesPlanAggregatesField
|
|
374
|
+
// No need to auto-apply, `filter` field does `return new PgBooleanFilter($having, aggregateExpression)`
|
|
375
|
+
})
|
|
376
|
+
}, '');
|
|
377
|
+
}, Object.create(null)), `Adding attributes to having filter for '${table.name}'`);
|
|
378
|
+
})();
|
|
379
|
+
fields = (() => {
|
|
380
|
+
if (!isPgHavingFilterInputType) {
|
|
381
|
+
return fields;
|
|
382
|
+
}
|
|
383
|
+
const rawCodec = (() => {
|
|
384
|
+
switch (pgHavingFilterSpec) {
|
|
385
|
+
case 'int':
|
|
386
|
+
return TYPES.int;
|
|
387
|
+
case 'bigint':
|
|
388
|
+
return TYPES.bigint;
|
|
389
|
+
case 'float':
|
|
390
|
+
return TYPES.float;
|
|
391
|
+
case 'bigfloat':
|
|
392
|
+
return TYPES.numeric;
|
|
393
|
+
case 'datetime':
|
|
394
|
+
return TYPES.timestamptz;
|
|
395
|
+
default: {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
})();
|
|
400
|
+
if (rawCodec === null) {
|
|
401
|
+
return fields;
|
|
402
|
+
}
|
|
403
|
+
const codec = rawCodec;
|
|
404
|
+
const FieldType = build.getGraphQLTypeByPgCodec(codec, 'input');
|
|
405
|
+
if (FieldType == null) {
|
|
406
|
+
return fields;
|
|
407
|
+
}
|
|
408
|
+
function addBinaryOp(fieldName, infix) {
|
|
409
|
+
fields = build.extend(fields, {
|
|
410
|
+
[fieldName]: fieldWithHooks({ fieldName }, {
|
|
411
|
+
type: FieldType,
|
|
412
|
+
apply: EXPORTABLE((codec, infix, pgAggregatesApplyHavingBinaryOperation, sql, sqlValueWithCodec) => ($booleanFilter, input) => {
|
|
413
|
+
return pgAggregatesApplyHavingBinaryOperation(codec, infix, sql, sqlValueWithCodec, $booleanFilter, input);
|
|
414
|
+
}, [
|
|
415
|
+
codec,
|
|
416
|
+
infix,
|
|
417
|
+
pgAggregatesApplyHavingBinaryOperation,
|
|
418
|
+
sql,
|
|
419
|
+
sqlValueWithCodec
|
|
420
|
+
])
|
|
421
|
+
// No need to auto-apply
|
|
422
|
+
})
|
|
423
|
+
}, '');
|
|
424
|
+
}
|
|
425
|
+
switch (pgHavingFilterSpec) {
|
|
426
|
+
case 'int':
|
|
427
|
+
case 'bigint':
|
|
428
|
+
case 'float':
|
|
429
|
+
case 'bigfloat':
|
|
430
|
+
case 'datetime': {
|
|
431
|
+
addBinaryOp('equalTo', EXPORTABLE((sql) => () => sql.fragment `=`, [sql]));
|
|
432
|
+
addBinaryOp('notEqualTo', EXPORTABLE((sql) => () => sql.fragment `<>`, [sql]));
|
|
433
|
+
addBinaryOp('greaterThan', EXPORTABLE((sql) => () => sql.fragment `>`, [sql]));
|
|
434
|
+
addBinaryOp('greaterThanOrEqualTo', EXPORTABLE((sql) => () => sql.fragment `>=`, [sql]));
|
|
435
|
+
addBinaryOp('lessThan', EXPORTABLE((sql) => () => sql.fragment `<`, [sql]));
|
|
436
|
+
addBinaryOp('lessThanOrEqualTo', EXPORTABLE((sql) => () => sql.fragment `<=`, [sql]));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return fields;
|
|
440
|
+
})();
|
|
441
|
+
return fields;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
export { Plugin as PgAggregatesAddHavingAggregateTypesPlugin };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PgAggregatesSpecsPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { EXPORTABLE } from './EXPORTABLE';
|
|
2
|
+
import { BIGINT_OID, FLOAT4_OID, FLOAT8_OID, INT2_OID, INT4_OID, INTERVAL_OID, MONEY_OID, NUMERIC_OID } from './interfaces';
|
|
3
|
+
const version = '1.0.0';
|
|
4
|
+
const isNumberLike = (codec) => !!codec.extensions?.isNumberLike;
|
|
5
|
+
const isIntervalLike = (codec) => !!codec.extensions?.isIntervalLike;
|
|
6
|
+
const isIntervalLikeOrNumberLike = EXPORTABLE((isIntervalLike, isNumberLike) => (codec) => isIntervalLike(codec) || isNumberLike(codec), [isIntervalLike, isNumberLike]);
|
|
7
|
+
export const PgAggregatesSpecsPlugin = {
|
|
8
|
+
name: 'PgAggregatesSpecsPlugin',
|
|
9
|
+
description: 'Created the default (extensible) aggregate specs and group-by specs used throughout this preset.',
|
|
10
|
+
version,
|
|
11
|
+
provides: ['aggregates'],
|
|
12
|
+
after: ['PgBasicsPlugin'],
|
|
13
|
+
gather: {
|
|
14
|
+
hooks: {
|
|
15
|
+
pgCodecs_PgCodec(_info, event) {
|
|
16
|
+
const { pgType, pgCodec } = event;
|
|
17
|
+
const isReg = pgType.getNamespace()?.nspname === 'pg_catalog' &&
|
|
18
|
+
pgType.typname.startsWith('reg');
|
|
19
|
+
const isCatN = !isReg && pgType.typcategory === 'N';
|
|
20
|
+
const isInterval = !isReg && pgType._id === INTERVAL_OID;
|
|
21
|
+
if (isCatN || isInterval) {
|
|
22
|
+
if (!pgCodec.extensions) {
|
|
23
|
+
pgCodec.extensions = Object.create(null);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (isCatN) {
|
|
27
|
+
pgCodec.extensions.isNumberLike = true;
|
|
28
|
+
}
|
|
29
|
+
if (isInterval) {
|
|
30
|
+
pgCodec.extensions.isIntervalLike = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
schema: {
|
|
36
|
+
hooks: {
|
|
37
|
+
build(build) {
|
|
38
|
+
if (!build.dataplanPg || !build.sql) {
|
|
39
|
+
throw new Error(`PgBasicsPlugin must be loaded first`);
|
|
40
|
+
}
|
|
41
|
+
const { sql, dataplanPg: { TYPES }, EXPORTABLE } = build;
|
|
42
|
+
/** Maps from the data type of the attribute to the data type of the sum aggregate */
|
|
43
|
+
/** BigFloat is our fallback type; it should be valid for almost all numeric types */
|
|
44
|
+
const convertWithMapAndFallback = (dataTypeToAggregateTypeMap, fallback) => {
|
|
45
|
+
return EXPORTABLE((dataTypeToAggregateTypeMap, fallback) => (codec) => {
|
|
46
|
+
const oid = codec.extensions?.oid;
|
|
47
|
+
const targetType = (oid ? dataTypeToAggregateTypeMap[oid] : null) ?? fallback;
|
|
48
|
+
return targetType;
|
|
49
|
+
}, [dataTypeToAggregateTypeMap, fallback]);
|
|
50
|
+
};
|
|
51
|
+
const pgAggregateSpecs = [
|
|
52
|
+
{
|
|
53
|
+
id: 'sum',
|
|
54
|
+
humanLabel: 'sum',
|
|
55
|
+
HumanLabel: 'Sum',
|
|
56
|
+
isSuitableType: isIntervalLikeOrNumberLike,
|
|
57
|
+
// I've wrapped it in `coalesce` so that it cannot be null
|
|
58
|
+
sqlAggregateWrap: EXPORTABLE((sql) => (sqlFrag) => sql `coalesce(sum(${sqlFrag}), '0')`, [sql]),
|
|
59
|
+
isNonNull: true,
|
|
60
|
+
// A SUM(...) often ends up significantly larger than any individual
|
|
61
|
+
// value; see
|
|
62
|
+
// https://www.postgresql.org/docs/current/functions-aggregate.html for
|
|
63
|
+
// how the sum aggregate changes result type.
|
|
64
|
+
pgTypeCodecModifier: convertWithMapAndFallback({
|
|
65
|
+
// TODO: this should use codecs rather than OIDs
|
|
66
|
+
[INT2_OID]: TYPES.bigint, // smallint -> bigint
|
|
67
|
+
[INT4_OID]: TYPES.bigint, // integer -> bigint
|
|
68
|
+
[BIGINT_OID]: TYPES.numeric, // bigint -> numeric
|
|
69
|
+
[FLOAT4_OID]: TYPES.float4, // real -> real
|
|
70
|
+
[FLOAT8_OID]: TYPES.float, // double precision -> double precision
|
|
71
|
+
[INTERVAL_OID]: TYPES.interval, // interval -> interval
|
|
72
|
+
[MONEY_OID]: TYPES.money // money -> money
|
|
73
|
+
}, TYPES.numeric /* numeric */)
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'distinctCount',
|
|
77
|
+
humanLabel: 'distinct count',
|
|
78
|
+
HumanLabel: 'Distinct count',
|
|
79
|
+
isSuitableType: () => true,
|
|
80
|
+
sqlAggregateWrap: EXPORTABLE((sql) => (sqlFrag) => sql `count(distinct ${sqlFrag})`, [sql]),
|
|
81
|
+
pgTypeCodecModifier: convertWithMapAndFallback({}, TYPES.bigint /* always use bigint */)
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'min',
|
|
85
|
+
humanLabel: 'minimum',
|
|
86
|
+
HumanLabel: 'Minimum',
|
|
87
|
+
isSuitableType: isIntervalLikeOrNumberLike,
|
|
88
|
+
sqlAggregateWrap: EXPORTABLE((sql) => (sqlFrag) => sql `min(${sqlFrag})`, [sql])
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'max',
|
|
92
|
+
humanLabel: 'maximum',
|
|
93
|
+
HumanLabel: 'Maximum',
|
|
94
|
+
isSuitableType: isIntervalLikeOrNumberLike,
|
|
95
|
+
sqlAggregateWrap: EXPORTABLE((sql) => (sqlFrag) => sql `max(${sqlFrag})`, [sql])
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'average',
|
|
99
|
+
humanLabel: 'mean average',
|
|
100
|
+
HumanLabel: 'Mean average',
|
|
101
|
+
isSuitableType: isIntervalLikeOrNumberLike,
|
|
102
|
+
sqlAggregateWrap: EXPORTABLE((sql) => (sqlFrag) => sql `avg(${sqlFrag})`, [sql]),
|
|
103
|
+
// An AVG(...) ends up more precise than any individual value; see
|
|
104
|
+
// https://www.postgresql.org/docs/current/functions-aggregate.html for
|
|
105
|
+
// how the avg aggregate changes result type.
|
|
106
|
+
pgTypeCodecModifier: convertWithMapAndFallback({
|
|
107
|
+
[INT2_OID]: TYPES.numeric, // smallint -> numeric
|
|
108
|
+
[INT4_OID]: TYPES.numeric, // integer -> numeric
|
|
109
|
+
[BIGINT_OID]: TYPES.numeric, // bigint -> numeric
|
|
110
|
+
[NUMERIC_OID]: TYPES.numeric, // numeric -> numeric
|
|
111
|
+
[FLOAT4_OID]: TYPES.float, // real -> double precision
|
|
112
|
+
[FLOAT8_OID]: TYPES.float, // double precision -> double precision
|
|
113
|
+
[INTERVAL_OID]: TYPES.interval // interval -> interval
|
|
114
|
+
}, TYPES.numeric /* numeric */)
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'stddevSample',
|
|
118
|
+
humanLabel: 'sample standard deviation',
|
|
119
|
+
HumanLabel: 'Sample standard deviation',
|
|
120
|
+
isSuitableType: isNumberLike,
|
|
121
|
+
sqlAggregateWrap: EXPORTABLE((sql) => (sqlFrag) => sql `stddev_samp(${sqlFrag})`, [sql]),
|
|
122
|
+
// See https://www.postgresql.org/docs/current/functions-aggregate.html
|
|
123
|
+
// for how this aggregate changes result type.
|
|
124
|
+
pgTypeCodecModifier: convertWithMapAndFallback({
|
|
125
|
+
[FLOAT4_OID]: TYPES.float, // real -> double precision
|
|
126
|
+
[FLOAT8_OID]: TYPES.float // double precision -> double precision
|
|
127
|
+
}, TYPES.numeric /* numeric */)
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'stddevPopulation',
|
|
131
|
+
humanLabel: 'population standard deviation',
|
|
132
|
+
HumanLabel: 'Population standard deviation',
|
|
133
|
+
isSuitableType: isNumberLike,
|
|
134
|
+
sqlAggregateWrap: EXPORTABLE((sql) => (sqlFrag) => sql `stddev_pop(${sqlFrag})`, [sql]),
|
|
135
|
+
// See https://www.postgresql.org/docs/current/functions-aggregate.html
|
|
136
|
+
// for how this aggregate changes result type.
|
|
137
|
+
pgTypeCodecModifier: convertWithMapAndFallback({
|
|
138
|
+
[FLOAT4_OID]: TYPES.float, // real -> double precision
|
|
139
|
+
[FLOAT8_OID]: TYPES.float // double precision -> double precision
|
|
140
|
+
}, TYPES.numeric /* numeric */)
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'varianceSample',
|
|
144
|
+
humanLabel: 'sample variance',
|
|
145
|
+
HumanLabel: 'Sample variance',
|
|
146
|
+
isSuitableType: isNumberLike,
|
|
147
|
+
sqlAggregateWrap: EXPORTABLE((sql) => (sqlFrag) => sql `var_samp(${sqlFrag})`, [sql]),
|
|
148
|
+
// See https://www.postgresql.org/docs/current/functions-aggregate.html
|
|
149
|
+
// for how this aggregate changes result type.
|
|
150
|
+
pgTypeCodecModifier: convertWithMapAndFallback({
|
|
151
|
+
[FLOAT4_OID]: TYPES.float, // real -> double precision
|
|
152
|
+
[FLOAT8_OID]: TYPES.float // double precision -> double precision
|
|
153
|
+
}, TYPES.numeric /* numeric */)
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'variancePopulation',
|
|
157
|
+
humanLabel: 'population variance',
|
|
158
|
+
HumanLabel: 'Population variance',
|
|
159
|
+
isSuitableType: isNumberLike,
|
|
160
|
+
sqlAggregateWrap: EXPORTABLE((sql) => (sqlFrag) => sql `var_pop(${sqlFrag})`, [sql]),
|
|
161
|
+
// See https://www.postgresql.org/docs/current/functions-aggregate.html
|
|
162
|
+
// for how this aggregate changes result type.
|
|
163
|
+
pgTypeCodecModifier: convertWithMapAndFallback({
|
|
164
|
+
[FLOAT4_OID]: TYPES.float, // real -> double precision
|
|
165
|
+
[FLOAT8_OID]: TYPES.float // double precision -> double precision
|
|
166
|
+
}, TYPES.numeric /* numeric */)
|
|
167
|
+
}
|
|
168
|
+
];
|
|
169
|
+
const pgAggregateGroupBySpecs = [
|
|
170
|
+
{
|
|
171
|
+
id: 'truncated-to-hour',
|
|
172
|
+
isSuitableType: EXPORTABLE((TYPES) => (codec) => codec === TYPES.timestamp || codec === TYPES.timestamptz, [TYPES]),
|
|
173
|
+
sqlWrap: EXPORTABLE((sql) => (sqlFrag) => sql `date_trunc('hour', ${sqlFrag})`, [sql]),
|
|
174
|
+
sqlWrapCodec(codec) {
|
|
175
|
+
return codec;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'truncated-to-day',
|
|
180
|
+
isSuitableType: EXPORTABLE((TYPES) => (codec) => codec === TYPES.timestamp || codec === TYPES.timestamptz, [TYPES]),
|
|
181
|
+
sqlWrap: EXPORTABLE((sql) => (sqlFrag) => sql `date_trunc('day', ${sqlFrag})`, [sql]),
|
|
182
|
+
sqlWrapCodec(codec) {
|
|
183
|
+
return codec;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
];
|
|
187
|
+
return build.extend(build, {
|
|
188
|
+
pgAggregateSpecs,
|
|
189
|
+
pgAggregateGroupBySpecs
|
|
190
|
+
}, 'Adding aggregate specs to build');
|
|
191
|
+
},
|
|
192
|
+
finalize(schema, build) {
|
|
193
|
+
build.pgAggregateSpecs.forEach((spec) => {
|
|
194
|
+
build.exportNameHint(spec, `pgAggregateSpec_${spec.id.replace(/-/g, '_')}`);
|
|
195
|
+
});
|
|
196
|
+
build.pgAggregateGroupBySpecs.forEach((spec) => {
|
|
197
|
+
build.exportNameHint(spec, `pgAggregateGroupBySpec_${spec.id.replace(/-/g, '_')}`);
|
|
198
|
+
});
|
|
199
|
+
return schema;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|