@simtlix/simfinity-js 2.0.2 → 2.2.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/AGGREGATION_CHANGES_SUMMARY.md +235 -0
- package/AGGREGATION_EXAMPLE.md +567 -0
- package/README.md +415 -3
- package/package.json +1 -1
- package/src/index.js +345 -0
- package/src/scalars.js +188 -0
- package/src/validators.js +250 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -10
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -10
- package/.github/workflows/master.yml +0 -19
- package/.github/workflows/publish.yml +0 -45
- package/.github/workflows/release.yml +0 -65
- package/BACKUP_README.md +0 -26
- package/README_INDEX_EXAMPLE.md +0 -252
- package/simtlix-simfinity-js-1.9.1.tgz +0 -0
- package/tests/objectid-indexes.test.js +0 -215
- package/tests/prevent-collection-creation.test.js +0 -67
- package/tests/scalar-naming.test.js +0 -125
- package/tests/validated-scalar.test.js +0 -172
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
# GraphQL Aggregation Query Support
|
|
2
|
+
|
|
3
|
+
This library now supports GraphQL aggregation queries with group by functionality, allowing you to perform aggregate operations (SUM, COUNT, AVG, MIN, MAX) on your data.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
For each entity type registered with `connect()`, an additional aggregation endpoint is automatically generated with the format `{entityname}_aggregate`.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Group By**: Group results by any field (direct or related entity field path)
|
|
12
|
+
- **Aggregation Operations**: SUM, COUNT, AVG, MIN, MAX
|
|
13
|
+
- **Filtering**: Use the same filter parameters as regular queries
|
|
14
|
+
- **Pagination**: Use the same pagination parameters as regular queries
|
|
15
|
+
- **Related Entity Fields**: Group by or aggregate on fields from related entities using dot notation
|
|
16
|
+
|
|
17
|
+
## GraphQL Types
|
|
18
|
+
|
|
19
|
+
### QLAggregationOperation (Enum)
|
|
20
|
+
- `SUM`: Sum of numeric values
|
|
21
|
+
- `COUNT`: Count of records
|
|
22
|
+
- `AVG`: Average of numeric values
|
|
23
|
+
- `MIN`: Minimum value
|
|
24
|
+
- `MAX`: Maximum value
|
|
25
|
+
|
|
26
|
+
### QLTypeAggregationFact (Input)
|
|
27
|
+
```graphql
|
|
28
|
+
input QLTypeAggregationFact {
|
|
29
|
+
operation: QLAggregationOperation!
|
|
30
|
+
factName: String!
|
|
31
|
+
path: String!
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### QLTypeAggregationExpression (Input)
|
|
36
|
+
```graphql
|
|
37
|
+
input QLTypeAggregationExpression {
|
|
38
|
+
groupId: String!
|
|
39
|
+
facts: [QLTypeAggregationFact!]!
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### QLTypeAggregationResult (Output)
|
|
44
|
+
```graphql
|
|
45
|
+
type QLTypeAggregationResult {
|
|
46
|
+
groupId: JSON
|
|
47
|
+
facts: JSON
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage Examples
|
|
52
|
+
|
|
53
|
+
### Example 1: Simple Group By with Direct Field
|
|
54
|
+
|
|
55
|
+
Group series by category and count them:
|
|
56
|
+
|
|
57
|
+
```graphql
|
|
58
|
+
query {
|
|
59
|
+
series_aggregate(
|
|
60
|
+
aggregation: {
|
|
61
|
+
groupId: "category"
|
|
62
|
+
facts: [
|
|
63
|
+
{
|
|
64
|
+
operation: COUNT
|
|
65
|
+
factName: "totalSeries"
|
|
66
|
+
path: "id"
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
) {
|
|
71
|
+
groupId
|
|
72
|
+
facts
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Result:**
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"data": {
|
|
81
|
+
"series_aggregate": [
|
|
82
|
+
{
|
|
83
|
+
"groupId": "Drama",
|
|
84
|
+
"facts": {
|
|
85
|
+
"totalSeries": 15
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"groupId": "Comedy",
|
|
90
|
+
"facts": {
|
|
91
|
+
"totalSeries": 23
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Example 2: Group By Related Entity Field
|
|
100
|
+
|
|
101
|
+
Group series by country name (where country is a related entity):
|
|
102
|
+
|
|
103
|
+
```graphql
|
|
104
|
+
query {
|
|
105
|
+
series_aggregate(
|
|
106
|
+
aggregation: {
|
|
107
|
+
groupId: "country.name"
|
|
108
|
+
facts: [
|
|
109
|
+
{
|
|
110
|
+
operation: COUNT
|
|
111
|
+
factName: "seriesCount"
|
|
112
|
+
path: "id"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
operation: AVG
|
|
116
|
+
factName: "avgRating"
|
|
117
|
+
path: "rating"
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
) {
|
|
122
|
+
groupId
|
|
123
|
+
facts
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Result:**
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"data": {
|
|
132
|
+
"series_aggregate": [
|
|
133
|
+
{
|
|
134
|
+
"groupId": "United States",
|
|
135
|
+
"facts": {
|
|
136
|
+
"seriesCount": 45,
|
|
137
|
+
"avgRating": 7.8
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"groupId": "United Kingdom",
|
|
142
|
+
"facts": {
|
|
143
|
+
"seriesCount": 32,
|
|
144
|
+
"avgRating": 8.2
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Example 3: Multiple Aggregation Facts
|
|
153
|
+
|
|
154
|
+
Calculate multiple metrics per group:
|
|
155
|
+
|
|
156
|
+
```graphql
|
|
157
|
+
query {
|
|
158
|
+
series_aggregate(
|
|
159
|
+
aggregation: {
|
|
160
|
+
groupId: "category"
|
|
161
|
+
facts: [
|
|
162
|
+
{
|
|
163
|
+
operation: COUNT
|
|
164
|
+
factName: "total"
|
|
165
|
+
path: "id"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
operation: SUM
|
|
169
|
+
factName: "totalEpisodes"
|
|
170
|
+
path: "episodeCount"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
operation: AVG
|
|
174
|
+
factName: "avgRating"
|
|
175
|
+
path: "rating"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
operation: MIN
|
|
179
|
+
factName: "minRating"
|
|
180
|
+
path: "rating"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
operation: MAX
|
|
184
|
+
factName: "maxRating"
|
|
185
|
+
path: "rating"
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
) {
|
|
190
|
+
groupId
|
|
191
|
+
facts
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Result:**
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"data": {
|
|
200
|
+
"series_aggregate": [
|
|
201
|
+
{
|
|
202
|
+
"groupId": "Drama",
|
|
203
|
+
"facts": {
|
|
204
|
+
"total": 15,
|
|
205
|
+
"totalEpisodes": 1234,
|
|
206
|
+
"avgRating": 7.5,
|
|
207
|
+
"minRating": 6.2,
|
|
208
|
+
"maxRating": 9.1
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Example 4: With Filtering
|
|
217
|
+
|
|
218
|
+
Filter data before aggregation:
|
|
219
|
+
|
|
220
|
+
```graphql
|
|
221
|
+
query {
|
|
222
|
+
series_aggregate(
|
|
223
|
+
category: {
|
|
224
|
+
operator: IN
|
|
225
|
+
value: ["Drama", "Thriller"]
|
|
226
|
+
}
|
|
227
|
+
rating: {
|
|
228
|
+
operator: GTE
|
|
229
|
+
value: 7.0
|
|
230
|
+
}
|
|
231
|
+
aggregation: {
|
|
232
|
+
groupId: "country.name"
|
|
233
|
+
facts: [
|
|
234
|
+
{
|
|
235
|
+
operation: COUNT
|
|
236
|
+
factName: "highRatedCount"
|
|
237
|
+
path: "id"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
operation: AVG
|
|
241
|
+
factName: "avgRating"
|
|
242
|
+
path: "rating"
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
) {
|
|
247
|
+
groupId
|
|
248
|
+
facts
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Example 5: Sorting by GroupId
|
|
254
|
+
|
|
255
|
+
Sort aggregation results by groupId (ascending or descending):
|
|
256
|
+
|
|
257
|
+
```graphql
|
|
258
|
+
query {
|
|
259
|
+
series_aggregate(
|
|
260
|
+
sort: {
|
|
261
|
+
terms: [
|
|
262
|
+
{
|
|
263
|
+
field: "groupId" # Sort by the groupId
|
|
264
|
+
order: "DESC" # ASC or DESC
|
|
265
|
+
}
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
aggregation: {
|
|
269
|
+
groupId: "category"
|
|
270
|
+
facts: [
|
|
271
|
+
{
|
|
272
|
+
operation: COUNT
|
|
273
|
+
factName: "total"
|
|
274
|
+
path: "id"
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
) {
|
|
279
|
+
groupId
|
|
280
|
+
facts
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Example 5b: Sorting by a Fact
|
|
286
|
+
|
|
287
|
+
Sort aggregation results by a calculated fact (metric):
|
|
288
|
+
|
|
289
|
+
```graphql
|
|
290
|
+
query {
|
|
291
|
+
series_aggregate(
|
|
292
|
+
sort: {
|
|
293
|
+
terms: [
|
|
294
|
+
{
|
|
295
|
+
field: "avgRating" # Sort by the avgRating fact
|
|
296
|
+
order: "DESC" # Highest rating first
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
aggregation: {
|
|
301
|
+
groupId: "category"
|
|
302
|
+
facts: [
|
|
303
|
+
{
|
|
304
|
+
operation: COUNT
|
|
305
|
+
factName: "total"
|
|
306
|
+
path: "id"
|
|
307
|
+
}
|
|
308
|
+
{
|
|
309
|
+
operation: AVG
|
|
310
|
+
factName: "avgRating"
|
|
311
|
+
path: "rating"
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
}
|
|
315
|
+
) {
|
|
316
|
+
groupId
|
|
317
|
+
facts
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Result (sorted by avgRating descending):**
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"data": {
|
|
326
|
+
"series_aggregate": [
|
|
327
|
+
{
|
|
328
|
+
"groupId": "SciFi",
|
|
329
|
+
"facts": {
|
|
330
|
+
"total": 18,
|
|
331
|
+
"avgRating": 8.9
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
"groupId": "Drama",
|
|
336
|
+
"facts": {
|
|
337
|
+
"total": 15,
|
|
338
|
+
"avgRating": 7.5
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"groupId": "Comedy",
|
|
343
|
+
"facts": {
|
|
344
|
+
"total": 23,
|
|
345
|
+
"avgRating": 7.2
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Note**: You can sort by either `groupId` or any of the fact names defined in your aggregation. If the field doesn't match any fact name or groupId, it defaults to sorting by groupId.
|
|
354
|
+
|
|
355
|
+
### Example 6: With Pagination
|
|
356
|
+
|
|
357
|
+
Paginate aggregation results:
|
|
358
|
+
|
|
359
|
+
```graphql
|
|
360
|
+
query {
|
|
361
|
+
series_aggregate(
|
|
362
|
+
pagination: {
|
|
363
|
+
page: 2
|
|
364
|
+
size: 10
|
|
365
|
+
}
|
|
366
|
+
aggregation: {
|
|
367
|
+
groupId: "category"
|
|
368
|
+
facts: [
|
|
369
|
+
{
|
|
370
|
+
operation: COUNT
|
|
371
|
+
factName: "total"
|
|
372
|
+
path: "id"
|
|
373
|
+
}
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
) {
|
|
377
|
+
groupId
|
|
378
|
+
facts
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Note**: The `count` parameter in pagination is ignored for aggregation queries. Results are sorted by `groupId` in ascending order by default (or by the field and direction specified in the sort parameter).
|
|
384
|
+
|
|
385
|
+
### Example 7: Multiple Sort Fields
|
|
386
|
+
|
|
387
|
+
Sort by multiple fields (e.g., by total count descending, then by groupId ascending):
|
|
388
|
+
|
|
389
|
+
```graphql
|
|
390
|
+
query {
|
|
391
|
+
series_aggregate(
|
|
392
|
+
sort: {
|
|
393
|
+
terms: [
|
|
394
|
+
{ field: "total", order: "DESC" }, # Primary sort: by total count
|
|
395
|
+
{ field: "groupId", order: "ASC" } # Secondary sort: by groupId
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
aggregation: {
|
|
399
|
+
groupId: "category"
|
|
400
|
+
facts: [
|
|
401
|
+
{
|
|
402
|
+
operation: COUNT
|
|
403
|
+
factName: "total"
|
|
404
|
+
path: "id"
|
|
405
|
+
}
|
|
406
|
+
{
|
|
407
|
+
operation: AVG
|
|
408
|
+
factName: "avgRating"
|
|
409
|
+
path: "rating"
|
|
410
|
+
}
|
|
411
|
+
]
|
|
412
|
+
}
|
|
413
|
+
) {
|
|
414
|
+
groupId
|
|
415
|
+
facts
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Result:**
|
|
421
|
+
```json
|
|
422
|
+
{
|
|
423
|
+
"data": {
|
|
424
|
+
"series_aggregate": [
|
|
425
|
+
{
|
|
426
|
+
"groupId": "Drama",
|
|
427
|
+
"facts": { "total": 45, "avgRating": 7.8 }
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
"groupId": "Comedy",
|
|
431
|
+
"facts": { "total": 32, "avgRating": 8.1 }
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
"groupId": "Action",
|
|
435
|
+
"facts": { "total": 32, "avgRating": 7.5 }
|
|
436
|
+
}
|
|
437
|
+
]
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
This shows how items with the same total count (Comedy and Action both have 32) are then sorted by groupId alphabetically.
|
|
443
|
+
|
|
444
|
+
### Example 8: With Sorting and Pagination Combined
|
|
445
|
+
|
|
446
|
+
Combine sorting by a fact with pagination (top 5 categories by total count):
|
|
447
|
+
|
|
448
|
+
```graphql
|
|
449
|
+
query {
|
|
450
|
+
series_aggregate(
|
|
451
|
+
sort: {
|
|
452
|
+
terms: [
|
|
453
|
+
{ field: "total", order: "DESC" } # Sort by total count
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
pagination: {
|
|
457
|
+
page: 1
|
|
458
|
+
size: 5
|
|
459
|
+
}
|
|
460
|
+
aggregation: {
|
|
461
|
+
groupId: "category"
|
|
462
|
+
facts: [
|
|
463
|
+
{
|
|
464
|
+
operation: COUNT
|
|
465
|
+
factName: "total"
|
|
466
|
+
path: "id"
|
|
467
|
+
}
|
|
468
|
+
{
|
|
469
|
+
operation: SUM
|
|
470
|
+
factName: "totalRevenue"
|
|
471
|
+
path: "revenue"
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
}
|
|
475
|
+
) {
|
|
476
|
+
groupId
|
|
477
|
+
facts
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
This query returns the top 5 categories with the most items, sorted by count in descending order.
|
|
483
|
+
|
|
484
|
+
### Example 9: Aggregate on Related Entity Field
|
|
485
|
+
|
|
486
|
+
Sum revenue from episodes (where episodes is a related entity collection):
|
|
487
|
+
|
|
488
|
+
```graphql
|
|
489
|
+
query {
|
|
490
|
+
series_aggregate(
|
|
491
|
+
aggregation: {
|
|
492
|
+
groupId: "category"
|
|
493
|
+
facts: [
|
|
494
|
+
{
|
|
495
|
+
operation: COUNT
|
|
496
|
+
factName: "seriesCount"
|
|
497
|
+
path: "id"
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
operation: SUM
|
|
501
|
+
factName: "totalRevenue"
|
|
502
|
+
path: "episodes.revenue"
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
}
|
|
506
|
+
) {
|
|
507
|
+
groupId
|
|
508
|
+
facts
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## Field Path Resolution
|
|
514
|
+
|
|
515
|
+
The `groupId` and `path` parameters support:
|
|
516
|
+
|
|
517
|
+
1. **Direct Fields**: Simple field names from the entity
|
|
518
|
+
- Example: `"category"`, `"rating"`, `"id"`
|
|
519
|
+
|
|
520
|
+
2. **Related Entity Fields**: Dot notation for fields in related entities
|
|
521
|
+
- Example: `"country.name"`, `"studio.foundedYear"`
|
|
522
|
+
|
|
523
|
+
3. **Nested Related Entities**: Multiple levels of relationships
|
|
524
|
+
- Example: `"country.region.name"`
|
|
525
|
+
|
|
526
|
+
## MongoDB Translation
|
|
527
|
+
|
|
528
|
+
The aggregation queries are translated to MongoDB aggregation pipelines:
|
|
529
|
+
|
|
530
|
+
1. **$lookup**: Used for non-embedded related entities
|
|
531
|
+
2. **$unwind**: Used to flatten joined collections
|
|
532
|
+
3. **$match**: Applied for filtering (before grouping)
|
|
533
|
+
4. **$group**: Groups by the specified field with aggregation operations
|
|
534
|
+
5. **$project**: Formats the final output with groupId and facts fields
|
|
535
|
+
6. **$sort**: Sorts results by groupId (ascending or descending)
|
|
536
|
+
7. **$limit** / **$skip**: Applied for pagination (after sorting)
|
|
537
|
+
|
|
538
|
+
## Notes
|
|
539
|
+
|
|
540
|
+
### Result Structure
|
|
541
|
+
- The `groupId` field in the result will contain the value used for grouping
|
|
542
|
+
- The `facts` field will contain a JSON object with all calculated metrics
|
|
543
|
+
- Both fields use the `GraphQLJSON` type to support flexible data structures
|
|
544
|
+
|
|
545
|
+
### Aggregation Operations
|
|
546
|
+
- **COUNT**: Counts the number of documents in each group
|
|
547
|
+
- **SUM, AVG, MIN, MAX**: Require numeric fields to operate on
|
|
548
|
+
|
|
549
|
+
### Filtering
|
|
550
|
+
- All filter parameters from regular queries work with aggregation
|
|
551
|
+
- Filters are applied **before** grouping
|
|
552
|
+
|
|
553
|
+
### Sorting
|
|
554
|
+
- You can sort by **groupId** or **any fact name**
|
|
555
|
+
- **Multiple sort fields are supported** - results are sorted by the first field, then by the second field for ties, etc.
|
|
556
|
+
- Set the `field` parameter to:
|
|
557
|
+
- `"groupId"` to sort by the grouping field
|
|
558
|
+
- Any fact name (e.g., `"avgRating"`, `"total"`) to sort by that calculated metric
|
|
559
|
+
- The `order` parameter (ASC/DESC) determines the sort direction for each field
|
|
560
|
+
- If a field doesn't match groupId or any fact name, it defaults to groupId
|
|
561
|
+
- If no sort is specified, defaults to sorting by groupId ascending
|
|
562
|
+
|
|
563
|
+
### Pagination
|
|
564
|
+
- The `page` and `size` parameters work as expected
|
|
565
|
+
- The `count` parameter is **ignored** for aggregation queries
|
|
566
|
+
- Pagination is applied **after** grouping and sorting
|
|
567
|
+
|