@simtlix/simfinity-js 2.4.6 → 2.5.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/.claude/worktrees/agitated-kepler/.claude/settings.local.json +23 -0
- package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +235 -0
- package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +567 -0
- package/.claude/worktrees/agitated-kepler/LICENSE +201 -0
- package/.claude/worktrees/agitated-kepler/README.md +3941 -0
- package/.claude/worktrees/agitated-kepler/eslint.config.mjs +71 -0
- package/.claude/worktrees/agitated-kepler/package-lock.json +4740 -0
- package/.claude/worktrees/agitated-kepler/package.json +41 -0
- package/.claude/worktrees/agitated-kepler/src/auth/errors.js +44 -0
- package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +273 -0
- package/.claude/worktrees/agitated-kepler/src/auth/index.js +391 -0
- package/.claude/worktrees/agitated-kepler/src/auth/rules.js +274 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +39 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +28 -0
- package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +39 -0
- package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +11 -0
- package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +15 -0
- package/.claude/worktrees/agitated-kepler/src/index.js +2412 -0
- package/.claude/worktrees/agitated-kepler/src/plugins.js +53 -0
- package/.claude/worktrees/agitated-kepler/src/scalars.js +188 -0
- package/.claude/worktrees/agitated-kepler/src/validators.js +250 -0
- package/.claude/worktrees/agitated-kepler/yarn.lock +1154 -0
- package/.cursor/rules/simfinity-core-functions.mdc +3 -1
- package/README.md +202 -0
- package/package.json +1 -1
- package/src/index.js +235 -21
|
@@ -55,11 +55,13 @@ Handles: scalars, enums, relation fields (`IdInputType` for refs, nested input f
|
|
|
55
55
|
`buildQuery(args, gqltype)` translates GraphQL filter arguments into a MongoDB aggregation pipeline:
|
|
56
56
|
|
|
57
57
|
- `buildQueryTerms()` -- converts scalar/enum filters to `$match`, object/relation filters to `$lookup` + `$unwind` + `$match`
|
|
58
|
+
- `buildFilterGroupMatch()` -- recursively processes `QLFilterGroup` (AND/OR) into nested MongoDB `$and`/`$or` match expressions. Reuses `buildQueryTerms()` for each leaf condition. Deduplicates `$lookup` stages via shared `aggregationsIncluded` dict. Max depth: 5.
|
|
58
59
|
- `buildAggregationsForSort()` -- adds `$lookup`/`$unwind` for sort on related fields
|
|
59
60
|
- Pagination: `$skip` + `$limit` from `QLPagination` input
|
|
60
61
|
- Sort: `$sort` from `QLSortExpression` input
|
|
62
|
+
- AND/OR: optional `AND`/`OR` arguments containing `QLFilterGroup` trees. Flat field filters and scope-injected filters are always ANDed at the top level (cannot be bypassed by user OR).
|
|
61
63
|
|
|
62
|
-
`buildAggregationQuery()` extends this with `$group` for aggregation operations (SUM, COUNT, AVG, MIN, MAX).
|
|
64
|
+
`buildAggregationQuery()` extends this with `$group` for aggregation operations (SUM, COUNT, AVG, MIN, MAX). Supports the same AND/OR filter pattern.
|
|
63
65
|
|
|
64
66
|
## Mutation Pipeline
|
|
65
67
|
|
package/README.md
CHANGED
|
@@ -265,6 +265,208 @@ query {
|
|
|
265
265
|
- `NIN` - Not in array
|
|
266
266
|
- `BTW` - Between two values
|
|
267
267
|
|
|
268
|
+
### Logical Filters (AND / OR)
|
|
269
|
+
|
|
270
|
+
By default, all field-level filters are combined with implicit AND logic. For complex conditions requiring OR logic or nested combinations, use the `AND` and `OR` query arguments.
|
|
271
|
+
|
|
272
|
+
#### Simple OR
|
|
273
|
+
|
|
274
|
+
Return books in either the Sci-Fi or Fantasy category:
|
|
275
|
+
|
|
276
|
+
```graphql
|
|
277
|
+
query {
|
|
278
|
+
books(
|
|
279
|
+
OR: [
|
|
280
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
|
|
281
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
|
|
282
|
+
]
|
|
283
|
+
) {
|
|
284
|
+
id
|
|
285
|
+
title
|
|
286
|
+
category
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### Flat Filters Combined with OR
|
|
292
|
+
|
|
293
|
+
Flat field filters are always ANDed at the top level, making them ideal for scope/security conditions that cannot be bypassed by user OR logic:
|
|
294
|
+
|
|
295
|
+
```graphql
|
|
296
|
+
query {
|
|
297
|
+
books(
|
|
298
|
+
rating: { operator: GTE, value: 7.0 }
|
|
299
|
+
OR: [
|
|
300
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
|
|
301
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
|
|
302
|
+
]
|
|
303
|
+
) {
|
|
304
|
+
id
|
|
305
|
+
title
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
This translates to: `rating >= 7.0 AND (category = "Sci-Fi" OR category = "Fantasy")`.
|
|
311
|
+
|
|
312
|
+
#### Nested AND inside OR
|
|
313
|
+
|
|
314
|
+
```graphql
|
|
315
|
+
query {
|
|
316
|
+
books(
|
|
317
|
+
OR: [
|
|
318
|
+
{ AND: [
|
|
319
|
+
{ conditions: [{ field: "rating", operator: GTE, value: 9.0 }] }
|
|
320
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
|
|
321
|
+
]}
|
|
322
|
+
{ AND: [
|
|
323
|
+
{ conditions: [{ field: "rating", operator: GTE, value: 8.0 }] }
|
|
324
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
|
|
325
|
+
]}
|
|
326
|
+
]
|
|
327
|
+
) {
|
|
328
|
+
id
|
|
329
|
+
title
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
This translates to: `(rating >= 9.0 AND category = "Sci-Fi") OR (rating >= 8.0 AND category = "Fantasy")`.
|
|
335
|
+
|
|
336
|
+
#### Filtering on Relationships within AND/OR
|
|
337
|
+
|
|
338
|
+
Use the `path` parameter to filter on related entity fields:
|
|
339
|
+
|
|
340
|
+
```graphql
|
|
341
|
+
query {
|
|
342
|
+
books(
|
|
343
|
+
OR: [
|
|
344
|
+
{ conditions: [{ field: "author", path: "name", operator: LIKE, value: "Adams" }] }
|
|
345
|
+
{ conditions: [{ field: "author", path: "name", operator: LIKE, value: "Pratchett" }] }
|
|
346
|
+
]
|
|
347
|
+
) {
|
|
348
|
+
id
|
|
349
|
+
title
|
|
350
|
+
author { name }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### Mixing Flat Filters with AND/OR
|
|
356
|
+
|
|
357
|
+
You can freely combine the existing flat filter syntax with AND/OR groups. Flat filters and AND groups are all ANDed together at the top level:
|
|
358
|
+
|
|
359
|
+
```graphql
|
|
360
|
+
query {
|
|
361
|
+
books(
|
|
362
|
+
rating: { operator: GTE, value: 7.0 }
|
|
363
|
+
author: { terms: [{ path: "country", operator: EQ, value: "UK" }] }
|
|
364
|
+
OR: [
|
|
365
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
|
|
366
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
|
|
367
|
+
]
|
|
368
|
+
) {
|
|
369
|
+
id
|
|
370
|
+
title
|
|
371
|
+
rating
|
|
372
|
+
category
|
|
373
|
+
author { name country }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
This translates to: `rating >= 7.0 AND author.country = "UK" AND (category = "Sci-Fi" OR category = "Fantasy")`. The flat field filters (`rating`, `author`) use the existing syntax while the OR group uses the new `QLFilterGroup` syntax.
|
|
379
|
+
|
|
380
|
+
You can also combine flat filters with explicit AND groups for more complex logic:
|
|
381
|
+
|
|
382
|
+
```graphql
|
|
383
|
+
query {
|
|
384
|
+
books(
|
|
385
|
+
rating: { operator: GTE, value: 5.0 }
|
|
386
|
+
AND: [
|
|
387
|
+
{
|
|
388
|
+
OR: [
|
|
389
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
|
|
390
|
+
{ conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
|
|
391
|
+
]
|
|
392
|
+
}
|
|
393
|
+
{
|
|
394
|
+
OR: [
|
|
395
|
+
{ conditions: [{ field: "author", path: "country", operator: EQ, value: "UK" }] }
|
|
396
|
+
{ conditions: [{ field: "author", path: "country", operator: EQ, value: "US" }] }
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
) {
|
|
401
|
+
id
|
|
402
|
+
title
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
This translates to: `rating >= 5.0 AND (category = "Sci-Fi" OR category = "Fantasy") AND (author.country = "UK" OR author.country = "US")`.
|
|
408
|
+
|
|
409
|
+
#### Collection Filtering with AND/OR
|
|
410
|
+
|
|
411
|
+
AND/OR filters are also available on collection fields (one-to-many relationships). The auto-generated resolvers for collection fields support the same `AND` and `OR` arguments:
|
|
412
|
+
|
|
413
|
+
```graphql
|
|
414
|
+
query {
|
|
415
|
+
series {
|
|
416
|
+
seasons(
|
|
417
|
+
OR: [
|
|
418
|
+
{ conditions: [{ field: "year", operator: EQ, value: 2020 }] }
|
|
419
|
+
{ conditions: [{ field: "year", operator: EQ, value: 2021 }] }
|
|
420
|
+
]
|
|
421
|
+
) {
|
|
422
|
+
number
|
|
423
|
+
year
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
You can mix flat collection filters with AND/OR:
|
|
430
|
+
|
|
431
|
+
```graphql
|
|
432
|
+
query {
|
|
433
|
+
series {
|
|
434
|
+
seasons(
|
|
435
|
+
number: { operator: GT, value: 1 }
|
|
436
|
+
OR: [
|
|
437
|
+
{ conditions: [{ field: "year", operator: EQ, value: 2020 }] }
|
|
438
|
+
{
|
|
439
|
+
AND: [
|
|
440
|
+
{ conditions: [{ field: "year", operator: GTE, value: 2022 }] }
|
|
441
|
+
{ conditions: [
|
|
442
|
+
{ field: "episodes", path: "name", operator: LIKE, value: "Final" }
|
|
443
|
+
] }
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
) {
|
|
448
|
+
number
|
|
449
|
+
year
|
|
450
|
+
episodes { name }
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
This translates to: `number > 1 AND (year = 2020 OR (year >= 2022 AND episodes.name LIKE "Final"))`.
|
|
457
|
+
|
|
458
|
+
#### Filter Types Reference
|
|
459
|
+
|
|
460
|
+
| Type | Fields | Description |
|
|
461
|
+
|------|--------|-------------|
|
|
462
|
+
| `QLFilterGroup` | `AND: [QLFilterGroup]`, `OR: [QLFilterGroup]`, `conditions: [QLFilterCondition]` | Recursive logical group |
|
|
463
|
+
| `QLFilterCondition` | `field: String!`, `operator: QLOperator`, `value: QLValue`, `path: String` | Individual filter condition |
|
|
464
|
+
|
|
465
|
+
- `field` identifies the entity field by name (e.g., `"title"`, `"author"`)
|
|
466
|
+
- `path` is required for object/relationship fields (e.g., `"name"`, `"country.name"`)
|
|
467
|
+
- Multiple `conditions` in the same group are combined with AND
|
|
468
|
+
- Maximum nesting depth: 5 levels
|
|
469
|
+
|
|
268
470
|
### Collection Field Filtering
|
|
269
471
|
|
|
270
472
|
Simfinity.js now supports filtering collection fields (one-to-many relationships) using the same powerful query format. This allows you to filter related objects directly within your GraphQL queries.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -167,6 +167,25 @@ const QLTypeFilterExpression = new GraphQLInputObjectType({
|
|
|
167
167
|
}),
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
const QLFilterCondition = new GraphQLInputObjectType({
|
|
171
|
+
name: 'QLFilterCondition',
|
|
172
|
+
fields: () => ({
|
|
173
|
+
field: { type: new GraphQLNonNull(GraphQLString) },
|
|
174
|
+
operator: { type: QLOperator },
|
|
175
|
+
value: { type: QLValue },
|
|
176
|
+
path: { type: GraphQLString },
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const QLFilterGroup = new GraphQLInputObjectType({
|
|
181
|
+
name: 'QLFilterGroup',
|
|
182
|
+
fields: () => ({
|
|
183
|
+
AND: { type: new GraphQLList(QLFilterGroup) },
|
|
184
|
+
OR: { type: new GraphQLList(QLFilterGroup) },
|
|
185
|
+
conditions: { type: new GraphQLList(QLFilterCondition) },
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
|
|
170
189
|
const QLPagination = new GraphQLInputObjectType({
|
|
171
190
|
name: 'QLPagination',
|
|
172
191
|
fields: () => ({
|
|
@@ -1492,10 +1511,122 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1492
1511
|
return { aggregateClauses, matchesClauses };
|
|
1493
1512
|
};
|
|
1494
1513
|
|
|
1514
|
+
const MAX_FILTER_GROUP_DEPTH = 5;
|
|
1515
|
+
|
|
1516
|
+
const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, aggregationsIncluded, depth = 0) => {
|
|
1517
|
+
if (depth > MAX_FILTER_GROUP_DEPTH) {
|
|
1518
|
+
throw new SimfinityError('Filter nesting too deep', 'FILTER_DEPTH_EXCEEDED', 400);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const parts = [];
|
|
1522
|
+
const fields = gqltype.getFields();
|
|
1523
|
+
|
|
1524
|
+
// Process leaf conditions
|
|
1525
|
+
if (filterGroup.conditions && filterGroup.conditions.length > 0) {
|
|
1526
|
+
for (const condition of filterGroup.conditions) {
|
|
1527
|
+
const qlField = fields[condition.field];
|
|
1528
|
+
if (!qlField) {
|
|
1529
|
+
throw new SimfinityError(
|
|
1530
|
+
`Unknown filter field: ${condition.field}`,
|
|
1531
|
+
'INVALID_FILTER_FIELD',
|
|
1532
|
+
400,
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
let filterInput;
|
|
1537
|
+
let fieldType = qlField.type;
|
|
1538
|
+
if (fieldType instanceof GraphQLList || fieldType instanceof GraphQLNonNull) {
|
|
1539
|
+
fieldType = fieldType.ofType;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (fieldType instanceof GraphQLObjectType
|
|
1543
|
+
|| isNonNullOfType(fieldType, GraphQLObjectType)) {
|
|
1544
|
+
// Object/relation field — wrap as QLTypeFilterExpression shape
|
|
1545
|
+
if (!condition.path) {
|
|
1546
|
+
throw new SimfinityError(
|
|
1547
|
+
`Filter on object field "${condition.field}" requires a path`,
|
|
1548
|
+
'MISSING_FILTER_PATH',
|
|
1549
|
+
400,
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
filterInput = {
|
|
1553
|
+
terms: [{
|
|
1554
|
+
path: condition.path,
|
|
1555
|
+
operator: condition.operator,
|
|
1556
|
+
value: condition.value,
|
|
1557
|
+
}],
|
|
1558
|
+
};
|
|
1559
|
+
} else {
|
|
1560
|
+
// Scalar/enum field
|
|
1561
|
+
filterInput = {
|
|
1562
|
+
operator: condition.operator,
|
|
1563
|
+
value: condition.value,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const result = await buildQueryTerms(filterInput, qlField, condition.field);
|
|
1568
|
+
|
|
1569
|
+
if (result) {
|
|
1570
|
+
// Collect lookups (deduplicated)
|
|
1571
|
+
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1572
|
+
if (!aggregationsIncluded[prop]) {
|
|
1573
|
+
aggregateClauses.push(aggregate.lookup);
|
|
1574
|
+
aggregateClauses.push(aggregate.unwind);
|
|
1575
|
+
aggregationsIncluded[prop] = true;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Collect match conditions
|
|
1580
|
+
for (const matchClause of Object.values(result.matchesClauses)) {
|
|
1581
|
+
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1582
|
+
parts.push({ [matchKey]: match });
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// Process AND sub-groups
|
|
1590
|
+
if (filterGroup.AND && filterGroup.AND.length > 0) {
|
|
1591
|
+
for (const subGroup of filterGroup.AND) {
|
|
1592
|
+
const subMatch = await buildFilterGroupMatch(
|
|
1593
|
+
subGroup, gqltype, aggregateClauses, aggregationsIncluded, depth + 1,
|
|
1594
|
+
);
|
|
1595
|
+
if (subMatch) {
|
|
1596
|
+
parts.push(subMatch);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Process OR sub-groups
|
|
1602
|
+
if (filterGroup.OR && filterGroup.OR.length > 0) {
|
|
1603
|
+
const orParts = [];
|
|
1604
|
+
for (const subGroup of filterGroup.OR) {
|
|
1605
|
+
const subMatch = await buildFilterGroupMatch(
|
|
1606
|
+
subGroup, gqltype, aggregateClauses, aggregationsIncluded, depth + 1,
|
|
1607
|
+
);
|
|
1608
|
+
if (subMatch) {
|
|
1609
|
+
orParts.push(subMatch);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (orParts.length === 1) {
|
|
1613
|
+
parts.push(orParts[0]);
|
|
1614
|
+
} else if (orParts.length > 1) {
|
|
1615
|
+
parts.push({ $or: orParts });
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
if (parts.length === 0) return null;
|
|
1620
|
+
if (parts.length === 1) return parts[0];
|
|
1621
|
+
return { $and: parts };
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
const RESERVED_QUERY_KEYS = new Set(['pagination', 'sort', 'AND', 'OR', 'aggregation']);
|
|
1625
|
+
|
|
1495
1626
|
const buildQuery = async (input, gqltype, isCount) => {
|
|
1496
1627
|
const aggregateClauses = [];
|
|
1497
|
-
const
|
|
1498
|
-
let
|
|
1628
|
+
const flatMatchConditions = {};
|
|
1629
|
+
let hasFlat = false;
|
|
1499
1630
|
let limitClause = { $limit: 100 };
|
|
1500
1631
|
let skipClause = { $skip: 0 };
|
|
1501
1632
|
let sortClause = {};
|
|
@@ -1503,7 +1634,7 @@ const buildQuery = async (input, gqltype, isCount) => {
|
|
|
1503
1634
|
const aggregationsIncluded = {};
|
|
1504
1635
|
|
|
1505
1636
|
for (const [key, filterField] of Object.entries(input)) {
|
|
1506
|
-
if (Object.prototype.hasOwnProperty.call(input, key) && key
|
|
1637
|
+
if (Object.prototype.hasOwnProperty.call(input, key) && !RESERVED_QUERY_KEYS.has(key)) {
|
|
1507
1638
|
const qlField = gqltype.getFields()[key];
|
|
1508
1639
|
|
|
1509
1640
|
const result = await buildQueryTerms(filterField, qlField, key);
|
|
@@ -1519,8 +1650,8 @@ const buildQuery = async (input, gqltype, isCount) => {
|
|
|
1519
1650
|
if (Object.prototype.hasOwnProperty.call(result.matchesClauses, matchClauseKey)) {
|
|
1520
1651
|
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1521
1652
|
if (Object.prototype.hasOwnProperty.call(matchClause, matchKey)) {
|
|
1522
|
-
|
|
1523
|
-
|
|
1653
|
+
flatMatchConditions[matchKey] = match;
|
|
1654
|
+
hasFlat = true;
|
|
1524
1655
|
}
|
|
1525
1656
|
}
|
|
1526
1657
|
}
|
|
@@ -1539,9 +1670,9 @@ const buildQuery = async (input, gqltype, isCount) => {
|
|
|
1539
1670
|
|
|
1540
1671
|
if (sort.field.indexOf('.') >= 0) {
|
|
1541
1672
|
const sortParts = sort.field.split('.');
|
|
1542
|
-
|
|
1673
|
+
|
|
1543
1674
|
fixedSortField = sortParts[0];
|
|
1544
|
-
|
|
1675
|
+
|
|
1545
1676
|
for (let i = 1; i < sortParts.length - 1; i++) {
|
|
1546
1677
|
fixedSortField += `_${sortParts[i]}`;
|
|
1547
1678
|
}
|
|
@@ -1564,8 +1695,45 @@ const buildQuery = async (input, gqltype, isCount) => {
|
|
|
1564
1695
|
}
|
|
1565
1696
|
}
|
|
1566
1697
|
|
|
1567
|
-
|
|
1568
|
-
|
|
1698
|
+
// Combine flat conditions with AND/OR groups
|
|
1699
|
+
const topLevelAndParts = [];
|
|
1700
|
+
|
|
1701
|
+
if (hasFlat) {
|
|
1702
|
+
topLevelAndParts.push(flatMatchConditions);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
if (input.AND && input.AND.length > 0) {
|
|
1706
|
+
for (const group of input.AND) {
|
|
1707
|
+
const groupMatch = await buildFilterGroupMatch(
|
|
1708
|
+
group, gqltype, aggregateClauses, aggregationsIncluded,
|
|
1709
|
+
);
|
|
1710
|
+
if (groupMatch) {
|
|
1711
|
+
topLevelAndParts.push(groupMatch);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (input.OR && input.OR.length > 0) {
|
|
1717
|
+
const orParts = [];
|
|
1718
|
+
for (const group of input.OR) {
|
|
1719
|
+
const groupMatch = await buildFilterGroupMatch(
|
|
1720
|
+
group, gqltype, aggregateClauses, aggregationsIncluded,
|
|
1721
|
+
);
|
|
1722
|
+
if (groupMatch) {
|
|
1723
|
+
orParts.push(groupMatch);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (orParts.length === 1) {
|
|
1727
|
+
topLevelAndParts.push(orParts[0]);
|
|
1728
|
+
} else if (orParts.length > 1) {
|
|
1729
|
+
topLevelAndParts.push({ $or: orParts });
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
if (topLevelAndParts.length === 1) {
|
|
1734
|
+
aggregateClauses.push({ $match: topLevelAndParts[0] });
|
|
1735
|
+
} else if (topLevelAndParts.length > 1) {
|
|
1736
|
+
aggregateClauses.push({ $match: { $and: topLevelAndParts } });
|
|
1569
1737
|
}
|
|
1570
1738
|
|
|
1571
1739
|
if (addSort && !isCount) {
|
|
@@ -1653,33 +1821,33 @@ const buildFieldPath = (gqltype, fieldPath) => {
|
|
|
1653
1821
|
|
|
1654
1822
|
const buildAggregationQuery = async (input, gqltype, aggregationExpression) => {
|
|
1655
1823
|
const aggregateClauses = [];
|
|
1656
|
-
const
|
|
1657
|
-
let
|
|
1824
|
+
const flatMatchConditions = {};
|
|
1825
|
+
let hasFlat = false;
|
|
1658
1826
|
const aggregationsIncluded = {};
|
|
1659
1827
|
const sortTerms = []; // Store multiple sort terms
|
|
1660
1828
|
let limitClause = null;
|
|
1661
1829
|
let skipClause = null;
|
|
1662
|
-
|
|
1830
|
+
|
|
1663
1831
|
// Build filter and lookup clauses (similar to buildQuery)
|
|
1664
1832
|
for (const [key, filterField] of Object.entries(input)) {
|
|
1665
|
-
if (Object.prototype.hasOwnProperty.call(input, key) && key
|
|
1833
|
+
if (Object.prototype.hasOwnProperty.call(input, key) && !RESERVED_QUERY_KEYS.has(key)) {
|
|
1666
1834
|
const qlField = gqltype.getFields()[key];
|
|
1667
|
-
|
|
1835
|
+
|
|
1668
1836
|
const result = await buildQueryTerms(filterField, qlField, key);
|
|
1669
|
-
|
|
1837
|
+
|
|
1670
1838
|
if (result) {
|
|
1671
1839
|
for (const [prop, aggregate] of Object.entries(result.aggregateClauses)) {
|
|
1672
1840
|
aggregateClauses.push(aggregate.lookup);
|
|
1673
1841
|
aggregateClauses.push(aggregate.unwind);
|
|
1674
1842
|
aggregationsIncluded[prop] = true;
|
|
1675
1843
|
}
|
|
1676
|
-
|
|
1844
|
+
|
|
1677
1845
|
for (const [matchClauseKey, matchClause] of Object.entries(result.matchesClauses)) {
|
|
1678
1846
|
if (Object.prototype.hasOwnProperty.call(result.matchesClauses, matchClauseKey)) {
|
|
1679
1847
|
for (const [matchKey, match] of Object.entries(matchClause)) {
|
|
1680
1848
|
if (Object.prototype.hasOwnProperty.call(matchClause, matchKey)) {
|
|
1681
|
-
|
|
1682
|
-
|
|
1849
|
+
flatMatchConditions[matchKey] = match;
|
|
1850
|
+
hasFlat = true;
|
|
1683
1851
|
}
|
|
1684
1852
|
}
|
|
1685
1853
|
}
|
|
@@ -1702,9 +1870,46 @@ const buildAggregationQuery = async (input, gqltype, aggregationExpression) => {
|
|
|
1702
1870
|
}
|
|
1703
1871
|
}
|
|
1704
1872
|
}
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1873
|
+
|
|
1874
|
+
// Combine flat conditions with AND/OR groups
|
|
1875
|
+
const topLevelAndParts = [];
|
|
1876
|
+
|
|
1877
|
+
if (hasFlat) {
|
|
1878
|
+
topLevelAndParts.push(flatMatchConditions);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
if (input.AND && input.AND.length > 0) {
|
|
1882
|
+
for (const group of input.AND) {
|
|
1883
|
+
const groupMatch = await buildFilterGroupMatch(
|
|
1884
|
+
group, gqltype, aggregateClauses, aggregationsIncluded,
|
|
1885
|
+
);
|
|
1886
|
+
if (groupMatch) {
|
|
1887
|
+
topLevelAndParts.push(groupMatch);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
if (input.OR && input.OR.length > 0) {
|
|
1893
|
+
const orParts = [];
|
|
1894
|
+
for (const group of input.OR) {
|
|
1895
|
+
const groupMatch = await buildFilterGroupMatch(
|
|
1896
|
+
group, gqltype, aggregateClauses, aggregationsIncluded,
|
|
1897
|
+
);
|
|
1898
|
+
if (groupMatch) {
|
|
1899
|
+
orParts.push(groupMatch);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (orParts.length === 1) {
|
|
1903
|
+
topLevelAndParts.push(orParts[0]);
|
|
1904
|
+
} else if (orParts.length > 1) {
|
|
1905
|
+
topLevelAndParts.push({ $or: orParts });
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
if (topLevelAndParts.length === 1) {
|
|
1910
|
+
aggregateClauses.push({ $match: topLevelAndParts[0] });
|
|
1911
|
+
} else if (topLevelAndParts.length > 1) {
|
|
1912
|
+
aggregateClauses.push({ $match: { $and: topLevelAndParts } });
|
|
1708
1913
|
}
|
|
1709
1914
|
|
|
1710
1915
|
// Now build the aggregation with $group
|
|
@@ -2150,6 +2355,8 @@ export { default as scalars } from './scalars.js';
|
|
|
2150
2355
|
export { default as plugins } from './plugins.js';
|
|
2151
2356
|
export { default as auth } from './auth/index.js';
|
|
2152
2357
|
|
|
2358
|
+
export { buildQuery, buildFilterGroupMatch };
|
|
2359
|
+
|
|
2153
2360
|
const createArgsForQuery = (argTypes) => {
|
|
2154
2361
|
const argsObject = {};
|
|
2155
2362
|
|
|
@@ -2182,6 +2389,13 @@ const createArgsForQuery = (argTypes) => {
|
|
|
2182
2389
|
|
|
2183
2390
|
argsObject.sort = {};
|
|
2184
2391
|
argsObject.sort.type = QLSortExpression;
|
|
2392
|
+
|
|
2393
|
+
argsObject.AND = {};
|
|
2394
|
+
argsObject.AND.type = new GraphQLList(QLFilterGroup);
|
|
2395
|
+
|
|
2396
|
+
argsObject.OR = {};
|
|
2397
|
+
argsObject.OR.type = new GraphQLList(QLFilterGroup);
|
|
2398
|
+
|
|
2185
2399
|
return argsObject;
|
|
2186
2400
|
};
|
|
2187
2401
|
|