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.
Files changed (63) hide show
  1. package/AddAggregateTypesPlugin.d.ts +38 -0
  2. package/AddAggregateTypesPlugin.js +268 -0
  3. package/AddConnectionAggregatesPlugin.d.ts +2 -0
  4. package/AddConnectionAggregatesPlugin.js +56 -0
  5. package/AddConnectionGroupedAggregatesPlugin.d.ts +10 -0
  6. package/AddConnectionGroupedAggregatesPlugin.js +118 -0
  7. package/AddGroupByAggregateEnumValuesForAttributesPlugin.d.ts +10 -0
  8. package/AddGroupByAggregateEnumValuesForAttributesPlugin.js +116 -0
  9. package/AddGroupByAggregateEnumsPlugin.d.ts +2 -0
  10. package/AddGroupByAggregateEnumsPlugin.js +51 -0
  11. package/AddHavingAggregateTypesPlugin.d.ts +22 -0
  12. package/AddHavingAggregateTypesPlugin.js +449 -0
  13. package/AggregateSpecsPlugin.d.ts +1 -0
  14. package/AggregateSpecsPlugin.js +206 -0
  15. package/AggregatesSmartTagsPlugin.d.ts +8 -0
  16. package/AggregatesSmartTagsPlugin.js +65 -0
  17. package/EXPORTABLE.d.ts +1 -0
  18. package/EXPORTABLE.js +15 -0
  19. package/FilterRelationalAggregatesPlugin.d.ts +74 -0
  20. package/FilterRelationalAggregatesPlugin.js +443 -0
  21. package/InflectionPlugin.d.ts +67 -0
  22. package/InflectionPlugin.js +71 -0
  23. package/LICENSE +23 -0
  24. package/OrderByAggregatesPlugin.d.ts +31 -0
  25. package/OrderByAggregatesPlugin.js +205 -0
  26. package/README.md +128 -0
  27. package/esm/AddAggregateTypesPlugin.d.ts +38 -0
  28. package/esm/AddAggregateTypesPlugin.js +265 -0
  29. package/esm/AddConnectionAggregatesPlugin.d.ts +2 -0
  30. package/esm/AddConnectionAggregatesPlugin.js +53 -0
  31. package/esm/AddConnectionGroupedAggregatesPlugin.d.ts +10 -0
  32. package/esm/AddConnectionGroupedAggregatesPlugin.js +115 -0
  33. package/esm/AddGroupByAggregateEnumValuesForAttributesPlugin.d.ts +10 -0
  34. package/esm/AddGroupByAggregateEnumValuesForAttributesPlugin.js +113 -0
  35. package/esm/AddGroupByAggregateEnumsPlugin.d.ts +2 -0
  36. package/esm/AddGroupByAggregateEnumsPlugin.js +48 -0
  37. package/esm/AddHavingAggregateTypesPlugin.d.ts +22 -0
  38. package/esm/AddHavingAggregateTypesPlugin.js +446 -0
  39. package/esm/AggregateSpecsPlugin.d.ts +1 -0
  40. package/esm/AggregateSpecsPlugin.js +203 -0
  41. package/esm/AggregatesSmartTagsPlugin.d.ts +8 -0
  42. package/esm/AggregatesSmartTagsPlugin.js +62 -0
  43. package/esm/EXPORTABLE.d.ts +1 -0
  44. package/esm/EXPORTABLE.js +12 -0
  45. package/esm/FilterRelationalAggregatesPlugin.d.ts +74 -0
  46. package/esm/FilterRelationalAggregatesPlugin.js +440 -0
  47. package/esm/InflectionPlugin.d.ts +67 -0
  48. package/esm/InflectionPlugin.js +68 -0
  49. package/esm/OrderByAggregatesPlugin.d.ts +31 -0
  50. package/esm/OrderByAggregatesPlugin.js +202 -0
  51. package/esm/index.d.ts +42 -0
  52. package/esm/index.js +37 -0
  53. package/esm/interfaces.d.ts +145 -0
  54. package/esm/interfaces.js +44 -0
  55. package/esm/utils.d.ts +23 -0
  56. package/esm/utils.js +22 -0
  57. package/index.d.ts +42 -0
  58. package/index.js +40 -0
  59. package/interfaces.d.ts +145 -0
  60. package/interfaces.js +47 -0
  61. package/package.json +61 -0
  62. package/utils.d.ts +23 -0
  63. 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 };
@@ -0,0 +1,2 @@
1
+ declare const Plugin: GraphileConfig.Plugin;
2
+ export { Plugin as PgAggregatesAddConnectionAggregatesPlugin };