@simtlix/simfinity-js 1.4.0 → 1.6.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
@@ -1031,3 +1369,757 @@ Contributions are welcome! Please feel free to submit a Pull Request.
1031
1369
  *Built with ❤️ by [Simtlix](https://github.com/simtlix)*
1032
1370
 
1033
1371
 
1372
+ ## 📚 Query Examples from Series-Sample
1373
+
1374
+ Here are some practical GraphQL query examples from the series-sample project, showcasing how to use simfinity.js effectively:
1375
+
1376
+ ### 1. Series with Directors from a Specific Country
1377
+
1378
+ Find all series that have directors from the United States:
1379
+
1380
+ ```graphql
1381
+ query {
1382
+ series(director: {
1383
+ terms: [
1384
+ {
1385
+ path: "country",
1386
+ operator: EQ,
1387
+ value: "United States"
1388
+ }
1389
+ ]
1390
+ }) {
1391
+ id
1392
+ name
1393
+ categories
1394
+ director {
1395
+ name
1396
+ country
1397
+ }
1398
+ }
1399
+ }
1400
+ ```
1401
+
1402
+ ### 2. Series with a Specific Episode Name
1403
+
1404
+ Find series that contain an episode with the name "Pilot":
1405
+
1406
+ ```graphql
1407
+ query {
1408
+ series(
1409
+ seasons: {
1410
+ terms: [
1411
+ {
1412
+ path: "episodes.name",
1413
+ operator: EQ,
1414
+ value: "Pilot"
1415
+ }
1416
+ ]
1417
+ }
1418
+ ) {
1419
+ id
1420
+ name
1421
+ seasons {
1422
+ number
1423
+ episodes {
1424
+ number
1425
+ name
1426
+ date
1427
+ }
1428
+ }
1429
+ }
1430
+ }
1431
+ ```
1432
+
1433
+ ### 3. Series with a Particular Star
1434
+
1435
+ Find series that feature "Bryan Cranston":
1436
+
1437
+ ```graphql
1438
+ query {
1439
+ assignedStarsAndSeries(star: {
1440
+ terms: [
1441
+ {
1442
+ path: "name",
1443
+ operator: EQ,
1444
+ value: "Bryan Cranston"
1445
+ }
1446
+ ]
1447
+ }) {
1448
+ id
1449
+ star {
1450
+ name
1451
+ }
1452
+ serie {
1453
+ id
1454
+ name
1455
+ categories
1456
+ director {
1457
+ name
1458
+ country
1459
+ }
1460
+ }
1461
+ }
1462
+ }
1463
+ ```
1464
+
1465
+ ### 4. Seasons from Series with Directors from a Given Country
1466
+
1467
+ Find all seasons that belong to series directed by someone from the United States:
1468
+
1469
+ ```graphql
1470
+ query {
1471
+ seasons(serie: {
1472
+ terms: [
1473
+ {
1474
+ path: "director.country",
1475
+ operator: EQ,
1476
+ value: "United States"
1477
+ }
1478
+ ]
1479
+ }) {
1480
+ id
1481
+ number
1482
+ year
1483
+ state
1484
+ serie {
1485
+ name
1486
+ categories
1487
+ director {
1488
+ name
1489
+ country
1490
+ }
1491
+ }
1492
+ episodes {
1493
+ number
1494
+ name
1495
+ date
1496
+ }
1497
+ }
1498
+ }
1499
+ ```
1500
+
1501
+ ### 5. Combining Scalar and ObjectType Filters
1502
+
1503
+ Find series named "Breaking Bad" that have at least one season with number 1:
1504
+
1505
+ ```graphql
1506
+ query {
1507
+ series(
1508
+ name: {
1509
+ operator: EQ,
1510
+ value: "Breaking Bad"
1511
+ }
1512
+ seasons: {
1513
+ terms: [
1514
+ {
1515
+ path: "number",
1516
+ operator: EQ,
1517
+ value: 1
1518
+ }
1519
+ ]
1520
+ }
1521
+ ) {
1522
+ id
1523
+ name
1524
+ director {
1525
+ name
1526
+ country
1527
+ }
1528
+ seasons {
1529
+ number
1530
+ episodes {
1531
+ name
1532
+ }
1533
+ }
1534
+ }
1535
+ }
1536
+ ```
1537
+
1538
+ ### 6. Complex Nested Queries
1539
+
1540
+ Get complete information for a specific series:
1541
+
1542
+ ```graphql
1543
+ query {
1544
+ series(name: {
1545
+ operator: EQ,
1546
+ value: "Breaking Bad"
1547
+ }) {
1548
+ id
1549
+ name
1550
+ categories
1551
+ director {
1552
+ name
1553
+ country
1554
+ }
1555
+ seasons {
1556
+ number
1557
+ year
1558
+ state
1559
+ episodes {
1560
+ number
1561
+ name
1562
+ date
1563
+ }
1564
+ }
1565
+ }
1566
+ }
1567
+ ```
1568
+
1569
+ ### 7. Episodes from a Specific Season and Series
1570
+
1571
+ Find all episodes from Season 1 of Breaking Bad:
1572
+
1573
+ ```graphql
1574
+ query {
1575
+ episodes(season: {
1576
+ terms: [
1577
+ {
1578
+ path: "number",
1579
+ operator: EQ,
1580
+ value: 1
1581
+ },
1582
+ {
1583
+ path: "serie.name",
1584
+ operator: EQ,
1585
+ value: "Breaking Bad"
1586
+ }
1587
+ ]
1588
+ }) {
1589
+ id
1590
+ number
1591
+ name
1592
+ date
1593
+ season {
1594
+ number
1595
+ serie {
1596
+ name
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ ```
1602
+
1603
+ ### 8. Series by Category
1604
+
1605
+ Find all crime series:
1606
+
1607
+ ```graphql
1608
+ query {
1609
+ series(categories: {
1610
+ operator: EQ,
1611
+ value: "Crime"
1612
+ }) {
1613
+ id
1614
+ name
1615
+ categories
1616
+ director {
1617
+ name
1618
+ country
1619
+ }
1620
+ }
1621
+ }
1622
+ ```
1623
+
1624
+ ### 9. Search by Partial Episode Name
1625
+
1626
+ Find episodes containing "Fire" in the name:
1627
+
1628
+ ```graphql
1629
+ query {
1630
+ episodes(name: {
1631
+ operator: LIKE,
1632
+ value: "Fire"
1633
+ }) {
1634
+ id
1635
+ number
1636
+ name
1637
+ date
1638
+ season {
1639
+ number
1640
+ serie {
1641
+ name
1642
+ }
1643
+ }
1644
+ }
1645
+ }
1646
+ ```
1647
+
1648
+ ### 10. Pagination
1649
+
1650
+ Simfinity.js supports built-in pagination with optional total count:
1651
+
1652
+ ```graphql
1653
+ query {
1654
+ series(
1655
+ categories: {
1656
+ operator: EQ,
1657
+ value: "Crime"
1658
+ }
1659
+ pagination: {
1660
+ page: 1,
1661
+ size: 2,
1662
+ count: true
1663
+ }
1664
+ ) {
1665
+ id
1666
+ name
1667
+ categories
1668
+ director {
1669
+ name
1670
+ country
1671
+ }
1672
+ }
1673
+ }
1674
+ ```
1675
+
1676
+ #### Pagination Parameters:
1677
+ - **page**: Page number (starts at 1, not 0)
1678
+ - **size**: Number of items per page
1679
+ - **count**: Optional boolean - if `true`, returns total count of matching records
1680
+
1681
+ #### Getting Total Count:
1682
+ When `count: true` is specified, the total count is available in the response extensions. You need to configure an Envelop plugin to expose it:
1683
+
1684
+ ```javascript
1685
+ // Envelop plugin for count in extensions
1686
+ function useCountPlugin() {
1687
+ return {
1688
+ onExecute() {
1689
+ return {
1690
+ onExecuteDone({ result, args }) {
1691
+ if (args.contextValue?.count) {
1692
+ result.extensions = {
1693
+ ...result.extensions,
1694
+ count: args.contextValue.count,
1695
+ };
1696
+ }
1697
+ }
1698
+ };
1699
+ }
1700
+ };
1701
+ }
1702
+ ```
1703
+
1704
+ #### Example Response:
1705
+ ```json
1706
+ {
1707
+ "data": {
1708
+ "series": [
1709
+ {
1710
+ "id": "1",
1711
+ "name": "Breaking Bad",
1712
+ "categories": ["Crime", "Drama"],
1713
+ "director": {
1714
+ "name": "Vince Gilligan",
1715
+ "country": "United States"
1716
+ }
1717
+ },
1718
+ {
1719
+ "id": "2",
1720
+ "name": "Better Call Saul",
1721
+ "categories": ["Crime", "Drama"],
1722
+ "director": {
1723
+ "name": "Vince Gilligan",
1724
+ "country": "United States"
1725
+ }
1726
+ }
1727
+ ]
1728
+ },
1729
+ "extensions": {
1730
+ "count": 15
1731
+ }
1732
+ }
1733
+ ```
1734
+
1735
+ ### 11. Sorting
1736
+
1737
+ Simfinity.js supports sorting with multiple fields and sort orders:
1738
+
1739
+ ```graphql
1740
+ query {
1741
+ series(
1742
+ categories: { operator: EQ, value: "Crime" }
1743
+ pagination: { page: 1, size: 5, count: true }
1744
+ sort: {
1745
+ terms: [
1746
+ {
1747
+ field: "name",
1748
+ order: DESC
1749
+ }
1750
+ ]
1751
+ }
1752
+ ) {
1753
+ id
1754
+ name
1755
+ categories
1756
+ director {
1757
+ name
1758
+ country
1759
+ }
1760
+ }
1761
+ }
1762
+ ```
1763
+
1764
+ #### Sorting Parameters:
1765
+ - **sort**: Contains sorting configuration
1766
+ - **terms**: Array of sort criteria (allows multiple sort fields)
1767
+ - **field**: The field name to sort by
1768
+ - **order**: Sort order - `ASC` (ascending) or `DESC` (descending)
1769
+
1770
+ #### Sorting by Nested Fields:
1771
+ You can sort by fields from related/nested objects using dot notation:
1772
+
1773
+ ```graphql
1774
+ query {
1775
+ series(
1776
+ categories: { operator: EQ, value: "Drama" }
1777
+ pagination: { page: 1, size: 5, count: true }
1778
+ sort: {
1779
+ terms: [
1780
+ {
1781
+ field: "director.name",
1782
+ order: DESC
1783
+ }
1784
+ ]
1785
+ }
1786
+ ) {
1787
+ id
1788
+ name
1789
+ categories
1790
+ director {
1791
+ name
1792
+ country
1793
+ }
1794
+ }
1795
+ }
1796
+ ```
1797
+
1798
+ #### Multiple Sort Fields:
1799
+ You can sort by multiple fields with different orders:
1800
+
1801
+ ```graphql
1802
+ query {
1803
+ series(
1804
+ sort: {
1805
+ terms: [
1806
+ { field: "director.country", order: ASC },
1807
+ { field: "name", order: DESC }
1808
+ ]
1809
+ }
1810
+ ) {
1811
+ id
1812
+ name
1813
+ director {
1814
+ name
1815
+ country
1816
+ }
1817
+ }
1818
+ }
1819
+ ```
1820
+
1821
+ #### Combining Features:
1822
+ The example above demonstrates combining **filtering**, **pagination**, and **sorting** in a single query - a common pattern for data tables and lists with full functionality.
1823
+
1824
+ ### 12. Series Released in a Specific Year Range
1825
+
1826
+ Find series with seasons released between 2010-2015:
1827
+
1828
+ ```graphql
1829
+ query {
1830
+ seasons(year: {
1831
+ operator: BETWEEN,
1832
+ value: [2010, 2015]
1833
+ }) {
1834
+ id
1835
+ number
1836
+ year
1837
+ serie {
1838
+ name
1839
+ director {
1840
+ name
1841
+ country
1842
+ }
1843
+ }
1844
+ }
1845
+ }
1846
+ ```
1847
+
1848
+
1849
+ ## 🔄 State Machine Example from Series-Sample
1850
+
1851
+ 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.
1852
+
1853
+ ### State Machine Configuration
1854
+
1855
+ State machines require **GraphQL Enum Types** to define states and proper state references:
1856
+
1857
+ **Step 1: Define the GraphQL Enum Type**
1858
+
1859
+ ```javascript
1860
+ const { GraphQLEnumType } = require('graphql');
1861
+
1862
+ const seasonState = new GraphQLEnumType({
1863
+ name: 'seasonState',
1864
+ values: {
1865
+ SCHEDULED: { value: 'SCHEDULED' },
1866
+ ACTIVE: { value: 'ACTIVE' },
1867
+ FINISHED: { value: 'FINISHED' }
1868
+ }
1869
+ });
1870
+ ```
1871
+
1872
+ **Step 2: Use Enum in GraphQL Object Type**
1873
+
1874
+ ```javascript
1875
+ const seasonType = new GraphQLObjectType({
1876
+ name: 'season',
1877
+ fields: () => ({
1878
+ id: { type: GraphQLID },
1879
+ number: { type: GraphQLInt },
1880
+ year: { type: GraphQLInt },
1881
+ state: { type: seasonState }, // ← Use the enum type
1882
+ // ... other fields
1883
+ })
1884
+ });
1885
+ ```
1886
+
1887
+ **Step 3: Define State Machine with Enum Values**
1888
+
1889
+ ```javascript
1890
+ const stateMachine = {
1891
+ initialState: seasonState.getValue('SCHEDULED'),
1892
+ actions: {
1893
+ activate: {
1894
+ from: seasonState.getValue('SCHEDULED'),
1895
+ to: seasonState.getValue('ACTIVE'),
1896
+ action: async (params) => {
1897
+ console.log('Season activated:', JSON.stringify(params));
1898
+ }
1899
+ },
1900
+ finalize: {
1901
+ from: seasonState.getValue('ACTIVE'),
1902
+ to: seasonState.getValue('FINISHED'),
1903
+ action: async (params) => {
1904
+ console.log('Season finalized:', JSON.stringify(params));
1905
+ }
1906
+ }
1907
+ }
1908
+ };
1909
+
1910
+ // Connect type with state machine
1911
+ simfinity.connect(null, seasonType, 'season', 'seasons', null, null, stateMachine);
1912
+ ```
1913
+
1914
+ ### Season States
1915
+
1916
+ The Season entity has three states:
1917
+
1918
+ 1. **SCHEDULED** - Initial state when season is created
1919
+ 2. **ACTIVE** - Season is currently airing
1920
+ 3. **FINISHED** - Season has completed airing
1921
+
1922
+ ### State Transitions
1923
+
1924
+ **Available transitions:**
1925
+ - `activate`: SCHEDULED → ACTIVE
1926
+ - `finalize`: ACTIVE → FINISHED
1927
+
1928
+ ### State Machine Mutations
1929
+
1930
+ Simfinity.js automatically generates state transition mutations:
1931
+
1932
+ ```graphql
1933
+ # Activate a scheduled season
1934
+ mutation {
1935
+ activateseason(id: "season_id_here") {
1936
+ id
1937
+ number
1938
+ year
1939
+ state
1940
+ serie {
1941
+ name
1942
+ }
1943
+ }
1944
+ }
1945
+ ```
1946
+
1947
+ ```graphql
1948
+ # Finalize an active season
1949
+ mutation {
1950
+ finalizeseason(id: "season_id_here") {
1951
+ id
1952
+ number
1953
+ year
1954
+ state
1955
+ serie {
1956
+ name
1957
+ }
1958
+ }
1959
+ }
1960
+ ```
1961
+
1962
+ ### State Machine Features
1963
+
1964
+ **Validation:**
1965
+ - Only valid transitions are allowed
1966
+ - Attempting invalid transitions returns an error
1967
+ - State field is read-only (managed by state machine)
1968
+
1969
+ **Custom Actions:**
1970
+ - Each transition can execute custom business logic
1971
+ - Actions receive parameters including entity data
1972
+ - Actions can perform side effects (logging, notifications, etc.)
1973
+
1974
+ **Query by State:**
1975
+ ```graphql
1976
+ query {
1977
+ seasons(state: {
1978
+ operator: EQ,
1979
+ value: ACTIVE
1980
+ }) {
1981
+ id
1982
+ number
1983
+ year
1984
+ state
1985
+ serie {
1986
+ name
1987
+ }
1988
+ }
1989
+ }
1990
+ ```
1991
+
1992
+ ### State Machine Best Practices
1993
+
1994
+ 1. **GraphQL Enum Types**: Always define states as GraphQL enums for type safety
1995
+ 2. **getValue() Method**: Use `enumType.getValue('VALUE')` for state machine configuration
1996
+ 3. **Initial State**: Define clear initial state using enum values
1997
+ 4. **Linear Flows**: Design logical progression (SCHEDULED → ACTIVE → FINISHED)
1998
+ 5. **Type Safety**: GraphQL enums provide validation and autocomplete
1999
+ 6. **Actions**: Implement side effects in transition actions
2000
+ 7. **Error Handling**: Handle transition failures gracefully
2001
+
2002
+ ### Key Implementation Points
2003
+
2004
+ - **Enum Definition**: States must be defined as `GraphQLEnumType`
2005
+ - **Type Reference**: Use the enum type in your GraphQL object: `state: { type: seasonState }`
2006
+ - **State Machine Values**: Reference enum values with `seasonState.getValue('STATE_NAME')`
2007
+ - **Automatic Validation**: GraphQL validates state values against the enum
2008
+ - **IDE Support**: Enum values provide autocomplete and type checking
2009
+
2010
+ ### Example Workflow
2011
+
2012
+ ```graphql
2013
+ # 1. Create season (automatically SCHEDULED)
2014
+ mutation {
2015
+ addseason(input: {
2016
+ number: 6
2017
+ year: 2024
2018
+ serie: "series_id_here"
2019
+ }) {
2020
+ id
2021
+ state # Will be "SCHEDULED"
2022
+ }
2023
+ }
2024
+
2025
+ # 2. Activate season when airing begins
2026
+ mutation {
2027
+ activateseason(id: "season_id_here") {
2028
+ id
2029
+ state # Will be "ACTIVE"
2030
+ }
2031
+ }
2032
+
2033
+ # 3. Finalize season when completed
2034
+ mutation {
2035
+ finalizeseason(id: "season_id_here") {
2036
+ id
2037
+ state # Will be "FINISHED"
2038
+ }
2039
+ }
2040
+ ```
2041
+
2042
+
2043
+ ## 📦 Envelop Plugin for Count in Extensions
2044
+
2045
+ 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.
2046
+
2047
+ ### Envelop Plugin Example
2048
+
2049
+ Here's how you can implement the plugin:
2050
+
2051
+ ```javascript
2052
+ // Envelop plugin for count in extensions
2053
+ function useCountPlugin() {
2054
+ return {
2055
+ onExecute() {
2056
+ return {
2057
+ onExecuteDone({ result, args }) {
2058
+ if (args.contextValue?.count) {
2059
+ result.extensions = {
2060
+ ...result.extensions,
2061
+ count: args.contextValue.count,
2062
+ };
2063
+ }
2064
+ }
2065
+ };
2066
+ }
2067
+ };
2068
+ }
2069
+ ```
2070
+
2071
+ ### How to Use
2072
+
2073
+ 1. **Integrate the Plugin**: Add the plugin to your GraphQL server setup.
2074
+ 2. **Configure Context**: Ensure that your context includes the count value when executing queries.
2075
+ 3. **Access Count**: The count will be available in the `extensions` field of the GraphQL response.
2076
+
2077
+ ### Example Usage
2078
+
2079
+ ```javascript
2080
+ const { envelop, useSchema } = require('@envelop/core');
2081
+ const { makeExecutableSchema } = require('@graphql-tools/schema');
2082
+
2083
+ const schema = makeExecutableSchema({
2084
+ typeDefs,
2085
+ resolvers,
2086
+ });
2087
+
2088
+ const getEnveloped = envelop({
2089
+ plugins: [
2090
+ useSchema(schema),
2091
+ useCountPlugin(), // Add the count plugin here
2092
+ ],
2093
+ });
2094
+
2095
+ // Use getEnveloped in your server setup
2096
+ ```
2097
+
2098
+ ### Example Response
2099
+
2100
+ When the plugin is correctly set up, your GraphQL response will include the count in the extensions:
2101
+
2102
+ ```json
2103
+ {
2104
+ "data": {
2105
+ "series": [
2106
+ {
2107
+ "id": "1",
2108
+ "name": "Breaking Bad",
2109
+ "categories": ["Crime", "Drama"],
2110
+ "director": {
2111
+ "name": "Vince Gilligan",
2112
+ "country": "United States"
2113
+ }
2114
+ }
2115
+ ]
2116
+ },
2117
+ "extensions": {
2118
+ "count": 15
2119
+ }
2120
+ }
2121
+ ```
2122
+
2123
+ This setup allows you to efficiently manage and display pagination information in your GraphQL applications.
2124
+
2125
+