@simtlix/simfinity-js 2.0.2 → 2.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.
@@ -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
+