@simtlix/simfinity-js 3.0.1 → 3.3.0

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