@simtlix/simfinity-js 2.4.6 โ†’ 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) 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/package.json +1 -1
  26. package/src/index.js +235 -21
@@ -0,0 +1,3941 @@
1
+ # Simfinity.js
2
+
3
+ A powerful Node.js framework that automatically generates GraphQL schemas from your data models, bringing all the power and flexibility of MongoDB query language to GraphQL interfaces.
4
+
5
+ ## ๐Ÿ“‘ Table of Contents
6
+
7
+ - [Features](#-features)
8
+ - [Installation](#-installation)
9
+ - [Quick Start](#-quick-start)
10
+ - [Core Concepts](#-core-concepts)
11
+ - [Connecting Models](#connecting-models)
12
+ - [Creating Schemas](#creating-schemas)
13
+ - [Global Configuration](#global-configuration)
14
+ - [Basic Usage](#-basic-usage)
15
+ - [Automatic Query Generation](#automatic-query-generation)
16
+ - [Automatic Mutation Generation](#automatic-mutation-generation)
17
+ - [Filtering and Querying](#filtering-and-querying)
18
+ - [Collection Field Filtering](#collection-field-filtering)
19
+ - [Relationships](#-relationships)
20
+ - [Defining Relationships](#defining-relationships)
21
+ - [Auto-Generated Resolve Methods](#auto-generated-resolve-methods)
22
+ - [Adding Types Without Endpoints](#adding-types-without-endpoints)
23
+ - [Embedded vs Referenced Relationships](#embedded-vs-referenced-relationships)
24
+ - [Querying Relationships](#querying-relationships)
25
+ - [Validations](#-validations)
26
+ - [Field-Level Validations](#field-level-validations)
27
+ - [Type-Level Validations](#type-level-validations)
28
+ - [Custom Validated Scalar Types](#custom-validated-scalar-types)
29
+ - [Custom Error Classes](#custom-error-classes)
30
+ - [State Machines](#-state-machines)
31
+ - [Controllers & Lifecycle Hooks](#๏ธ-controllers--lifecycle-hooks)
32
+ - [Hook Parameters](#hook-parameters)
33
+ - [Query Scope](#-query-scope)
34
+ - [Overview](#overview)
35
+ - [Defining Scope](#defining-scope)
36
+ - [Scope for Find Operations](#scope-for-find-operations)
37
+ - [Scope for Aggregate Operations](#scope-for-aggregate-operations)
38
+ - [Scope for Get By ID Operations](#scope-for-get-by-id-operations)
39
+ - [Scope Function Parameters](#scope-function-parameters)
40
+ - [Authorization](#-authorization)
41
+ - [Quick Start](#quick-start-1)
42
+ - [Permission Schema](#permission-schema)
43
+ - [Rule Helpers](#rule-helpers)
44
+ - [Policy Expressions (JSON AST)](#policy-expressions-json-ast)
45
+ - [Integration with GraphQL Yoga / Envelop](#integration-with-graphql-yoga--envelop)
46
+ - [Legacy: Integration with graphql-middleware](#legacy-integration-with-graphql-middleware)
47
+ - [Middlewares](#-middlewares)
48
+ - [Adding Middlewares](#adding-middlewares)
49
+ - [Middleware Parameters](#middleware-parameters)
50
+ - [Common Use Cases](#common-use-cases)
51
+ - [Advanced Features](#-advanced-features)
52
+ - [Field Extensions](#field-extensions)
53
+ - [Custom Mutations](#custom-mutations)
54
+ - [Working with Existing Mongoose Models](#working-with-existing-mongoose-models)
55
+ - [Programmatic Data Access](#programmatic-data-access)
56
+ - [Aggregation Queries](#-aggregation-queries)
57
+ - [Complete Example](#-complete-example)
58
+ - [Resources](#-resources)
59
+ - [License](#-license)
60
+ - [Contributing](#-contributing)
61
+
62
+ ## โœจ Features
63
+
64
+ - **Automatic Schema Generation**: Define your object model, and Simfinity.js generates all queries and mutations
65
+ - **MongoDB Integration**: Seamless translation between GraphQL and MongoDB
66
+ - **Powerful Querying**: Any query that can be executed in MongoDB can be executed in GraphQL
67
+ - **Aggregation Queries**: Built-in support for GROUP BY queries with aggregation operations (SUM, COUNT, AVG, MIN, MAX)
68
+ - **Auto-Generated Resolvers**: Automatically generates resolve methods for relationship fields
69
+ - **Automatic Index Creation**: Automatically creates MongoDB indexes for all ObjectId fields, including nested embedded objects and relationship fields
70
+ - **Business Logic**: Implement business logic and domain validations declaratively
71
+ - **State Machines**: Built-in support for declarative state machine workflows
72
+ - **Lifecycle Hooks**: Controller methods for granular control over operations
73
+ - **Custom Validation**: Field-level and type-level custom validations
74
+ - **Relationship Management**: Support for embedded and referenced relationships
75
+ - **Authorization**: Production-grade GraphQL authorization with RBAC/ABAC, function-based rules, declarative policy expressions, and native Envelop/Yoga plugin support
76
+
77
+ ## ๐Ÿ“ฆ Installation
78
+
79
+ ```bash
80
+ npm install mongoose graphql @simtlix/simfinity-js
81
+ ```
82
+
83
+ **Prerequisites**: Simfinity.js requires `mongoose` and `graphql` as peer dependencies.
84
+
85
+ ## ๐Ÿš€ Quick Start
86
+
87
+ ### 1. Basic Setup
88
+
89
+ ```javascript
90
+ const express = require('express');
91
+ const { graphqlHTTP } = require('express-graphql');
92
+ const mongoose = require('mongoose');
93
+ const simfinity = require('@simtlix/simfinity-js');
94
+
95
+ // Connect to MongoDB
96
+ mongoose.connect('mongodb://localhost:27017/bookstore', {
97
+ useNewUrlParser: true,
98
+ useUnifiedTopology: true,
99
+ });
100
+
101
+ const app = express();
102
+ ```
103
+
104
+ ### 2. Define Your GraphQL Type
105
+
106
+ ```javascript
107
+ const { GraphQLObjectType, GraphQLString, GraphQLNonNull, GraphQLID } = require('graphql');
108
+
109
+ const BookType = new GraphQLObjectType({
110
+ name: 'Book',
111
+ fields: () => ({
112
+ id: { type: new GraphQLNonNull(GraphQLID) },
113
+ title: { type: new GraphQLNonNull(GraphQLString) },
114
+ author: { type: GraphQLString },
115
+ }),
116
+ });
117
+ ```
118
+
119
+ ### 3. Connect to Simfinity
120
+
121
+ ```javascript
122
+ // Connect the type to Simfinity
123
+ simfinity.connect(null, BookType, 'book', 'books');
124
+
125
+ // Create the GraphQL schema
126
+ const schema = simfinity.createSchema();
127
+ ```
128
+
129
+ ### 4. Setup GraphQL Endpoint
130
+
131
+ ```javascript
132
+ app.use('/graphql', graphqlHTTP({
133
+ schema,
134
+ graphiql: true,
135
+ formatError: simfinity.buildErrorFormatter((err) => {
136
+ console.log(err);
137
+ })
138
+ }));
139
+
140
+ app.listen(4000, () => {
141
+ console.log('Server is running on port 4000');
142
+ });
143
+ ```
144
+
145
+ ### 5. Try It Out
146
+
147
+ Open [http://localhost:4000/graphql](http://localhost:4000/graphql) and try these queries:
148
+
149
+ **Create a book:**
150
+ ```graphql
151
+ mutation {
152
+ addBook(input: {
153
+ title: "The Hitchhiker's Guide to the Galaxy"
154
+ author: "Douglas Adams"
155
+ }) {
156
+ id
157
+ title
158
+ author
159
+ }
160
+ }
161
+ ```
162
+
163
+ **List all books:**
164
+ ```graphql
165
+ query {
166
+ books {
167
+ id
168
+ title
169
+ author
170
+ }
171
+ }
172
+ ```
173
+
174
+ > For a full working application, see the [Series Sample Project](https://github.com/simtlix/series-sample) -- a complete TV series microservice with types, relationships, state machines, controllers, and authorization.
175
+
176
+ ## ๐Ÿ”ง Core Concepts
177
+
178
+ ### Connecting Models
179
+
180
+ The `simfinity.connect()` method links your GraphQL types to Simfinity's automatic schema generation:
181
+
182
+ ```javascript
183
+ simfinity.connect(
184
+ mongooseModel, // Optional: Custom Mongoose model (null for auto-generation)
185
+ graphQLType, // Required: Your GraphQLObjectType
186
+ singularEndpointName, // Required: Singular name for mutations (e.g., 'book')
187
+ pluralEndpointName, // Required: Plural name for queries (e.g., 'books')
188
+ controller, // Optional: Controller with lifecycle hooks
189
+ onModelCreated, // Optional: Callback when Mongoose model is created
190
+ stateMachine // Optional: State machine configuration
191
+ );
192
+ ```
193
+
194
+ ### Creating Schemas
195
+
196
+ Generate your complete GraphQL schema with optional type filtering:
197
+
198
+ ```javascript
199
+ const schema = simfinity.createSchema(
200
+ includedQueryTypes, // Optional: Array of types to include in queries
201
+ includedMutationTypes, // Optional: Array of types to include in mutations
202
+ includedCustomMutations // Optional: Array of custom mutations to include
203
+ );
204
+ ```
205
+
206
+ ### Global Configuration
207
+
208
+ ```javascript
209
+ // Prevent automatic MongoDB collection creation (useful for testing)
210
+ simfinity.preventCreatingCollection(true);
211
+ ```
212
+
213
+ ## ๐Ÿ“‹ Basic Usage
214
+
215
+ ### Automatic Query Generation
216
+
217
+ Simfinity automatically generates queries for each connected type:
218
+
219
+ ```javascript
220
+ // For a BookType, you get:
221
+ // - book(id: ID): Book - Get single book by ID
222
+ // - books(...filters): [Book] - Get filtered list of books
223
+ ```
224
+
225
+ ### Automatic Mutation Generation
226
+
227
+ Simfinity automatically generates mutations for each connected type:
228
+
229
+ ```javascript
230
+ // For a BookType, you get:
231
+ // - addBook(input: BookInput): Book
232
+ // - updateBook(input: BookInputForUpdate): Book
233
+ // - deleteBook(id: ID): Book
234
+ ```
235
+
236
+ ### Filtering and Querying
237
+
238
+ Query with powerful filtering options:
239
+
240
+ ```graphql
241
+ query {
242
+ books(
243
+ title: { operator: LIKE, value: "Galaxy" }
244
+ author: { operator: EQ, value: "Douglas Adams" }
245
+ pagination: { page: 1, size: 10, count: true }
246
+ sort: { terms: [{ field: "title", order: ASC }] }
247
+ ) {
248
+ id
249
+ title
250
+ author
251
+ }
252
+ }
253
+ ```
254
+
255
+ #### Available Operators
256
+
257
+ - `EQ` - Equal
258
+ - `NE` - Not equal
259
+ - `GT` - Greater than
260
+ - `LT` - Less than
261
+ - `GTE` - Greater than or equal
262
+ - `LTE` - Less than or equal
263
+ - `LIKE` - Pattern matching
264
+ - `IN` - In array
265
+ - `NIN` - Not in array
266
+ - `BTW` - Between two values
267
+
268
+ ### Logical Filters (AND / OR)
269
+
270
+ By default, all field-level filters are combined with implicit AND logic. For complex conditions requiring OR logic or nested combinations, use the `AND` and `OR` query arguments.
271
+
272
+ #### Simple OR
273
+
274
+ Return books in either the Sci-Fi or Fantasy category:
275
+
276
+ ```graphql
277
+ query {
278
+ books(
279
+ OR: [
280
+ { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
281
+ { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
282
+ ]
283
+ ) {
284
+ id
285
+ title
286
+ category
287
+ }
288
+ }
289
+ ```
290
+
291
+ #### Flat Filters Combined with OR
292
+
293
+ Flat field filters are always ANDed at the top level, making them ideal for scope/security conditions that cannot be bypassed by user OR logic:
294
+
295
+ ```graphql
296
+ query {
297
+ books(
298
+ rating: { operator: GTE, value: 7.0 }
299
+ OR: [
300
+ { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
301
+ { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
302
+ ]
303
+ ) {
304
+ id
305
+ title
306
+ }
307
+ }
308
+ ```
309
+
310
+ This translates to: `rating >= 7.0 AND (category = "Sci-Fi" OR category = "Fantasy")`.
311
+
312
+ #### Nested AND inside OR
313
+
314
+ ```graphql
315
+ query {
316
+ books(
317
+ OR: [
318
+ { AND: [
319
+ { conditions: [{ field: "rating", operator: GTE, value: 9.0 }] }
320
+ { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
321
+ ]}
322
+ { AND: [
323
+ { conditions: [{ field: "rating", operator: GTE, value: 8.0 }] }
324
+ { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
325
+ ]}
326
+ ]
327
+ ) {
328
+ id
329
+ title
330
+ }
331
+ }
332
+ ```
333
+
334
+ This translates to: `(rating >= 9.0 AND category = "Sci-Fi") OR (rating >= 8.0 AND category = "Fantasy")`.
335
+
336
+ #### Filtering on Relationships within AND/OR
337
+
338
+ Use the `path` parameter to filter on related entity fields:
339
+
340
+ ```graphql
341
+ query {
342
+ books(
343
+ OR: [
344
+ { conditions: [{ field: "author", path: "name", operator: LIKE, value: "Adams" }] }
345
+ { conditions: [{ field: "author", path: "name", operator: LIKE, value: "Pratchett" }] }
346
+ ]
347
+ ) {
348
+ id
349
+ title
350
+ author { name }
351
+ }
352
+ }
353
+ ```
354
+
355
+ #### Mixing Flat Filters with AND/OR
356
+
357
+ You can freely combine the existing flat filter syntax with AND/OR groups. Flat filters and AND groups are all ANDed together at the top level:
358
+
359
+ ```graphql
360
+ query {
361
+ books(
362
+ rating: { operator: GTE, value: 7.0 }
363
+ author: { terms: [{ path: "country", operator: EQ, value: "UK" }] }
364
+ OR: [
365
+ { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
366
+ { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
367
+ ]
368
+ ) {
369
+ id
370
+ title
371
+ rating
372
+ category
373
+ author { name country }
374
+ }
375
+ }
376
+ ```
377
+
378
+ This translates to: `rating >= 7.0 AND author.country = "UK" AND (category = "Sci-Fi" OR category = "Fantasy")`. The flat field filters (`rating`, `author`) use the existing syntax while the OR group uses the new `QLFilterGroup` syntax.
379
+
380
+ You can also combine flat filters with explicit AND groups for more complex logic:
381
+
382
+ ```graphql
383
+ query {
384
+ books(
385
+ rating: { operator: GTE, value: 5.0 }
386
+ AND: [
387
+ {
388
+ OR: [
389
+ { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
390
+ { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
391
+ ]
392
+ }
393
+ {
394
+ OR: [
395
+ { conditions: [{ field: "author", path: "country", operator: EQ, value: "UK" }] }
396
+ { conditions: [{ field: "author", path: "country", operator: EQ, value: "US" }] }
397
+ ]
398
+ }
399
+ ]
400
+ ) {
401
+ id
402
+ title
403
+ }
404
+ }
405
+ ```
406
+
407
+ This translates to: `rating >= 5.0 AND (category = "Sci-Fi" OR category = "Fantasy") AND (author.country = "UK" OR author.country = "US")`.
408
+
409
+ #### Collection Filtering with AND/OR
410
+
411
+ AND/OR filters are also available on collection fields (one-to-many relationships). The auto-generated resolvers for collection fields support the same `AND` and `OR` arguments:
412
+
413
+ ```graphql
414
+ query {
415
+ series {
416
+ seasons(
417
+ OR: [
418
+ { conditions: [{ field: "year", operator: EQ, value: 2020 }] }
419
+ { conditions: [{ field: "year", operator: EQ, value: 2021 }] }
420
+ ]
421
+ ) {
422
+ number
423
+ year
424
+ }
425
+ }
426
+ }
427
+ ```
428
+
429
+ You can mix flat collection filters with AND/OR:
430
+
431
+ ```graphql
432
+ query {
433
+ series {
434
+ seasons(
435
+ number: { operator: GT, value: 1 }
436
+ OR: [
437
+ { conditions: [{ field: "year", operator: EQ, value: 2020 }] }
438
+ {
439
+ AND: [
440
+ { conditions: [{ field: "year", operator: GTE, value: 2022 }] }
441
+ { conditions: [
442
+ { field: "episodes", path: "name", operator: LIKE, value: "Final" }
443
+ ] }
444
+ ]
445
+ }
446
+ ]
447
+ ) {
448
+ number
449
+ year
450
+ episodes { name }
451
+ }
452
+ }
453
+ }
454
+ ```
455
+
456
+ This translates to: `number > 1 AND (year = 2020 OR (year >= 2022 AND episodes.name LIKE "Final"))`.
457
+
458
+ #### Filter Types Reference
459
+
460
+ | Type | Fields | Description |
461
+ |------|--------|-------------|
462
+ | `QLFilterGroup` | `AND: [QLFilterGroup]`, `OR: [QLFilterGroup]`, `conditions: [QLFilterCondition]` | Recursive logical group |
463
+ | `QLFilterCondition` | `field: String!`, `operator: QLOperator`, `value: QLValue`, `path: String` | Individual filter condition |
464
+
465
+ - `field` identifies the entity field by name (e.g., `"title"`, `"author"`)
466
+ - `path` is required for object/relationship fields (e.g., `"name"`, `"country.name"`)
467
+ - Multiple `conditions` in the same group are combined with AND
468
+ - Maximum nesting depth: 5 levels
469
+
470
+ ### Collection Field Filtering
471
+
472
+ Simfinity.js now supports filtering collection fields (one-to-many relationships) using the same powerful query format. This allows you to filter related objects directly within your GraphQL queries.
473
+
474
+ #### Basic Collection Filtering
475
+
476
+ Filter collection fields using the same operators and format as main queries:
477
+
478
+ ```graphql
479
+ query {
480
+ series {
481
+ seasons(number: { operator: EQ, value: 1 }) {
482
+ number
483
+ id
484
+ year
485
+ }
486
+ }
487
+ }
488
+ ```
489
+
490
+ #### Advanced Collection Filtering
491
+
492
+ You can use complex filtering with nested object properties:
493
+
494
+ ```graphql
495
+ query {
496
+ series {
497
+ seasons(
498
+ year: { operator: GTE, value: 2020 }
499
+ episodes: {
500
+ terms: [
501
+ {
502
+ path: "name",
503
+ operator: LIKE,
504
+ value: "Pilot"
505
+ }
506
+ ]
507
+ }
508
+ ) {
509
+ number
510
+ year
511
+ episodes {
512
+ name
513
+ date
514
+ }
515
+ }
516
+ }
517
+ }
518
+ ```
519
+
520
+ #### Collection Filtering with Multiple Conditions
521
+
522
+ Combine multiple filter conditions for collection fields:
523
+
524
+ ```graphql
525
+ query {
526
+ series {
527
+ seasons(
528
+ number: { operator: GT, value: 1 }
529
+ year: { operator: BTW, value: [2015, 2023] }
530
+ ) {
531
+ number
532
+ year
533
+ state
534
+ }
535
+ }
536
+ }
537
+ ```
538
+
539
+ #### Nested Collection Filtering
540
+
541
+ Filter deeply nested collections using dot notation:
542
+
543
+ ```graphql
544
+ query {
545
+ series {
546
+ seasons(
547
+ episodes: {
548
+ terms: [
549
+ {
550
+ path: "name",
551
+ operator: LIKE,
552
+ value: "Final"
553
+ }
554
+ ]
555
+ }
556
+ ) {
557
+ number
558
+ episodes {
559
+ name
560
+ date
561
+ }
562
+ }
563
+ }
564
+ }
565
+ ```
566
+
567
+ #### Collection Filtering with Array Operations
568
+
569
+ Use array operations for collection fields:
570
+
571
+ ```graphql
572
+ query {
573
+ series {
574
+ seasons(
575
+ categories: { operator: IN, value: ["Drama", "Crime"] }
576
+ ) {
577
+ number
578
+ categories
579
+ }
580
+ }
581
+ }
582
+ ```
583
+
584
+ **Note**: Collection field filtering uses the exact same format as main query filtering, ensuring consistency across your GraphQL API. All available operators (`EQ`, `NE`, `GT`, `LT`, `GTE`, `LTE`, `LIKE`, `IN`, `NIN`, `BTW`) work with collection fields.
585
+
586
+ ## ๐Ÿ”— Relationships
587
+
588
+ ### Defining Relationships
589
+
590
+ Use the `extensions.relation` field to define relationships between types:
591
+
592
+ ```javascript
593
+ const AuthorType = new GraphQLObjectType({
594
+ name: 'Author',
595
+ fields: () => ({
596
+ id: { type: new GraphQLNonNull(GraphQLID) },
597
+ name: { type: new GraphQLNonNull(GraphQLString) },
598
+ books: {
599
+ type: new GraphQLList(BookType),
600
+ extensions: {
601
+ relation: {
602
+ connectionField: 'author',
603
+ displayField: 'title'
604
+ },
605
+ },
606
+ // resolve method automatically generated! ๐ŸŽ‰
607
+ },
608
+ }),
609
+ });
610
+
611
+ const BookType = new GraphQLObjectType({
612
+ name: 'Book',
613
+ fields: () => ({
614
+ id: { type: new GraphQLNonNull(GraphQLID) },
615
+ title: { type: new GraphQLNonNull(GraphQLString) },
616
+ author: {
617
+ type: AuthorType,
618
+ extensions: {
619
+ relation: {
620
+ displayField: 'name'
621
+ },
622
+ },
623
+ // resolve method automatically generated! ๐ŸŽ‰
624
+ },
625
+ }),
626
+ });
627
+ ```
628
+
629
+ ### Relationship Configuration
630
+
631
+ - `connectionField`: **(Required for collections)** The field storing the related object's ID - only needed for one-to-many relationships (GraphQLList). For single object relationships, the field name is automatically inferred from the GraphQL field name.
632
+ - `displayField`: **(Optional)** Field to use for display in UI components
633
+ - `embedded`: **(Optional)** Whether the relation is embedded (default: false)
634
+
635
+ ### Auto-Generated Resolve Methods
636
+
637
+ ๐ŸŽ‰ **NEW**: Simfinity.js automatically generates resolve methods for relationship fields when types are connected, eliminating the need for manual resolver boilerplate.
638
+
639
+ #### Before (Manual Resolvers)
640
+
641
+ ```javascript
642
+ const BookType = new GraphQLObjectType({
643
+ name: 'Book',
644
+ fields: () => ({
645
+ id: { type: new GraphQLNonNull(GraphQLID) },
646
+ title: { type: new GraphQLNonNull(GraphQLString) },
647
+ author: {
648
+ type: AuthorType,
649
+ extensions: {
650
+ relation: {
651
+ displayField: 'name'
652
+ },
653
+ },
654
+ // You had to manually write this
655
+ resolve(parent) {
656
+ return simfinity.getModel(AuthorType).findById(parent.author);
657
+ }
658
+ },
659
+ comments: {
660
+ type: new GraphQLList(CommentType),
661
+ extensions: {
662
+ relation: {
663
+ connectionField: 'bookId',
664
+ displayField: 'text'
665
+ },
666
+ },
667
+ // You had to manually write this too
668
+ resolve(parent) {
669
+ return simfinity.getModel(CommentType).find({ bookId: parent.id });
670
+ }
671
+ }
672
+ }),
673
+ });
674
+ ```
675
+
676
+ #### After (Auto-Generated Resolvers)
677
+
678
+ ```javascript
679
+ const BookType = new GraphQLObjectType({
680
+ name: 'Book',
681
+ fields: () => ({
682
+ id: { type: new GraphQLNonNull(GraphQLID) },
683
+ title: { type: new GraphQLNonNull(GraphQLString) },
684
+ author: {
685
+ type: AuthorType,
686
+ extensions: {
687
+ relation: {
688
+ displayField: 'name'
689
+ },
690
+ },
691
+ // resolve method automatically generated! ๐ŸŽ‰
692
+ },
693
+ comments: {
694
+ type: new GraphQLList(CommentType),
695
+ extensions: {
696
+ relation: {
697
+ connectionField: 'bookId',
698
+ displayField: 'text'
699
+ },
700
+ },
701
+ // resolve method automatically generated! ๐ŸŽ‰
702
+ }
703
+ }),
704
+ });
705
+ ```
706
+
707
+ #### How It Works
708
+
709
+ - **Single Object Relationships**: Automatically generates `findById()` resolvers using the field name or `connectionField`
710
+ - **Collection Relationships**: Automatically generates `find()` resolvers using the `connectionField` to query related objects
711
+ - **Lazy Loading**: Models are looked up at runtime, so types can be connected in any order
712
+ - **Backwards Compatible**: Existing manual resolve methods are preserved and not overwritten
713
+ - **Type Safety**: Clear error messages if related types aren't properly connected
714
+
715
+ #### Connect Your Types
716
+
717
+ ```javascript
718
+ // Connect all your types to Simfinity
719
+ simfinity.connect(null, AuthorType, 'author', 'authors');
720
+ simfinity.connect(null, BookType, 'book', 'books');
721
+ simfinity.connect(null, CommentType, 'comment', 'comments');
722
+
723
+ // Or use addNoEndpointType for types that don't need direct queries/mutations
724
+ simfinity.addNoEndpointType(AuthorType);
725
+ ```
726
+
727
+ That's it! All relationship resolvers are automatically generated when you connect your types.
728
+
729
+ ### Adding Types Without Endpoints
730
+
731
+ Use `addNoEndpointType()` for types that should be included in the GraphQL schema but don't need their own CRUD operations:
732
+
733
+ ```javascript
734
+ simfinity.addNoEndpointType(TypeName);
735
+ ```
736
+
737
+ **When to use `addNoEndpointType()` vs `connect()`:**
738
+
739
+ | Method | Use Case | Creates Endpoints | Use Example |
740
+ |--------|----------|-------------------|-------------|
741
+ | `connect()` | Types that need CRUD operations | โœ… Yes | User, Product, Order |
742
+ | `addNoEndpointType()` | Types only used in relationships | โŒ No | Address, Settings, Director |
743
+
744
+ #### Perfect Example: TV Series with Embedded Director
745
+
746
+ From the [series-sample](https://github.com/simtlix/series-sample) project:
747
+
748
+ ```javascript
749
+ // Director type - Used only as embedded data, no direct API access needed
750
+ const directorType = new GraphQLObjectType({
751
+ name: 'director',
752
+ fields: () => ({
753
+ id: { type: GraphQLID },
754
+ name: { type: new GraphQLNonNull(GraphQLString) },
755
+ country: { type: GraphQLString }
756
+ })
757
+ });
758
+
759
+ // Add to schema WITHOUT creating endpoints
760
+ simfinity.addNoEndpointType(directorType);
761
+
762
+ // Serie type - Has its own endpoints and embeds director data
763
+ const serieType = new GraphQLObjectType({
764
+ name: 'serie',
765
+ fields: () => ({
766
+ id: { type: GraphQLID },
767
+ name: { type: new GraphQLNonNull(GraphQLString) },
768
+ categories: { type: new GraphQLList(GraphQLString) },
769
+ director: {
770
+ type: new GraphQLNonNull(directorType),
771
+ extensions: {
772
+ relation: {
773
+ embedded: true, // Director data stored within serie document
774
+ displayField: 'name'
775
+ }
776
+ }
777
+ }
778
+ })
779
+ });
780
+
781
+ // Create full CRUD endpoints for series
782
+ simfinity.connect(null, serieType, 'serie', 'series');
783
+ ```
784
+
785
+ **Result:**
786
+ - โœ… `addserie`, `updateserie`, `deleteserie` mutations available
787
+ - โœ… `serie`, `series` queries available
788
+ - โŒ No `adddirector`, `director`, `directors` endpoints (director is embedded)
789
+
790
+ **Usage:**
791
+ ```graphql
792
+ mutation {
793
+ addserie(input: {
794
+ name: "Breaking Bad"
795
+ categories: ["crime", "drama", "thriller"]
796
+ director: {
797
+ name: "Vince Gilligan"
798
+ country: "United States"
799
+ }
800
+ }) {
801
+ id
802
+ name
803
+ director {
804
+ name
805
+ country
806
+ }
807
+ }
808
+ }
809
+ ```
810
+
811
+ #### When to Use Each Approach
812
+
813
+ **Use `addNoEndpointType()` for:**
814
+ - Simple data objects with few fields
815
+ - Data that doesn't need CRUD operations
816
+ - Objects that belong to a single parent (1:1 relationships)
817
+ - Configuration or settings objects
818
+ - **Examples**: Address, Director info, Product specifications
819
+
820
+ **Use `connect()` for:**
821
+ - Complex entities that need their own endpoints
822
+ - Data that needs CRUD operations
823
+ - Objects shared between multiple parents (many:many relationships)
824
+ - Objects with business logic (controllers, state machines)
825
+ - **Examples**: User, Product, Order, Season, Episode
826
+
827
+ ### Embedded vs Referenced Relationships
828
+
829
+ **Referenced Relationships** (default):
830
+ ```javascript
831
+ // Stores author ID in the book document
832
+ author: {
833
+ type: AuthorType,
834
+ extensions: {
835
+ relation: {
836
+ // connectionField not needed for single object relationships
837
+ embedded: false // This is the default
838
+ }
839
+ }
840
+ }
841
+ ```
842
+
843
+ **Embedded Relationships**:
844
+ ```javascript
845
+ // Stores the full publisher object in the book document
846
+ publisher: {
847
+ type: PublisherType,
848
+ extensions: {
849
+ relation: {
850
+ embedded: true
851
+ }
852
+ }
853
+ }
854
+ ```
855
+
856
+ ### Querying Relationships
857
+
858
+ Query nested relationships with dot notation:
859
+
860
+ ```graphql
861
+ query {
862
+ books(author: {
863
+ terms: [
864
+ {
865
+ path: "country.name",
866
+ operator: EQ,
867
+ value: "England"
868
+ }
869
+ ]
870
+ }) {
871
+ id
872
+ title
873
+ author {
874
+ name
875
+ country {
876
+ name
877
+ }
878
+ }
879
+ }
880
+ }
881
+ ```
882
+
883
+ ### Creating Objects with Relationships
884
+
885
+ **Link to existing objects:**
886
+ ```graphql
887
+ mutation {
888
+ addBook(input: {
889
+ title: "New Book"
890
+ author: {
891
+ id: "existing_author_id"
892
+ }
893
+ }) {
894
+ id
895
+ title
896
+ author {
897
+ name
898
+ }
899
+ }
900
+ }
901
+ ```
902
+
903
+ **Create embedded objects:**
904
+ ```graphql
905
+ mutation {
906
+ addBook(input: {
907
+ title: "New Book"
908
+ publisher: {
909
+ name: "Penguin Books"
910
+ location: "London"
911
+ }
912
+ }) {
913
+ id
914
+ title
915
+ publisher {
916
+ name
917
+ location
918
+ }
919
+ }
920
+ }
921
+ ```
922
+
923
+ ### Collection Fields
924
+
925
+ Work with arrays of related objects:
926
+
927
+ ```graphql
928
+ mutation {
929
+ updateBook(input: {
930
+ id: "book_id"
931
+ reviews: {
932
+ added: [
933
+ { rating: 5, comment: "Amazing!" }
934
+ { rating: 4, comment: "Good read" }
935
+ ]
936
+ updated: [
937
+ { id: "review_id", rating: 3 }
938
+ ]
939
+ deleted: ["review_id_to_delete"]
940
+ }
941
+ }) {
942
+ id
943
+ title
944
+ reviews {
945
+ rating
946
+ comment
947
+ }
948
+ }
949
+ }
950
+ ```
951
+
952
+ ## โœ… Validations
953
+
954
+ ### Declarative Validation Helpers
955
+
956
+ Simfinity.js provides built-in validation helpers to simplify common validation patterns, eliminating verbose boilerplate code.
957
+
958
+ #### Using Validators
959
+
960
+ ```javascript
961
+ const { validators } = require('@simtlix/simfinity-js');
962
+
963
+ const PersonType = new GraphQLObjectType({
964
+ name: 'Person',
965
+ fields: () => ({
966
+ id: { type: GraphQLID },
967
+ name: {
968
+ type: GraphQLString,
969
+ extensions: {
970
+ validations: validators.stringLength('Name', 2, 100)
971
+ }
972
+ },
973
+ email: {
974
+ type: GraphQLString,
975
+ extensions: {
976
+ validations: validators.email()
977
+ }
978
+ },
979
+ website: {
980
+ type: GraphQLString,
981
+ extensions: {
982
+ validations: validators.url()
983
+ }
984
+ },
985
+ age: {
986
+ type: GraphQLInt,
987
+ extensions: {
988
+ validations: validators.numberRange('Age', 0, 120)
989
+ }
990
+ },
991
+ price: {
992
+ type: GraphQLFloat,
993
+ extensions: {
994
+ validations: validators.positive('Price')
995
+ }
996
+ }
997
+ })
998
+ });
999
+ ```
1000
+
1001
+ #### Available Validators
1002
+
1003
+ **String Validators:**
1004
+ - `validators.stringLength(name, min, max)` - Validates string length with min/max bounds (required for CREATE)
1005
+ - `validators.maxLength(name, max)` - Validates maximum string length
1006
+ - `validators.pattern(name, regex, message)` - Validates against a regex pattern
1007
+ - `validators.email()` - Validates email format
1008
+ - `validators.url()` - Validates URL format
1009
+
1010
+ **Number Validators:**
1011
+ - `validators.numberRange(name, min, max)` - Validates number range
1012
+ - `validators.positive(name)` - Ensures number is positive
1013
+
1014
+ **Array Validators:**
1015
+ - `validators.arrayLength(name, maxItems, itemValidator)` - Validates array length and optionally each item
1016
+
1017
+ **Date Validators:**
1018
+ - `validators.dateFormat(name, format)` - Validates date format
1019
+ - `validators.futureDate(name)` - Ensures date is in the future
1020
+
1021
+ #### Validator Features
1022
+
1023
+ - **Automatic Operation Handling**: Validators work for both `CREATE` (save) and `UPDATE` operations
1024
+ - **Smart Validation**: For CREATE operations, values are required. For UPDATE operations, undefined/null values are allowed (field might not be updated)
1025
+ - **Consistent Error Messages**: All validators throw `SimfinityError` with appropriate messages
1026
+
1027
+ #### Example: Multiple Validators
1028
+
1029
+ ```javascript
1030
+ const ProductType = new GraphQLObjectType({
1031
+ name: 'Product',
1032
+ fields: () => ({
1033
+ id: { type: GraphQLID },
1034
+ name: {
1035
+ type: GraphQLString,
1036
+ extensions: {
1037
+ validations: validators.stringLength('Product Name', 3, 200)
1038
+ }
1039
+ },
1040
+ sku: {
1041
+ type: GraphQLString,
1042
+ extensions: {
1043
+ validations: validators.pattern('SKU', /^[A-Z0-9-]+$/, 'SKU must be uppercase alphanumeric with hyphens')
1044
+ }
1045
+ },
1046
+ price: {
1047
+ type: GraphQLFloat,
1048
+ extensions: {
1049
+ validations: validators.positive('Price')
1050
+ }
1051
+ },
1052
+ tags: {
1053
+ type: new GraphQLList(GraphQLString),
1054
+ extensions: {
1055
+ validations: validators.arrayLength('Tags', 10)
1056
+ }
1057
+ }
1058
+ })
1059
+ });
1060
+ ```
1061
+
1062
+ ### Field-Level Validations (Manual)
1063
+
1064
+ For custom validation logic, you can still write manual validators:
1065
+
1066
+ ```javascript
1067
+ const { SimfinityError } = require('@simtlix/simfinity-js');
1068
+
1069
+ const validateAge = {
1070
+ validate: async (typeName, fieldName, value, session) => {
1071
+ if (value < 0 || value > 120) {
1072
+ throw new SimfinityError(`Invalid age: ${value}`, 'VALIDATION_ERROR', 400);
1073
+ }
1074
+ }
1075
+ };
1076
+
1077
+ const PersonType = new GraphQLObjectType({
1078
+ name: 'Person',
1079
+ fields: () => ({
1080
+ id: { type: GraphQLID },
1081
+ name: {
1082
+ type: GraphQLString,
1083
+ extensions: {
1084
+ validations: {
1085
+ save: [{
1086
+ validate: async (typeName, fieldName, value, session) => {
1087
+ if (!value || value.length < 2) {
1088
+ throw new SimfinityError('Name must be at least 2 characters', 'VALIDATION_ERROR', 400);
1089
+ }
1090
+ }
1091
+ }],
1092
+ update: [{
1093
+ validate: async (typeName, fieldName, value, session) => {
1094
+ if (value && value.length < 2) {
1095
+ throw new SimfinityError('Name must be at least 2 characters', 'VALIDATION_ERROR', 400);
1096
+ }
1097
+ }
1098
+ }]
1099
+ }
1100
+ }
1101
+ },
1102
+ age: {
1103
+ type: GraphQLInt,
1104
+ extensions: {
1105
+ validations: {
1106
+ save: [validateAge],
1107
+ update: [validateAge]
1108
+ }
1109
+ }
1110
+ }
1111
+ })
1112
+ });
1113
+ ```
1114
+
1115
+ ### Type-Level Validations
1116
+
1117
+ Validate objects as a whole:
1118
+
1119
+ ```javascript
1120
+ const orderValidator = {
1121
+ validate: async (typeName, args, modelArgs, session) => {
1122
+ // Cross-field validation
1123
+ if (modelArgs.deliveryDate < modelArgs.orderDate) {
1124
+ throw new SimfinityError('Delivery date cannot be before order date', 'VALIDATION_ERROR', 400);
1125
+ }
1126
+
1127
+ // Business rule validation
1128
+ if (modelArgs.items.length === 0) {
1129
+ throw new SimfinityError('Order must contain at least one item', 'BUSINESS_ERROR', 400);
1130
+ }
1131
+ }
1132
+ };
1133
+
1134
+ const OrderType = new GraphQLObjectType({
1135
+ name: 'Order',
1136
+ extensions: {
1137
+ validations: {
1138
+ save: [orderValidator],
1139
+ update: [orderValidator]
1140
+ }
1141
+ },
1142
+ fields: () => ({
1143
+ // ... fields
1144
+ })
1145
+ });
1146
+ ```
1147
+
1148
+ ### Custom Validated Scalar Types
1149
+
1150
+ Create custom scalar types with built-in validation. The generated type names follow the pattern `{name}_{baseScalarTypeName}`.
1151
+
1152
+ #### Pre-built Scalars
1153
+
1154
+ Simfinity.js provides ready-to-use validated scalars for common patterns:
1155
+
1156
+ ```javascript
1157
+ const { scalars } = require('@simtlix/simfinity-js');
1158
+
1159
+ const UserType = new GraphQLObjectType({
1160
+ name: 'User',
1161
+ fields: () => ({
1162
+ id: { type: GraphQLID },
1163
+ email: { type: scalars.EmailScalar }, // Type name: Email_String
1164
+ website: { type: scalars.URLScalar }, // Type name: URL_String
1165
+ age: { type: scalars.PositiveIntScalar }, // Type name: PositiveInt_Int
1166
+ price: { type: scalars.PositiveFloatScalar } // Type name: PositiveFloat_Float
1167
+ }),
1168
+ });
1169
+ ```
1170
+
1171
+ **Available Pre-built Scalars:**
1172
+ - `scalars.EmailScalar` - Validates email format (`Email_String`)
1173
+ - `scalars.URLScalar` - Validates URL format (`URL_String`)
1174
+ - `scalars.PositiveIntScalar` - Validates positive integers (`PositiveInt_Int`)
1175
+ - `scalars.PositiveFloatScalar` - Validates positive floats (`PositiveFloat_Float`)
1176
+
1177
+ #### Factory Functions for Custom Scalars
1178
+
1179
+ Create custom validated scalars with parameters:
1180
+
1181
+ ```javascript
1182
+ const { scalars } = require('@simtlix/simfinity-js');
1183
+
1184
+ // Create a bounded string scalar (name length between 2-100 characters)
1185
+ const NameScalar = scalars.createBoundedStringScalar('Name', 2, 100);
1186
+
1187
+ // Create a bounded integer scalar (age between 0-120)
1188
+ const AgeScalar = scalars.createBoundedIntScalar('Age', 0, 120);
1189
+
1190
+ // Create a bounded float scalar (rating between 0-10)
1191
+ const RatingScalar = scalars.createBoundedFloatScalar('Rating', 0, 10);
1192
+
1193
+ // Create a pattern-based string scalar (phone number format)
1194
+ const PhoneScalar = scalars.createPatternStringScalar(
1195
+ 'Phone',
1196
+ /^\+?[\d\s\-()]+$/,
1197
+ 'Invalid phone number format'
1198
+ );
1199
+
1200
+ // Use in your types
1201
+ const PersonType = new GraphQLObjectType({
1202
+ name: 'Person',
1203
+ fields: () => ({
1204
+ id: { type: GraphQLID },
1205
+ name: { type: NameScalar }, // Type name: Name_String
1206
+ age: { type: AgeScalar }, // Type name: Age_Int
1207
+ rating: { type: RatingScalar }, // Type name: Rating_Float
1208
+ phone: { type: PhoneScalar } // Type name: Phone_String
1209
+ }),
1210
+ });
1211
+ ```
1212
+
1213
+ **Available Factory Functions:**
1214
+ - `scalars.createBoundedStringScalar(name, min, max)` - String with length bounds
1215
+ - `scalars.createBoundedIntScalar(name, min, max)` - Integer with range validation
1216
+ - `scalars.createBoundedFloatScalar(name, min, max)` - Float with range validation
1217
+ - `scalars.createPatternStringScalar(name, pattern, message)` - String with regex pattern validation
1218
+
1219
+ #### Creating Custom Scalars Manually
1220
+
1221
+ You can also create custom scalars using `createValidatedScalar` directly:
1222
+
1223
+ ```javascript
1224
+ const { GraphQLString, GraphQLInt } = require('graphql');
1225
+ const { createValidatedScalar } = require('@simtlix/simfinity-js');
1226
+
1227
+ // Email scalar with validation (generates type name: Email_String)
1228
+ const EmailScalar = createValidatedScalar(
1229
+ 'Email',
1230
+ 'A valid email address',
1231
+ GraphQLString,
1232
+ (value) => {
1233
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1234
+ if (!emailRegex.test(value)) {
1235
+ throw new Error('Invalid email format');
1236
+ }
1237
+ }
1238
+ );
1239
+
1240
+ // Positive integer scalar (generates type name: PositiveInt_Int)
1241
+ const PositiveIntScalar = createValidatedScalar(
1242
+ 'PositiveInt',
1243
+ 'A positive integer',
1244
+ GraphQLInt,
1245
+ (value) => {
1246
+ if (value <= 0) {
1247
+ throw new Error('Value must be positive');
1248
+ }
1249
+ }
1250
+ );
1251
+
1252
+ // Use in your types
1253
+ const UserType = new GraphQLObjectType({
1254
+ name: 'User',
1255
+ fields: () => ({
1256
+ id: { type: GraphQLID },
1257
+ email: { type: EmailScalar }, // Type name: Email_String
1258
+ age: { type: PositiveIntScalar }, // Type name: PositiveInt_Int
1259
+ }),
1260
+ });
1261
+ ```
1262
+
1263
+ ### Custom Error Classes
1264
+
1265
+ Create domain-specific error classes:
1266
+
1267
+ ```javascript
1268
+ const { SimfinityError } = require('@simtlix/simfinity-js');
1269
+
1270
+ // Business logic error
1271
+ class BusinessError extends SimfinityError {
1272
+ constructor(message) {
1273
+ super(message, 'BUSINESS_ERROR', 400);
1274
+ }
1275
+ }
1276
+
1277
+ // Authorization error
1278
+ class AuthorizationError extends SimfinityError {
1279
+ constructor(message) {
1280
+ super(message, 'UNAUTHORIZED', 401);
1281
+ }
1282
+ }
1283
+
1284
+ // Not found error
1285
+ class NotFoundError extends SimfinityError {
1286
+ constructor(message) {
1287
+ super(message, 'NOT_FOUND', 404);
1288
+ }
1289
+ }
1290
+ ```
1291
+
1292
+ ## ๐Ÿ”„ State Machines
1293
+
1294
+ Implement declarative state machine workflows:
1295
+
1296
+ ### 1. Define States
1297
+
1298
+ ```javascript
1299
+ const { GraphQLEnumType } = require('graphql');
1300
+
1301
+ const OrderState = new GraphQLEnumType({
1302
+ name: 'OrderState',
1303
+ values: {
1304
+ PENDING: { value: 'PENDING' },
1305
+ PROCESSING: { value: 'PROCESSING' },
1306
+ SHIPPED: { value: 'SHIPPED' },
1307
+ DELIVERED: { value: 'DELIVERED' },
1308
+ CANCELLED: { value: 'CANCELLED' }
1309
+ }
1310
+ });
1311
+ ```
1312
+
1313
+ ### 2. Define Type with State Field
1314
+
1315
+ ```javascript
1316
+ const OrderType = new GraphQLObjectType({
1317
+ name: 'Order',
1318
+ fields: () => ({
1319
+ id: { type: GraphQLID },
1320
+ customer: { type: GraphQLString },
1321
+ state: { type: OrderState }
1322
+ })
1323
+ });
1324
+ ```
1325
+
1326
+ ### 3. Configure State Machine
1327
+
1328
+ ```javascript
1329
+ const stateMachine = {
1330
+ initialState: { name: 'PENDING', value: 'PENDING' },
1331
+ actions: {
1332
+ process: {
1333
+ from: { name: 'PENDING', value: 'PENDING' },
1334
+ to: { name: 'PROCESSING', value: 'PROCESSING' },
1335
+ description: 'Process the order',
1336
+ action: async (args, session) => {
1337
+ // Business logic for processing
1338
+ console.log(`Processing order ${args.id}`);
1339
+ // You can perform additional operations here
1340
+ }
1341
+ },
1342
+ ship: {
1343
+ from: { name: 'PROCESSING', value: 'PROCESSING' },
1344
+ to: { name: 'SHIPPED', value: 'SHIPPED' },
1345
+ description: 'Ship the order',
1346
+ action: async (args, session) => {
1347
+ // Business logic for shipping
1348
+ console.log(`Shipping order ${args.id}`);
1349
+ }
1350
+ },
1351
+ deliver: {
1352
+ from: { name: 'SHIPPED', value: 'SHIPPED' },
1353
+ to: { name: 'DELIVERED', value: 'DELIVERED' },
1354
+ description: 'Mark as delivered'
1355
+ },
1356
+ cancel: {
1357
+ from: { name: 'PENDING', value: 'PENDING' },
1358
+ to: { name: 'CANCELLED', value: 'CANCELLED' },
1359
+ description: 'Cancel the order'
1360
+ }
1361
+ }
1362
+ };
1363
+ ```
1364
+
1365
+ ### 4. Connect with State Machine
1366
+
1367
+ ```javascript
1368
+ simfinity.connect(null, OrderType, 'order', 'orders', null, null, stateMachine);
1369
+ ```
1370
+
1371
+ ### 5. Use State Machine Mutations
1372
+
1373
+ The state machine automatically generates mutations for each action:
1374
+
1375
+ ```graphql
1376
+ mutation {
1377
+ process_order(input: {
1378
+ id: "order_id"
1379
+ }) {
1380
+ id
1381
+ state
1382
+ customer
1383
+ }
1384
+ }
1385
+ ```
1386
+
1387
+ **Important Notes**:
1388
+ - The `state` field is automatically read-only and managed by the state machine
1389
+ - State transitions are only allowed based on the defined actions
1390
+ - Business logic in the `action` function is executed during transitions
1391
+ - Invalid transitions throw errors automatically
1392
+
1393
+ ## ๐ŸŽ›๏ธ Controllers & Lifecycle Hooks
1394
+
1395
+ Controllers provide fine-grained control over operations with lifecycle hooks:
1396
+
1397
+ ```javascript
1398
+ const bookController = {
1399
+ onSaving: async (doc, args, session, context) => {
1400
+ // Before saving - doc is a Mongoose document
1401
+ if (!doc.title || doc.title.trim().length === 0) {
1402
+ throw new Error('Book title cannot be empty');
1403
+ }
1404
+ // Access user from context to set owner
1405
+ if (context && context.user) {
1406
+ doc.owner = context.user.id;
1407
+ }
1408
+ console.log(`Creating book: ${doc.title}`);
1409
+ },
1410
+
1411
+ onSaved: async (doc, args, session, context) => {
1412
+ // After saving - doc is a plain object
1413
+ console.log(`Book saved: ${doc._id}`);
1414
+ // Can access context.user for post-save operations like notifications
1415
+ },
1416
+
1417
+ onUpdating: async (id, doc, session, context) => {
1418
+ // Before updating - doc contains only changed fields
1419
+ // Validate user has permission to update
1420
+ if (context && context.user && context.user.role !== 'admin') {
1421
+ throw new simfinity.SimfinityError('Only admins can update books', 'FORBIDDEN', 403);
1422
+ }
1423
+ console.log(`Updating book ${id}`);
1424
+ },
1425
+
1426
+ onUpdated: async (doc, session, context) => {
1427
+ // After updating - doc is the updated document
1428
+ console.log(`Book updated: ${doc.title}`);
1429
+ },
1430
+
1431
+ onDelete: async (doc, session, context) => {
1432
+ // Before deleting - doc is the document to be deleted
1433
+ // Validate user has permission to delete
1434
+ if (context && context.user && context.user.role !== 'admin') {
1435
+ throw new simfinity.SimfinityError('Only admins can delete books', 'FORBIDDEN', 403);
1436
+ }
1437
+ console.log(`Deleting book: ${doc.title}`);
1438
+ }
1439
+ };
1440
+
1441
+ // Connect with controller
1442
+ simfinity.connect(null, BookType, 'book', 'books', bookController);
1443
+ ```
1444
+
1445
+ ### Hook Parameters
1446
+
1447
+ **`onSaving(doc, args, session, context)`**:
1448
+ - `doc`: Mongoose Document instance (not yet saved)
1449
+ - `args`: Raw GraphQL mutation input
1450
+ - `session`: Mongoose session for transaction
1451
+ - `context`: GraphQL context object (includes request info, user data, etc.)
1452
+
1453
+ **`onSaved(doc, args, session, context)`**:
1454
+ - `doc`: Plain object of saved document
1455
+ - `args`: Raw GraphQL mutation input
1456
+ - `session`: Mongoose session for transaction
1457
+ - `context`: GraphQL context object (includes request info, user data, etc.)
1458
+
1459
+ **`onUpdating(id, doc, session, context)`**:
1460
+ - `id`: Document ID being updated
1461
+ - `doc`: Plain object with only changed fields
1462
+ - `session`: Mongoose session for transaction
1463
+ - `context`: GraphQL context object (includes request info, user data, etc.)
1464
+
1465
+ **`onUpdated(doc, session, context)`**:
1466
+ - `doc`: Full updated Mongoose document
1467
+ - `session`: Mongoose session for transaction
1468
+ - `context`: GraphQL context object (includes request info, user data, etc.)
1469
+
1470
+ **`onDelete(doc, session, context)`**:
1471
+ - `doc`: Plain object of document to be deleted
1472
+ - `session`: Mongoose session for transaction
1473
+ - `context`: GraphQL context object (includes request info, user data, etc.)
1474
+
1475
+ ### Using Context in Controllers
1476
+
1477
+ The `context` parameter provides access to the GraphQL request context, which typically includes user information, request metadata, and other application-specific data. This is particularly useful for:
1478
+
1479
+ - **Setting ownership**: Automatically assign the current user as the owner of new entities
1480
+ - **Authorization checks**: Validate user permissions before allowing operations
1481
+ - **Audit logging**: Track who performed which operations
1482
+ - **User-specific business logic**: Apply different logic based on user roles or attributes
1483
+
1484
+ **Example: Setting Owner on Creation**
1485
+
1486
+ ```javascript
1487
+ const documentController = {
1488
+ onSaving: async (doc, args, session, context) => {
1489
+ // Automatically set the owner to the current user
1490
+ if (context && context.user) {
1491
+ doc.owner = context.user.id;
1492
+ }
1493
+ }
1494
+ };
1495
+ ```
1496
+
1497
+ **Example: Role-Based Authorization**
1498
+
1499
+ ```javascript
1500
+ const adminOnlyController = {
1501
+ onUpdating: async (id, doc, session, context) => {
1502
+ if (!context || !context.user || context.user.role !== 'admin') {
1503
+ throw new simfinity.SimfinityError('Admin access required', 'FORBIDDEN', 403);
1504
+ }
1505
+ },
1506
+
1507
+ onDelete: async (doc, session, context) => {
1508
+ if (!context || !context.user || context.user.role !== 'admin') {
1509
+ throw new simfinity.SimfinityError('Admin access required', 'FORBIDDEN', 403);
1510
+ }
1511
+ }
1512
+ };
1513
+ ```
1514
+
1515
+ **Note**: When using `saveObject` programmatically (outside of GraphQL), the `context` parameter is optional and may be `undefined`. Always check for context existence before accessing its properties.
1516
+
1517
+ ## ๐Ÿ”’ Query Scope
1518
+
1519
+ ### Overview
1520
+
1521
+ Query scope allows you to automatically modify query arguments based on context (e.g., user permissions). This enables automatic filtering so that users can only see documents they're authorized to access. Scope functions are executed after middleware and before query execution, allowing you to append filter conditions to queries and aggregations.
1522
+
1523
+ ### Defining Scope
1524
+
1525
+ Define scope in the type extensions, similar to how validations are defined:
1526
+
1527
+ ```javascript
1528
+ const EpisodeType = new GraphQLObjectType({
1529
+ name: 'episode',
1530
+ extensions: {
1531
+ validations: {
1532
+ create: [validateEpisodeFields],
1533
+ update: [validateEpisodeBusinessRules]
1534
+ },
1535
+ scope: {
1536
+ find: async ({ type, args, operation, context }) => {
1537
+ // Modify args in place to add filter conditions
1538
+ args.owner = {
1539
+ terms: [
1540
+ {
1541
+ path: 'id',
1542
+ operator: 'EQ',
1543
+ value: context.user.id
1544
+ }
1545
+ ]
1546
+ };
1547
+ },
1548
+ aggregate: async ({ type, args, operation, context }) => {
1549
+ // Apply same scope to aggregate queries
1550
+ args.owner = {
1551
+ terms: [
1552
+ {
1553
+ path: 'id',
1554
+ operator: 'EQ',
1555
+ value: context.user.id
1556
+ }
1557
+ ]
1558
+ };
1559
+ },
1560
+ get_by_id: async ({ type, args, operation, context }) => {
1561
+ // For get_by_id, scope is automatically merged with id filter
1562
+ args.owner = {
1563
+ terms: [
1564
+ {
1565
+ path: 'id',
1566
+ operator: 'EQ',
1567
+ value: context.user.id
1568
+ }
1569
+ ]
1570
+ };
1571
+ }
1572
+ }
1573
+ },
1574
+ fields: () => ({
1575
+ id: { type: GraphQLID },
1576
+ name: { type: GraphQLString },
1577
+ owner: {
1578
+ type: new GraphQLNonNull(simfinity.getType('user')),
1579
+ extensions: {
1580
+ relation: {
1581
+ connectionField: 'owner',
1582
+ displayField: 'name'
1583
+ }
1584
+ }
1585
+ }
1586
+ })
1587
+ });
1588
+ ```
1589
+
1590
+ ### Scope for Find Operations
1591
+
1592
+ Scope functions for `find` operations modify the query arguments that are passed to `buildQuery`. The modified arguments are automatically used to filter results:
1593
+
1594
+ ```javascript
1595
+ const DocumentType = new GraphQLObjectType({
1596
+ name: 'Document',
1597
+ extensions: {
1598
+ scope: {
1599
+ find: async ({ type, args, operation, context }) => {
1600
+ // Only show documents owned by the current user
1601
+ args.owner = {
1602
+ terms: [
1603
+ {
1604
+ path: 'id',
1605
+ operator: 'EQ',
1606
+ value: context.user.id
1607
+ }
1608
+ ]
1609
+ };
1610
+ }
1611
+ }
1612
+ },
1613
+ fields: () => ({
1614
+ id: { type: GraphQLID },
1615
+ title: { type: GraphQLString },
1616
+ owner: {
1617
+ type: new GraphQLNonNull(simfinity.getType('user')),
1618
+ extensions: {
1619
+ relation: {
1620
+ connectionField: 'owner',
1621
+ displayField: 'name'
1622
+ }
1623
+ }
1624
+ }
1625
+ })
1626
+ });
1627
+ ```
1628
+
1629
+ **Result**: All `documents` queries will automatically filter to only return documents where `owner.id` equals `context.user.id`.
1630
+
1631
+ ### Scope for Aggregate Operations
1632
+
1633
+ Scope functions for `aggregate` operations work the same way, ensuring aggregation queries also respect the scope:
1634
+
1635
+ ```javascript
1636
+ const OrderType = new GraphQLObjectType({
1637
+ name: 'Order',
1638
+ extensions: {
1639
+ scope: {
1640
+ aggregate: async ({ type, args, operation, context }) => {
1641
+ // Only aggregate orders for the current user's organization
1642
+ args.organization = {
1643
+ terms: [
1644
+ {
1645
+ path: 'id',
1646
+ operator: 'EQ',
1647
+ value: context.user.organizationId
1648
+ }
1649
+ ]
1650
+ };
1651
+ }
1652
+ }
1653
+ },
1654
+ fields: () => ({
1655
+ // ... fields
1656
+ })
1657
+ });
1658
+ ```
1659
+
1660
+ **Result**: All `orders_aggregate` queries will automatically filter to only aggregate orders from the user's organization.
1661
+
1662
+ ### Scope for Get By ID Operations
1663
+
1664
+ For `get_by_id` operations, scope functions modify a temporary query arguments object that includes the id filter. The system automatically combines the id filter with scope filters:
1665
+
1666
+ ```javascript
1667
+ const PrivateDocumentType = new GraphQLObjectType({
1668
+ name: 'PrivateDocument',
1669
+ extensions: {
1670
+ scope: {
1671
+ get_by_id: async ({ type, args, operation, context }) => {
1672
+ // Ensure user can only access their own documents
1673
+ args.owner = {
1674
+ terms: [
1675
+ {
1676
+ path: 'id',
1677
+ operator: 'EQ',
1678
+ value: context.user.id
1679
+ }
1680
+ ]
1681
+ };
1682
+ }
1683
+ }
1684
+ },
1685
+ fields: () => ({
1686
+ // ... fields
1687
+ })
1688
+ });
1689
+ ```
1690
+
1691
+ **Result**: When querying `privatedocument(id: "some_id")`, the system will:
1692
+ 1. Create a query that includes both the id filter and the owner scope filter
1693
+ 2. Only return the document if it matches both conditions
1694
+ 3. Return `null` if the document exists but doesn't match the scope
1695
+
1696
+ ### Scope Function Parameters
1697
+
1698
+ Scope functions receive the same parameters as middleware for consistency:
1699
+
1700
+ ```javascript
1701
+ {
1702
+ type, // Type information (model, gqltype, controller, etc.)
1703
+ args, // GraphQL arguments passed to the operation (modify this object)
1704
+ operation, // Operation type: 'find', 'aggregate', or 'get_by_id'
1705
+ context // GraphQL context object (includes request info, user data, etc.)
1706
+ }
1707
+ ```
1708
+
1709
+ ### Filter Structure
1710
+
1711
+ When modifying `args` in scope functions, use the appropriate filter structure:
1712
+
1713
+ **For scalar fields:**
1714
+ ```javascript
1715
+ args.fieldName = {
1716
+ operator: 'EQ',
1717
+ value: 'someValue'
1718
+ };
1719
+ ```
1720
+
1721
+ **For object/relation fields (QLTypeFilterExpression):**
1722
+ ```javascript
1723
+ args.relationField = {
1724
+ terms: [
1725
+ {
1726
+ path: 'fieldName',
1727
+ operator: 'EQ',
1728
+ value: 'someValue'
1729
+ }
1730
+ ]
1731
+ };
1732
+ ```
1733
+
1734
+ ### Complete Example
1735
+
1736
+ Here's a complete example showing scope for all query operations:
1737
+
1738
+ ```javascript
1739
+ const EpisodeType = new GraphQLObjectType({
1740
+ name: 'episode',
1741
+ extensions: {
1742
+ validations: {
1743
+ save: [validateEpisodeFields],
1744
+ update: [validateEpisodeBusinessRules]
1745
+ },
1746
+ scope: {
1747
+ find: async ({ type, args, operation, context }) => {
1748
+ // Only show episodes from seasons the user has access to
1749
+ args.season = {
1750
+ terms: [
1751
+ {
1752
+ path: 'owner.id',
1753
+ operator: 'EQ',
1754
+ value: context.user.id
1755
+ }
1756
+ ]
1757
+ };
1758
+ },
1759
+ aggregate: async ({ type, args, operation, context }) => {
1760
+ // Apply same scope to aggregations
1761
+ args.season = {
1762
+ terms: [
1763
+ {
1764
+ path: 'owner.id',
1765
+ operator: 'EQ',
1766
+ value: context.user.id
1767
+ }
1768
+ ]
1769
+ };
1770
+ },
1771
+ get_by_id: async ({ type, args, operation, context }) => {
1772
+ // Ensure user can only access their own episodes
1773
+ args.owner = {
1774
+ terms: [
1775
+ {
1776
+ path: 'id',
1777
+ operator: 'EQ',
1778
+ value: context.user.id
1779
+ }
1780
+ ]
1781
+ };
1782
+ }
1783
+ }
1784
+ },
1785
+ fields: () => ({
1786
+ id: { type: GraphQLID },
1787
+ number: { type: GraphQLInt },
1788
+ name: { type: GraphQLString },
1789
+ season: {
1790
+ type: new GraphQLNonNull(simfinity.getType('season')),
1791
+ extensions: {
1792
+ relation: {
1793
+ connectionField: 'season',
1794
+ displayField: 'number'
1795
+ }
1796
+ }
1797
+ },
1798
+ owner: {
1799
+ type: new GraphQLNonNull(simfinity.getType('user')),
1800
+ extensions: {
1801
+ relation: {
1802
+ connectionField: 'owner',
1803
+ displayField: 'name'
1804
+ }
1805
+ }
1806
+ }
1807
+ })
1808
+ });
1809
+ ```
1810
+
1811
+ ### Important Notes
1812
+
1813
+ - **Execution Order**: Scope functions are executed **after** middleware, so middleware can set up context (e.g., user info) that scope functions can use
1814
+ - **Modify Args In Place**: Scope functions should modify the `args` object directly
1815
+ - **Filter Structure**: Use the correct filter structure (`QLFilter` for scalars, `QLTypeFilterExpression` for relations)
1816
+ - **All Query Operations**: Scope applies to `find`, `aggregate`, and `get_by_id` operations
1817
+ - **Automatic Merging**: For `get_by_id`, the id filter is automatically combined with scope filters
1818
+ - **Context Access**: Use `context.user`, `context.ip`, or other context properties to determine scope
1819
+
1820
+ ### Use Cases
1821
+
1822
+ - **Multi-tenancy**: Filter documents by organization or tenant
1823
+ - **User-specific data**: Only show documents owned by the current user
1824
+ - **Role-based access**: Filter based on user roles or permissions
1825
+ - **Department/Team scoping**: Show only data relevant to user's department
1826
+ - **Geographic scoping**: Filter by user's location or region
1827
+
1828
+ ## ๐Ÿ” Authorization
1829
+
1830
+ Simfinity.js provides production-grade centralized GraphQL authorization supporting RBAC/ABAC, function-based rules, declarative policy expressions (JSON AST), wildcard permissions, and configurable default policies. It ships as a native Envelop plugin for GraphQL Yoga (recommended) and also supports the legacy graphql-middleware approach.
1831
+
1832
+ ### Quick Start
1833
+
1834
+ ```javascript
1835
+ const { auth } = require('@simtlix/simfinity-js');
1836
+ const { createYoga } = require('graphql-yoga');
1837
+
1838
+ const { createAuthPlugin, requireAuth, requireRole } = auth;
1839
+
1840
+ // Define your permission schema
1841
+ // Query/Mutation names match the ones generated by simfinity.connect()
1842
+ const permissions = {
1843
+ Query: {
1844
+ series: requireAuth(),
1845
+ seasons: requireAuth(),
1846
+ },
1847
+ Mutation: {
1848
+ deleteserie: requireRole('admin'),
1849
+ deletestar: requireRole('admin'),
1850
+ },
1851
+ serie: {
1852
+ '*': requireAuth(), // Wildcard: all fields require auth
1853
+ },
1854
+ };
1855
+
1856
+ // Create the Envelop auth plugin and pass it to your server
1857
+ const authPlugin = createAuthPlugin(permissions, { defaultPolicy: 'ALLOW' });
1858
+ const yoga = createYoga({ schema, plugins: [authPlugin] });
1859
+ ```
1860
+
1861
+ ### Permission Schema
1862
+
1863
+ The permission schema defines authorization rules per type and field:
1864
+
1865
+ ```javascript
1866
+ const permissions = {
1867
+ // Operation types (Query, Mutation, Subscription)
1868
+ Query: {
1869
+ fieldName: ruleOrRules,
1870
+ },
1871
+
1872
+ // Object types
1873
+ TypeName: {
1874
+ '*': wildcardRule, // Applies to all fields unless overridden
1875
+ fieldName: specificRule, // Overrides wildcard for this field
1876
+ },
1877
+ };
1878
+ ```
1879
+
1880
+ **Resolution Order:**
1881
+ 1. Check exact field rule: `permissions[TypeName][fieldName]`
1882
+ 2. Fallback to wildcard: `permissions[TypeName]['*']`
1883
+ 3. Apply default policy (ALLOW or DENY)
1884
+
1885
+ **Rule Types:**
1886
+ - **Function**: `(parent, args, ctx, info) => boolean | void | Promise<boolean | void>`
1887
+ - **Array of functions**: All rules must pass (AND logic)
1888
+ - **Policy expression**: JSON AST object (see below)
1889
+
1890
+ **Rule Semantics:**
1891
+ - `return true` or `return void` โ†’ allow
1892
+ - `return false` โ†’ deny
1893
+ - `throw Error` โ†’ deny with error
1894
+
1895
+ ### Rule Helpers
1896
+
1897
+ Simfinity.js provides reusable rule builders:
1898
+
1899
+ ```javascript
1900
+ const { auth } = require('@simtlix/simfinity-js');
1901
+
1902
+ const {
1903
+ resolvePath, // Utility to resolve dotted paths in objects
1904
+ requireAuth, // Requires ctx.user to exist
1905
+ requireRole, // Requires specific role(s)
1906
+ requirePermission, // Requires specific permission(s)
1907
+ composeRules, // Combine rules (AND logic)
1908
+ anyRule, // Combine rules (OR logic)
1909
+ isOwner, // Check resource ownership
1910
+ allow, // Always allow
1911
+ deny, // Always deny
1912
+ createRule, // Create custom rule
1913
+ } = auth;
1914
+ ```
1915
+
1916
+ #### requireAuth(userPath?)
1917
+
1918
+ Requires the user to be authenticated. Supports custom user paths in context:
1919
+
1920
+ ```javascript
1921
+ const permissions = {
1922
+ Query: {
1923
+ // Default: checks ctx.user
1924
+ me: requireAuth(),
1925
+
1926
+ // Custom path: checks ctx.auth.currentUser
1927
+ profile: requireAuth('auth.currentUser'),
1928
+
1929
+ // Deep path: checks ctx.session.data.user
1930
+ settings: requireAuth('session.data.user'),
1931
+ },
1932
+ };
1933
+ ```
1934
+
1935
+ #### requireRole(role, options?)
1936
+
1937
+ Requires the user to have a specific role. Supports custom paths:
1938
+
1939
+ ```javascript
1940
+ const permissions = {
1941
+ Query: {
1942
+ // Default: checks ctx.user.role
1943
+ adminDashboard: requireRole('ADMIN'),
1944
+ modTools: requireRole(['ADMIN', 'MODERATOR']), // Any of these roles
1945
+
1946
+ // Custom paths: checks ctx.auth.user.profile.role
1947
+ superAdmin: requireRole('SUPER_ADMIN', {
1948
+ userPath: 'auth.user',
1949
+ rolePath: 'profile.role',
1950
+ }),
1951
+ },
1952
+ };
1953
+ ```
1954
+
1955
+ #### requirePermission(permission, options?)
1956
+
1957
+ Requires the user to have specific permission(s). Supports custom paths:
1958
+
1959
+ ```javascript
1960
+ const permissions = {
1961
+ Mutation: {
1962
+ // Default: checks ctx.user.permissions
1963
+ deletePost: requirePermission('posts:delete'),
1964
+ manageUsers: requirePermission(['users:read', 'users:write']), // All required
1965
+
1966
+ // Custom paths: checks ctx.session.user.access.grants
1967
+ admin: requirePermission('admin:all', {
1968
+ userPath: 'session.user',
1969
+ permissionsPath: 'access.grants',
1970
+ }),
1971
+ },
1972
+ };
1973
+ ```
1974
+
1975
+ #### composeRules(...rules)
1976
+
1977
+ Combines multiple rules with AND logic (all must pass):
1978
+
1979
+ ```javascript
1980
+ const permissions = {
1981
+ Mutation: {
1982
+ updatePost: composeRules(
1983
+ requireAuth(),
1984
+ requireRole('EDITOR'),
1985
+ async (post, args, ctx) => post.authorId === ctx.user.id,
1986
+ ),
1987
+ },
1988
+ };
1989
+ ```
1990
+
1991
+ #### anyRule(...rules)
1992
+
1993
+ Combines multiple rules with OR logic (any must pass):
1994
+
1995
+ ```javascript
1996
+ const permissions = {
1997
+ Post: {
1998
+ content: anyRule(
1999
+ requireRole('ADMIN'),
2000
+ async (post, args, ctx) => post.authorId === ctx.user.id,
2001
+ ),
2002
+ },
2003
+ };
2004
+ ```
2005
+
2006
+ #### isOwner(ownerField, userIdField)
2007
+
2008
+ Checks if the authenticated user owns the resource:
2009
+
2010
+ ```javascript
2011
+ const permissions = {
2012
+ Post: {
2013
+ '*': composeRules(
2014
+ requireAuth(),
2015
+ isOwner('authorId', 'id'), // Compares post.authorId with ctx.user.id
2016
+ ),
2017
+ },
2018
+ };
2019
+ ```
2020
+
2021
+ ### Policy Expressions (JSON AST)
2022
+
2023
+ For declarative rules, use JSON AST policy expressions:
2024
+
2025
+ ```javascript
2026
+ const permissions = {
2027
+ Post: {
2028
+ content: {
2029
+ anyOf: [
2030
+ { eq: [{ ref: 'parent.published' }, true] },
2031
+ { eq: [{ ref: 'parent.authorId' }, { ref: 'ctx.user.id' }] },
2032
+ ],
2033
+ },
2034
+ },
2035
+ };
2036
+ ```
2037
+
2038
+ **Supported Operators:**
2039
+
2040
+ | Operator | Description | Example |
2041
+ |----------|-------------|---------|
2042
+ | `eq` | Equals | `{ eq: [{ ref: 'parent.status' }, 'active'] }` |
2043
+ | `in` | Value in array | `{ in: [{ ref: 'ctx.user.role' }, ['ADMIN', 'MOD']] }` |
2044
+ | `allOf` | All must be true (AND) | `{ allOf: [expr1, expr2] }` |
2045
+ | `anyOf` | Any must be true (OR) | `{ anyOf: [expr1, expr2] }` |
2046
+ | `not` | Negation | `{ not: { eq: [{ ref: 'parent.deleted' }, true] } }` |
2047
+
2048
+ **References:**
2049
+
2050
+ Use `{ ref: 'path' }` to reference values:
2051
+ - `parent.*` - Parent resolver result (the object being resolved)
2052
+ - `args.*` - GraphQL arguments
2053
+ - `ctx.*` - GraphQL context
2054
+
2055
+ **Security:**
2056
+ - Only `parent`, `args`, and `ctx` roots are allowed
2057
+ - Unknown operators fail closed (deny)
2058
+ - No `eval()` or `Function()` - pure object traversal
2059
+
2060
+ ### Integration with GraphQL Yoga / Envelop
2061
+
2062
+ The recommended way to use the auth system is via the Envelop plugin, which works natively with GraphQL Yoga and any Envelop-based server. The plugin wraps resolvers in-place without rebuilding the schema, avoiding compatibility issues.
2063
+
2064
+ ```javascript
2065
+ const { createYoga } = require('graphql-yoga');
2066
+ const { createServer } = require('http');
2067
+ const simfinity = require('@simtlix/simfinity-js');
2068
+
2069
+ const { auth } = simfinity;
2070
+ const { createAuthPlugin, requireAuth, requireRole, composeRules, isOwner, deny } = auth;
2071
+
2072
+ // Define your types and connect them
2073
+ simfinity.connect(null, SerieType, 'serie', 'series');
2074
+ simfinity.connect(null, SeasonType, 'season', 'seasons');
2075
+ simfinity.connect(null, StarType, 'star', 'stars');
2076
+
2077
+ // Create base schema
2078
+ const schema = simfinity.createSchema();
2079
+
2080
+ // Define permissions
2081
+ // Query/Mutation names match the ones generated by simfinity.connect()
2082
+ const permissions = {
2083
+ Query: {
2084
+ series: requireAuth(),
2085
+ seasons: requireAuth(),
2086
+ stars: requireAuth(),
2087
+ },
2088
+ Mutation: {
2089
+ addserie: requireAuth(),
2090
+ updateserie: composeRules(requireAuth(), isOwner('createdBy')),
2091
+ deleteserie: requireRole('admin'),
2092
+ deletestar: requireRole('admin'),
2093
+ },
2094
+ serie: {
2095
+ '*': requireAuth(),
2096
+ },
2097
+ season: {
2098
+ '*': requireAuth(),
2099
+ },
2100
+ };
2101
+
2102
+ // Create auth plugin
2103
+ const authPlugin = createAuthPlugin(permissions, {
2104
+ defaultPolicy: 'ALLOW',
2105
+ debug: false,
2106
+ });
2107
+
2108
+ // Setup Yoga with the auth plugin
2109
+ const yoga = createYoga({
2110
+ schema,
2111
+ plugins: [authPlugin],
2112
+ context: (req) => ({
2113
+ user: req.user, // Set by your authentication layer
2114
+ }),
2115
+ });
2116
+
2117
+ const server = createServer(yoga);
2118
+ server.listen(4000);
2119
+ ```
2120
+
2121
+ ### Legacy: Integration with graphql-middleware
2122
+
2123
+ > **Deprecated:** `applyMiddleware` from `graphql-middleware` rebuilds the schema via `mapSchema`,
2124
+ > which can cause `"Schema must contain uniquely named types"` errors with Simfinity schemas.
2125
+ > Use `createAuthPlugin` with GraphQL Yoga / Envelop instead.
2126
+
2127
+ ```javascript
2128
+ const { applyMiddleware } = require('graphql-middleware');
2129
+ const simfinity = require('@simtlix/simfinity-js');
2130
+
2131
+ const { auth } = simfinity;
2132
+ const { createAuthMiddleware, requireAuth, requireRole } = auth;
2133
+
2134
+ const baseSchema = simfinity.createSchema();
2135
+
2136
+ const authMiddleware = createAuthMiddleware(permissions, {
2137
+ defaultPolicy: 'DENY',
2138
+ });
2139
+
2140
+ const schema = applyMiddleware(baseSchema, authMiddleware);
2141
+ ```
2142
+
2143
+ ### Plugin / Middleware Options
2144
+
2145
+ ```javascript
2146
+ const plugin = createAuthPlugin(permissions, {
2147
+ defaultPolicy: 'DENY', // 'ALLOW' or 'DENY' (default: 'DENY')
2148
+ debug: false, // Enable debug logging
2149
+ });
2150
+ ```
2151
+
2152
+ | Option | Type | Default | Description |
2153
+ |--------|------|---------|-------------|
2154
+ | `defaultPolicy` | `'ALLOW' \| 'DENY'` | `'DENY'` | Policy when no rule matches |
2155
+ | `debug` | `boolean` | `false` | Log authorization decisions |
2156
+
2157
+ ### Error Handling
2158
+
2159
+ The auth middleware uses Simfinity error classes:
2160
+
2161
+ ```javascript
2162
+ const { auth } = require('@simtlix/simfinity-js');
2163
+
2164
+ const { UnauthenticatedError, ForbiddenError } = auth;
2165
+
2166
+ // UnauthenticatedError: code 'UNAUTHENTICATED', status 401
2167
+ // ForbiddenError: code 'FORBIDDEN', status 403
2168
+ ```
2169
+
2170
+ Custom error handling in rules:
2171
+
2172
+ ```javascript
2173
+ const permissions = {
2174
+ Mutation: {
2175
+ deleteserie: async (parent, args, ctx) => {
2176
+ if (!ctx.user) {
2177
+ throw new auth.UnauthenticatedError('Please log in');
2178
+ }
2179
+ if (ctx.user.role !== 'admin') {
2180
+ throw new auth.ForbiddenError('Only admins can delete series');
2181
+ }
2182
+ return true;
2183
+ },
2184
+ },
2185
+ };
2186
+ ```
2187
+
2188
+ ### Best Practices
2189
+
2190
+ 1. **Default to DENY**: Use `defaultPolicy: 'DENY'` for security
2191
+ 2. **Use wildcards wisely**: `'*'` rules provide baseline security per type
2192
+ 3. **Prefer helper rules**: Use `requireAuth()`, `requireRole()` over custom functions
2193
+ 4. **Fail closed**: Custom rules should deny on unexpected conditions
2194
+ 5. **Keep rules simple**: Complex logic belongs in controllers, not auth rules
2195
+ 6. **Test thoroughly**: Auth rules are critical - test all scenarios
2196
+
2197
+ ## ๐Ÿ”ง Middlewares
2198
+
2199
+ 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.
2200
+
2201
+ ### Adding Middlewares
2202
+
2203
+ Register middlewares using `simfinity.use()`. Middlewares execute in the order they're registered:
2204
+
2205
+ ```javascript
2206
+ // Basic logging middleware
2207
+ simfinity.use((params, next) => {
2208
+ console.log(`Executing ${params.operation} on ${params.type?.name || 'custom mutation'}`);
2209
+ next();
2210
+ });
2211
+ ```
2212
+
2213
+ ### Middleware Parameters
2214
+
2215
+ Each middleware receives a `params` object containing:
2216
+
2217
+ ```javascript
2218
+ simfinity.use((params, next) => {
2219
+ // params object contains:
2220
+ const {
2221
+ type, // Type information (model, gqltype, controller, etc.)
2222
+ args, // GraphQL arguments passed to the operation
2223
+ operation, // Operation type: 'save', 'update', 'delete', 'get_by_id', 'find', 'state_changed', 'custom_mutation'
2224
+ context, // GraphQL context object (includes request info, user data, etc.)
2225
+ actionName, // For state machine actions (only present for state_changed operations)
2226
+ actionField, // State machine action details (only present for state_changed operations)
2227
+ entry // Custom mutation name (only present for custom_mutation operations)
2228
+ } = params;
2229
+
2230
+ // Always call next() to continue the middleware chain
2231
+ next();
2232
+ });
2233
+ ```
2234
+
2235
+ ### Common Use Cases
2236
+
2237
+ #### 1. Authentication & Authorization
2238
+
2239
+ ```javascript
2240
+ simfinity.use((params, next) => {
2241
+ const { context, operation, type } = params;
2242
+
2243
+ // Skip authentication for read operations
2244
+ if (operation === 'get_by_id' || operation === 'find') {
2245
+ return next();
2246
+ }
2247
+
2248
+ // Check if user is authenticated
2249
+ if (!context.user) {
2250
+ throw new simfinity.SimfinityError('Authentication required', 'UNAUTHORIZED', 401);
2251
+ }
2252
+
2253
+ // Check permissions for specific types
2254
+ if (type?.name === 'User' && context.user.role !== 'admin') {
2255
+ throw new simfinity.SimfinityError('Admin access required', 'FORBIDDEN', 403);
2256
+ }
2257
+
2258
+ next();
2259
+ });
2260
+ ```
2261
+
2262
+ #### 2. Request Logging & Monitoring
2263
+
2264
+ ```javascript
2265
+ simfinity.use((params, next) => {
2266
+ const { operation, type, args, context } = params;
2267
+ const startTime = Date.now();
2268
+
2269
+ console.log(`[${new Date().toISOString()}] Starting ${operation}${type ? ` on ${type.name}` : ''}`);
2270
+
2271
+ // Continue with the operation
2272
+ next();
2273
+
2274
+ const duration = Date.now() - startTime;
2275
+ console.log(`[${new Date().toISOString()}] Completed ${operation} in ${duration}ms`);
2276
+ });
2277
+ ```
2278
+
2279
+ #### 3. Input Validation & Sanitization
2280
+
2281
+ ```javascript
2282
+ simfinity.use((params, next) => {
2283
+ const { operation, args, type } = params;
2284
+
2285
+ // Validate input for save operations
2286
+ if (operation === 'save' && args.input) {
2287
+ // Trim string fields
2288
+ Object.keys(args.input).forEach(key => {
2289
+ if (typeof args.input[key] === 'string') {
2290
+ args.input[key] = args.input[key].trim();
2291
+ }
2292
+ });
2293
+
2294
+ // Validate required business rules
2295
+ if (type?.name === 'Book' && args.input.title && args.input.title.length < 3) {
2296
+ throw new simfinity.SimfinityError('Book title must be at least 3 characters', 'VALIDATION_ERROR', 400);
2297
+ }
2298
+ }
2299
+
2300
+ next();
2301
+ });
2302
+ ```
2303
+
2304
+ #### 4. Rate Limiting
2305
+
2306
+ ```javascript
2307
+ const requestCounts = new Map();
2308
+
2309
+ simfinity.use((params, next) => {
2310
+ const { context, operation } = params;
2311
+ const userId = context.user?.id || context.ip;
2312
+ const now = Date.now();
2313
+ const windowMs = 60000; // 1 minute
2314
+ const maxRequests = 100;
2315
+
2316
+ // Only apply rate limiting to mutations
2317
+ if (operation === 'save' || operation === 'update' || operation === 'delete') {
2318
+ const userRequests = requestCounts.get(userId) || [];
2319
+ const recentRequests = userRequests.filter(time => now - time < windowMs);
2320
+
2321
+ if (recentRequests.length >= maxRequests) {
2322
+ throw new simfinity.SimfinityError('Rate limit exceeded', 'TOO_MANY_REQUESTS', 429);
2323
+ }
2324
+
2325
+ recentRequests.push(now);
2326
+ requestCounts.set(userId, recentRequests);
2327
+ }
2328
+
2329
+ next();
2330
+ });
2331
+ ```
2332
+
2333
+ #### 5. Audit Trail
2334
+
2335
+ ```javascript
2336
+ simfinity.use((params, next) => {
2337
+ const { operation, type, args, context } = params;
2338
+
2339
+ // Log all mutations for audit purposes
2340
+ if (operation === 'save' || operation === 'update' || operation === 'delete') {
2341
+ const auditEntry = {
2342
+ timestamp: new Date(),
2343
+ user: context.user?.id,
2344
+ operation,
2345
+ type: type?.name,
2346
+ entityId: args.id || 'new',
2347
+ data: operation === 'delete' ? null : args.input,
2348
+ ip: context.ip,
2349
+ userAgent: context.userAgent
2350
+ };
2351
+
2352
+ // Save to audit log (could be database, file, or external service)
2353
+ console.log('AUDIT:', JSON.stringify(auditEntry));
2354
+ }
2355
+
2356
+ next();
2357
+ });
2358
+ ```
2359
+
2360
+ ### Multiple Middlewares
2361
+
2362
+ Middlewares execute in registration order. Each middleware must call `next()` to continue the chain:
2363
+
2364
+ ```javascript
2365
+ // Middleware 1: Authentication
2366
+ simfinity.use((params, next) => {
2367
+ console.log('1. Checking authentication...');
2368
+ // Authentication logic here
2369
+ next(); // Continue to next middleware
2370
+ });
2371
+
2372
+ // Middleware 2: Authorization
2373
+ simfinity.use((params, next) => {
2374
+ console.log('2. Checking permissions...');
2375
+ // Authorization logic here
2376
+ next(); // Continue to next middleware
2377
+ });
2378
+
2379
+ // Middleware 3: Logging
2380
+ simfinity.use((params, next) => {
2381
+ console.log('3. Logging request...');
2382
+ // Logging logic here
2383
+ next(); // Continue to GraphQL operation
2384
+ });
2385
+ ```
2386
+
2387
+ ### Error Handling in Middlewares
2388
+
2389
+ Middlewares can throw errors to stop the operation:
2390
+
2391
+ ```javascript
2392
+ simfinity.use((params, next) => {
2393
+ const { context, operation } = params;
2394
+
2395
+ try {
2396
+ // Validation logic
2397
+ if (!context.user && operation !== 'find') {
2398
+ throw new simfinity.SimfinityError('Authentication required', 'UNAUTHORIZED', 401);
2399
+ }
2400
+
2401
+ next(); // Continue only if validation passes
2402
+ } catch (error) {
2403
+ // Error automatically bubbles up to GraphQL error handling
2404
+ throw error;
2405
+ }
2406
+ });
2407
+ ```
2408
+
2409
+ ### Conditional Middleware Execution
2410
+
2411
+ Execute middleware logic conditionally based on operation type or context:
2412
+
2413
+ ```javascript
2414
+ simfinity.use((params, next) => {
2415
+ const { operation, type, context } = params;
2416
+
2417
+ // Only apply to specific types
2418
+ if (type?.name === 'SensitiveData') {
2419
+ // Special handling for sensitive data
2420
+ if (!context.user?.hasHighSecurity) {
2421
+ throw new simfinity.SimfinityError('High security clearance required', 'FORBIDDEN', 403);
2422
+ }
2423
+ }
2424
+
2425
+ // Only apply to mutation operations
2426
+ if (['save', 'update', 'delete', 'state_changed'].includes(operation)) {
2427
+ // Mutation-specific logic
2428
+ console.log(`Mutation ${operation} executing...`);
2429
+ }
2430
+
2431
+ next();
2432
+ });
2433
+ ```
2434
+
2435
+ ### Best Practices
2436
+
2437
+ 1. **Always call `next()`**: Failing to call `next()` will hang the request
2438
+ 2. **Handle errors gracefully**: Use try-catch blocks for error-prone operations
2439
+ 3. **Keep middlewares focused**: Each middleware should handle one concern
2440
+ 4. **Order matters**: Register middlewares in logical order (auth โ†’ validation โ†’ logging)
2441
+ 5. **Performance consideration**: Middlewares run on every operation, keep them lightweight
2442
+ 6. **Use context wisely**: Store request-specific data in the GraphQL context object
2443
+
2444
+ ## ๐Ÿ”ง Advanced Features
2445
+
2446
+ ### Field Extensions
2447
+
2448
+ Control field behavior with extensions:
2449
+
2450
+ ```javascript
2451
+ const BookType = new GraphQLObjectType({
2452
+ name: 'Book',
2453
+ fields: () => ({
2454
+ id: { type: GraphQLID },
2455
+ title: {
2456
+ type: GraphQLString,
2457
+ extensions: {
2458
+ unique: true, // Creates unique index in MongoDB
2459
+ readOnly: true // Excludes from input types
2460
+ }
2461
+ },
2462
+ isbn: {
2463
+ type: GraphQLString,
2464
+ extensions: {
2465
+ unique: true
2466
+ }
2467
+ }
2468
+ })
2469
+ });
2470
+ ```
2471
+
2472
+ ### Custom Mutations
2473
+
2474
+ Register custom mutations beyond the automatic CRUD operations:
2475
+
2476
+ ```javascript
2477
+ simfinity.registerMutation(
2478
+ 'sendBookNotification',
2479
+ 'Send notification about a book',
2480
+ BookNotificationInput, // Input type
2481
+ NotificationResult, // Output type
2482
+ async (args, session) => {
2483
+ // Custom business logic
2484
+ const book = await BookModel.findById(args.bookId);
2485
+ // Send notification logic here
2486
+ return { success: true, message: 'Notification sent' };
2487
+ }
2488
+ );
2489
+ ```
2490
+
2491
+ ### Adding Types Without Endpoints
2492
+
2493
+ 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:
2494
+
2495
+ ```javascript
2496
+ // This type can be used in relationships but won't have queries/mutations
2497
+ simfinity.addNoEndpointType(AddressType);
2498
+ ```
2499
+
2500
+ ### Working with Existing Mongoose Models
2501
+
2502
+ Use your existing Mongoose models:
2503
+
2504
+ ```javascript
2505
+ const mongoose = require('mongoose');
2506
+
2507
+ const BookSchema = new mongoose.Schema({
2508
+ title: String,
2509
+ author: String,
2510
+ publishedDate: Date
2511
+ });
2512
+
2513
+ const BookModel = mongoose.model('Book', BookSchema);
2514
+
2515
+ // Use existing model
2516
+ simfinity.connect(BookModel, BookType, 'book', 'books');
2517
+ ```
2518
+
2519
+ ### Programmatic Data Access
2520
+
2521
+ Access data programmatically outside of GraphQL:
2522
+
2523
+ ```javascript
2524
+ // Save an object programmatically
2525
+ const newBook = await simfinity.saveObject('Book', {
2526
+ title: 'New Book',
2527
+ author: 'Author Name'
2528
+ }, session);
2529
+
2530
+ // Get the Mongoose model for a type
2531
+ const BookModel = simfinity.getModel(BookType);
2532
+ const books = await BookModel.find({ author: 'Douglas Adams' });
2533
+
2534
+ // Get the GraphQL type definition by name
2535
+ const UserType = simfinity.getType('User');
2536
+ console.log(UserType.name); // 'User'
2537
+ console.log(UserType.getFields()); // Access GraphQL fields
2538
+
2539
+ // Get the input type for a GraphQL type
2540
+ const BookInput = simfinity.getInputType(BookType);
2541
+ ```
2542
+
2543
+ ## ๐Ÿ“Š Aggregation Queries
2544
+
2545
+ 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.
2546
+
2547
+ ### Overview
2548
+
2549
+ For each entity type registered with `connect()`, an additional aggregation endpoint is automatically generated with the format `{entityname}_aggregate`.
2550
+
2551
+ ### Features
2552
+
2553
+ - **Group By**: Group results by any field (direct or related entity field path)
2554
+ - **Aggregation Operations**: SUM, COUNT, AVG, MIN, MAX
2555
+ - **Filtering**: Use the same filter parameters as regular queries
2556
+ - **Sorting**: Sort by groupId or any calculated fact (metrics), with support for multiple sort fields
2557
+ - **Pagination**: Use the same pagination parameters as regular queries
2558
+ - **Related Entity Fields**: Group by or aggregate on fields from related entities using dot notation
2559
+
2560
+ ### GraphQL Types
2561
+
2562
+ #### QLAggregationOperation (Enum)
2563
+ - `SUM`: Sum of numeric values
2564
+ - `COUNT`: Count of records
2565
+ - `AVG`: Average of numeric values
2566
+ - `MIN`: Minimum value
2567
+ - `MAX`: Maximum value
2568
+
2569
+ #### QLTypeAggregationFact (Input)
2570
+ ```graphql
2571
+ input QLTypeAggregationFact {
2572
+ operation: QLAggregationOperation!
2573
+ factName: String!
2574
+ path: String!
2575
+ }
2576
+ ```
2577
+
2578
+ #### QLTypeAggregationExpression (Input)
2579
+ ```graphql
2580
+ input QLTypeAggregationExpression {
2581
+ groupId: String!
2582
+ facts: [QLTypeAggregationFact!]!
2583
+ }
2584
+ ```
2585
+
2586
+ #### QLTypeAggregationResult (Output)
2587
+ ```graphql
2588
+ type QLTypeAggregationResult {
2589
+ groupId: JSON
2590
+ facts: JSON
2591
+ }
2592
+ ```
2593
+
2594
+ ### Quick Examples
2595
+
2596
+ #### Simple Group By
2597
+ ```graphql
2598
+ query {
2599
+ series_aggregate(
2600
+ aggregation: {
2601
+ groupId: "category"
2602
+ facts: [
2603
+ { operation: COUNT, factName: "total", path: "id" }
2604
+ ]
2605
+ }
2606
+ ) {
2607
+ groupId
2608
+ facts
2609
+ }
2610
+ }
2611
+ ```
2612
+
2613
+ #### Group By Related Entity
2614
+ ```graphql
2615
+ query {
2616
+ series_aggregate(
2617
+ aggregation: {
2618
+ groupId: "country.name"
2619
+ facts: [
2620
+ { operation: COUNT, factName: "count", path: "id" }
2621
+ { operation: AVG, factName: "avgRating", path: "rating" }
2622
+ ]
2623
+ }
2624
+ ) {
2625
+ groupId
2626
+ facts
2627
+ }
2628
+ }
2629
+ ```
2630
+
2631
+ #### Multiple Aggregation Facts
2632
+ ```graphql
2633
+ query {
2634
+ series_aggregate(
2635
+ aggregation: {
2636
+ groupId: "category"
2637
+ facts: [
2638
+ { operation: COUNT, factName: "total", path: "id" }
2639
+ { operation: SUM, factName: "totalEpisodes", path: "episodeCount" }
2640
+ { operation: AVG, factName: "avgRating", path: "rating" }
2641
+ { operation: MIN, factName: "minRating", path: "rating" }
2642
+ { operation: MAX, factName: "maxRating", path: "rating" }
2643
+ ]
2644
+ }
2645
+ ) {
2646
+ groupId
2647
+ facts
2648
+ }
2649
+ }
2650
+ ```
2651
+
2652
+ #### With Filtering
2653
+ ```graphql
2654
+ query {
2655
+ series_aggregate(
2656
+ rating: { operator: GTE, value: 8.0 }
2657
+ aggregation: {
2658
+ groupId: "category"
2659
+ facts: [
2660
+ { operation: COUNT, factName: "highRated", path: "id" }
2661
+ ]
2662
+ }
2663
+ ) {
2664
+ groupId
2665
+ facts
2666
+ }
2667
+ }
2668
+ ```
2669
+
2670
+ #### Sorting by Multiple Fields
2671
+ ```graphql
2672
+ query {
2673
+ series_aggregate(
2674
+ sort: {
2675
+ terms: [
2676
+ { field: "total", order: "DESC" }, # Sort by count first
2677
+ { field: "groupId", order: "ASC" } # Then by name
2678
+ ]
2679
+ }
2680
+ aggregation: {
2681
+ groupId: "category"
2682
+ facts: [
2683
+ { operation: COUNT, factName: "total", path: "id" }
2684
+ { operation: AVG, factName: "avgRating", path: "rating" }
2685
+ ]
2686
+ }
2687
+ ) {
2688
+ groupId
2689
+ facts
2690
+ }
2691
+ }
2692
+ ```
2693
+
2694
+ #### With Pagination (Top 5)
2695
+ ```graphql
2696
+ query {
2697
+ series_aggregate(
2698
+ sort: {
2699
+ terms: [{ field: "total", order: "DESC" }]
2700
+ }
2701
+ pagination: {
2702
+ page: 1
2703
+ size: 5
2704
+ }
2705
+ aggregation: {
2706
+ groupId: "category"
2707
+ facts: [
2708
+ { operation: COUNT, factName: "total", path: "id" }
2709
+ ]
2710
+ }
2711
+ ) {
2712
+ groupId
2713
+ facts
2714
+ }
2715
+ }
2716
+ ```
2717
+
2718
+ ### Field Path Resolution
2719
+
2720
+ The `groupId` and `path` parameters support:
2721
+
2722
+ 1. **Direct Fields**: Simple field names from the entity
2723
+ - Example: `"category"`, `"rating"`, `"id"`
2724
+
2725
+ 2. **Related Entity Fields**: Dot notation for fields in related entities
2726
+ - Example: `"country.name"`, `"studio.foundedYear"`
2727
+
2728
+ 3. **Nested Related Entities**: Multiple levels of relationships
2729
+ - Example: `"country.region.name"`
2730
+
2731
+ ### Sorting Options
2732
+
2733
+ - Sort by **groupId** or **any fact name**
2734
+ - **Multiple sort fields supported** - results are sorted by the first field, then by the second field for ties, etc.
2735
+ - Set the `field` parameter to:
2736
+ - `"groupId"` to sort by the grouping field
2737
+ - Any fact name (e.g., `"avgRating"`, `"total"`) to sort by that calculated metric
2738
+ - The `order` parameter (ASC/DESC) determines the sort direction for each field
2739
+ - If a field doesn't match groupId or any fact name, it defaults to groupId
2740
+ - If no sort is specified, defaults to sorting by groupId ascending
2741
+
2742
+ ### Pagination Notes
2743
+
2744
+ - The `page` and `size` parameters work as expected
2745
+ - The `count` parameter is **ignored** for aggregation queries
2746
+ - Pagination is applied **after** grouping and sorting
2747
+
2748
+ ### MongoDB Translation
2749
+
2750
+ Aggregation queries are translated to efficient MongoDB aggregation pipelines:
2751
+
2752
+ 1. **$lookup**: Joins with related entity collections
2753
+ 2. **$unwind**: Flattens joined arrays
2754
+ 3. **$match**: Applies filters (before grouping)
2755
+ 4. **$group**: Groups by the specified field with aggregation operations
2756
+ 5. **$project**: Formats final output with groupId and facts fields
2757
+ 6. **$sort**: Sorts results by groupId or facts (with multiple fields support)
2758
+ 7. **$limit** / **$skip**: Applied for pagination (after sorting)
2759
+
2760
+ ### Result Structure
2761
+
2762
+ Results are returned in a consistent format:
2763
+ ```json
2764
+ {
2765
+ "groupId": <value>,
2766
+ "facts": {
2767
+ "factName1": <calculated_value>,
2768
+ "factName2": <calculated_value>
2769
+ }
2770
+ }
2771
+ ```
2772
+
2773
+ For complete documentation with more examples, see [AGGREGATION_EXAMPLE.md](./AGGREGATION_EXAMPLE.md) and [AGGREGATION_CHANGES_SUMMARY.md](./AGGREGATION_CHANGES_SUMMARY.md).
2774
+
2775
+ ## ๐Ÿ“š Complete Example
2776
+
2777
+ Here's a complete bookstore example with relationships, validations, and state machines:
2778
+
2779
+ ```javascript
2780
+ const express = require('express');
2781
+ const { graphqlHTTP } = require('express-graphql');
2782
+ const mongoose = require('mongoose');
2783
+ const {
2784
+ GraphQLObjectType,
2785
+ GraphQLString,
2786
+ GraphQLNonNull,
2787
+ GraphQLID,
2788
+ GraphQLList,
2789
+ GraphQLInt,
2790
+ GraphQLEnumType
2791
+ } = require('graphql');
2792
+ const simfinity = require('@simtlix/simfinity-js');
2793
+
2794
+ // Connect to MongoDB
2795
+ mongoose.connect('mongodb://localhost:27017/bookstore', {
2796
+ useNewUrlParser: true,
2797
+ useUnifiedTopology: true,
2798
+ });
2799
+
2800
+ // Define Types
2801
+ const AuthorType = new GraphQLObjectType({
2802
+ name: 'Author',
2803
+ fields: () => ({
2804
+ id: { type: new GraphQLNonNull(GraphQLID) },
2805
+ name: { type: new GraphQLNonNull(GraphQLString) },
2806
+ email: { type: GraphQLString },
2807
+ books: {
2808
+ type: new GraphQLList(BookType),
2809
+ extensions: {
2810
+ relation: {
2811
+ connectionField: 'author',
2812
+ displayField: 'title'
2813
+ },
2814
+ },
2815
+ resolve(parent) {
2816
+ return simfinity.getModel(BookType).find({ author: parent.id });
2817
+ }
2818
+ },
2819
+ }),
2820
+ });
2821
+
2822
+ const BookType = new GraphQLObjectType({
2823
+ name: 'Book',
2824
+ fields: () => ({
2825
+ id: { type: new GraphQLNonNull(GraphQLID) },
2826
+ title: {
2827
+ type: new GraphQLNonNull(GraphQLString),
2828
+ extensions: {
2829
+ validations: {
2830
+ save: [{
2831
+ validate: async (typeName, fieldName, value, session) => {
2832
+ if (!value || value.length < 2) {
2833
+ throw new simfinity.SimfinityError('Title must be at least 2 characters', 'VALIDATION_ERROR', 400);
2834
+ }
2835
+ }
2836
+ }]
2837
+ }
2838
+ }
2839
+ },
2840
+ pages: { type: GraphQLInt },
2841
+ author: {
2842
+ type: AuthorType,
2843
+ extensions: {
2844
+ relation: {
2845
+ displayField: 'name'
2846
+ },
2847
+ },
2848
+ resolve(parent) {
2849
+ return simfinity.getModel(AuthorType).findById(parent.author);
2850
+ }
2851
+ },
2852
+ }),
2853
+ });
2854
+
2855
+ // Define Controllers
2856
+ const bookController = {
2857
+ onSaving: async (doc, args, session) => {
2858
+ console.log(`Creating book: ${doc.title}`);
2859
+ },
2860
+
2861
+ onSaved: async (doc, args, session) => {
2862
+ console.log(`Book saved: ${doc.title}`);
2863
+ }
2864
+ };
2865
+
2866
+ // Connect Types
2867
+ simfinity.connect(null, AuthorType, 'author', 'authors');
2868
+ simfinity.connect(null, BookType, 'book', 'books', bookController);
2869
+
2870
+ // Create Schema
2871
+ const schema = simfinity.createSchema();
2872
+
2873
+ // Setup Express Server
2874
+ const app = express();
2875
+
2876
+ app.use('/graphql', graphqlHTTP({
2877
+ schema,
2878
+ graphiql: true,
2879
+ formatError: simfinity.buildErrorFormatter((err) => {
2880
+ console.log(err);
2881
+ })
2882
+ }));
2883
+
2884
+ app.listen(4000, () => {
2885
+ console.log('Bookstore API running on http://localhost:4000/graphql');
2886
+ });
2887
+ ```
2888
+
2889
+ ## ๐Ÿ”— Resources
2890
+
2891
+ - **[Series Sample Project](https://github.com/simtlix/series-sample)** - A complete TV series microservice built with Simfinity.js demonstrating types, relationships, state machines, controllers, and authorization
2892
+ - **[Samples Repository](https://github.com/simtlix/simfinity.js-samples)** - Complete examples and use cases
2893
+ - **[MongoDB Query Language](https://docs.mongodb.com/manual/tutorial/query-documents/)** - Learn about MongoDB querying
2894
+ - **[GraphQL Documentation](https://graphql.org/learn/)** - Learn about GraphQL
2895
+
2896
+ ## ๐Ÿ“„ License
2897
+
2898
+ Apache-2.0 License - see the [LICENSE](LICENSE) file for details.
2899
+
2900
+ ## ๐Ÿค Contributing
2901
+
2902
+ Contributions are welcome! Please feel free to submit a Pull Request.
2903
+
2904
+ ---
2905
+
2906
+ *Built with โค๏ธ by [Simtlix](https://github.com/simtlix)*
2907
+
2908
+
2909
+ ## ๐Ÿ“š Query Examples from Series-Sample
2910
+
2911
+ Here are some practical GraphQL query examples from the series-sample project, showcasing how to use simfinity.js effectively:
2912
+
2913
+ ### 1. Series with Directors from a Specific Country
2914
+
2915
+ Find all series that have directors from the United States:
2916
+
2917
+ ```graphql
2918
+ query {
2919
+ series(director: {
2920
+ terms: [
2921
+ {
2922
+ path: "country",
2923
+ operator: EQ,
2924
+ value: "United States"
2925
+ }
2926
+ ]
2927
+ }) {
2928
+ id
2929
+ name
2930
+ categories
2931
+ director {
2932
+ name
2933
+ country
2934
+ }
2935
+ }
2936
+ }
2937
+ ```
2938
+
2939
+ ### 2. Series with a Specific Episode Name
2940
+
2941
+ Find series that contain an episode with the name "Pilot":
2942
+
2943
+ ```graphql
2944
+ query {
2945
+ series(
2946
+ seasons: {
2947
+ terms: [
2948
+ {
2949
+ path: "episodes.name",
2950
+ operator: EQ,
2951
+ value: "Pilot"
2952
+ }
2953
+ ]
2954
+ }
2955
+ ) {
2956
+ id
2957
+ name
2958
+ seasons {
2959
+ number
2960
+ episodes {
2961
+ number
2962
+ name
2963
+ date
2964
+ }
2965
+ }
2966
+ }
2967
+ }
2968
+ ```
2969
+
2970
+ ### 3. Series with a Particular Star
2971
+
2972
+ Find series that feature "Bryan Cranston":
2973
+
2974
+ ```graphql
2975
+ query {
2976
+ assignedStarsAndSeries(star: {
2977
+ terms: [
2978
+ {
2979
+ path: "name",
2980
+ operator: EQ,
2981
+ value: "Bryan Cranston"
2982
+ }
2983
+ ]
2984
+ }) {
2985
+ id
2986
+ star {
2987
+ name
2988
+ }
2989
+ serie {
2990
+ id
2991
+ name
2992
+ categories
2993
+ director {
2994
+ name
2995
+ country
2996
+ }
2997
+ }
2998
+ }
2999
+ }
3000
+ ```
3001
+
3002
+ ### 4. Seasons from Series with Directors from a Given Country
3003
+
3004
+ Find all seasons that belong to series directed by someone from the United States:
3005
+
3006
+ ```graphql
3007
+ query {
3008
+ seasons(serie: {
3009
+ terms: [
3010
+ {
3011
+ path: "director.country",
3012
+ operator: EQ,
3013
+ value: "United States"
3014
+ }
3015
+ ]
3016
+ }) {
3017
+ id
3018
+ number
3019
+ year
3020
+ state
3021
+ serie {
3022
+ name
3023
+ categories
3024
+ director {
3025
+ name
3026
+ country
3027
+ }
3028
+ }
3029
+ episodes {
3030
+ number
3031
+ name
3032
+ date
3033
+ }
3034
+ }
3035
+ }
3036
+ ```
3037
+
3038
+ ### 5. Combining Scalar and ObjectType Filters
3039
+
3040
+ Find series named "Breaking Bad" that have at least one season with number 1:
3041
+
3042
+ ```graphql
3043
+ query {
3044
+ series(
3045
+ name: {
3046
+ operator: EQ,
3047
+ value: "Breaking Bad"
3048
+ }
3049
+ seasons: {
3050
+ terms: [
3051
+ {
3052
+ path: "number",
3053
+ operator: EQ,
3054
+ value: 1
3055
+ }
3056
+ ]
3057
+ }
3058
+ ) {
3059
+ id
3060
+ name
3061
+ director {
3062
+ name
3063
+ country
3064
+ }
3065
+ seasons {
3066
+ number
3067
+ episodes {
3068
+ name
3069
+ }
3070
+ }
3071
+ }
3072
+ }
3073
+ ```
3074
+
3075
+ ### 6. Complex Nested Queries
3076
+
3077
+ Get complete information for a specific series:
3078
+
3079
+ ```graphql
3080
+ query {
3081
+ series(name: {
3082
+ operator: EQ,
3083
+ value: "Breaking Bad"
3084
+ }) {
3085
+ id
3086
+ name
3087
+ categories
3088
+ director {
3089
+ name
3090
+ country
3091
+ }
3092
+ seasons {
3093
+ number
3094
+ year
3095
+ state
3096
+ episodes {
3097
+ number
3098
+ name
3099
+ date
3100
+ }
3101
+ }
3102
+ }
3103
+ }
3104
+ ```
3105
+
3106
+ ### 7. Episodes from a Specific Season and Series
3107
+
3108
+ Find all episodes from Season 1 of Breaking Bad:
3109
+
3110
+ ```graphql
3111
+ query {
3112
+ episodes(season: {
3113
+ terms: [
3114
+ {
3115
+ path: "number",
3116
+ operator: EQ,
3117
+ value: 1
3118
+ },
3119
+ {
3120
+ path: "serie.name",
3121
+ operator: EQ,
3122
+ value: "Breaking Bad"
3123
+ }
3124
+ ]
3125
+ }) {
3126
+ id
3127
+ number
3128
+ name
3129
+ date
3130
+ season {
3131
+ number
3132
+ serie {
3133
+ name
3134
+ }
3135
+ }
3136
+ }
3137
+ }
3138
+ ```
3139
+
3140
+ ### 8. Series by Category
3141
+
3142
+ Find all crime series:
3143
+
3144
+ ```graphql
3145
+ query {
3146
+ series(categories: {
3147
+ operator: EQ,
3148
+ value: "Crime"
3149
+ }) {
3150
+ id
3151
+ name
3152
+ categories
3153
+ director {
3154
+ name
3155
+ country
3156
+ }
3157
+ }
3158
+ }
3159
+ ```
3160
+
3161
+ ### 9. Search by Partial Episode Name
3162
+
3163
+ Find episodes containing "Fire" in the name:
3164
+
3165
+ ```graphql
3166
+ query {
3167
+ episodes(name: {
3168
+ operator: LIKE,
3169
+ value: "Fire"
3170
+ }) {
3171
+ id
3172
+ number
3173
+ name
3174
+ date
3175
+ season {
3176
+ number
3177
+ serie {
3178
+ name
3179
+ }
3180
+ }
3181
+ }
3182
+ }
3183
+ ```
3184
+
3185
+ ### 10. Pagination
3186
+
3187
+ Simfinity.js supports built-in pagination with optional total count:
3188
+
3189
+ ```graphql
3190
+ query {
3191
+ series(
3192
+ categories: {
3193
+ operator: EQ,
3194
+ value: "Crime"
3195
+ }
3196
+ pagination: {
3197
+ page: 1,
3198
+ size: 2,
3199
+ count: true
3200
+ }
3201
+ ) {
3202
+ id
3203
+ name
3204
+ categories
3205
+ director {
3206
+ name
3207
+ country
3208
+ }
3209
+ }
3210
+ }
3211
+ ```
3212
+
3213
+ #### Pagination Parameters:
3214
+ - **page**: Page number (starts at 1, not 0)
3215
+ - **size**: Number of items per page
3216
+ - **count**: Optional boolean - if `true`, returns total count of matching records
3217
+
3218
+ #### Getting Total Count:
3219
+ When `count: true` is specified, the total count is available in the response extensions. You need to configure a plugin to expose it. Simfinity.js provides utility plugins for both Apollo Server and Envelop:
3220
+
3221
+ ```javascript
3222
+ const simfinity = require('@simtlix/simfinity-js');
3223
+
3224
+ // For Envelop
3225
+ const getEnveloped = envelop({
3226
+ plugins: [
3227
+ useSchema(schema),
3228
+ simfinity.plugins.envelopCountPlugin(),
3229
+ ],
3230
+ });
3231
+
3232
+ // For Apollo Server
3233
+ const server = new ApolloServer({
3234
+ schema,
3235
+ plugins: [
3236
+ simfinity.plugins.apolloCountPlugin(),
3237
+ ],
3238
+ });
3239
+ ```
3240
+
3241
+ See the [Plugins for Count in Extensions](#-plugins-for-count-in-extensions) section for complete examples.
3242
+
3243
+ #### Example Response:
3244
+ ```json
3245
+ {
3246
+ "data": {
3247
+ "series": [
3248
+ {
3249
+ "id": "1",
3250
+ "name": "Breaking Bad",
3251
+ "categories": ["Crime", "Drama"],
3252
+ "director": {
3253
+ "name": "Vince Gilligan",
3254
+ "country": "United States"
3255
+ }
3256
+ },
3257
+ {
3258
+ "id": "2",
3259
+ "name": "Better Call Saul",
3260
+ "categories": ["Crime", "Drama"],
3261
+ "director": {
3262
+ "name": "Vince Gilligan",
3263
+ "country": "United States"
3264
+ }
3265
+ }
3266
+ ]
3267
+ },
3268
+ "extensions": {
3269
+ "count": 15
3270
+ }
3271
+ }
3272
+ ```
3273
+
3274
+ ### 11. Sorting
3275
+
3276
+ Simfinity.js supports sorting with multiple fields and sort orders:
3277
+
3278
+ ```graphql
3279
+ query {
3280
+ series(
3281
+ categories: { operator: EQ, value: "Crime" }
3282
+ pagination: { page: 1, size: 5, count: true }
3283
+ sort: {
3284
+ terms: [
3285
+ {
3286
+ field: "name",
3287
+ order: DESC
3288
+ }
3289
+ ]
3290
+ }
3291
+ ) {
3292
+ id
3293
+ name
3294
+ categories
3295
+ director {
3296
+ name
3297
+ country
3298
+ }
3299
+ }
3300
+ }
3301
+ ```
3302
+
3303
+ #### Sorting Parameters:
3304
+ - **sort**: Contains sorting configuration
3305
+ - **terms**: Array of sort criteria (allows multiple sort fields)
3306
+ - **field**: The field name to sort by
3307
+ - **order**: Sort order - `ASC` (ascending) or `DESC` (descending)
3308
+
3309
+ #### Sorting by Nested Fields:
3310
+ You can sort by fields from related/nested objects using dot notation:
3311
+
3312
+ ```graphql
3313
+ query {
3314
+ series(
3315
+ categories: { operator: EQ, value: "Drama" }
3316
+ pagination: { page: 1, size: 5, count: true }
3317
+ sort: {
3318
+ terms: [
3319
+ {
3320
+ field: "director.name",
3321
+ order: DESC
3322
+ }
3323
+ ]
3324
+ }
3325
+ ) {
3326
+ id
3327
+ name
3328
+ categories
3329
+ director {
3330
+ name
3331
+ country
3332
+ }
3333
+ }
3334
+ }
3335
+ ```
3336
+
3337
+ #### Multiple Sort Fields:
3338
+ You can sort by multiple fields with different orders:
3339
+
3340
+ ```graphql
3341
+ query {
3342
+ series(
3343
+ sort: {
3344
+ terms: [
3345
+ { field: "director.country", order: ASC },
3346
+ { field: "name", order: DESC }
3347
+ ]
3348
+ }
3349
+ ) {
3350
+ id
3351
+ name
3352
+ director {
3353
+ name
3354
+ country
3355
+ }
3356
+ }
3357
+ }
3358
+ ```
3359
+
3360
+ #### Combining Features:
3361
+ The example above demonstrates combining **filtering**, **pagination**, and **sorting** in a single query - a common pattern for data tables and lists with full functionality.
3362
+
3363
+ ### 12. Series Released in a Specific Year Range
3364
+
3365
+ Find series with seasons released between 2010-2015:
3366
+
3367
+ ```graphql
3368
+ query {
3369
+ seasons(year: {
3370
+ operator: BETWEEN,
3371
+ value: [2010, 2015]
3372
+ }) {
3373
+ id
3374
+ number
3375
+ year
3376
+ serie {
3377
+ name
3378
+ director {
3379
+ name
3380
+ country
3381
+ }
3382
+ }
3383
+ }
3384
+ }
3385
+ ```
3386
+
3387
+
3388
+ ## ๐Ÿ”„ State Machine Example from Series-Sample
3389
+
3390
+ 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.
3391
+
3392
+ ### State Machine Configuration
3393
+
3394
+ State machines require **GraphQL Enum Types** to define states and proper state references:
3395
+
3396
+ **Step 1: Define the GraphQL Enum Type**
3397
+
3398
+ ```javascript
3399
+ const { GraphQLEnumType } = require('graphql');
3400
+
3401
+ const seasonState = new GraphQLEnumType({
3402
+ name: 'seasonState',
3403
+ values: {
3404
+ SCHEDULED: { value: 'SCHEDULED' },
3405
+ ACTIVE: { value: 'ACTIVE' },
3406
+ FINISHED: { value: 'FINISHED' }
3407
+ }
3408
+ });
3409
+ ```
3410
+
3411
+ **Step 2: Use Enum in GraphQL Object Type**
3412
+
3413
+ ```javascript
3414
+ const seasonType = new GraphQLObjectType({
3415
+ name: 'season',
3416
+ fields: () => ({
3417
+ id: { type: GraphQLID },
3418
+ number: { type: GraphQLInt },
3419
+ year: { type: GraphQLInt },
3420
+ state: { type: seasonState }, // โ† Use the enum type
3421
+ // ... other fields
3422
+ })
3423
+ });
3424
+ ```
3425
+
3426
+ **Step 3: Define State Machine with Enum Values**
3427
+
3428
+ ```javascript
3429
+ const stateMachine = {
3430
+ initialState: seasonState.getValue('SCHEDULED'),
3431
+ actions: {
3432
+ activate: {
3433
+ from: seasonState.getValue('SCHEDULED'),
3434
+ to: seasonState.getValue('ACTIVE'),
3435
+ action: async (params) => {
3436
+ console.log('Season activated:', JSON.stringify(params));
3437
+ }
3438
+ },
3439
+ finalize: {
3440
+ from: seasonState.getValue('ACTIVE'),
3441
+ to: seasonState.getValue('FINISHED'),
3442
+ action: async (params) => {
3443
+ console.log('Season finalized:', JSON.stringify(params));
3444
+ }
3445
+ }
3446
+ }
3447
+ };
3448
+
3449
+ // Connect type with state machine
3450
+ simfinity.connect(null, seasonType, 'season', 'seasons', null, null, stateMachine);
3451
+ ```
3452
+
3453
+ ### Season States
3454
+
3455
+ The Season entity has three states:
3456
+
3457
+ 1. **SCHEDULED** - Initial state when season is created
3458
+ 2. **ACTIVE** - Season is currently airing
3459
+ 3. **FINISHED** - Season has completed airing
3460
+
3461
+ ### State Transitions
3462
+
3463
+ **Available transitions:**
3464
+ - `activate`: SCHEDULED โ†’ ACTIVE
3465
+ - `finalize`: ACTIVE โ†’ FINISHED
3466
+
3467
+ ### State Machine Mutations
3468
+
3469
+ Simfinity.js automatically generates state transition mutations:
3470
+
3471
+ ```graphql
3472
+ # Activate a scheduled season
3473
+ mutation {
3474
+ activateseason(id: "season_id_here") {
3475
+ id
3476
+ number
3477
+ year
3478
+ state
3479
+ serie {
3480
+ name
3481
+ }
3482
+ }
3483
+ }
3484
+ ```
3485
+
3486
+ ```graphql
3487
+ # Finalize an active season
3488
+ mutation {
3489
+ finalizeseason(id: "season_id_here") {
3490
+ id
3491
+ number
3492
+ year
3493
+ state
3494
+ serie {
3495
+ name
3496
+ }
3497
+ }
3498
+ }
3499
+ ```
3500
+
3501
+ ### State Machine Features
3502
+
3503
+ **Validation:**
3504
+ - Only valid transitions are allowed
3505
+ - Attempting invalid transitions returns an error
3506
+ - State field is read-only (managed by state machine)
3507
+
3508
+ **Custom Actions:**
3509
+ - Each transition can execute custom business logic
3510
+ - Actions receive parameters including entity data
3511
+ - Actions can perform side effects (logging, notifications, etc.)
3512
+
3513
+ **Query by State:**
3514
+ ```graphql
3515
+ query {
3516
+ seasons(state: {
3517
+ operator: EQ,
3518
+ value: ACTIVE
3519
+ }) {
3520
+ id
3521
+ number
3522
+ year
3523
+ state
3524
+ serie {
3525
+ name
3526
+ }
3527
+ }
3528
+ }
3529
+ ```
3530
+
3531
+ ### State Machine Best Practices
3532
+
3533
+ 1. **GraphQL Enum Types**: Always define states as GraphQL enums for type safety
3534
+ 2. **getValue() Method**: Use `enumType.getValue('VALUE')` for state machine configuration
3535
+ 3. **Initial State**: Define clear initial state using enum values
3536
+ 4. **Linear Flows**: Design logical progression (SCHEDULED โ†’ ACTIVE โ†’ FINISHED)
3537
+ 5. **Type Safety**: GraphQL enums provide validation and autocomplete
3538
+ 6. **Actions**: Implement side effects in transition actions
3539
+ 7. **Error Handling**: Handle transition failures gracefully
3540
+
3541
+ ### Key Implementation Points
3542
+
3543
+ - **Enum Definition**: States must be defined as `GraphQLEnumType`
3544
+ - **Type Reference**: Use the enum type in your GraphQL object: `state: { type: seasonState }`
3545
+ - **State Machine Values**: Reference enum values with `seasonState.getValue('STATE_NAME')`
3546
+ - **Automatic Validation**: GraphQL validates state values against the enum
3547
+ - **IDE Support**: Enum values provide autocomplete and type checking
3548
+
3549
+ ### Example Workflow
3550
+
3551
+ ```graphql
3552
+ # 1. Create season (automatically SCHEDULED)
3553
+ mutation {
3554
+ addseason(input: {
3555
+ number: 6
3556
+ year: 2024
3557
+ serie: "series_id_here"
3558
+ }) {
3559
+ id
3560
+ state # Will be "SCHEDULED"
3561
+ }
3562
+ }
3563
+
3564
+ # 2. Activate season when airing begins
3565
+ mutation {
3566
+ activateseason(id: "season_id_here") {
3567
+ id
3568
+ state # Will be "ACTIVE"
3569
+ }
3570
+ }
3571
+
3572
+ # 3. Finalize season when completed
3573
+ mutation {
3574
+ finalizeseason(id: "season_id_here") {
3575
+ id
3576
+ state # Will be "FINISHED"
3577
+ }
3578
+ }
3579
+ ```
3580
+
3581
+
3582
+ ## ๐Ÿ“ฆ Plugins for Count in Extensions
3583
+
3584
+ To include the total count in the extensions of your GraphQL response, Simfinity.js provides utility plugins for both Apollo Server and Envelop. This is particularly useful for pagination and analytics.
3585
+
3586
+ ### Envelop Plugin
3587
+
3588
+ Use `simfinity.plugins.envelopCountPlugin()` to add count to extensions when using Envelop:
3589
+
3590
+ ```javascript
3591
+ const { envelop, useSchema } = require('@envelop/core');
3592
+ const { makeExecutableSchema } = require('@graphql-tools/schema');
3593
+ const simfinity = require('@simtlix/simfinity-js');
3594
+
3595
+ const schema = makeExecutableSchema({
3596
+ typeDefs,
3597
+ resolvers,
3598
+ });
3599
+
3600
+ const getEnveloped = envelop({
3601
+ plugins: [
3602
+ useSchema(schema),
3603
+ simfinity.plugins.envelopCountPlugin(), // Add the count plugin here
3604
+ ],
3605
+ });
3606
+
3607
+ // Use getEnveloped in your server setup
3608
+ ```
3609
+
3610
+ ### Apollo Server Plugin
3611
+
3612
+ Use `simfinity.plugins.apolloCountPlugin()` to add count to extensions when using Apollo Server:
3613
+
3614
+ ```javascript
3615
+ const { ApolloServer } = require('apollo-server-express');
3616
+ const simfinity = require('@simtlix/simfinity-js');
3617
+
3618
+ const server = new ApolloServer({
3619
+ schema,
3620
+ plugins: [
3621
+ simfinity.plugins.apolloCountPlugin(), // Add the count plugin here
3622
+ ],
3623
+ context: ({ req }) => {
3624
+ // Your context setup
3625
+ return {
3626
+ user: req.user,
3627
+ // count will be automatically added to extensions if present in context
3628
+ };
3629
+ },
3630
+ });
3631
+ ```
3632
+
3633
+ ### How to Use
3634
+
3635
+ 1. **Import the Plugin**: Use `simfinity.plugins.envelopCountPlugin()` or `simfinity.plugins.apolloCountPlugin()` depending on your GraphQL server.
3636
+ 2. **Configure Context**: Ensure that your context includes the count value when executing queries (Simfinity.js automatically sets `context.count` when `count: true` is specified in pagination).
3637
+ 3. **Access Count**: The count will be available in the `extensions` field of the GraphQL response.
3638
+
3639
+ ### Example Response
3640
+
3641
+ When the plugin is correctly set up, your GraphQL response will include the count in the extensions:
3642
+
3643
+ ```json
3644
+ {
3645
+ "data": {
3646
+ "series": [
3647
+ {
3648
+ "id": "1",
3649
+ "name": "Breaking Bad",
3650
+ "categories": ["Crime", "Drama"],
3651
+ "director": {
3652
+ "name": "Vince Gilligan",
3653
+ "country": "United States"
3654
+ }
3655
+ }
3656
+ ]
3657
+ },
3658
+ "extensions": {
3659
+ "count": 15
3660
+ }
3661
+ }
3662
+ ```
3663
+
3664
+ This setup allows you to efficiently manage and display pagination information in your GraphQL applications.
3665
+
3666
+ ## ๐Ÿ“– API Reference
3667
+
3668
+ Simfinity.js provides several utility methods for programmatic access to your GraphQL types and data:
3669
+
3670
+ ### `getType(typeName)`
3671
+
3672
+ Retrieves a GraphQL type definition from the internal types registry.
3673
+
3674
+ **Parameters:**
3675
+ - `typeName` (string | GraphQLObjectType): The name of the type or a GraphQL type object
3676
+
3677
+ **Returns:**
3678
+ - `GraphQLObjectType | null`: The GraphQL type definition, or null if not found
3679
+
3680
+ **Examples:**
3681
+
3682
+ ```javascript
3683
+ import { getType } from '@simtlix/simfinity-js';
3684
+
3685
+ // Get type by string name
3686
+ const UserType = getType('User');
3687
+ if (UserType) {
3688
+ console.log(UserType.name); // 'User'
3689
+
3690
+ // Access field definitions
3691
+ const fields = UserType.getFields();
3692
+ console.log(Object.keys(fields)); // ['id', 'name', 'email', ...]
3693
+
3694
+ // Check specific field
3695
+ const nameField = fields.name;
3696
+ console.log(nameField.type); // GraphQLString
3697
+ }
3698
+
3699
+ // Get type by GraphQL type object
3700
+ const BookType = getType(SomeBookType);
3701
+
3702
+ // Safe access - returns null if not found
3703
+ const nonExistentType = getType('NonExistent');
3704
+ console.log(nonExistentType); // null
3705
+ ```
3706
+
3707
+ **Use Cases:**
3708
+ - **Type introspection**: Examine type definitions programmatically
3709
+ - **Dynamic schema analysis**: Build tools that analyze your GraphQL schema
3710
+ - **Runtime type checking**: Validate types exist before operations
3711
+ - **Admin interfaces**: Build dynamic forms based on type definitions
3712
+ - **Circular reference resolution**: Prevent import cycles when types reference each other
3713
+
3714
+ ### Preventing Circular References with `getType`
3715
+
3716
+ When you have types that reference each other (like User and Group), using `getType` prevents circular import issues:
3717
+
3718
+ ```javascript
3719
+ import { GraphQLObjectType, GraphQLID, GraphQLString, GraphQLList } from 'graphql';
3720
+ import { getType } from '@simtlix/simfinity-js';
3721
+
3722
+ // User type that references Group
3723
+ const UserType = new GraphQLObjectType({
3724
+ name: 'User',
3725
+ fields: () => ({
3726
+ id: { type: GraphQLID },
3727
+ name: { type: GraphQLString },
3728
+ email: { type: GraphQLString },
3729
+
3730
+ // Reference Group type by name to avoid circular imports
3731
+ groups: {
3732
+ type: new GraphQLList(() => getType('Group')), // Use getType instead of direct import
3733
+ extensions: {
3734
+ relation: {
3735
+ connectionField: 'members',
3736
+ displayField: 'name'
3737
+ }
3738
+ }
3739
+ },
3740
+
3741
+ // Single group reference
3742
+ primaryGroup: {
3743
+ type: () => getType('Group'), // Lazy evaluation with getType
3744
+ extensions: {
3745
+ relation: {
3746
+ connectionField: 'primaryGroupId',
3747
+ displayField: 'name'
3748
+ }
3749
+ }
3750
+ }
3751
+ })
3752
+ });
3753
+
3754
+ // Group type that references User
3755
+ const GroupType = new GraphQLObjectType({
3756
+ name: 'Group',
3757
+ fields: () => ({
3758
+ id: { type: GraphQLID },
3759
+ name: { type: GraphQLString },
3760
+ description: { type: GraphQLString },
3761
+
3762
+ // Reference User type by name to avoid circular imports
3763
+ members: {
3764
+ type: new GraphQLList(() => getType('User')), // Use getType instead of direct import
3765
+ extensions: {
3766
+ relation: {
3767
+ connectionField: 'groups',
3768
+ displayField: 'name'
3769
+ }
3770
+ }
3771
+ },
3772
+
3773
+ // Single user reference (admin)
3774
+ admin: {
3775
+ type: () => getType('User'), // Lazy evaluation with getType
3776
+ extensions: {
3777
+ relation: {
3778
+ connectionField: 'adminId',
3779
+ displayField: 'name'
3780
+ }
3781
+ }
3782
+ }
3783
+ })
3784
+ });
3785
+
3786
+ // Register types with simfinity
3787
+ simfinity.connect(null, UserType, 'user', 'users');
3788
+ simfinity.connect(null, GroupType, 'group', 'groups');
3789
+
3790
+ // Create schema - resolvers will be auto-generated for all relationships
3791
+ const schema = simfinity.createSchema();
3792
+ ```
3793
+
3794
+ **Benefits of this approach:**
3795
+
3796
+ 1. **๐Ÿ”„ No Circular Imports**: Each file can import `getType` without importing other type definitions
3797
+ 2. **โšก Lazy Resolution**: Types are resolved at schema creation time when all types are registered
3798
+ 3. **๐Ÿ›ก๏ธ Type Safety**: Still maintains GraphQL type checking and validation
3799
+ 4. **๐Ÿงน Clean Architecture**: Separates type definitions from type relationships
3800
+ 5. **๐Ÿ“ฆ Better Modularity**: Each type can be in its own file without import dependencies
3801
+
3802
+ **File Structure Example:**
3803
+
3804
+ ```
3805
+ types/
3806
+ โ”œโ”€โ”€ User.js // Defines UserType using getType('Group')
3807
+ โ”œโ”€โ”€ Group.js // Defines GroupType using getType('User')
3808
+ โ””โ”€โ”€ index.js // Registers all types and creates schema
3809
+ ```
3810
+
3811
+ ```javascript
3812
+ // types/User.js
3813
+ import { GraphQLObjectType, GraphQLID, GraphQLString, GraphQLList } from 'graphql';
3814
+ import { getType } from '@simtlix/simfinity-js';
3815
+
3816
+ export const UserType = new GraphQLObjectType({
3817
+ name: 'User',
3818
+ fields: () => ({
3819
+ id: { type: GraphQLID },
3820
+ name: { type: GraphQLString },
3821
+ groups: {
3822
+ type: new GraphQLList(() => getType('Group')),
3823
+ extensions: { relation: { connectionField: 'members' } }
3824
+ }
3825
+ })
3826
+ });
3827
+
3828
+ // types/Group.js
3829
+ import { GraphQLObjectType, GraphQLID, GraphQLString, GraphQLList } from 'graphql';
3830
+ import { getType } from '@simtlix/simfinity-js';
3831
+
3832
+ export const GroupType = new GraphQLObjectType({
3833
+ name: 'Group',
3834
+ fields: () => ({
3835
+ id: { type: GraphQLID },
3836
+ name: { type: GraphQLString },
3837
+ members: {
3838
+ type: new GraphQLList(() => getType('User')),
3839
+ extensions: { relation: { connectionField: 'groups' } }
3840
+ }
3841
+ })
3842
+ });
3843
+
3844
+ // types/index.js
3845
+ import { UserType } from './User.js';
3846
+ import { GroupType } from './Group.js';
3847
+ import simfinity from '@simtlix/simfinity-js';
3848
+
3849
+ // Register all types
3850
+ simfinity.connect(null, UserType, 'user', 'users');
3851
+ simfinity.connect(null, GroupType, 'group', 'groups');
3852
+
3853
+ // Create schema with auto-generated resolvers
3854
+ export const schema = simfinity.createSchema();
3855
+ ```
3856
+
3857
+ ### `getModel(gqltype)`
3858
+
3859
+ Retrieves the Mongoose model associated with a GraphQL type.
3860
+
3861
+ **Parameters:**
3862
+ - `gqltype` (GraphQLObjectType): The GraphQL type object
3863
+
3864
+ **Returns:**
3865
+ - `MongooseModel`: The associated Mongoose model
3866
+
3867
+ **Example:**
3868
+
3869
+ ```javascript
3870
+ const BookModel = simfinity.getModel(BookType);
3871
+ const books = await BookModel.find({ author: 'Douglas Adams' });
3872
+ ```
3873
+
3874
+ ### `getInputType(type)`
3875
+
3876
+ Retrieves the input type for mutations associated with a GraphQL type.
3877
+
3878
+ **Parameters:**
3879
+ - `type` (GraphQLObjectType): The GraphQL type object
3880
+
3881
+ **Returns:**
3882
+ - `GraphQLInputObjectType`: The input type for mutations
3883
+
3884
+ **Example:**
3885
+
3886
+ ```javascript
3887
+ const BookInput = simfinity.getInputType(BookType);
3888
+ console.log(BookInput.getFields()); // Input fields for mutations
3889
+ ```
3890
+
3891
+ ### `saveObject(typeName, args, session?, context?)`
3892
+
3893
+ Programmatically save an object outside of GraphQL mutations.
3894
+
3895
+ **Parameters:**
3896
+ - `typeName` (string): The name of the GraphQL type
3897
+ - `args` (object): The data to save
3898
+ - `session` (MongooseSession, optional): Database session for transactions
3899
+ - `context` (object, optional): GraphQL context object (includes request info, user data, etc.)
3900
+
3901
+ **Returns:**
3902
+ - `Promise<object>`: The saved object
3903
+
3904
+ **Example:**
3905
+
3906
+ ```javascript
3907
+ const newBook = await simfinity.saveObject('Book', {
3908
+ title: 'New Book',
3909
+ author: 'Author Name'
3910
+ }, session, context);
3911
+
3912
+ // Without context (context will be undefined in controller hooks)
3913
+ const newBook = await simfinity.saveObject('Book', {
3914
+ title: 'New Book',
3915
+ author: 'Author Name'
3916
+ }, session);
3917
+ ```
3918
+
3919
+ **Note**: When `context` is not provided, it will be `undefined` in controller hooks. This is acceptable for programmatic usage where context may not be available.
3920
+
3921
+ ### `createSchema(includedQueryTypes?, includedMutationTypes?, includedCustomMutations?)`
3922
+
3923
+ Creates the final GraphQL schema with all connected types.
3924
+
3925
+ **Parameters:**
3926
+ - `includedQueryTypes` (array, optional): Limit query types to include
3927
+ - `includedMutationTypes` (array, optional): Limit mutation types to include
3928
+ - `includedCustomMutations` (array, optional): Limit custom mutations to include
3929
+
3930
+ **Returns:**
3931
+ - `GraphQLSchema`: The complete GraphQL schema
3932
+
3933
+ **Example:**
3934
+
3935
+ ```javascript
3936
+ const schema = simfinity.createSchema();
3937
+ ```
3938
+
3939
+ *Built with โค๏ธ by [Simtlix](https://github.com/simtlix)*
3940
+
3941
+