@simtlix/simfinity-js 1.5.0 โ†’ 1.7.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
@@ -146,13 +146,6 @@ const schema = simfinity.createSchema(
146
146
  ```javascript
147
147
  // Prevent automatic MongoDB collection creation (useful for testing)
148
148
  simfinity.preventCreatingCollection(true);
149
-
150
- // Add middleware for all operations
151
- simfinity.use((params, next) => {
152
- // params contains: type, args, operation, context
153
- console.log(`Executing ${params.operation} on ${params.type.name}`);
154
- next();
155
- });
156
149
  ```
157
150
 
158
151
  ## ๐Ÿ“‹ Basic Usage
@@ -210,6 +203,253 @@ query {
210
203
  - `NIN` - Not in array
211
204
  - `BTW` - Between two values
212
205
 
206
+ ## ๐Ÿ”ง Middlewares
207
+
208
+ Middlewares provide a powerful way to intercept and process all GraphQL operations before they execute. Use them for cross-cutting concerns like authentication, logging, validation, and performance monitoring.
209
+
210
+ ### Adding Middlewares
211
+
212
+ Register middlewares using `simfinity.use()`. Middlewares execute in the order they're registered:
213
+
214
+ ```javascript
215
+ // Basic logging middleware
216
+ simfinity.use((params, next) => {
217
+ console.log(`Executing ${params.operation} on ${params.type?.name || 'custom mutation'}`);
218
+ next();
219
+ });
220
+ ```
221
+
222
+ ### Middleware Parameters
223
+
224
+ Each middleware receives a `params` object containing:
225
+
226
+ ```javascript
227
+ simfinity.use((params, next) => {
228
+ // params object contains:
229
+ const {
230
+ type, // Type information (model, gqltype, controller, etc.)
231
+ args, // GraphQL arguments passed to the operation
232
+ operation, // Operation type: 'save', 'update', 'delete', 'get_by_id', 'find', 'state_changed', 'custom_mutation'
233
+ context, // GraphQL context object (includes request info, user data, etc.)
234
+ actionName, // For state machine actions (only present for state_changed operations)
235
+ actionField, // State machine action details (only present for state_changed operations)
236
+ entry // Custom mutation name (only present for custom_mutation operations)
237
+ } = params;
238
+
239
+ // Always call next() to continue the middleware chain
240
+ next();
241
+ });
242
+ ```
243
+
244
+ ### Common Use Cases
245
+
246
+ #### 1. Authentication & Authorization
247
+
248
+ ```javascript
249
+ simfinity.use((params, next) => {
250
+ const { context, operation, type } = params;
251
+
252
+ // Skip authentication for read operations
253
+ if (operation === 'get_by_id' || operation === 'find') {
254
+ return next();
255
+ }
256
+
257
+ // Check if user is authenticated
258
+ if (!context.user) {
259
+ throw new simfinity.SimfinityError('Authentication required', 'UNAUTHORIZED', 401);
260
+ }
261
+
262
+ // Check permissions for specific types
263
+ if (type?.name === 'User' && context.user.role !== 'admin') {
264
+ throw new simfinity.SimfinityError('Admin access required', 'FORBIDDEN', 403);
265
+ }
266
+
267
+ next();
268
+ });
269
+ ```
270
+
271
+ #### 2. Request Logging & Monitoring
272
+
273
+ ```javascript
274
+ simfinity.use((params, next) => {
275
+ const { operation, type, args, context } = params;
276
+ const startTime = Date.now();
277
+
278
+ console.log(`[${new Date().toISOString()}] Starting ${operation}${type ? ` on ${type.name}` : ''}`);
279
+
280
+ // Continue with the operation
281
+ next();
282
+
283
+ const duration = Date.now() - startTime;
284
+ console.log(`[${new Date().toISOString()}] Completed ${operation} in ${duration}ms`);
285
+ });
286
+ ```
287
+
288
+ #### 3. Input Validation & Sanitization
289
+
290
+ ```javascript
291
+ simfinity.use((params, next) => {
292
+ const { operation, args, type } = params;
293
+
294
+ // Validate input for save operations
295
+ if (operation === 'save' && args.input) {
296
+ // Trim string fields
297
+ Object.keys(args.input).forEach(key => {
298
+ if (typeof args.input[key] === 'string') {
299
+ args.input[key] = args.input[key].trim();
300
+ }
301
+ });
302
+
303
+ // Validate required business rules
304
+ if (type?.name === 'Book' && args.input.title && args.input.title.length < 3) {
305
+ throw new simfinity.SimfinityError('Book title must be at least 3 characters', 'VALIDATION_ERROR', 400);
306
+ }
307
+ }
308
+
309
+ next();
310
+ });
311
+ ```
312
+
313
+ #### 4. Rate Limiting
314
+
315
+ ```javascript
316
+ const requestCounts = new Map();
317
+
318
+ simfinity.use((params, next) => {
319
+ const { context, operation } = params;
320
+ const userId = context.user?.id || context.ip;
321
+ const now = Date.now();
322
+ const windowMs = 60000; // 1 minute
323
+ const maxRequests = 100;
324
+
325
+ // Only apply rate limiting to mutations
326
+ if (operation === 'save' || operation === 'update' || operation === 'delete') {
327
+ const userRequests = requestCounts.get(userId) || [];
328
+ const recentRequests = userRequests.filter(time => now - time < windowMs);
329
+
330
+ if (recentRequests.length >= maxRequests) {
331
+ throw new simfinity.SimfinityError('Rate limit exceeded', 'TOO_MANY_REQUESTS', 429);
332
+ }
333
+
334
+ recentRequests.push(now);
335
+ requestCounts.set(userId, recentRequests);
336
+ }
337
+
338
+ next();
339
+ });
340
+ ```
341
+
342
+ #### 5. Audit Trail
343
+
344
+ ```javascript
345
+ simfinity.use((params, next) => {
346
+ const { operation, type, args, context } = params;
347
+
348
+ // Log all mutations for audit purposes
349
+ if (operation === 'save' || operation === 'update' || operation === 'delete') {
350
+ const auditEntry = {
351
+ timestamp: new Date(),
352
+ user: context.user?.id,
353
+ operation,
354
+ type: type?.name,
355
+ entityId: args.id || 'new',
356
+ data: operation === 'delete' ? null : args.input,
357
+ ip: context.ip,
358
+ userAgent: context.userAgent
359
+ };
360
+
361
+ // Save to audit log (could be database, file, or external service)
362
+ console.log('AUDIT:', JSON.stringify(auditEntry));
363
+ }
364
+
365
+ next();
366
+ });
367
+ ```
368
+
369
+ ### Multiple Middlewares
370
+
371
+ Middlewares execute in registration order. Each middleware must call `next()` to continue the chain:
372
+
373
+ ```javascript
374
+ // Middleware 1: Authentication
375
+ simfinity.use((params, next) => {
376
+ console.log('1. Checking authentication...');
377
+ // Authentication logic here
378
+ next(); // Continue to next middleware
379
+ });
380
+
381
+ // Middleware 2: Authorization
382
+ simfinity.use((params, next) => {
383
+ console.log('2. Checking permissions...');
384
+ // Authorization logic here
385
+ next(); // Continue to next middleware
386
+ });
387
+
388
+ // Middleware 3: Logging
389
+ simfinity.use((params, next) => {
390
+ console.log('3. Logging request...');
391
+ // Logging logic here
392
+ next(); // Continue to GraphQL operation
393
+ });
394
+ ```
395
+
396
+ ### Error Handling in Middlewares
397
+
398
+ Middlewares can throw errors to stop the operation:
399
+
400
+ ```javascript
401
+ simfinity.use((params, next) => {
402
+ const { context, operation } = params;
403
+
404
+ try {
405
+ // Validation logic
406
+ if (!context.user && operation !== 'find') {
407
+ throw new simfinity.SimfinityError('Authentication required', 'UNAUTHORIZED', 401);
408
+ }
409
+
410
+ next(); // Continue only if validation passes
411
+ } catch (error) {
412
+ // Error automatically bubbles up to GraphQL error handling
413
+ throw error;
414
+ }
415
+ });
416
+ ```
417
+
418
+ ### Conditional Middleware Execution
419
+
420
+ Execute middleware logic conditionally based on operation type or context:
421
+
422
+ ```javascript
423
+ simfinity.use((params, next) => {
424
+ const { operation, type, context } = params;
425
+
426
+ // Only apply to specific types
427
+ if (type?.name === 'SensitiveData') {
428
+ // Special handling for sensitive data
429
+ if (!context.user?.hasHighSecurity) {
430
+ throw new simfinity.SimfinityError('High security clearance required', 'FORBIDDEN', 403);
431
+ }
432
+ }
433
+
434
+ // Only apply to mutation operations
435
+ if (['save', 'update', 'delete', 'state_changed'].includes(operation)) {
436
+ // Mutation-specific logic
437
+ console.log(`Mutation ${operation} executing...`);
438
+ }
439
+
440
+ next();
441
+ });
442
+ ```
443
+
444
+ ### Best Practices
445
+
446
+ 1. **Always call `next()`**: Failing to call `next()` will hang the request
447
+ 2. **Handle errors gracefully**: Use try-catch blocks for error-prone operations
448
+ 3. **Keep middlewares focused**: Each middleware should handle one concern
449
+ 4. **Order matters**: Register middlewares in logical order (auth โ†’ validation โ†’ logging)
450
+ 5. **Performance consideration**: Middlewares run on every operation, keep them lightweight
451
+ 6. **Use context wisely**: Store request-specific data in the GraphQL context object
452
+
213
453
  ## ๐Ÿ”— Relationships
214
454
 
215
455
  ### Defining Relationships
@@ -353,6 +593,104 @@ simfinity.addNoEndpointType(AuthorType);
353
593
 
354
594
  That's it! All relationship resolvers are automatically generated when you connect your types.
355
595
 
596
+ ### Adding Types Without Endpoints
597
+
598
+ Use `addNoEndpointType()` for types that should be included in the GraphQL schema but don't need their own CRUD operations:
599
+
600
+ ```javascript
601
+ simfinity.addNoEndpointType(TypeName);
602
+ ```
603
+
604
+ **When to use `addNoEndpointType()` vs `connect()`:**
605
+
606
+ | Method | Use Case | Creates Endpoints | Use Example |
607
+ |--------|----------|-------------------|-------------|
608
+ | `connect()` | Types that need CRUD operations | โœ… Yes | User, Product, Order |
609
+ | `addNoEndpointType()` | Types only used in relationships | โŒ No | Address, Settings, Director |
610
+
611
+ #### Perfect Example: TV Series with Embedded Director
612
+
613
+ From the [series-sample](https://github.com/simtlix/series-sample) project:
614
+
615
+ ```javascript
616
+ // Director type - Used only as embedded data, no direct API access needed
617
+ const directorType = new GraphQLObjectType({
618
+ name: 'director',
619
+ fields: () => ({
620
+ id: { type: GraphQLID },
621
+ name: { type: new GraphQLNonNull(GraphQLString) },
622
+ country: { type: GraphQLString }
623
+ })
624
+ });
625
+
626
+ // Add to schema WITHOUT creating endpoints
627
+ simfinity.addNoEndpointType(directorType);
628
+
629
+ // Serie type - Has its own endpoints and embeds director data
630
+ const serieType = new GraphQLObjectType({
631
+ name: 'serie',
632
+ fields: () => ({
633
+ id: { type: GraphQLID },
634
+ name: { type: new GraphQLNonNull(GraphQLString) },
635
+ categories: { type: new GraphQLList(GraphQLString) },
636
+ director: {
637
+ type: new GraphQLNonNull(directorType),
638
+ extensions: {
639
+ relation: {
640
+ embedded: true, // Director data stored within serie document
641
+ displayField: 'name'
642
+ }
643
+ }
644
+ }
645
+ })
646
+ });
647
+
648
+ // Create full CRUD endpoints for series
649
+ simfinity.connect(null, serieType, 'serie', 'series');
650
+ ```
651
+
652
+ **Result:**
653
+ - โœ… `addserie`, `updateserie`, `deleteserie` mutations available
654
+ - โœ… `serie`, `series` queries available
655
+ - โŒ No `adddirector`, `director`, `directors` endpoints (director is embedded)
656
+
657
+ **Usage:**
658
+ ```graphql
659
+ mutation {
660
+ addserie(input: {
661
+ name: "Breaking Bad"
662
+ categories: ["crime", "drama", "thriller"]
663
+ director: {
664
+ name: "Vince Gilligan"
665
+ country: "United States"
666
+ }
667
+ }) {
668
+ id
669
+ name
670
+ director {
671
+ name
672
+ country
673
+ }
674
+ }
675
+ }
676
+ ```
677
+
678
+ #### When to Use Each Approach
679
+
680
+ **Use `addNoEndpointType()` for:**
681
+ - Simple data objects with few fields
682
+ - Data that doesn't need CRUD operations
683
+ - Objects that belong to a single parent (1:1 relationships)
684
+ - Configuration or settings objects
685
+ - **Examples**: Address, Director info, Product specifications
686
+
687
+ **Use `connect()` for:**
688
+ - Complex entities that need their own endpoints
689
+ - Data that needs CRUD operations
690
+ - Objects shared between multiple parents (many:many relationships)
691
+ - Objects with business logic (controllers, state machines)
692
+ - **Examples**: User, Product, Order, Season, Episode
693
+
356
694
  ### Embedded vs Referenced Relationships
357
695
 
358
696
  **Referenced Relationships** (default):
@@ -853,7 +1191,7 @@ simfinity.registerMutation(
853
1191
 
854
1192
  ### Adding Types Without Endpoints
855
1193
 
856
- Include types in the schema without generating endpoints:
1194
+ Include types in the schema without generating endpoints. See the [detailed guide on addNoEndpointType()](#adding-types-without-endpoints) for when and how to use this pattern:
857
1195
 
858
1196
  ```javascript
859
1197
  // This type can be used in relationships but won't have queries/mutations
@@ -894,6 +1232,11 @@ const newBook = await simfinity.saveObject('Book', {
894
1232
  const BookModel = simfinity.getModel(BookType);
895
1233
  const books = await BookModel.find({ author: 'Douglas Adams' });
896
1234
 
1235
+ // Get the GraphQL type definition by name
1236
+ const UserType = simfinity.getType('User');
1237
+ console.log(UserType.name); // 'User'
1238
+ console.log(UserType.getFields()); // Access GraphQL fields
1239
+
897
1240
  // Get the input type for a GraphQL type
898
1241
  const BookInput = simfinity.getInputType(BookType);
899
1242
  ```
@@ -1031,3 +1374,1023 @@ Contributions are welcome! Please feel free to submit a Pull Request.
1031
1374
  *Built with โค๏ธ by [Simtlix](https://github.com/simtlix)*
1032
1375
 
1033
1376
 
1377
+ ## ๐Ÿ“š Query Examples from Series-Sample
1378
+
1379
+ Here are some practical GraphQL query examples from the series-sample project, showcasing how to use simfinity.js effectively:
1380
+
1381
+ ### 1. Series with Directors from a Specific Country
1382
+
1383
+ Find all series that have directors from the United States:
1384
+
1385
+ ```graphql
1386
+ query {
1387
+ series(director: {
1388
+ terms: [
1389
+ {
1390
+ path: "country",
1391
+ operator: EQ,
1392
+ value: "United States"
1393
+ }
1394
+ ]
1395
+ }) {
1396
+ id
1397
+ name
1398
+ categories
1399
+ director {
1400
+ name
1401
+ country
1402
+ }
1403
+ }
1404
+ }
1405
+ ```
1406
+
1407
+ ### 2. Series with a Specific Episode Name
1408
+
1409
+ Find series that contain an episode with the name "Pilot":
1410
+
1411
+ ```graphql
1412
+ query {
1413
+ series(
1414
+ seasons: {
1415
+ terms: [
1416
+ {
1417
+ path: "episodes.name",
1418
+ operator: EQ,
1419
+ value: "Pilot"
1420
+ }
1421
+ ]
1422
+ }
1423
+ ) {
1424
+ id
1425
+ name
1426
+ seasons {
1427
+ number
1428
+ episodes {
1429
+ number
1430
+ name
1431
+ date
1432
+ }
1433
+ }
1434
+ }
1435
+ }
1436
+ ```
1437
+
1438
+ ### 3. Series with a Particular Star
1439
+
1440
+ Find series that feature "Bryan Cranston":
1441
+
1442
+ ```graphql
1443
+ query {
1444
+ assignedStarsAndSeries(star: {
1445
+ terms: [
1446
+ {
1447
+ path: "name",
1448
+ operator: EQ,
1449
+ value: "Bryan Cranston"
1450
+ }
1451
+ ]
1452
+ }) {
1453
+ id
1454
+ star {
1455
+ name
1456
+ }
1457
+ serie {
1458
+ id
1459
+ name
1460
+ categories
1461
+ director {
1462
+ name
1463
+ country
1464
+ }
1465
+ }
1466
+ }
1467
+ }
1468
+ ```
1469
+
1470
+ ### 4. Seasons from Series with Directors from a Given Country
1471
+
1472
+ Find all seasons that belong to series directed by someone from the United States:
1473
+
1474
+ ```graphql
1475
+ query {
1476
+ seasons(serie: {
1477
+ terms: [
1478
+ {
1479
+ path: "director.country",
1480
+ operator: EQ,
1481
+ value: "United States"
1482
+ }
1483
+ ]
1484
+ }) {
1485
+ id
1486
+ number
1487
+ year
1488
+ state
1489
+ serie {
1490
+ name
1491
+ categories
1492
+ director {
1493
+ name
1494
+ country
1495
+ }
1496
+ }
1497
+ episodes {
1498
+ number
1499
+ name
1500
+ date
1501
+ }
1502
+ }
1503
+ }
1504
+ ```
1505
+
1506
+ ### 5. Combining Scalar and ObjectType Filters
1507
+
1508
+ Find series named "Breaking Bad" that have at least one season with number 1:
1509
+
1510
+ ```graphql
1511
+ query {
1512
+ series(
1513
+ name: {
1514
+ operator: EQ,
1515
+ value: "Breaking Bad"
1516
+ }
1517
+ seasons: {
1518
+ terms: [
1519
+ {
1520
+ path: "number",
1521
+ operator: EQ,
1522
+ value: 1
1523
+ }
1524
+ ]
1525
+ }
1526
+ ) {
1527
+ id
1528
+ name
1529
+ director {
1530
+ name
1531
+ country
1532
+ }
1533
+ seasons {
1534
+ number
1535
+ episodes {
1536
+ name
1537
+ }
1538
+ }
1539
+ }
1540
+ }
1541
+ ```
1542
+
1543
+ ### 6. Complex Nested Queries
1544
+
1545
+ Get complete information for a specific series:
1546
+
1547
+ ```graphql
1548
+ query {
1549
+ series(name: {
1550
+ operator: EQ,
1551
+ value: "Breaking Bad"
1552
+ }) {
1553
+ id
1554
+ name
1555
+ categories
1556
+ director {
1557
+ name
1558
+ country
1559
+ }
1560
+ seasons {
1561
+ number
1562
+ year
1563
+ state
1564
+ episodes {
1565
+ number
1566
+ name
1567
+ date
1568
+ }
1569
+ }
1570
+ }
1571
+ }
1572
+ ```
1573
+
1574
+ ### 7. Episodes from a Specific Season and Series
1575
+
1576
+ Find all episodes from Season 1 of Breaking Bad:
1577
+
1578
+ ```graphql
1579
+ query {
1580
+ episodes(season: {
1581
+ terms: [
1582
+ {
1583
+ path: "number",
1584
+ operator: EQ,
1585
+ value: 1
1586
+ },
1587
+ {
1588
+ path: "serie.name",
1589
+ operator: EQ,
1590
+ value: "Breaking Bad"
1591
+ }
1592
+ ]
1593
+ }) {
1594
+ id
1595
+ number
1596
+ name
1597
+ date
1598
+ season {
1599
+ number
1600
+ serie {
1601
+ name
1602
+ }
1603
+ }
1604
+ }
1605
+ }
1606
+ ```
1607
+
1608
+ ### 8. Series by Category
1609
+
1610
+ Find all crime series:
1611
+
1612
+ ```graphql
1613
+ query {
1614
+ series(categories: {
1615
+ operator: EQ,
1616
+ value: "Crime"
1617
+ }) {
1618
+ id
1619
+ name
1620
+ categories
1621
+ director {
1622
+ name
1623
+ country
1624
+ }
1625
+ }
1626
+ }
1627
+ ```
1628
+
1629
+ ### 9. Search by Partial Episode Name
1630
+
1631
+ Find episodes containing "Fire" in the name:
1632
+
1633
+ ```graphql
1634
+ query {
1635
+ episodes(name: {
1636
+ operator: LIKE,
1637
+ value: "Fire"
1638
+ }) {
1639
+ id
1640
+ number
1641
+ name
1642
+ date
1643
+ season {
1644
+ number
1645
+ serie {
1646
+ name
1647
+ }
1648
+ }
1649
+ }
1650
+ }
1651
+ ```
1652
+
1653
+ ### 10. Pagination
1654
+
1655
+ Simfinity.js supports built-in pagination with optional total count:
1656
+
1657
+ ```graphql
1658
+ query {
1659
+ series(
1660
+ categories: {
1661
+ operator: EQ,
1662
+ value: "Crime"
1663
+ }
1664
+ pagination: {
1665
+ page: 1,
1666
+ size: 2,
1667
+ count: true
1668
+ }
1669
+ ) {
1670
+ id
1671
+ name
1672
+ categories
1673
+ director {
1674
+ name
1675
+ country
1676
+ }
1677
+ }
1678
+ }
1679
+ ```
1680
+
1681
+ #### Pagination Parameters:
1682
+ - **page**: Page number (starts at 1, not 0)
1683
+ - **size**: Number of items per page
1684
+ - **count**: Optional boolean - if `true`, returns total count of matching records
1685
+
1686
+ #### Getting Total Count:
1687
+ When `count: true` is specified, the total count is available in the response extensions. You need to configure an Envelop plugin to expose it:
1688
+
1689
+ ```javascript
1690
+ // Envelop plugin for count in extensions
1691
+ function useCountPlugin() {
1692
+ return {
1693
+ onExecute() {
1694
+ return {
1695
+ onExecuteDone({ result, args }) {
1696
+ if (args.contextValue?.count) {
1697
+ result.extensions = {
1698
+ ...result.extensions,
1699
+ count: args.contextValue.count,
1700
+ };
1701
+ }
1702
+ }
1703
+ };
1704
+ }
1705
+ };
1706
+ }
1707
+ ```
1708
+
1709
+ #### Example Response:
1710
+ ```json
1711
+ {
1712
+ "data": {
1713
+ "series": [
1714
+ {
1715
+ "id": "1",
1716
+ "name": "Breaking Bad",
1717
+ "categories": ["Crime", "Drama"],
1718
+ "director": {
1719
+ "name": "Vince Gilligan",
1720
+ "country": "United States"
1721
+ }
1722
+ },
1723
+ {
1724
+ "id": "2",
1725
+ "name": "Better Call Saul",
1726
+ "categories": ["Crime", "Drama"],
1727
+ "director": {
1728
+ "name": "Vince Gilligan",
1729
+ "country": "United States"
1730
+ }
1731
+ }
1732
+ ]
1733
+ },
1734
+ "extensions": {
1735
+ "count": 15
1736
+ }
1737
+ }
1738
+ ```
1739
+
1740
+ ### 11. Sorting
1741
+
1742
+ Simfinity.js supports sorting with multiple fields and sort orders:
1743
+
1744
+ ```graphql
1745
+ query {
1746
+ series(
1747
+ categories: { operator: EQ, value: "Crime" }
1748
+ pagination: { page: 1, size: 5, count: true }
1749
+ sort: {
1750
+ terms: [
1751
+ {
1752
+ field: "name",
1753
+ order: DESC
1754
+ }
1755
+ ]
1756
+ }
1757
+ ) {
1758
+ id
1759
+ name
1760
+ categories
1761
+ director {
1762
+ name
1763
+ country
1764
+ }
1765
+ }
1766
+ }
1767
+ ```
1768
+
1769
+ #### Sorting Parameters:
1770
+ - **sort**: Contains sorting configuration
1771
+ - **terms**: Array of sort criteria (allows multiple sort fields)
1772
+ - **field**: The field name to sort by
1773
+ - **order**: Sort order - `ASC` (ascending) or `DESC` (descending)
1774
+
1775
+ #### Sorting by Nested Fields:
1776
+ You can sort by fields from related/nested objects using dot notation:
1777
+
1778
+ ```graphql
1779
+ query {
1780
+ series(
1781
+ categories: { operator: EQ, value: "Drama" }
1782
+ pagination: { page: 1, size: 5, count: true }
1783
+ sort: {
1784
+ terms: [
1785
+ {
1786
+ field: "director.name",
1787
+ order: DESC
1788
+ }
1789
+ ]
1790
+ }
1791
+ ) {
1792
+ id
1793
+ name
1794
+ categories
1795
+ director {
1796
+ name
1797
+ country
1798
+ }
1799
+ }
1800
+ }
1801
+ ```
1802
+
1803
+ #### Multiple Sort Fields:
1804
+ You can sort by multiple fields with different orders:
1805
+
1806
+ ```graphql
1807
+ query {
1808
+ series(
1809
+ sort: {
1810
+ terms: [
1811
+ { field: "director.country", order: ASC },
1812
+ { field: "name", order: DESC }
1813
+ ]
1814
+ }
1815
+ ) {
1816
+ id
1817
+ name
1818
+ director {
1819
+ name
1820
+ country
1821
+ }
1822
+ }
1823
+ }
1824
+ ```
1825
+
1826
+ #### Combining Features:
1827
+ The example above demonstrates combining **filtering**, **pagination**, and **sorting** in a single query - a common pattern for data tables and lists with full functionality.
1828
+
1829
+ ### 12. Series Released in a Specific Year Range
1830
+
1831
+ Find series with seasons released between 2010-2015:
1832
+
1833
+ ```graphql
1834
+ query {
1835
+ seasons(year: {
1836
+ operator: BETWEEN,
1837
+ value: [2010, 2015]
1838
+ }) {
1839
+ id
1840
+ number
1841
+ year
1842
+ serie {
1843
+ name
1844
+ director {
1845
+ name
1846
+ country
1847
+ }
1848
+ }
1849
+ }
1850
+ }
1851
+ ```
1852
+
1853
+
1854
+ ## ๐Ÿ”„ State Machine Example from Series-Sample
1855
+
1856
+ Simfinity.js provides built-in state machine support for managing entity lifecycles. Here's an example of how a state machine is implemented in the Season entity from the series-sample project.
1857
+
1858
+ ### State Machine Configuration
1859
+
1860
+ State machines require **GraphQL Enum Types** to define states and proper state references:
1861
+
1862
+ **Step 1: Define the GraphQL Enum Type**
1863
+
1864
+ ```javascript
1865
+ const { GraphQLEnumType } = require('graphql');
1866
+
1867
+ const seasonState = new GraphQLEnumType({
1868
+ name: 'seasonState',
1869
+ values: {
1870
+ SCHEDULED: { value: 'SCHEDULED' },
1871
+ ACTIVE: { value: 'ACTIVE' },
1872
+ FINISHED: { value: 'FINISHED' }
1873
+ }
1874
+ });
1875
+ ```
1876
+
1877
+ **Step 2: Use Enum in GraphQL Object Type**
1878
+
1879
+ ```javascript
1880
+ const seasonType = new GraphQLObjectType({
1881
+ name: 'season',
1882
+ fields: () => ({
1883
+ id: { type: GraphQLID },
1884
+ number: { type: GraphQLInt },
1885
+ year: { type: GraphQLInt },
1886
+ state: { type: seasonState }, // โ† Use the enum type
1887
+ // ... other fields
1888
+ })
1889
+ });
1890
+ ```
1891
+
1892
+ **Step 3: Define State Machine with Enum Values**
1893
+
1894
+ ```javascript
1895
+ const stateMachine = {
1896
+ initialState: seasonState.getValue('SCHEDULED'),
1897
+ actions: {
1898
+ activate: {
1899
+ from: seasonState.getValue('SCHEDULED'),
1900
+ to: seasonState.getValue('ACTIVE'),
1901
+ action: async (params) => {
1902
+ console.log('Season activated:', JSON.stringify(params));
1903
+ }
1904
+ },
1905
+ finalize: {
1906
+ from: seasonState.getValue('ACTIVE'),
1907
+ to: seasonState.getValue('FINISHED'),
1908
+ action: async (params) => {
1909
+ console.log('Season finalized:', JSON.stringify(params));
1910
+ }
1911
+ }
1912
+ }
1913
+ };
1914
+
1915
+ // Connect type with state machine
1916
+ simfinity.connect(null, seasonType, 'season', 'seasons', null, null, stateMachine);
1917
+ ```
1918
+
1919
+ ### Season States
1920
+
1921
+ The Season entity has three states:
1922
+
1923
+ 1. **SCHEDULED** - Initial state when season is created
1924
+ 2. **ACTIVE** - Season is currently airing
1925
+ 3. **FINISHED** - Season has completed airing
1926
+
1927
+ ### State Transitions
1928
+
1929
+ **Available transitions:**
1930
+ - `activate`: SCHEDULED โ†’ ACTIVE
1931
+ - `finalize`: ACTIVE โ†’ FINISHED
1932
+
1933
+ ### State Machine Mutations
1934
+
1935
+ Simfinity.js automatically generates state transition mutations:
1936
+
1937
+ ```graphql
1938
+ # Activate a scheduled season
1939
+ mutation {
1940
+ activateseason(id: "season_id_here") {
1941
+ id
1942
+ number
1943
+ year
1944
+ state
1945
+ serie {
1946
+ name
1947
+ }
1948
+ }
1949
+ }
1950
+ ```
1951
+
1952
+ ```graphql
1953
+ # Finalize an active season
1954
+ mutation {
1955
+ finalizeseason(id: "season_id_here") {
1956
+ id
1957
+ number
1958
+ year
1959
+ state
1960
+ serie {
1961
+ name
1962
+ }
1963
+ }
1964
+ }
1965
+ ```
1966
+
1967
+ ### State Machine Features
1968
+
1969
+ **Validation:**
1970
+ - Only valid transitions are allowed
1971
+ - Attempting invalid transitions returns an error
1972
+ - State field is read-only (managed by state machine)
1973
+
1974
+ **Custom Actions:**
1975
+ - Each transition can execute custom business logic
1976
+ - Actions receive parameters including entity data
1977
+ - Actions can perform side effects (logging, notifications, etc.)
1978
+
1979
+ **Query by State:**
1980
+ ```graphql
1981
+ query {
1982
+ seasons(state: {
1983
+ operator: EQ,
1984
+ value: ACTIVE
1985
+ }) {
1986
+ id
1987
+ number
1988
+ year
1989
+ state
1990
+ serie {
1991
+ name
1992
+ }
1993
+ }
1994
+ }
1995
+ ```
1996
+
1997
+ ### State Machine Best Practices
1998
+
1999
+ 1. **GraphQL Enum Types**: Always define states as GraphQL enums for type safety
2000
+ 2. **getValue() Method**: Use `enumType.getValue('VALUE')` for state machine configuration
2001
+ 3. **Initial State**: Define clear initial state using enum values
2002
+ 4. **Linear Flows**: Design logical progression (SCHEDULED โ†’ ACTIVE โ†’ FINISHED)
2003
+ 5. **Type Safety**: GraphQL enums provide validation and autocomplete
2004
+ 6. **Actions**: Implement side effects in transition actions
2005
+ 7. **Error Handling**: Handle transition failures gracefully
2006
+
2007
+ ### Key Implementation Points
2008
+
2009
+ - **Enum Definition**: States must be defined as `GraphQLEnumType`
2010
+ - **Type Reference**: Use the enum type in your GraphQL object: `state: { type: seasonState }`
2011
+ - **State Machine Values**: Reference enum values with `seasonState.getValue('STATE_NAME')`
2012
+ - **Automatic Validation**: GraphQL validates state values against the enum
2013
+ - **IDE Support**: Enum values provide autocomplete and type checking
2014
+
2015
+ ### Example Workflow
2016
+
2017
+ ```graphql
2018
+ # 1. Create season (automatically SCHEDULED)
2019
+ mutation {
2020
+ addseason(input: {
2021
+ number: 6
2022
+ year: 2024
2023
+ serie: "series_id_here"
2024
+ }) {
2025
+ id
2026
+ state # Will be "SCHEDULED"
2027
+ }
2028
+ }
2029
+
2030
+ # 2. Activate season when airing begins
2031
+ mutation {
2032
+ activateseason(id: "season_id_here") {
2033
+ id
2034
+ state # Will be "ACTIVE"
2035
+ }
2036
+ }
2037
+
2038
+ # 3. Finalize season when completed
2039
+ mutation {
2040
+ finalizeseason(id: "season_id_here") {
2041
+ id
2042
+ state # Will be "FINISHED"
2043
+ }
2044
+ }
2045
+ ```
2046
+
2047
+
2048
+ ## ๐Ÿ“ฆ Envelop Plugin for Count in Extensions
2049
+
2050
+ To include the total count in the extensions of your GraphQL response, you can use an Envelop plugin. This is particularly useful for pagination and analytics.
2051
+
2052
+ ### Envelop Plugin Example
2053
+
2054
+ Here's how you can implement the plugin:
2055
+
2056
+ ```javascript
2057
+ // Envelop plugin for count in extensions
2058
+ function useCountPlugin() {
2059
+ return {
2060
+ onExecute() {
2061
+ return {
2062
+ onExecuteDone({ result, args }) {
2063
+ if (args.contextValue?.count) {
2064
+ result.extensions = {
2065
+ ...result.extensions,
2066
+ count: args.contextValue.count,
2067
+ };
2068
+ }
2069
+ }
2070
+ };
2071
+ }
2072
+ };
2073
+ }
2074
+ ```
2075
+
2076
+ ### How to Use
2077
+
2078
+ 1. **Integrate the Plugin**: Add the plugin to your GraphQL server setup.
2079
+ 2. **Configure Context**: Ensure that your context includes the count value when executing queries.
2080
+ 3. **Access Count**: The count will be available in the `extensions` field of the GraphQL response.
2081
+
2082
+ ### Example Usage
2083
+
2084
+ ```javascript
2085
+ const { envelop, useSchema } = require('@envelop/core');
2086
+ const { makeExecutableSchema } = require('@graphql-tools/schema');
2087
+
2088
+ const schema = makeExecutableSchema({
2089
+ typeDefs,
2090
+ resolvers,
2091
+ });
2092
+
2093
+ const getEnveloped = envelop({
2094
+ plugins: [
2095
+ useSchema(schema),
2096
+ useCountPlugin(), // Add the count plugin here
2097
+ ],
2098
+ });
2099
+
2100
+ // Use getEnveloped in your server setup
2101
+ ```
2102
+
2103
+ ### Example Response
2104
+
2105
+ When the plugin is correctly set up, your GraphQL response will include the count in the extensions:
2106
+
2107
+ ```json
2108
+ {
2109
+ "data": {
2110
+ "series": [
2111
+ {
2112
+ "id": "1",
2113
+ "name": "Breaking Bad",
2114
+ "categories": ["Crime", "Drama"],
2115
+ "director": {
2116
+ "name": "Vince Gilligan",
2117
+ "country": "United States"
2118
+ }
2119
+ }
2120
+ ]
2121
+ },
2122
+ "extensions": {
2123
+ "count": 15
2124
+ }
2125
+ }
2126
+ ```
2127
+
2128
+ This setup allows you to efficiently manage and display pagination information in your GraphQL applications.
2129
+
2130
+ ## ๐Ÿ“– API Reference
2131
+
2132
+ Simfinity.js provides several utility methods for programmatic access to your GraphQL types and data:
2133
+
2134
+ ### `getType(typeName)`
2135
+
2136
+ Retrieves a GraphQL type definition from the internal types registry.
2137
+
2138
+ **Parameters:**
2139
+ - `typeName` (string | GraphQLObjectType): The name of the type or a GraphQL type object
2140
+
2141
+ **Returns:**
2142
+ - `GraphQLObjectType | null`: The GraphQL type definition, or null if not found
2143
+
2144
+ **Examples:**
2145
+
2146
+ ```javascript
2147
+ import { getType } from '@simtlix/simfinity-js';
2148
+
2149
+ // Get type by string name
2150
+ const UserType = getType('User');
2151
+ if (UserType) {
2152
+ console.log(UserType.name); // 'User'
2153
+
2154
+ // Access field definitions
2155
+ const fields = UserType.getFields();
2156
+ console.log(Object.keys(fields)); // ['id', 'name', 'email', ...]
2157
+
2158
+ // Check specific field
2159
+ const nameField = fields.name;
2160
+ console.log(nameField.type); // GraphQLString
2161
+ }
2162
+
2163
+ // Get type by GraphQL type object
2164
+ const BookType = getType(SomeBookType);
2165
+
2166
+ // Safe access - returns null if not found
2167
+ const nonExistentType = getType('NonExistent');
2168
+ console.log(nonExistentType); // null
2169
+ ```
2170
+
2171
+ **Use Cases:**
2172
+ - **Type introspection**: Examine type definitions programmatically
2173
+ - **Dynamic schema analysis**: Build tools that analyze your GraphQL schema
2174
+ - **Runtime type checking**: Validate types exist before operations
2175
+ - **Admin interfaces**: Build dynamic forms based on type definitions
2176
+ - **Circular reference resolution**: Prevent import cycles when types reference each other
2177
+
2178
+ ### Preventing Circular References with `getType`
2179
+
2180
+ When you have types that reference each other (like User and Group), using `getType` prevents circular import issues:
2181
+
2182
+ ```javascript
2183
+ import { GraphQLObjectType, GraphQLID, GraphQLString, GraphQLList } from 'graphql';
2184
+ import { getType } from '@simtlix/simfinity-js';
2185
+
2186
+ // User type that references Group
2187
+ const UserType = new GraphQLObjectType({
2188
+ name: 'User',
2189
+ fields: () => ({
2190
+ id: { type: GraphQLID },
2191
+ name: { type: GraphQLString },
2192
+ email: { type: GraphQLString },
2193
+
2194
+ // Reference Group type by name to avoid circular imports
2195
+ groups: {
2196
+ type: new GraphQLList(() => getType('Group')), // Use getType instead of direct import
2197
+ extensions: {
2198
+ relation: {
2199
+ connectionField: 'members',
2200
+ displayField: 'name'
2201
+ }
2202
+ }
2203
+ },
2204
+
2205
+ // Single group reference
2206
+ primaryGroup: {
2207
+ type: () => getType('Group'), // Lazy evaluation with getType
2208
+ extensions: {
2209
+ relation: {
2210
+ connectionField: 'primaryGroupId',
2211
+ displayField: 'name'
2212
+ }
2213
+ }
2214
+ }
2215
+ })
2216
+ });
2217
+
2218
+ // Group type that references User
2219
+ const GroupType = new GraphQLObjectType({
2220
+ name: 'Group',
2221
+ fields: () => ({
2222
+ id: { type: GraphQLID },
2223
+ name: { type: GraphQLString },
2224
+ description: { type: GraphQLString },
2225
+
2226
+ // Reference User type by name to avoid circular imports
2227
+ members: {
2228
+ type: new GraphQLList(() => getType('User')), // Use getType instead of direct import
2229
+ extensions: {
2230
+ relation: {
2231
+ connectionField: 'groups',
2232
+ displayField: 'name'
2233
+ }
2234
+ }
2235
+ },
2236
+
2237
+ // Single user reference (admin)
2238
+ admin: {
2239
+ type: () => getType('User'), // Lazy evaluation with getType
2240
+ extensions: {
2241
+ relation: {
2242
+ connectionField: 'adminId',
2243
+ displayField: 'name'
2244
+ }
2245
+ }
2246
+ }
2247
+ })
2248
+ });
2249
+
2250
+ // Register types with simfinity
2251
+ simfinity.connect(null, UserType, 'user', 'users');
2252
+ simfinity.connect(null, GroupType, 'group', 'groups');
2253
+
2254
+ // Create schema - resolvers will be auto-generated for all relationships
2255
+ const schema = simfinity.createSchema();
2256
+ ```
2257
+
2258
+ **Benefits of this approach:**
2259
+
2260
+ 1. **๐Ÿ”„ No Circular Imports**: Each file can import `getType` without importing other type definitions
2261
+ 2. **โšก Lazy Resolution**: Types are resolved at schema creation time when all types are registered
2262
+ 3. **๐Ÿ›ก๏ธ Type Safety**: Still maintains GraphQL type checking and validation
2263
+ 4. **๐Ÿงน Clean Architecture**: Separates type definitions from type relationships
2264
+ 5. **๐Ÿ“ฆ Better Modularity**: Each type can be in its own file without import dependencies
2265
+
2266
+ **File Structure Example:**
2267
+
2268
+ ```
2269
+ types/
2270
+ โ”œโ”€โ”€ User.js // Defines UserType using getType('Group')
2271
+ โ”œโ”€โ”€ Group.js // Defines GroupType using getType('User')
2272
+ โ””โ”€โ”€ index.js // Registers all types and creates schema
2273
+ ```
2274
+
2275
+ ```javascript
2276
+ // types/User.js
2277
+ import { GraphQLObjectType, GraphQLID, GraphQLString, GraphQLList } from 'graphql';
2278
+ import { getType } from '@simtlix/simfinity-js';
2279
+
2280
+ export const UserType = new GraphQLObjectType({
2281
+ name: 'User',
2282
+ fields: () => ({
2283
+ id: { type: GraphQLID },
2284
+ name: { type: GraphQLString },
2285
+ groups: {
2286
+ type: new GraphQLList(() => getType('Group')),
2287
+ extensions: { relation: { connectionField: 'members' } }
2288
+ }
2289
+ })
2290
+ });
2291
+
2292
+ // types/Group.js
2293
+ import { GraphQLObjectType, GraphQLID, GraphQLString, GraphQLList } from 'graphql';
2294
+ import { getType } from '@simtlix/simfinity-js';
2295
+
2296
+ export const GroupType = new GraphQLObjectType({
2297
+ name: 'Group',
2298
+ fields: () => ({
2299
+ id: { type: GraphQLID },
2300
+ name: { type: GraphQLString },
2301
+ members: {
2302
+ type: new GraphQLList(() => getType('User')),
2303
+ extensions: { relation: { connectionField: 'groups' } }
2304
+ }
2305
+ })
2306
+ });
2307
+
2308
+ // types/index.js
2309
+ import { UserType } from './User.js';
2310
+ import { GroupType } from './Group.js';
2311
+ import simfinity from '@simtlix/simfinity-js';
2312
+
2313
+ // Register all types
2314
+ simfinity.connect(null, UserType, 'user', 'users');
2315
+ simfinity.connect(null, GroupType, 'group', 'groups');
2316
+
2317
+ // Create schema with auto-generated resolvers
2318
+ export const schema = simfinity.createSchema();
2319
+ ```
2320
+
2321
+ ### `getModel(gqltype)`
2322
+
2323
+ Retrieves the Mongoose model associated with a GraphQL type.
2324
+
2325
+ **Parameters:**
2326
+ - `gqltype` (GraphQLObjectType): The GraphQL type object
2327
+
2328
+ **Returns:**
2329
+ - `MongooseModel`: The associated Mongoose model
2330
+
2331
+ **Example:**
2332
+
2333
+ ```javascript
2334
+ const BookModel = simfinity.getModel(BookType);
2335
+ const books = await BookModel.find({ author: 'Douglas Adams' });
2336
+ ```
2337
+
2338
+ ### `getInputType(type)`
2339
+
2340
+ Retrieves the input type for mutations associated with a GraphQL type.
2341
+
2342
+ **Parameters:**
2343
+ - `type` (GraphQLObjectType): The GraphQL type object
2344
+
2345
+ **Returns:**
2346
+ - `GraphQLInputObjectType`: The input type for mutations
2347
+
2348
+ **Example:**
2349
+
2350
+ ```javascript
2351
+ const BookInput = simfinity.getInputType(BookType);
2352
+ console.log(BookInput.getFields()); // Input fields for mutations
2353
+ ```
2354
+
2355
+ ### `saveObject(typeName, args, session?)`
2356
+
2357
+ Programmatically save an object outside of GraphQL mutations.
2358
+
2359
+ **Parameters:**
2360
+ - `typeName` (string): The name of the GraphQL type
2361
+ - `args` (object): The data to save
2362
+ - `session` (MongooseSession, optional): Database session for transactions
2363
+
2364
+ **Returns:**
2365
+ - `Promise<object>`: The saved object
2366
+
2367
+ **Example:**
2368
+
2369
+ ```javascript
2370
+ const newBook = await simfinity.saveObject('Book', {
2371
+ title: 'New Book',
2372
+ author: 'Author Name'
2373
+ }, session);
2374
+ ```
2375
+
2376
+ ### `createSchema(includedQueryTypes?, includedMutationTypes?, includedCustomMutations?)`
2377
+
2378
+ Creates the final GraphQL schema with all connected types.
2379
+
2380
+ **Parameters:**
2381
+ - `includedQueryTypes` (array, optional): Limit query types to include
2382
+ - `includedMutationTypes` (array, optional): Limit mutation types to include
2383
+ - `includedCustomMutations` (array, optional): Limit custom mutations to include
2384
+
2385
+ **Returns:**
2386
+ - `GraphQLSchema`: The complete GraphQL schema
2387
+
2388
+ **Example:**
2389
+
2390
+ ```javascript
2391
+ const schema = simfinity.createSchema();
2392
+ ```
2393
+
2394
+ *Built with โค๏ธ by [Simtlix](https://github.com/simtlix)*
2395
+
2396
+