@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/README.md CHANGED
@@ -7,6 +7,7 @@ A powerful Node.js framework that automatically generates GraphQL schemas from y
7
7
  - **Automatic Schema Generation**: Define your object model, and Simfinity.js generates all queries and mutations
8
8
  - **MongoDB Integration**: Seamless translation between GraphQL and MongoDB
9
9
  - **Powerful Querying**: Any query that can be executed in MongoDB can be executed in GraphQL
10
+ - **Aggregation Queries**: Built-in support for GROUP BY queries with aggregation operations (SUM, COUNT, AVG, MIN, MAX)
10
11
  - **Auto-Generated Resolvers**: Automatically generates resolve methods for relationship fields
11
12
  - **Automatic Index Creation**: Automatically creates MongoDB indexes for all ObjectId fields, including nested embedded objects and relationship fields
12
13
  - **Business Logic**: Implement business logic and domain validations declaratively
@@ -1100,9 +1101,117 @@ mutation {
1100
1101
 
1101
1102
  ## ✅ Validations
1102
1103
 
1103
- ### Field-Level Validations
1104
+ ### Declarative Validation Helpers
1104
1105
 
1105
- Add validation logic directly to fields:
1106
+ Simfinity.js provides built-in validation helpers to simplify common validation patterns, eliminating verbose boilerplate code.
1107
+
1108
+ #### Using Validators
1109
+
1110
+ ```javascript
1111
+ const { validators } = require('@simtlix/simfinity-js');
1112
+
1113
+ const PersonType = new GraphQLObjectType({
1114
+ name: 'Person',
1115
+ fields: () => ({
1116
+ id: { type: GraphQLID },
1117
+ name: {
1118
+ type: GraphQLString,
1119
+ extensions: {
1120
+ validations: validators.stringLength('Name', 2, 100)
1121
+ }
1122
+ },
1123
+ email: {
1124
+ type: GraphQLString,
1125
+ extensions: {
1126
+ validations: validators.email()
1127
+ }
1128
+ },
1129
+ website: {
1130
+ type: GraphQLString,
1131
+ extensions: {
1132
+ validations: validators.url()
1133
+ }
1134
+ },
1135
+ age: {
1136
+ type: GraphQLInt,
1137
+ extensions: {
1138
+ validations: validators.numberRange('Age', 0, 120)
1139
+ }
1140
+ },
1141
+ price: {
1142
+ type: GraphQLFloat,
1143
+ extensions: {
1144
+ validations: validators.positive('Price')
1145
+ }
1146
+ }
1147
+ })
1148
+ });
1149
+ ```
1150
+
1151
+ #### Available Validators
1152
+
1153
+ **String Validators:**
1154
+ - `validators.stringLength(name, min, max)` - Validates string length with min/max bounds (required for CREATE)
1155
+ - `validators.maxLength(name, max)` - Validates maximum string length
1156
+ - `validators.pattern(name, regex, message)` - Validates against a regex pattern
1157
+ - `validators.email()` - Validates email format
1158
+ - `validators.url()` - Validates URL format
1159
+
1160
+ **Number Validators:**
1161
+ - `validators.numberRange(name, min, max)` - Validates number range
1162
+ - `validators.positive(name)` - Ensures number is positive
1163
+
1164
+ **Array Validators:**
1165
+ - `validators.arrayLength(name, maxItems, itemValidator)` - Validates array length and optionally each item
1166
+
1167
+ **Date Validators:**
1168
+ - `validators.dateFormat(name, format)` - Validates date format
1169
+ - `validators.futureDate(name)` - Ensures date is in the future
1170
+
1171
+ #### Validator Features
1172
+
1173
+ - **Automatic Operation Handling**: Validators work for both `CREATE` (save) and `UPDATE` operations
1174
+ - **Smart Validation**: For CREATE operations, values are required. For UPDATE operations, undefined/null values are allowed (field might not be updated)
1175
+ - **Consistent Error Messages**: All validators throw `SimfinityError` with appropriate messages
1176
+
1177
+ #### Example: Multiple Validators
1178
+
1179
+ ```javascript
1180
+ const ProductType = new GraphQLObjectType({
1181
+ name: 'Product',
1182
+ fields: () => ({
1183
+ id: { type: GraphQLID },
1184
+ name: {
1185
+ type: GraphQLString,
1186
+ extensions: {
1187
+ validations: validators.stringLength('Product Name', 3, 200)
1188
+ }
1189
+ },
1190
+ sku: {
1191
+ type: GraphQLString,
1192
+ extensions: {
1193
+ validations: validators.pattern('SKU', /^[A-Z0-9-]+$/, 'SKU must be uppercase alphanumeric with hyphens')
1194
+ }
1195
+ },
1196
+ price: {
1197
+ type: GraphQLFloat,
1198
+ extensions: {
1199
+ validations: validators.positive('Price')
1200
+ }
1201
+ },
1202
+ tags: {
1203
+ type: new GraphQLList(GraphQLString),
1204
+ extensions: {
1205
+ validations: validators.arrayLength('Tags', 10)
1206
+ }
1207
+ }
1208
+ })
1209
+ });
1210
+ ```
1211
+
1212
+ ### Field-Level Validations (Manual)
1213
+
1214
+ For custom validation logic, you can still write manual validators:
1106
1215
 
1107
1216
  ```javascript
1108
1217
  const { SimfinityError } = require('@simtlix/simfinity-js');
@@ -1188,7 +1297,78 @@ const OrderType = new GraphQLObjectType({
1188
1297
 
1189
1298
  ### Custom Validated Scalar Types
1190
1299
 
1191
- Create custom scalar types with built-in validation. The generated type names follow the pattern `{name}_{baseScalarTypeName}`:
1300
+ Create custom scalar types with built-in validation. The generated type names follow the pattern `{name}_{baseScalarTypeName}`.
1301
+
1302
+ #### Pre-built Scalars
1303
+
1304
+ Simfinity.js provides ready-to-use validated scalars for common patterns:
1305
+
1306
+ ```javascript
1307
+ const { scalars } = require('@simtlix/simfinity-js');
1308
+
1309
+ const UserType = new GraphQLObjectType({
1310
+ name: 'User',
1311
+ fields: () => ({
1312
+ id: { type: GraphQLID },
1313
+ email: { type: scalars.EmailScalar }, // Type name: Email_String
1314
+ website: { type: scalars.URLScalar }, // Type name: URL_String
1315
+ age: { type: scalars.PositiveIntScalar }, // Type name: PositiveInt_Int
1316
+ price: { type: scalars.PositiveFloatScalar } // Type name: PositiveFloat_Float
1317
+ }),
1318
+ });
1319
+ ```
1320
+
1321
+ **Available Pre-built Scalars:**
1322
+ - `scalars.EmailScalar` - Validates email format (`Email_String`)
1323
+ - `scalars.URLScalar` - Validates URL format (`URL_String`)
1324
+ - `scalars.PositiveIntScalar` - Validates positive integers (`PositiveInt_Int`)
1325
+ - `scalars.PositiveFloatScalar` - Validates positive floats (`PositiveFloat_Float`)
1326
+
1327
+ #### Factory Functions for Custom Scalars
1328
+
1329
+ Create custom validated scalars with parameters:
1330
+
1331
+ ```javascript
1332
+ const { scalars } = require('@simtlix/simfinity-js');
1333
+
1334
+ // Create a bounded string scalar (name length between 2-100 characters)
1335
+ const NameScalar = scalars.createBoundedStringScalar('Name', 2, 100);
1336
+
1337
+ // Create a bounded integer scalar (age between 0-120)
1338
+ const AgeScalar = scalars.createBoundedIntScalar('Age', 0, 120);
1339
+
1340
+ // Create a bounded float scalar (rating between 0-10)
1341
+ const RatingScalar = scalars.createBoundedFloatScalar('Rating', 0, 10);
1342
+
1343
+ // Create a pattern-based string scalar (phone number format)
1344
+ const PhoneScalar = scalars.createPatternStringScalar(
1345
+ 'Phone',
1346
+ /^\+?[\d\s\-()]+$/,
1347
+ 'Invalid phone number format'
1348
+ );
1349
+
1350
+ // Use in your types
1351
+ const PersonType = new GraphQLObjectType({
1352
+ name: 'Person',
1353
+ fields: () => ({
1354
+ id: { type: GraphQLID },
1355
+ name: { type: NameScalar }, // Type name: Name_String
1356
+ age: { type: AgeScalar }, // Type name: Age_Int
1357
+ rating: { type: RatingScalar }, // Type name: Rating_Float
1358
+ phone: { type: PhoneScalar } // Type name: Phone_String
1359
+ }),
1360
+ });
1361
+ ```
1362
+
1363
+ **Available Factory Functions:**
1364
+ - `scalars.createBoundedStringScalar(name, min, max)` - String with length bounds
1365
+ - `scalars.createBoundedIntScalar(name, min, max)` - Integer with range validation
1366
+ - `scalars.createBoundedFloatScalar(name, min, max)` - Float with range validation
1367
+ - `scalars.createPatternStringScalar(name, pattern, message)` - String with regex pattern validation
1368
+
1369
+ #### Creating Custom Scalars Manually
1370
+
1371
+ You can also create custom scalars using `createValidatedScalar` directly:
1192
1372
 
1193
1373
  ```javascript
1194
1374
  const { GraphQLString, GraphQLInt } = require('graphql');
@@ -1358,6 +1538,238 @@ console.log(UserType.getFields()); // Access GraphQL fields
1358
1538
  const BookInput = simfinity.getInputType(BookType);
1359
1539
  ```
1360
1540
 
1541
+ ## 📊 Aggregation Queries
1542
+
1543
+ Simfinity.js now supports powerful GraphQL aggregation queries with GROUP BY functionality, allowing you to perform aggregate operations (SUM, COUNT, AVG, MIN, MAX) on your data.
1544
+
1545
+ ### Overview
1546
+
1547
+ For each entity type registered with `connect()`, an additional aggregation endpoint is automatically generated with the format `{entityname}_aggregate`.
1548
+
1549
+ ### Features
1550
+
1551
+ - **Group By**: Group results by any field (direct or related entity field path)
1552
+ - **Aggregation Operations**: SUM, COUNT, AVG, MIN, MAX
1553
+ - **Filtering**: Use the same filter parameters as regular queries
1554
+ - **Sorting**: Sort by groupId or any calculated fact (metrics), with support for multiple sort fields
1555
+ - **Pagination**: Use the same pagination parameters as regular queries
1556
+ - **Related Entity Fields**: Group by or aggregate on fields from related entities using dot notation
1557
+
1558
+ ### GraphQL Types
1559
+
1560
+ #### QLAggregationOperation (Enum)
1561
+ - `SUM`: Sum of numeric values
1562
+ - `COUNT`: Count of records
1563
+ - `AVG`: Average of numeric values
1564
+ - `MIN`: Minimum value
1565
+ - `MAX`: Maximum value
1566
+
1567
+ #### QLTypeAggregationFact (Input)
1568
+ ```graphql
1569
+ input QLTypeAggregationFact {
1570
+ operation: QLAggregationOperation!
1571
+ factName: String!
1572
+ path: String!
1573
+ }
1574
+ ```
1575
+
1576
+ #### QLTypeAggregationExpression (Input)
1577
+ ```graphql
1578
+ input QLTypeAggregationExpression {
1579
+ groupId: String!
1580
+ facts: [QLTypeAggregationFact!]!
1581
+ }
1582
+ ```
1583
+
1584
+ #### QLTypeAggregationResult (Output)
1585
+ ```graphql
1586
+ type QLTypeAggregationResult {
1587
+ groupId: JSON
1588
+ facts: JSON
1589
+ }
1590
+ ```
1591
+
1592
+ ### Quick Examples
1593
+
1594
+ #### Simple Group By
1595
+ ```graphql
1596
+ query {
1597
+ series_aggregate(
1598
+ aggregation: {
1599
+ groupId: "category"
1600
+ facts: [
1601
+ { operation: COUNT, factName: "total", path: "id" }
1602
+ ]
1603
+ }
1604
+ ) {
1605
+ groupId
1606
+ facts
1607
+ }
1608
+ }
1609
+ ```
1610
+
1611
+ #### Group By Related Entity
1612
+ ```graphql
1613
+ query {
1614
+ series_aggregate(
1615
+ aggregation: {
1616
+ groupId: "country.name"
1617
+ facts: [
1618
+ { operation: COUNT, factName: "count", path: "id" }
1619
+ { operation: AVG, factName: "avgRating", path: "rating" }
1620
+ ]
1621
+ }
1622
+ ) {
1623
+ groupId
1624
+ facts
1625
+ }
1626
+ }
1627
+ ```
1628
+
1629
+ #### Multiple Aggregation Facts
1630
+ ```graphql
1631
+ query {
1632
+ series_aggregate(
1633
+ aggregation: {
1634
+ groupId: "category"
1635
+ facts: [
1636
+ { operation: COUNT, factName: "total", path: "id" }
1637
+ { operation: SUM, factName: "totalEpisodes", path: "episodeCount" }
1638
+ { operation: AVG, factName: "avgRating", path: "rating" }
1639
+ { operation: MIN, factName: "minRating", path: "rating" }
1640
+ { operation: MAX, factName: "maxRating", path: "rating" }
1641
+ ]
1642
+ }
1643
+ ) {
1644
+ groupId
1645
+ facts
1646
+ }
1647
+ }
1648
+ ```
1649
+
1650
+ #### With Filtering
1651
+ ```graphql
1652
+ query {
1653
+ series_aggregate(
1654
+ rating: { operator: GTE, value: 8.0 }
1655
+ aggregation: {
1656
+ groupId: "category"
1657
+ facts: [
1658
+ { operation: COUNT, factName: "highRated", path: "id" }
1659
+ ]
1660
+ }
1661
+ ) {
1662
+ groupId
1663
+ facts
1664
+ }
1665
+ }
1666
+ ```
1667
+
1668
+ #### Sorting by Multiple Fields
1669
+ ```graphql
1670
+ query {
1671
+ series_aggregate(
1672
+ sort: {
1673
+ terms: [
1674
+ { field: "total", order: "DESC" }, # Sort by count first
1675
+ { field: "groupId", order: "ASC" } # Then by name
1676
+ ]
1677
+ }
1678
+ aggregation: {
1679
+ groupId: "category"
1680
+ facts: [
1681
+ { operation: COUNT, factName: "total", path: "id" }
1682
+ { operation: AVG, factName: "avgRating", path: "rating" }
1683
+ ]
1684
+ }
1685
+ ) {
1686
+ groupId
1687
+ facts
1688
+ }
1689
+ }
1690
+ ```
1691
+
1692
+ #### With Pagination (Top 5)
1693
+ ```graphql
1694
+ query {
1695
+ series_aggregate(
1696
+ sort: {
1697
+ terms: [{ field: "total", order: "DESC" }]
1698
+ }
1699
+ pagination: {
1700
+ page: 1
1701
+ size: 5
1702
+ }
1703
+ aggregation: {
1704
+ groupId: "category"
1705
+ facts: [
1706
+ { operation: COUNT, factName: "total", path: "id" }
1707
+ ]
1708
+ }
1709
+ ) {
1710
+ groupId
1711
+ facts
1712
+ }
1713
+ }
1714
+ ```
1715
+
1716
+ ### Field Path Resolution
1717
+
1718
+ The `groupId` and `path` parameters support:
1719
+
1720
+ 1. **Direct Fields**: Simple field names from the entity
1721
+ - Example: `"category"`, `"rating"`, `"id"`
1722
+
1723
+ 2. **Related Entity Fields**: Dot notation for fields in related entities
1724
+ - Example: `"country.name"`, `"studio.foundedYear"`
1725
+
1726
+ 3. **Nested Related Entities**: Multiple levels of relationships
1727
+ - Example: `"country.region.name"`
1728
+
1729
+ ### Sorting Options
1730
+
1731
+ - Sort by **groupId** or **any fact name**
1732
+ - **Multiple sort fields supported** - results are sorted by the first field, then by the second field for ties, etc.
1733
+ - Set the `field` parameter to:
1734
+ - `"groupId"` to sort by the grouping field
1735
+ - Any fact name (e.g., `"avgRating"`, `"total"`) to sort by that calculated metric
1736
+ - The `order` parameter (ASC/DESC) determines the sort direction for each field
1737
+ - If a field doesn't match groupId or any fact name, it defaults to groupId
1738
+ - If no sort is specified, defaults to sorting by groupId ascending
1739
+
1740
+ ### Pagination Notes
1741
+
1742
+ - The `page` and `size` parameters work as expected
1743
+ - The `count` parameter is **ignored** for aggregation queries
1744
+ - Pagination is applied **after** grouping and sorting
1745
+
1746
+ ### MongoDB Translation
1747
+
1748
+ Aggregation queries are translated to efficient MongoDB aggregation pipelines:
1749
+
1750
+ 1. **$lookup**: Joins with related entity collections
1751
+ 2. **$unwind**: Flattens joined arrays
1752
+ 3. **$match**: Applies filters (before grouping)
1753
+ 4. **$group**: Groups by the specified field with aggregation operations
1754
+ 5. **$project**: Formats final output with groupId and facts fields
1755
+ 6. **$sort**: Sorts results by groupId or facts (with multiple fields support)
1756
+ 7. **$limit** / **$skip**: Applied for pagination (after sorting)
1757
+
1758
+ ### Result Structure
1759
+
1760
+ Results are returned in a consistent format:
1761
+ ```json
1762
+ {
1763
+ "groupId": <value>,
1764
+ "facts": {
1765
+ "factName1": <calculated_value>,
1766
+ "factName2": <calculated_value>
1767
+ }
1768
+ }
1769
+ ```
1770
+
1771
+ For complete documentation with more examples, see [AGGREGATION_EXAMPLE.md](./AGGREGATION_EXAMPLE.md) and [AGGREGATION_CHANGES_SUMMARY.md](./AGGREGATION_CHANGES_SUMMARY.md).
1772
+
1361
1773
  ## 📚 Complete Example
1362
1774
 
1363
1775
  Here's a complete bookstore example with relationships, validations, and state machines:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "2.0.2",
3
+ "version": "2.2.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "type": "module",