@simtlix/simfinity-js 2.4.4 → 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.
Files changed (27) hide show
  1. package/.claude/worktrees/agitated-kepler/.claude/settings.local.json +23 -0
  2. package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +235 -0
  3. package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +567 -0
  4. package/.claude/worktrees/agitated-kepler/LICENSE +201 -0
  5. package/.claude/worktrees/agitated-kepler/README.md +3941 -0
  6. package/.claude/worktrees/agitated-kepler/eslint.config.mjs +71 -0
  7. package/.claude/worktrees/agitated-kepler/package-lock.json +4740 -0
  8. package/.claude/worktrees/agitated-kepler/package.json +41 -0
  9. package/.claude/worktrees/agitated-kepler/src/auth/errors.js +44 -0
  10. package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +273 -0
  11. package/.claude/worktrees/agitated-kepler/src/auth/index.js +391 -0
  12. package/.claude/worktrees/agitated-kepler/src/auth/rules.js +274 -0
  13. package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +39 -0
  14. package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +28 -0
  15. package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +39 -0
  16. package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +11 -0
  17. package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +15 -0
  18. package/.claude/worktrees/agitated-kepler/src/index.js +2412 -0
  19. package/.claude/worktrees/agitated-kepler/src/plugins.js +53 -0
  20. package/.claude/worktrees/agitated-kepler/src/scalars.js +188 -0
  21. package/.claude/worktrees/agitated-kepler/src/validators.js +250 -0
  22. package/.claude/worktrees/agitated-kepler/yarn.lock +1154 -0
  23. package/.cursor/rules/simfinity-core-functions.mdc +3 -1
  24. package/README.md +202 -0
  25. package/git-report.js +224 -0
  26. package/package.json +1 -1
  27. package/src/index.js +237 -23
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: () => ({
@@ -416,8 +435,8 @@ const buildInputType = (gqltype) => {
416
435
  if (fieldEntry.type.ofType === gqltype) {
417
436
  selfReferenceCollections[fieldEntryName] = fieldEntry;
418
437
  } else {
419
- const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, 'A', fieldEntry.extensions?.relation?.connectionField);
420
- const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, 'U', fieldEntry.extensions?.relation?.connectionField);
438
+ const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, gqltype.name + 'A', fieldEntry.extensions?.relation?.connectionField);
439
+ const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, gqltype.name +'U', fieldEntry.extensions?.relation?.connectionField);
421
440
  if (listInputTypeForAdd && listInputTypeForUpdate) {
422
441
  fieldArg.type = listInputTypeForAdd;
423
442
  fieldArgForUpdate.type = listInputTypeForUpdate;
@@ -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 matchesClauses = { $match: {} };
1498
- let addMatch = false;
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 !== 'pagination' && key !== 'sort') {
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
- matchesClauses.$match[matchKey] = match;
1523
- addMatch = true;
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
- if (addMatch) {
1568
- aggregateClauses.push(matchesClauses);
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 matchesClauses = { $match: {} };
1657
- let addMatch = false;
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 !== 'pagination' && key !== 'sort' && key !== 'aggregation') {
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
- matchesClauses.$match[matchKey] = match;
1682
- addMatch = true;
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
- if (addMatch) {
1707
- aggregateClauses.push(matchesClauses);
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