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,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PgAggregatesOrderByAggregatesPlugin = void 0;
|
|
4
|
+
const EXPORTABLE_1 = require("./EXPORTABLE");
|
|
5
|
+
const version = '1.0.0';
|
|
6
|
+
const pgAggregatesApplyOrderByTotalCount = (0, EXPORTABLE_1.EXPORTABLE)(() => (TYPES, direction, relation, sql, table, $select) => {
|
|
7
|
+
const foreignTableAlias = $select.alias;
|
|
8
|
+
const conditions = [];
|
|
9
|
+
const tableAlias = sql.identifier(Symbol(table.name));
|
|
10
|
+
relation.localAttributes.forEach((localAttribute, i) => {
|
|
11
|
+
const remoteAttribute = relation.remoteAttributes[i];
|
|
12
|
+
conditions.push(sql.fragment `${tableAlias}.${sql.identifier(remoteAttribute)} = ${foreignTableAlias}.${sql.identifier(localAttribute)}`);
|
|
13
|
+
});
|
|
14
|
+
if (typeof table.from === 'function') {
|
|
15
|
+
throw new Error(`Function source unsupported`);
|
|
16
|
+
}
|
|
17
|
+
// TODO: refactor this to use joins instead of subqueries
|
|
18
|
+
const fragment = sql `(${sql.indent `select count(*)
|
|
19
|
+
from ${table.from} ${tableAlias}
|
|
20
|
+
where ${sql.parens(sql.join(conditions.map((c) => sql.parens(c)), ' AND '))}`})`;
|
|
21
|
+
$select.orderBy({
|
|
22
|
+
fragment,
|
|
23
|
+
codec: TYPES.bigint,
|
|
24
|
+
direction
|
|
25
|
+
});
|
|
26
|
+
}, [], 'pgAggregatesApplyOrderByTotalCount');
|
|
27
|
+
const pgAggregatesApplyOrderByAttribute = (0, EXPORTABLE_1.EXPORTABLE)(() => (aggregateSpec, attribute, attributeName, direction, relation, sql, table, $select) => {
|
|
28
|
+
const foreignTableAlias = $select.alias;
|
|
29
|
+
const conditions = [];
|
|
30
|
+
const tableAlias = sql.identifier(Symbol(table.name));
|
|
31
|
+
relation.localAttributes.forEach((localAttribute, i) => {
|
|
32
|
+
const remoteAttribute = relation.remoteAttributes[i];
|
|
33
|
+
conditions.push(sql.fragment `${tableAlias}.${sql.identifier(remoteAttribute)} = ${foreignTableAlias}.${sql.identifier(localAttribute)}`);
|
|
34
|
+
});
|
|
35
|
+
if (typeof table.from === 'function') {
|
|
36
|
+
throw new Error(`Function source unsupported`);
|
|
37
|
+
}
|
|
38
|
+
// TODO: refactor this to use joins instead of subqueries
|
|
39
|
+
const fragment = sql `(${sql.indent `
|
|
40
|
+
select ${aggregateSpec.sqlAggregateWrap(sql.fragment `${tableAlias}.${sql.identifier(attributeName)}`, attribute.codec)}
|
|
41
|
+
from ${table.from} ${tableAlias}
|
|
42
|
+
where ${sql.join(conditions.map((c) => sql.parens(c)), ' AND ')}`})`;
|
|
43
|
+
$select.orderBy({
|
|
44
|
+
fragment,
|
|
45
|
+
codec: aggregateSpec.pgTypeCodecModifier?.(attribute.codec) ??
|
|
46
|
+
attribute.codec,
|
|
47
|
+
direction
|
|
48
|
+
});
|
|
49
|
+
}, [], 'pgAggregatesApplyOrderByAttribute');
|
|
50
|
+
exports.PgAggregatesOrderByAggregatesPlugin = {
|
|
51
|
+
name: 'PgAggregatesOrderByAggregatesPlugin',
|
|
52
|
+
description: 'Adds enum values to the OrderBy enum to allow ordering by aggregates on relations.',
|
|
53
|
+
version,
|
|
54
|
+
provides: ['aggregates'],
|
|
55
|
+
schema: {
|
|
56
|
+
behaviorRegistry: {
|
|
57
|
+
add: {
|
|
58
|
+
'relatedAggregates:orderBy': {
|
|
59
|
+
description: '',
|
|
60
|
+
entities: ['pgResource']
|
|
61
|
+
},
|
|
62
|
+
'aggregates:orderBy': {
|
|
63
|
+
description: '',
|
|
64
|
+
entities: ['pgCodecRelation']
|
|
65
|
+
},
|
|
66
|
+
'aggregate:orderBy': {
|
|
67
|
+
description: '',
|
|
68
|
+
entities: ['pgCodecAttribute']
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
entityBehavior: {
|
|
73
|
+
pgResource: 'resource:relatedAggregates:orderBy',
|
|
74
|
+
pgCodecRelation: ['select', 'manyRelation:aggregates:orderBy'],
|
|
75
|
+
pgCodecAttribute: ['attribute:aggregate:orderBy']
|
|
76
|
+
},
|
|
77
|
+
hooks: {
|
|
78
|
+
GraphQLEnumType_values(values, build, context) {
|
|
79
|
+
const { extend, sql, inflection, dataplanPg: { TYPES }, EXPORTABLE } = build;
|
|
80
|
+
const pgAggregateSpecs = build.pgAggregateSpecs;
|
|
81
|
+
const { scope: { isPgRowSortEnum, pgTypeResource, pgCodec } } = context;
|
|
82
|
+
const foreignTable = pgTypeResource ??
|
|
83
|
+
Object.values(build.input.pgRegistry.pgResources).find((s) => s.codec === pgCodec && !s.parameters);
|
|
84
|
+
if (!isPgRowSortEnum ||
|
|
85
|
+
!foreignTable ||
|
|
86
|
+
foreignTable.parameters ||
|
|
87
|
+
!foreignTable.codec.attributes) {
|
|
88
|
+
return values;
|
|
89
|
+
}
|
|
90
|
+
if (!build.behavior.pgResourceMatches(foreignTable, 'resource:relatedAggregates:orderBy')) {
|
|
91
|
+
return values;
|
|
92
|
+
}
|
|
93
|
+
const relations = foreignTable.getRelations();
|
|
94
|
+
const referenceeRelations = Object.entries(relations).filter(([_, rel]) => rel.isReferencee);
|
|
95
|
+
const newValues = referenceeRelations.reduce((memo, [relationName, relation]) => {
|
|
96
|
+
if (!build.behavior.pgCodecRelationMatches(relation, 'select')) {
|
|
97
|
+
return memo;
|
|
98
|
+
}
|
|
99
|
+
if (!build.behavior.pgCodecRelationMatches(relation, 'manyRelation:aggregates:orderBy')) {
|
|
100
|
+
return memo;
|
|
101
|
+
}
|
|
102
|
+
const table = relation.remoteResource;
|
|
103
|
+
const isUnique = !!relation.isUnique;
|
|
104
|
+
if (isUnique) {
|
|
105
|
+
// No point aggregating over a relation that's unique
|
|
106
|
+
return memo;
|
|
107
|
+
}
|
|
108
|
+
// Add count
|
|
109
|
+
const totalCountBaseName = inflection.orderByCountOfManyRelationByKeys({
|
|
110
|
+
registry: foreignTable.registry,
|
|
111
|
+
codec: foreignTable.codec,
|
|
112
|
+
relationName
|
|
113
|
+
});
|
|
114
|
+
const makeTotalCountApply = (direction) => {
|
|
115
|
+
return EXPORTABLE((TYPES, direction, pgAggregatesApplyOrderByTotalCount, relation, sql, table) => function apply($select) {
|
|
116
|
+
pgAggregatesApplyOrderByTotalCount(TYPES, direction, relation, sql, table, $select);
|
|
117
|
+
}, [
|
|
118
|
+
TYPES,
|
|
119
|
+
direction,
|
|
120
|
+
pgAggregatesApplyOrderByTotalCount,
|
|
121
|
+
relation,
|
|
122
|
+
sql,
|
|
123
|
+
table
|
|
124
|
+
]);
|
|
125
|
+
};
|
|
126
|
+
memo = build.extend(memo, {
|
|
127
|
+
[`${totalCountBaseName}_ASC`]: {
|
|
128
|
+
extensions: {
|
|
129
|
+
grafast: {
|
|
130
|
+
apply: makeTotalCountApply('ASC')
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
[`${totalCountBaseName}_DESC`]: {
|
|
135
|
+
extensions: {
|
|
136
|
+
grafast: {
|
|
137
|
+
apply: makeTotalCountApply('DESC')
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}, `Adding orderBy count to '${foreignTable.name}' using relation '${relationName}'`);
|
|
142
|
+
// Add other aggregates
|
|
143
|
+
pgAggregateSpecs.forEach((aggregateSpec) => {
|
|
144
|
+
if (!build.behavior.pgCodecRelationMatches(relation, `${aggregateSpec.id}:manyRelation:aggregates:orderBy`)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
for (const [attributeName, attribute] of Object.entries(table.codec.attributes)) {
|
|
148
|
+
if (!build.behavior.pgCodecAttributeMatches([table.codec, attributeName], `${aggregateSpec.id}:attribute:aggregate:orderBy`)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if ((aggregateSpec.shouldApplyToEntity &&
|
|
152
|
+
!aggregateSpec.shouldApplyToEntity({
|
|
153
|
+
type: 'attribute',
|
|
154
|
+
codec: table.codec,
|
|
155
|
+
attributeName: attributeName
|
|
156
|
+
})) ||
|
|
157
|
+
!aggregateSpec.isSuitableType(attribute.codec)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const baseName = inflection.orderByAttributeAggregateOfManyRelationByKeys({
|
|
161
|
+
registry: foreignTable.registry,
|
|
162
|
+
codec: foreignTable.codec,
|
|
163
|
+
relationName,
|
|
164
|
+
attributeName: attributeName,
|
|
165
|
+
aggregateSpec
|
|
166
|
+
});
|
|
167
|
+
const makeApply = (direction) => {
|
|
168
|
+
return EXPORTABLE((aggregateSpec, attribute, attributeName, direction, pgAggregatesApplyOrderByAttribute, relation, sql, table) => function apply($select) {
|
|
169
|
+
pgAggregatesApplyOrderByAttribute(aggregateSpec, attribute, attributeName, direction, relation, sql, table, $select);
|
|
170
|
+
}, [
|
|
171
|
+
aggregateSpec,
|
|
172
|
+
attribute,
|
|
173
|
+
attributeName,
|
|
174
|
+
direction,
|
|
175
|
+
pgAggregatesApplyOrderByAttribute,
|
|
176
|
+
relation,
|
|
177
|
+
sql,
|
|
178
|
+
table
|
|
179
|
+
]);
|
|
180
|
+
};
|
|
181
|
+
memo = build.extend(memo, {
|
|
182
|
+
[`${baseName}_ASC`]: {
|
|
183
|
+
extensions: {
|
|
184
|
+
grafast: {
|
|
185
|
+
apply: makeApply('ASC')
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
[`${baseName}_DESC`]: {
|
|
190
|
+
extensions: {
|
|
191
|
+
grafast: {
|
|
192
|
+
apply: makeApply('DESC')
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}, `Adding orderBy ${aggregateSpec.id} of '${attributeName}' to '${foreignTable.name}' using constraint '${relationName}'`);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
return memo;
|
|
200
|
+
}, Object.create(null));
|
|
201
|
+
return extend(values, newValues, `Adding aggregate orders to '${foreignTable.name}'`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# graphile-pg-aggregates
|
|
2
|
+
|
|
3
|
+
<p align="center" width="100%">
|
|
4
|
+
<img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center" width="100%">
|
|
8
|
+
<a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
|
|
9
|
+
<img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://github.com/constructive-io/constructive/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/graphile-pg-aggregates"><img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphile%2Fgraphile-pg-aggregates%2Fpackage.json"/></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
PostGraphile v5 aggregate plugin for the Constructive monorepo.
|
|
16
|
+
|
|
17
|
+
Adds aggregate support to PostGraphile connections:
|
|
18
|
+
|
|
19
|
+
- **Aggregates:** `sum`, `avg`, `min`, `max`, `stddevSample`, `stddevPopulation`, `varianceSample`, `variancePopulation`, `distinctCount`
|
|
20
|
+
- **Grouped aggregates:** `groupedAggregates(groupBy: [...], having: {...})` on connections
|
|
21
|
+
- **Order by aggregates:** Order parent connections by aggregates on related tables
|
|
22
|
+
- **Filter by aggregates:** Filter parent connections by aggregate values on related tables (via connection-filter integration)
|
|
23
|
+
- **Custom aggregates:** Register your own aggregate functions via the `pgAggregateSpecs` build extension
|
|
24
|
+
- **Smart tags:** `@behavior -aggregates` to disable per-table
|
|
25
|
+
|
|
26
|
+
> **Origin:** Forked from [`@graphile/pg-aggregates`](https://github.com/graphile/pg-aggregates) (v5 branch) by Benjie Gillam. Adapted for the Constructive connection-filter fork and build system.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { PgAggregatesPreset } from 'graphile-pg-aggregates';
|
|
32
|
+
|
|
33
|
+
const preset: GraphileConfig.Preset = {
|
|
34
|
+
extends: [
|
|
35
|
+
PgAggregatesPreset,
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or via the `ConstructivePreset` which includes it automatically:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { ConstructivePreset } from 'graphile-settings';
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Example Query
|
|
47
|
+
|
|
48
|
+
```graphql
|
|
49
|
+
{
|
|
50
|
+
allOrders {
|
|
51
|
+
aggregates {
|
|
52
|
+
sum { total }
|
|
53
|
+
avg { total }
|
|
54
|
+
distinctCount { status }
|
|
55
|
+
}
|
|
56
|
+
groupedAggregates(groupBy: [STATUS]) {
|
|
57
|
+
keys
|
|
58
|
+
sum { total }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Per-Table Opt-Out
|
|
65
|
+
|
|
66
|
+
Disable aggregates on a specific table via smart tags:
|
|
67
|
+
|
|
68
|
+
```sql
|
|
69
|
+
COMMENT ON TABLE my_table IS E'@behavior -aggregates';
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Education and Tutorials
|
|
75
|
+
|
|
76
|
+
1. 🚀 [Quickstart: Getting Up and Running](https://constructive.io/learn/quickstart)
|
|
77
|
+
Get started with modular databases in minutes. Install prerequisites and deploy your first module.
|
|
78
|
+
|
|
79
|
+
2. 📦 [Modular PostgreSQL Development with Database Packages](https://constructive.io/learn/modular-postgres)
|
|
80
|
+
Learn to organize PostgreSQL projects with pgpm workspaces and reusable database modules.
|
|
81
|
+
|
|
82
|
+
3. ✏️ [Authoring Database Changes](https://constructive.io/learn/authoring-database-changes)
|
|
83
|
+
Master the workflow for adding, organizing, and managing database changes with pgpm.
|
|
84
|
+
|
|
85
|
+
4. 🧪 [End-to-End PostgreSQL Testing with TypeScript](https://constructive.io/learn/e2e-postgres-testing)
|
|
86
|
+
Master end-to-end PostgreSQL testing with ephemeral databases, RLS testing, and CI/CD automation.
|
|
87
|
+
|
|
88
|
+
5. ⚡ [Supabase Testing](https://constructive.io/learn/supabase)
|
|
89
|
+
Use TypeScript-first tools to test Supabase projects with realistic RLS, policies, and auth contexts.
|
|
90
|
+
|
|
91
|
+
6. 💧 [Drizzle ORM Testing](https://constructive.io/learn/drizzle-testing)
|
|
92
|
+
Run full-stack tests with Drizzle ORM, including database setup, teardown, and RLS enforcement.
|
|
93
|
+
|
|
94
|
+
7. 🔧 [Troubleshooting](https://constructive.io/learn/troubleshooting)
|
|
95
|
+
Common issues and solutions for pgpm, PostgreSQL, and testing.
|
|
96
|
+
|
|
97
|
+
## Related Constructive Tooling
|
|
98
|
+
|
|
99
|
+
### 📦 Package Management
|
|
100
|
+
|
|
101
|
+
* [pgpm](https://github.com/constructive-io/constructive/tree/main/pgpm/pgpm): **🖥️ PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages.
|
|
102
|
+
|
|
103
|
+
### 🧪 Testing
|
|
104
|
+
|
|
105
|
+
* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-test): **📊 Isolated testing environments** with per-test transaction rollbacks—ideal for integration tests, complex migrations, and RLS simulation.
|
|
106
|
+
* [pgsql-seed](https://github.com/constructive-io/constructive/tree/main/postgres/pgsql-seed): **🌱 PostgreSQL seeding utilities** for CSV, JSON, SQL data loading, and pgpm deployment.
|
|
107
|
+
* [supabase-test](https://github.com/constructive-io/constructive/tree/main/postgres/supabase-test): **🧪 Supabase-native test harness** preconfigured for the local Supabase stack—per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready.
|
|
108
|
+
* [graphile-test](https://github.com/constructive-io/constructive/tree/main/graphile/graphile-test): **🔐 Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts.
|
|
109
|
+
* [pg-query-context](https://github.com/constructive-io/constructive/tree/main/postgres/pg-query-context): **🔒 Session context injection** to add session-local context (e.g., `SET LOCAL`) into queries—ideal for setting `role`, `jwt.claims`, and other session settings.
|
|
110
|
+
|
|
111
|
+
### 🧠 Parsing & AST
|
|
112
|
+
|
|
113
|
+
* [pgsql-parser](https://www.npmjs.com/package/pgsql-parser): **🔄 SQL conversion engine** that interprets and converts PostgreSQL syntax.
|
|
114
|
+
* [libpg-query-node](https://www.npmjs.com/package/libpg-query): **🌉 Node.js bindings** for `libpg_query`, converting SQL into parse trees.
|
|
115
|
+
* [pg-proto-parser](https://www.npmjs.com/package/pg-proto-parser): **📦 Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums.
|
|
116
|
+
* [@pgsql/enums](https://www.npmjs.com/package/@pgsql/enums): **🏷️ TypeScript enums** for PostgreSQL AST for safe and ergonomic parsing logic.
|
|
117
|
+
* [@pgsql/types](https://www.npmjs.com/package/@pgsql/types): **📝 Type definitions** for PostgreSQL AST nodes in TypeScript.
|
|
118
|
+
* [@pgsql/utils](https://www.npmjs.com/package/@pgsql/utils): **🛠️ AST utilities** for constructing and transforming PostgreSQL syntax trees.
|
|
119
|
+
|
|
120
|
+
## Credits
|
|
121
|
+
|
|
122
|
+
**🛠 Built by the [Constructive](https://constructive.io) team — creators of modular Postgres tooling for secure, composable backends. If you like our work, contribute on [GitHub](https://github.com/constructive-io).**
|
|
123
|
+
|
|
124
|
+
## Disclaimer
|
|
125
|
+
|
|
126
|
+
AS DESCRIBED IN THE LICENSES, THE SOFTWARE IS PROVIDED "AS IS", AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND.
|
|
127
|
+
|
|
128
|
+
No developer or entity involved in creating this software will be liable for any claims or damages whatsoever associated with your use, inability to use, or your interaction with other users of the code, including any direct, indirect, incidental, special, exemplary, punitive or consequential damages, or loss of profits, cryptocurrencies, tokens, or anything else of value.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
namespace GraphileBuild {
|
|
3
|
+
interface BehaviorStrings {
|
|
4
|
+
'resource:aggregates': true;
|
|
5
|
+
aggregates: true;
|
|
6
|
+
aggregate: true;
|
|
7
|
+
'sum:resource:aggregates': true;
|
|
8
|
+
'distinctCount:resource:aggregates': true;
|
|
9
|
+
'min:resource:aggregates': true;
|
|
10
|
+
'max:resource:aggregates': true;
|
|
11
|
+
'average:resource:aggregates': true;
|
|
12
|
+
'stddevSample:resource:aggregates': true;
|
|
13
|
+
'stddevPopulation:resource:aggregates': true;
|
|
14
|
+
'varianceSample:resource:aggregates': true;
|
|
15
|
+
'variancePopulation:resource:aggregates': true;
|
|
16
|
+
'sum:resource:aggregate': true;
|
|
17
|
+
'distinctCount:resource:aggregate': true;
|
|
18
|
+
'min:resource:aggregate': true;
|
|
19
|
+
'max:resource:aggregate': true;
|
|
20
|
+
'average:resource:aggregate': true;
|
|
21
|
+
'stddevSample:resource:aggregate': true;
|
|
22
|
+
'stddevPopulation:resource:aggregate': true;
|
|
23
|
+
'varianceSample:resource:aggregate': true;
|
|
24
|
+
'variancePopulation:resource:aggregate': true;
|
|
25
|
+
'sum:attribute:aggregate': true;
|
|
26
|
+
'distinctCount:attribute:aggregate': true;
|
|
27
|
+
'min:attribute:aggregate': true;
|
|
28
|
+
'max:attribute:aggregate': true;
|
|
29
|
+
'average:attribute:aggregate': true;
|
|
30
|
+
'stddevSample:attribute:aggregate': true;
|
|
31
|
+
'stddevPopulation:attribute:aggregate': true;
|
|
32
|
+
'varianceSample:attribute:aggregate': true;
|
|
33
|
+
'variancePopulation:attribute:aggregate': true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
declare const Plugin: GraphileConfig.Plugin;
|
|
38
|
+
export { Plugin as PgAggregatesAddAggregateTypesPlugin };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { EXPORTABLE } from './EXPORTABLE';
|
|
2
|
+
import { getComputedAttributeResources } from './utils';
|
|
3
|
+
const version = '1.0.0';
|
|
4
|
+
const isSuitableSource = (build, resource) => {
|
|
5
|
+
if (resource.parameters || !resource.codec.attributes) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
if (!build.behavior.pgResourceMatches(resource, 'select')) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
return !!build.behavior.pgResourceMatches(resource, 'resource:aggregates');
|
|
12
|
+
};
|
|
13
|
+
const pgAggregatesPlanKeys = EXPORTABLE(() => (lambda, $pgSelectSingle) => {
|
|
14
|
+
const $pgSelect = $pgSelectSingle.getClassStep();
|
|
15
|
+
const $groupDetails = $pgSelect.getGroupDetails();
|
|
16
|
+
return lambda([$groupDetails, $pgSelectSingle], ([groupDetails, item]) => {
|
|
17
|
+
if (groupDetails.indicies.length === 0 || item == null) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return groupDetails.indicies.map(({ index }) => item[index]);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}, [], 'pgAggregatesPlanKeys');
|
|
25
|
+
const pgAggregatesPlanAggregates = EXPORTABLE(() => function plan($pgSelectSingle) {
|
|
26
|
+
return $pgSelectSingle;
|
|
27
|
+
}, [], 'pgAggregatesPlanAggregates');
|
|
28
|
+
const pgAggregatesPlanAggregateAttribute = EXPORTABLE(() => (attributeCodec, attributeName, codec, spec, sql, $pgSelectSingle) => {
|
|
29
|
+
// Note this expression is just an sql fragment, so you
|
|
30
|
+
// could add CASE statements, function calls, or whatever
|
|
31
|
+
// you need here
|
|
32
|
+
const sqlAttribute = sql.fragment `${$pgSelectSingle.getClassStep().alias}.${sql.identifier(attributeName)}`;
|
|
33
|
+
const sqlAggregate = spec.sqlAggregateWrap(sqlAttribute, attributeCodec);
|
|
34
|
+
return $pgSelectSingle.select(sqlAggregate, codec);
|
|
35
|
+
}, [], 'pgAggregatesPlanAggregateAttribute');
|
|
36
|
+
const pgAggregatesPlanComputedColumnAggregates = EXPORTABLE(() => (codec, computedAttributeResource, makeArgs, pgFromExpression, spec, targetCodec, $pgSelectSingle, fieldArgs) => {
|
|
37
|
+
// Because we require that the computed attribute is
|
|
38
|
+
// evaluated inline, we have to convert it to an
|
|
39
|
+
// expression here; this is only needed because of the
|
|
40
|
+
// aggregation.
|
|
41
|
+
const src = pgFromExpression($pgSelectSingle, computedAttributeResource.from, computedAttributeResource.parameters, [
|
|
42
|
+
{
|
|
43
|
+
placeholder: $pgSelectSingle.getClassStep().alias,
|
|
44
|
+
position: 0
|
|
45
|
+
},
|
|
46
|
+
...makeArgs(fieldArgs)
|
|
47
|
+
]);
|
|
48
|
+
const sqlAggregate = spec.sqlAggregateWrap(src, codec);
|
|
49
|
+
return $pgSelectSingle.select(sqlAggregate, targetCodec);
|
|
50
|
+
}, [], 'pgAggregatesPlanComputedColumnAggregates');
|
|
51
|
+
const Plugin = {
|
|
52
|
+
name: 'PgAggregatesAddAggregateTypesPlugin',
|
|
53
|
+
description: `\
|
|
54
|
+
Creates the FooAggregates type for each suitable resource, creates the 'sum' \
|
|
55
|
+
and similar fields on this type, and the entries within these types for \
|
|
56
|
+
attributes and computed columns.`,
|
|
57
|
+
version,
|
|
58
|
+
provides: ['aggregates'],
|
|
59
|
+
// Create the aggregates type for each table
|
|
60
|
+
schema: {
|
|
61
|
+
behaviorRegistry: {
|
|
62
|
+
add: {
|
|
63
|
+
aggregates: {
|
|
64
|
+
description: 'For collection resources (e.g. returning a setof records)',
|
|
65
|
+
entities: ['pgResource']
|
|
66
|
+
},
|
|
67
|
+
aggregate: {
|
|
68
|
+
description: 'for computed column resources (e.g. returning a scalar)',
|
|
69
|
+
entities: ['pgResource']
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
entityBehavior: {
|
|
74
|
+
// `aggregates` - for collection resources (e.g. returning a setof records)
|
|
75
|
+
// `aggregate` - for computed column resources (e.g. returning a scalar)
|
|
76
|
+
pgResource: ['select', 'aggregates', 'aggregate'],
|
|
77
|
+
pgCodecAttribute: 'aggregate'
|
|
78
|
+
},
|
|
79
|
+
hooks: {
|
|
80
|
+
init(init, build, _context) {
|
|
81
|
+
const { graphql: { GraphQLList, GraphQLString }, grafast: { lambda }, dataplanPg: { assertPgClassSingleStep }, inflection, input: { pgRegistry: { pgResources } }, EXPORTABLE } = build;
|
|
82
|
+
// TODO: should we be using the codec rather than the source here? What if two sources share the same codec?
|
|
83
|
+
for (const resource of Object.values(pgResources)) {
|
|
84
|
+
if (!isSuitableSource(build, resource)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
/* const AggregateContainerType = */
|
|
88
|
+
build.registerObjectType(inflection.aggregateContainerType({ resource: resource }), {
|
|
89
|
+
isPgAggregateContainerType: true,
|
|
90
|
+
pgTypeResource: resource
|
|
91
|
+
}, () => ({
|
|
92
|
+
assertStep: assertPgClassSingleStep,
|
|
93
|
+
fields: {
|
|
94
|
+
keys: {
|
|
95
|
+
type: new GraphQLList(GraphQLString),
|
|
96
|
+
plan: EXPORTABLE((lambda, pgAggregatesPlanKeys) => function plan($pgSelectSingle) {
|
|
97
|
+
return pgAggregatesPlanKeys(lambda, $pgSelectSingle);
|
|
98
|
+
}, [lambda, pgAggregatesPlanKeys])
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}), `@graphile/pg-aggregates aggregate container type for ${resource.name}`);
|
|
102
|
+
for (const aggregateSpec of build.pgAggregateSpecs) {
|
|
103
|
+
const aggregateTypeName = inflection.aggregateType({
|
|
104
|
+
resource: resource,
|
|
105
|
+
aggregateSpec
|
|
106
|
+
});
|
|
107
|
+
if (!build.behavior.pgResourceMatches(resource, `${aggregateSpec.id}:resource:aggregates`)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
build.registerObjectType(aggregateTypeName, {
|
|
111
|
+
isPgAggregateType: true,
|
|
112
|
+
pgAggregateSpec: aggregateSpec,
|
|
113
|
+
pgTypeResource: resource
|
|
114
|
+
}, () => ({}), `${aggregateTypeName} aggregate type for '${resource.name}' source`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return init;
|
|
118
|
+
},
|
|
119
|
+
GraphQLObjectType_fields(inFields, build, context) {
|
|
120
|
+
let fields = inFields;
|
|
121
|
+
const { inflection, sql, graphql: { GraphQLNonNull, isOutputType }, dataplanPg: { pgFromExpression }, EXPORTABLE } = build;
|
|
122
|
+
const { fieldWithHooks, Self, scope: { isPgAggregateContainerType, isPgAggregateType, pgTypeResource: resource, pgAggregateSpec: spec } } = context;
|
|
123
|
+
if (!resource || !isSuitableSource(build, resource)) {
|
|
124
|
+
return fields;
|
|
125
|
+
}
|
|
126
|
+
// Hook the '*Aggregates' type for each source to add the "sum" operation
|
|
127
|
+
if (isPgAggregateContainerType) {
|
|
128
|
+
fields = build.extend(fields, build.pgAggregateSpecs.reduce((memo, aggregateSpec) => {
|
|
129
|
+
return build.recoverable(memo, () => {
|
|
130
|
+
if (!build.behavior.pgResourceMatches(resource, `${aggregateSpec.id}:resource:aggregates`)) {
|
|
131
|
+
return memo;
|
|
132
|
+
}
|
|
133
|
+
const aggregateTypeName = inflection.aggregateType({
|
|
134
|
+
resource: resource,
|
|
135
|
+
aggregateSpec
|
|
136
|
+
});
|
|
137
|
+
const AggregateType = build.getTypeByName(aggregateTypeName);
|
|
138
|
+
if (!AggregateType || !isOutputType(AggregateType)) {
|
|
139
|
+
return memo;
|
|
140
|
+
}
|
|
141
|
+
const fieldName = inflection.aggregatesField({ aggregateSpec });
|
|
142
|
+
return build.extend(memo, {
|
|
143
|
+
[fieldName]: fieldWithHooks({
|
|
144
|
+
fieldName,
|
|
145
|
+
isPgAggregateField: true,
|
|
146
|
+
pgAggregateSpec: aggregateSpec,
|
|
147
|
+
pgFieldResource: resource
|
|
148
|
+
}, () => ({
|
|
149
|
+
description: `${aggregateSpec.HumanLabel} aggregates across the matching connection (ignoring before/after/first/last/offset)`,
|
|
150
|
+
type: AggregateType,
|
|
151
|
+
plan: pgAggregatesPlanAggregates
|
|
152
|
+
}))
|
|
153
|
+
}, `Adding aggregates field to ${Self.name}`);
|
|
154
|
+
});
|
|
155
|
+
}, Object.create(null)), 'Adding sum operation to aggregate type');
|
|
156
|
+
}
|
|
157
|
+
// Hook the sum aggregates type to add fields for each numeric source attribute
|
|
158
|
+
if (isPgAggregateType && spec) {
|
|
159
|
+
fields = build.extend(fields,
|
|
160
|
+
// Figure out the attributes that we're allowed to do a `SUM(...)` of
|
|
161
|
+
Object.entries(resource.codec.attributes).reduce((memo, [attributeName, attribute]) => {
|
|
162
|
+
if (!build.behavior.pgCodecAttributeMatches([resource.codec, attributeName], `${spec.id}:attribute:aggregate`)) {
|
|
163
|
+
return memo;
|
|
164
|
+
}
|
|
165
|
+
if ((spec.shouldApplyToEntity &&
|
|
166
|
+
!spec.shouldApplyToEntity({
|
|
167
|
+
type: 'attribute',
|
|
168
|
+
codec: resource.codec,
|
|
169
|
+
attributeName
|
|
170
|
+
})) ||
|
|
171
|
+
!spec.isSuitableType(attribute.codec)) {
|
|
172
|
+
return memo;
|
|
173
|
+
}
|
|
174
|
+
const codec = spec.pgTypeCodecModifier
|
|
175
|
+
? spec.pgTypeCodecModifier(attribute.codec)
|
|
176
|
+
: attribute.codec;
|
|
177
|
+
const Type = build.getGraphQLTypeByPgCodec(codec, 'output');
|
|
178
|
+
if (!Type) {
|
|
179
|
+
return memo;
|
|
180
|
+
}
|
|
181
|
+
const fieldName = inflection.attribute({
|
|
182
|
+
attributeName,
|
|
183
|
+
codec: resource.codec
|
|
184
|
+
});
|
|
185
|
+
return build.extend(memo, {
|
|
186
|
+
[fieldName]: fieldWithHooks({
|
|
187
|
+
fieldName,
|
|
188
|
+
// In case anyone wants to hook us, describe ourselves
|
|
189
|
+
isPgConnectionAggregateField: true
|
|
190
|
+
//pgFieldIntrospection: attr,
|
|
191
|
+
//TODO: add more details here
|
|
192
|
+
}, () => {
|
|
193
|
+
const attributeCodec = attribute.codec;
|
|
194
|
+
return {
|
|
195
|
+
description: `${spec.HumanLabel} of ${fieldName} across the matching connection`,
|
|
196
|
+
type: spec.isNonNull ? new GraphQLNonNull(Type) : Type,
|
|
197
|
+
plan: EXPORTABLE((attributeCodec, attributeName, codec, pgAggregatesPlanAggregateAttribute, spec, sql) => function plan($pgSelectSingle) {
|
|
198
|
+
return pgAggregatesPlanAggregateAttribute(attributeCodec, attributeName, codec, spec, sql, $pgSelectSingle);
|
|
199
|
+
}, [
|
|
200
|
+
attributeCodec,
|
|
201
|
+
attributeName,
|
|
202
|
+
codec,
|
|
203
|
+
pgAggregatesPlanAggregateAttribute,
|
|
204
|
+
spec,
|
|
205
|
+
sql
|
|
206
|
+
])
|
|
207
|
+
};
|
|
208
|
+
})
|
|
209
|
+
}, `Add attribute '${attributeName}' compatible with this aggregate`);
|
|
210
|
+
}, Object.create(null)), 'Add attributes compatible with this aggregate');
|
|
211
|
+
const computedAttributeSources = getComputedAttributeResources(build, resource);
|
|
212
|
+
fields = build.extend(fields, computedAttributeSources.reduce((memo, computedAttributeResource) => {
|
|
213
|
+
if (!build.behavior.pgResourceMatches(computedAttributeResource, `${spec.id}:resource:aggregate`)) {
|
|
214
|
+
return memo;
|
|
215
|
+
}
|
|
216
|
+
const codec = computedAttributeResource.codec;
|
|
217
|
+
if ((spec.shouldApplyToEntity &&
|
|
218
|
+
!spec.shouldApplyToEntity({
|
|
219
|
+
type: 'computedAttribute',
|
|
220
|
+
resource: computedAttributeResource
|
|
221
|
+
})) ||
|
|
222
|
+
!spec.isSuitableType(codec)) {
|
|
223
|
+
return memo;
|
|
224
|
+
}
|
|
225
|
+
const fieldName = inflection.computedAttributeField({
|
|
226
|
+
resource: computedAttributeResource
|
|
227
|
+
});
|
|
228
|
+
const targetCodec = spec.pgTypeCodecModifier?.(codec) ?? codec;
|
|
229
|
+
const targetType = build.getGraphQLTypeByPgCodec(targetCodec, 'output');
|
|
230
|
+
if (!targetType) {
|
|
231
|
+
return memo;
|
|
232
|
+
}
|
|
233
|
+
return build.extend(memo, {
|
|
234
|
+
[fieldName]: fieldWithHooks({
|
|
235
|
+
fieldName
|
|
236
|
+
}, () => {
|
|
237
|
+
const { makeFieldArgs, makeArgs } = build.pgGetArgDetailsFromParameters(computedAttributeResource, computedAttributeResource.parameters, 1);
|
|
238
|
+
return {
|
|
239
|
+
type: targetType,
|
|
240
|
+
description: `${spec.HumanLabel} of this field across the matching connection.${computedAttributeResource.description
|
|
241
|
+
? `\n\n---\n\n${computedAttributeResource.description}`
|
|
242
|
+
: ''}`,
|
|
243
|
+
args: makeFieldArgs(),
|
|
244
|
+
plan: EXPORTABLE((codec, computedAttributeResource, makeArgs, pgAggregatesPlanComputedColumnAggregates, pgFromExpression, spec, targetCodec) => function plan($pgSelectSingle, fieldArgs) {
|
|
245
|
+
return pgAggregatesPlanComputedColumnAggregates(codec, computedAttributeResource, makeArgs, pgFromExpression, spec, targetCodec, $pgSelectSingle, fieldArgs);
|
|
246
|
+
}, [
|
|
247
|
+
codec,
|
|
248
|
+
computedAttributeResource,
|
|
249
|
+
makeArgs,
|
|
250
|
+
pgAggregatesPlanComputedColumnAggregates,
|
|
251
|
+
pgFromExpression,
|
|
252
|
+
spec,
|
|
253
|
+
targetCodec
|
|
254
|
+
])
|
|
255
|
+
};
|
|
256
|
+
})
|
|
257
|
+
}, '');
|
|
258
|
+
}, Object.create(null)), '');
|
|
259
|
+
}
|
|
260
|
+
return fields;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
export { Plugin as PgAggregatesAddAggregateTypesPlugin };
|