@simtlix/simfinity-js 1.1.0 → 1.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,139 +1,1033 @@
1
+ # Simfinity.js
1
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.
2
4
 
3
- [![npm version](https://badge.fury.io/js/%40simtlix%2Fsimfinity-js.svg)](https://badge.fury.io/js/%40simtlix%2Fsimfinity-js)
5
+ ## ✨ Features
4
6
 
5
- # About SimfinityJS
6
- SimfinityJS is a Node.js framework that allows bringing all the power and flexibility of MongoDB query language to GraphQL interfaces.
7
+ - **Automatic Schema Generation**: Define your object model, and Simfinity.js generates all queries and mutations
8
+ - **MongoDB Integration**: Seamless translation between GraphQL and MongoDB
9
+ - **Powerful Querying**: Any query that can be executed in MongoDB can be executed in GraphQL
10
+ - **Auto-Generated Resolvers**: Automatically generates resolve methods for relationship fields
11
+ - **Business Logic**: Implement business logic and domain validations declaratively
12
+ - **State Machines**: Built-in support for declarative state machine workflows
13
+ - **Lifecycle Hooks**: Controller methods for granular control over operations
14
+ - **Custom Validation**: Field-level and type-level custom validations
15
+ - **Relationship Management**: Support for embedded and referenced relationships
7
16
 
8
- In pure GraphQL, you have to define every query and mutation. With SimfinityJS you define the object model, and the framework itself interprets all queries and mutations. SimfinityJS acts as a glue. It translates GraphQL to MongoDB and viceversa.
17
+ ## 📦 Installation
9
18
 
10
- As a result, developers can focus on model structure and object relationships.
19
+ ```bash
20
+ npm install mongoose graphql @simtlix/simfinity-js
21
+ ```
11
22
 
12
- ## Features
13
- - Translation between GraphQL and MongoDB and viceversa
14
- - Implement business logic in a declarative way
15
- - Implement domain validations in a declarative way.
16
- - Supports declarative state machine. Business logic can be included in each state transition.
17
- - Powerful semantic API. Basically, any query that can be executed in mongocli can be executed in GraphQL, thanks to SimfinityJS.
23
+ **Prerequisites**: Simfinity.js requires `mongoose` and `graphql` as peer dependencies.
18
24
 
25
+ ## 🚀 Quick Start
19
26
 
27
+ ### 1. Basic Setup
20
28
 
21
- # Quick Start
22
- ## Install
23
- ```bash
24
- npm install @simtlix/simfinity-js --save
29
+ ```javascript
30
+ const express = require('express');
31
+ const { graphqlHTTP } = require('express-graphql');
32
+ const mongoose = require('mongoose');
33
+ const simfinity = require('@simtlix/simfinity-js');
34
+
35
+ // Connect to MongoDB
36
+ mongoose.connect('mongodb://localhost:27017/bookstore', {
37
+ useNewUrlParser: true,
38
+ useUnifiedTopology: true,
39
+ });
40
+
41
+ const app = express();
25
42
  ```
26
43
 
27
- ## Adding Simfinity to your application
44
+ ### 2. Define Your GraphQL Type
28
45
 
29
46
  ```javascript
30
- const express = require('express')
31
- const graphqlHTTP = require('express-graphql')
32
- const simfinity = require('@simtlix/simfinity-js')
33
- const app = express()
34
- const mongoose = require('mongoose')
47
+ const { GraphQLObjectType, GraphQLString, GraphQLNonNull, GraphQLID } = require('graphql');
35
48
 
36
- //Replace with your Mongo DB connection string
37
- mongoose.connect('mongodb://localhost:27017,localhost:27018,localhost:27019/example2', { replicaSet: 'rs', useNewUrlParser: true, useUnifiedTopology: true })
49
+ const BookType = new GraphQLObjectType({
50
+ name: 'Book',
51
+ fields: () => ({
52
+ id: { type: new GraphQLNonNull(GraphQLID) },
53
+ title: { type: new GraphQLNonNull(GraphQLString) },
54
+ author: { type: GraphQLString },
55
+ }),
56
+ });
57
+ ```
38
58
 
39
- mongoose.connection.once('open', () => {
40
- console.log('connected to database')
41
- })
59
+ ### 3. Connect to Simfinity
42
60
 
43
- mongoose.set('debug', true);
61
+ ```javascript
62
+ // Connect the type to Simfinity
63
+ simfinity.connect(null, BookType, 'book', 'books');
44
64
 
45
- const type = require('./types')
46
- const includedTypes = [type.Book]
65
+ // Create the GraphQL schema
66
+ const schema = simfinity.createSchema();
67
+ ```
47
68
 
48
- const schema = simfinity.createSchema(includedTypes)
69
+ ### 4. Setup GraphQL Endpoint
49
70
 
71
+ ```javascript
50
72
  app.use('/graphql', graphqlHTTP({
51
- // Directing express-graphql to use this schema to map out the graph
52
73
  schema,
53
- /* Directing express-graphql to use graphiql when goto '/graphql' address in the browser
54
- which provides an interface to make GraphQl queries */
55
74
  graphiql: true,
56
75
  formatError: simfinity.buildErrorFormatter((err) => {
57
- console.log(err)
76
+ console.log(err);
58
77
  })
78
+ }));
79
+
80
+ app.listen(4000, () => {
81
+ console.log('Server is running on port 4000');
82
+ });
83
+ ```
84
+
85
+ ### 5. Try It Out
86
+
87
+ Open [http://localhost:4000/graphql](http://localhost:4000/graphql) and try these queries:
88
+
89
+ **Create a book:**
90
+ ```graphql
91
+ mutation {
92
+ addBook(input: {
93
+ title: "The Hitchhiker's Guide to the Galaxy"
94
+ author: "Douglas Adams"
95
+ }) {
96
+ id
97
+ title
98
+ author
99
+ }
100
+ }
101
+ ```
102
+
103
+ **List all books:**
104
+ ```graphql
105
+ query {
106
+ books {
107
+ id
108
+ title
109
+ author
110
+ }
111
+ }
112
+ ```
113
+
114
+ ## 🔧 Core Concepts
115
+
116
+ ### Connecting Models
117
+
118
+ The `simfinity.connect()` method links your GraphQL types to Simfinity's automatic schema generation:
119
+
120
+ ```javascript
121
+ simfinity.connect(
122
+ mongooseModel, // Optional: Custom Mongoose model (null for auto-generation)
123
+ graphQLType, // Required: Your GraphQLObjectType
124
+ singularEndpointName, // Required: Singular name for mutations (e.g., 'book')
125
+ pluralEndpointName, // Required: Plural name for queries (e.g., 'books')
126
+ controller, // Optional: Controller with lifecycle hooks
127
+ onModelCreated, // Optional: Callback when Mongoose model is created
128
+ stateMachine // Optional: State machine configuration
129
+ );
130
+ ```
131
+
132
+ ### Creating Schemas
133
+
134
+ Generate your complete GraphQL schema with optional type filtering:
59
135
 
60
- }))
136
+ ```javascript
137
+ const schema = simfinity.createSchema(
138
+ includedQueryTypes, // Optional: Array of types to include in queries
139
+ includedMutationTypes, // Optional: Array of types to include in mutations
140
+ includedCustomMutations // Optional: Array of custom mutations to include
141
+ );
142
+ ```
143
+
144
+ ### Global Configuration
145
+
146
+ ```javascript
147
+ // Prevent automatic MongoDB collection creation (useful for testing)
148
+ simfinity.preventCreatingCollection(true);
61
149
 
62
- app.listen(3000, () => {
63
- console.log('Listening on port 3000')
64
- })
150
+ // Add middleware for all operations
151
+ simfinity.use((params, next) => {
152
+ // params contains: type, args, operation, context
153
+ console.log(`Executing ${params.operation} on ${params.type.name}`);
154
+ next();
155
+ });
65
156
  ```
66
157
 
158
+ ## 📋 Basic Usage
67
159
 
68
- ### Defining the model
160
+ ### Automatic Query Generation
161
+
162
+ Simfinity automatically generates queries for each connected type:
69
163
 
70
164
  ```javascript
71
- const graphql = require('graphql')
72
- const simfinity = require('@simtlix/simfinity-js')
165
+ // For a BookType, you get:
166
+ // - book(id: ID): Book - Get single book by ID
167
+ // - books(...filters): [Book] - Get filtered list of books
168
+ ```
169
+
170
+ ### Automatic Mutation Generation
171
+
172
+ Simfinity automatically generates mutations for each connected type:
173
+
174
+ ```javascript
175
+ // For a BookType, you get:
176
+ // - addBook(input: BookInput): Book
177
+ // - updateBook(input: BookInputForUpdate): Book
178
+ // - deleteBook(id: ID): Book
179
+ ```
180
+
181
+ ### Filtering and Querying
182
+
183
+ Query with powerful filtering options:
184
+
185
+ ```graphql
186
+ query {
187
+ books(
188
+ title: { operator: LIKE, value: "Galaxy" }
189
+ author: { operator: EQ, value: "Douglas Adams" }
190
+ pagination: { page: 1, size: 10, count: true }
191
+ sort: { terms: [{ field: "title", order: ASC }] }
192
+ ) {
193
+ id
194
+ title
195
+ author
196
+ }
197
+ }
198
+ ```
199
+
200
+ #### Available Operators
201
+
202
+ - `EQ` - Equal
203
+ - `NE` - Not equal
204
+ - `GT` - Greater than
205
+ - `LT` - Less than
206
+ - `GTE` - Greater than or equal
207
+ - `LTE` - Less than or equal
208
+ - `LIKE` - Pattern matching
209
+ - `IN` - In array
210
+ - `NIN` - Not in array
211
+ - `BTW` - Between two values
212
+
213
+ ## 🔗 Relationships
214
+
215
+ ### Defining Relationships
73
216
 
74
- const {
75
- GraphQLObjectType,GraphQLString,
76
- GraphQLID, GraphQLInt
77
- } = graphql
217
+ Use the `extensions.relation` field to define relationships between types:
78
218
 
219
+ ```javascript
220
+ const AuthorType = new GraphQLObjectType({
221
+ name: 'Author',
222
+ fields: () => ({
223
+ id: { type: new GraphQLNonNull(GraphQLID) },
224
+ name: { type: new GraphQLNonNull(GraphQLString) },
225
+ books: {
226
+ type: new GraphQLList(BookType),
227
+ extensions: {
228
+ relation: {
229
+ connectionField: 'author',
230
+ displayField: 'title'
231
+ },
232
+ },
233
+ // resolve method automatically generated! 🎉
234
+ },
235
+ }),
236
+ });
79
237
 
80
238
  const BookType = new GraphQLObjectType({
81
239
  name: 'Book',
82
240
  fields: () => ({
83
- id: {
84
- type: GraphQLID
241
+ id: { type: new GraphQLNonNull(GraphQLID) },
242
+ title: { type: new GraphQLNonNull(GraphQLString) },
243
+ author: {
244
+ type: AuthorType,
245
+ extensions: {
246
+ relation: {
247
+ displayField: 'name'
248
+ },
249
+ },
250
+ // resolve method automatically generated! 🎉
85
251
  },
86
- name: { type: GraphQLString },
87
- pages: { type: GraphQLInt }
88
- })
89
- })
252
+ }),
253
+ });
254
+ ```
90
255
 
91
- module.exports = BookType
256
+ ### Relationship Configuration
92
257
 
93
- simfinity.connect(null, BookType, 'book', 'books', null, null, null)
258
+ - `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.
259
+ - `displayField`: **(Optional)** Field to use for display in UI components
260
+ - `embedded`: **(Optional)** Whether the relation is embedded (default: false)
261
+
262
+ ### Auto-Generated Resolve Methods
263
+
264
+ 🎉 **NEW**: Simfinity.js automatically generates resolve methods for relationship fields when types are connected, eliminating the need for manual resolver boilerplate.
265
+
266
+ #### Before (Manual Resolvers)
267
+
268
+ ```javascript
269
+ const BookType = new GraphQLObjectType({
270
+ name: 'Book',
271
+ fields: () => ({
272
+ id: { type: new GraphQLNonNull(GraphQLID) },
273
+ title: { type: new GraphQLNonNull(GraphQLString) },
274
+ author: {
275
+ type: AuthorType,
276
+ extensions: {
277
+ relation: {
278
+ displayField: 'name'
279
+ },
280
+ },
281
+ // You had to manually write this
282
+ resolve(parent) {
283
+ return simfinity.getModel(AuthorType).findById(parent.author);
284
+ }
285
+ },
286
+ comments: {
287
+ type: new GraphQLList(CommentType),
288
+ extensions: {
289
+ relation: {
290
+ connectionField: 'bookId',
291
+ displayField: 'text'
292
+ },
293
+ },
294
+ // You had to manually write this too
295
+ resolve(parent) {
296
+ return simfinity.getModel(CommentType).find({ bookId: parent.id });
297
+ }
298
+ }
299
+ }),
300
+ });
94
301
  ```
95
302
 
303
+ #### After (Auto-Generated Resolvers)
304
+
305
+ ```javascript
306
+ const BookType = new GraphQLObjectType({
307
+ name: 'Book',
308
+ fields: () => ({
309
+ id: { type: new GraphQLNonNull(GraphQLID) },
310
+ title: { type: new GraphQLNonNull(GraphQLString) },
311
+ author: {
312
+ type: AuthorType,
313
+ extensions: {
314
+ relation: {
315
+ displayField: 'name'
316
+ },
317
+ },
318
+ // resolve method automatically generated! 🎉
319
+ },
320
+ comments: {
321
+ type: new GraphQLList(CommentType),
322
+ extensions: {
323
+ relation: {
324
+ connectionField: 'bookId',
325
+ displayField: 'text'
326
+ },
327
+ },
328
+ // resolve method automatically generated! 🎉
329
+ }
330
+ }),
331
+ });
332
+ ```
96
333
 
97
- # Run
98
- Start replica set
334
+ #### How It Works
99
335
 
100
- `run-rs`
336
+ - **Single Object Relationships**: Automatically generates `findById()` resolvers using the field name or `connectionField`
337
+ - **Collection Relationships**: Automatically generates `find()` resolvers using the `connectionField` to query related objects
338
+ - **Lazy Loading**: Models are looked up at runtime, so types can be connected in any order
339
+ - **Backwards Compatible**: Existing manual resolve methods are preserved and not overwritten
340
+ - **Type Safety**: Clear error messages if related types aren't properly connected
101
341
 
102
- Run the application
342
+ #### Connect Your Types
103
343
 
104
- `node app.js`
344
+ ```javascript
345
+ // Connect all your types to Simfinity
346
+ simfinity.connect(null, AuthorType, 'author', 'authors');
347
+ simfinity.connect(null, BookType, 'book', 'books');
348
+ simfinity.connect(null, CommentType, 'comment', 'comments');
349
+
350
+ // Or use addNoEndpointType for types that don't need direct queries/mutations
351
+ simfinity.addNoEndpointType(AuthorType);
352
+ ```
353
+
354
+ That's it! All relationship resolvers are automatically generated when you connect your types.
355
+
356
+ ### Embedded vs Referenced Relationships
357
+
358
+ **Referenced Relationships** (default):
359
+ ```javascript
360
+ // Stores author ID in the book document
361
+ author: {
362
+ type: AuthorType,
363
+ extensions: {
364
+ relation: {
365
+ // connectionField not needed for single object relationships
366
+ embedded: false // This is the default
367
+ }
368
+ }
369
+ }
370
+ ```
105
371
 
372
+ **Embedded Relationships**:
373
+ ```javascript
374
+ // Stores the full publisher object in the book document
375
+ publisher: {
376
+ type: PublisherType,
377
+ extensions: {
378
+ relation: {
379
+ embedded: true
380
+ }
381
+ }
382
+ }
383
+ ```
106
384
 
385
+ ### Querying Relationships
107
386
 
108
- # Try it
387
+ Query nested relationships with dot notation:
109
388
 
110
- Open http://localhost:3000/graphql endpoint defined on app.js
389
+ ```graphql
390
+ query {
391
+ books(author: {
392
+ terms: [
393
+ {
394
+ path: "country.name",
395
+ operator: EQ,
396
+ value: "England"
397
+ }
398
+ ]
399
+ }) {
400
+ id
401
+ title
402
+ author {
403
+ name
404
+ country {
405
+ name
406
+ }
407
+ }
408
+ }
409
+ }
410
+ ```
111
411
 
412
+ ### Creating Objects with Relationships
112
413
 
113
- Create a book
414
+ **Link to existing objects:**
114
415
  ```graphql
115
416
  mutation {
116
- addbook (
117
- input:{
118
- name: "Hello World Book",
119
- pages: 333
417
+ addBook(input: {
418
+ title: "New Book"
419
+ author: {
420
+ id: "existing_author_id"
421
+ }
422
+ }) {
423
+ id
424
+ title
425
+ author {
426
+ name
120
427
  }
121
- )
428
+ }
122
429
  }
123
430
  ```
124
431
 
432
+ **Create embedded objects:**
433
+ ```graphql
434
+ mutation {
435
+ addBook(input: {
436
+ title: "New Book"
437
+ publisher: {
438
+ name: "Penguin Books"
439
+ location: "London"
440
+ }
441
+ }) {
442
+ id
443
+ title
444
+ publisher {
445
+ name
446
+ location
447
+ }
448
+ }
449
+ }
450
+ ```
451
+
452
+ ### Collection Fields
453
+
454
+ Work with arrays of related objects:
125
455
 
126
- List all books
127
456
  ```graphql
128
- query {
129
- books {
130
- id, name, pages
457
+ mutation {
458
+ updateBook(input: {
459
+ id: "book_id"
460
+ reviews: {
461
+ added: [
462
+ { rating: 5, comment: "Amazing!" }
463
+ { rating: 4, comment: "Good read" }
464
+ ]
465
+ updated: [
466
+ { id: "review_id", rating: 3 }
467
+ ]
468
+ deleted: ["review_id_to_delete"]
469
+ }
470
+ }) {
471
+ id
472
+ title
473
+ reviews {
474
+ rating
475
+ comment
476
+ }
131
477
  }
132
478
  }
133
479
  ```
134
480
 
481
+ ## 🎛️ Controllers & Lifecycle Hooks
482
+
483
+ Controllers provide fine-grained control over operations with lifecycle hooks:
484
+
485
+ ```javascript
486
+ const bookController = {
487
+ onSaving: async (doc, args, session) => {
488
+ // Before saving - doc is a Mongoose document
489
+ if (!doc.title || doc.title.trim().length === 0) {
490
+ throw new Error('Book title cannot be empty');
491
+ }
492
+ console.log(`Creating book: ${doc.title}`);
493
+ },
494
+
495
+ onSaved: async (doc, args, session) => {
496
+ // After saving - doc is a plain object
497
+ console.log(`Book saved: ${doc._id}`);
498
+ },
499
+
500
+ onUpdating: async (id, doc, session) => {
501
+ // Before updating - doc contains only changed fields
502
+ console.log(`Updating book ${id}`);
503
+ },
504
+
505
+ onUpdated: async (doc, session) => {
506
+ // After updating - doc is the updated document
507
+ console.log(`Book updated: ${doc.title}`);
508
+ },
509
+
510
+ onDelete: async (doc, session) => {
511
+ // Before deleting - doc is the document to be deleted
512
+ console.log(`Deleting book: ${doc.title}`);
513
+ }
514
+ };
515
+
516
+ // Connect with controller
517
+ simfinity.connect(null, BookType, 'book', 'books', bookController);
518
+ ```
519
+
520
+ ### Hook Parameters
521
+
522
+ **`onSaving(doc, args, session)`**:
523
+ - `doc`: Mongoose Document instance (not yet saved)
524
+ - `args`: Raw GraphQL mutation input
525
+ - `session`: Mongoose session for transaction
526
+
527
+ **`onSaved(doc, args, session)`**:
528
+ - `doc`: Plain object of saved document
529
+ - `args`: Raw GraphQL mutation input
530
+ - `session`: Mongoose session for transaction
531
+
532
+ **`onUpdating(id, doc, session)`**:
533
+ - `id`: Document ID being updated
534
+ - `doc`: Plain object with only changed fields
535
+ - `session`: Mongoose session for transaction
536
+
537
+ **`onUpdated(doc, session)`**:
538
+ - `doc`: Full updated Mongoose document
539
+ - `session`: Mongoose session for transaction
540
+
541
+ **`onDelete(doc, session)`**:
542
+ - `doc`: Plain object of document to be deleted
543
+ - `session`: Mongoose session for transaction
544
+
545
+ ## 🔄 State Machines
546
+
547
+ Implement declarative state machine workflows:
548
+
549
+ ### 1. Define States
550
+
551
+ ```javascript
552
+ const { GraphQLEnumType } = require('graphql');
553
+
554
+ const OrderState = new GraphQLEnumType({
555
+ name: 'OrderState',
556
+ values: {
557
+ PENDING: { value: 'PENDING' },
558
+ PROCESSING: { value: 'PROCESSING' },
559
+ SHIPPED: { value: 'SHIPPED' },
560
+ DELIVERED: { value: 'DELIVERED' },
561
+ CANCELLED: { value: 'CANCELLED' }
562
+ }
563
+ });
564
+ ```
565
+
566
+ ### 2. Define Type with State Field
567
+
568
+ ```javascript
569
+ const OrderType = new GraphQLObjectType({
570
+ name: 'Order',
571
+ fields: () => ({
572
+ id: { type: GraphQLID },
573
+ customer: { type: GraphQLString },
574
+ state: { type: OrderState }
575
+ })
576
+ });
577
+ ```
578
+
579
+ ### 3. Configure State Machine
580
+
581
+ ```javascript
582
+ const stateMachine = {
583
+ initialState: { name: 'PENDING', value: 'PENDING' },
584
+ actions: {
585
+ process: {
586
+ from: { name: 'PENDING', value: 'PENDING' },
587
+ to: { name: 'PROCESSING', value: 'PROCESSING' },
588
+ description: 'Process the order',
589
+ action: async (args, session) => {
590
+ // Business logic for processing
591
+ console.log(`Processing order ${args.id}`);
592
+ // You can perform additional operations here
593
+ }
594
+ },
595
+ ship: {
596
+ from: { name: 'PROCESSING', value: 'PROCESSING' },
597
+ to: { name: 'SHIPPED', value: 'SHIPPED' },
598
+ description: 'Ship the order',
599
+ action: async (args, session) => {
600
+ // Business logic for shipping
601
+ console.log(`Shipping order ${args.id}`);
602
+ }
603
+ },
604
+ deliver: {
605
+ from: { name: 'SHIPPED', value: 'SHIPPED' },
606
+ to: { name: 'DELIVERED', value: 'DELIVERED' },
607
+ description: 'Mark as delivered'
608
+ },
609
+ cancel: {
610
+ from: { name: 'PENDING', value: 'PENDING' },
611
+ to: { name: 'CANCELLED', value: 'CANCELLED' },
612
+ description: 'Cancel the order'
613
+ }
614
+ }
615
+ };
616
+ ```
617
+
618
+ ### 4. Connect with State Machine
619
+
620
+ ```javascript
621
+ simfinity.connect(null, OrderType, 'order', 'orders', null, null, stateMachine);
622
+ ```
623
+
624
+ ### 5. Use State Machine Mutations
625
+
626
+ The state machine automatically generates mutations for each action:
627
+
628
+ ```graphql
629
+ mutation {
630
+ process_order(input: {
631
+ id: "order_id"
632
+ }) {
633
+ id
634
+ state
635
+ customer
636
+ }
637
+ }
638
+ ```
639
+
640
+ **Important Notes**:
641
+ - The `state` field is automatically read-only and managed by the state machine
642
+ - State transitions are only allowed based on the defined actions
643
+ - Business logic in the `action` function is executed during transitions
644
+ - Invalid transitions throw errors automatically
645
+
646
+ ## ✅ Validations
647
+
648
+ ### Field-Level Validations
649
+
650
+ Add validation logic directly to fields:
651
+
652
+ ```javascript
653
+ const { SimfinityError } = require('@simtlix/simfinity-js');
654
+
655
+ const validateAge = {
656
+ validate: async (typeName, fieldName, value, session) => {
657
+ if (value < 0 || value > 120) {
658
+ throw new SimfinityError(`Invalid age: ${value}`, 'VALIDATION_ERROR', 400);
659
+ }
660
+ }
661
+ };
662
+
663
+ const PersonType = new GraphQLObjectType({
664
+ name: 'Person',
665
+ fields: () => ({
666
+ id: { type: GraphQLID },
667
+ name: {
668
+ type: GraphQLString,
669
+ extensions: {
670
+ validations: {
671
+ save: [{
672
+ validate: async (typeName, fieldName, value, session) => {
673
+ if (!value || value.length < 2) {
674
+ throw new SimfinityError('Name must be at least 2 characters', 'VALIDATION_ERROR', 400);
675
+ }
676
+ }
677
+ }],
678
+ update: [{
679
+ validate: async (typeName, fieldName, value, session) => {
680
+ if (value && value.length < 2) {
681
+ throw new SimfinityError('Name must be at least 2 characters', 'VALIDATION_ERROR', 400);
682
+ }
683
+ }
684
+ }]
685
+ }
686
+ }
687
+ },
688
+ age: {
689
+ type: GraphQLInt,
690
+ extensions: {
691
+ validations: {
692
+ save: [validateAge],
693
+ update: [validateAge]
694
+ }
695
+ }
696
+ }
697
+ })
698
+ });
699
+ ```
700
+
701
+ ### Type-Level Validations
702
+
703
+ Validate objects as a whole:
704
+
705
+ ```javascript
706
+ const orderValidator = {
707
+ validate: async (typeName, args, modelArgs, session) => {
708
+ // Cross-field validation
709
+ if (modelArgs.deliveryDate < modelArgs.orderDate) {
710
+ throw new SimfinityError('Delivery date cannot be before order date', 'VALIDATION_ERROR', 400);
711
+ }
712
+
713
+ // Business rule validation
714
+ if (modelArgs.items.length === 0) {
715
+ throw new SimfinityError('Order must contain at least one item', 'BUSINESS_ERROR', 400);
716
+ }
717
+ }
718
+ };
719
+
720
+ const OrderType = new GraphQLObjectType({
721
+ name: 'Order',
722
+ extensions: {
723
+ validations: {
724
+ save: [orderValidator],
725
+ update: [orderValidator]
726
+ }
727
+ },
728
+ fields: () => ({
729
+ // ... fields
730
+ })
731
+ });
732
+ ```
733
+
734
+ ### Custom Validated Scalar Types
735
+
736
+ Create custom scalar types with built-in validation:
737
+
738
+ ```javascript
739
+ const { GraphQLString, GraphQLInt } = require('graphql');
740
+ const { createValidatedScalar } = require('@simtlix/simfinity-js');
741
+
742
+ // Email scalar with validation
743
+ const EmailScalar = createValidatedScalar(
744
+ 'Email',
745
+ 'A valid email address',
746
+ GraphQLString,
747
+ (value) => {
748
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
749
+ if (!emailRegex.test(value)) {
750
+ throw new Error('Invalid email format');
751
+ }
752
+ }
753
+ );
754
+
755
+ // Positive integer scalar
756
+ const PositiveIntScalar = createValidatedScalar(
757
+ 'PositiveInt',
758
+ 'A positive integer',
759
+ GraphQLInt,
760
+ (value) => {
761
+ if (value <= 0) {
762
+ throw new Error('Value must be positive');
763
+ }
764
+ }
765
+ );
766
+
767
+ // Use in your types
768
+ const UserType = new GraphQLObjectType({
769
+ name: 'User',
770
+ fields: () => ({
771
+ id: { type: GraphQLID },
772
+ email: { type: EmailScalar },
773
+ age: { type: PositiveIntScalar },
774
+ }),
775
+ });
776
+ ```
777
+
778
+ ### Custom Error Classes
779
+
780
+ Create domain-specific error classes:
781
+
782
+ ```javascript
783
+ const { SimfinityError } = require('@simtlix/simfinity-js');
784
+
785
+ // Business logic error
786
+ class BusinessError extends SimfinityError {
787
+ constructor(message) {
788
+ super(message, 'BUSINESS_ERROR', 400);
789
+ }
790
+ }
791
+
792
+ // Authorization error
793
+ class AuthorizationError extends SimfinityError {
794
+ constructor(message) {
795
+ super(message, 'UNAUTHORIZED', 401);
796
+ }
797
+ }
798
+
799
+ // Not found error
800
+ class NotFoundError extends SimfinityError {
801
+ constructor(message) {
802
+ super(message, 'NOT_FOUND', 404);
803
+ }
804
+ }
805
+ ```
806
+
807
+ ## 🔧 Advanced Features
808
+
809
+ ### Field Extensions
810
+
811
+ Control field behavior with extensions:
812
+
813
+ ```javascript
814
+ const BookType = new GraphQLObjectType({
815
+ name: 'Book',
816
+ fields: () => ({
817
+ id: { type: GraphQLID },
818
+ title: {
819
+ type: GraphQLString,
820
+ extensions: {
821
+ unique: true, // Creates unique index in MongoDB
822
+ readOnly: true // Excludes from input types
823
+ }
824
+ },
825
+ isbn: {
826
+ type: GraphQLString,
827
+ extensions: {
828
+ unique: true
829
+ }
830
+ }
831
+ })
832
+ });
833
+ ```
834
+
835
+ ### Custom Mutations
836
+
837
+ Register custom mutations beyond the automatic CRUD operations:
838
+
839
+ ```javascript
840
+ simfinity.registerMutation(
841
+ 'sendBookNotification',
842
+ 'Send notification about a book',
843
+ BookNotificationInput, // Input type
844
+ NotificationResult, // Output type
845
+ async (args, session) => {
846
+ // Custom business logic
847
+ const book = await BookModel.findById(args.bookId);
848
+ // Send notification logic here
849
+ return { success: true, message: 'Notification sent' };
850
+ }
851
+ );
852
+ ```
853
+
854
+ ### Adding Types Without Endpoints
855
+
856
+ Include types in the schema without generating endpoints:
857
+
858
+ ```javascript
859
+ // This type can be used in relationships but won't have queries/mutations
860
+ simfinity.addNoEndpointType(AddressType);
861
+ ```
862
+
863
+ ### Working with Existing Mongoose Models
864
+
865
+ Use your existing Mongoose models:
866
+
867
+ ```javascript
868
+ const mongoose = require('mongoose');
869
+
870
+ const BookSchema = new mongoose.Schema({
871
+ title: String,
872
+ author: String,
873
+ publishedDate: Date
874
+ });
875
+
876
+ const BookModel = mongoose.model('Book', BookSchema);
877
+
878
+ // Use existing model
879
+ simfinity.connect(BookModel, BookType, 'book', 'books');
880
+ ```
881
+
882
+ ### Programmatic Data Access
883
+
884
+ Access data programmatically outside of GraphQL:
885
+
886
+ ```javascript
887
+ // Save an object programmatically
888
+ const newBook = await simfinity.saveObject('Book', {
889
+ title: 'New Book',
890
+ author: 'Author Name'
891
+ }, session);
892
+
893
+ // Get the Mongoose model for a type
894
+ const BookModel = simfinity.getModel(BookType);
895
+ const books = await BookModel.find({ author: 'Douglas Adams' });
896
+
897
+ // Get the input type for a GraphQL type
898
+ const BookInput = simfinity.getInputType(BookType);
899
+ ```
900
+
901
+ ## 📚 Complete Example
902
+
903
+ Here's a complete bookstore example with relationships, validations, and state machines:
904
+
905
+ ```javascript
906
+ const express = require('express');
907
+ const { graphqlHTTP } = require('express-graphql');
908
+ const mongoose = require('mongoose');
909
+ const {
910
+ GraphQLObjectType,
911
+ GraphQLString,
912
+ GraphQLNonNull,
913
+ GraphQLID,
914
+ GraphQLList,
915
+ GraphQLInt,
916
+ GraphQLEnumType
917
+ } = require('graphql');
918
+ const simfinity = require('@simtlix/simfinity-js');
919
+
920
+ // Connect to MongoDB
921
+ mongoose.connect('mongodb://localhost:27017/bookstore', {
922
+ useNewUrlParser: true,
923
+ useUnifiedTopology: true,
924
+ });
925
+
926
+ // Define Types
927
+ const AuthorType = new GraphQLObjectType({
928
+ name: 'Author',
929
+ fields: () => ({
930
+ id: { type: new GraphQLNonNull(GraphQLID) },
931
+ name: { type: new GraphQLNonNull(GraphQLString) },
932
+ email: { type: GraphQLString },
933
+ books: {
934
+ type: new GraphQLList(BookType),
935
+ extensions: {
936
+ relation: {
937
+ connectionField: 'author',
938
+ displayField: 'title'
939
+ },
940
+ },
941
+ resolve(parent) {
942
+ return simfinity.getModel(BookType).find({ author: parent.id });
943
+ }
944
+ },
945
+ }),
946
+ });
947
+
948
+ const BookType = new GraphQLObjectType({
949
+ name: 'Book',
950
+ fields: () => ({
951
+ id: { type: new GraphQLNonNull(GraphQLID) },
952
+ title: {
953
+ type: new GraphQLNonNull(GraphQLString),
954
+ extensions: {
955
+ validations: {
956
+ save: [{
957
+ validate: async (typeName, fieldName, value, session) => {
958
+ if (!value || value.length < 2) {
959
+ throw new simfinity.SimfinityError('Title must be at least 2 characters', 'VALIDATION_ERROR', 400);
960
+ }
961
+ }
962
+ }]
963
+ }
964
+ }
965
+ },
966
+ pages: { type: GraphQLInt },
967
+ author: {
968
+ type: AuthorType,
969
+ extensions: {
970
+ relation: {
971
+ displayField: 'name'
972
+ },
973
+ },
974
+ resolve(parent) {
975
+ return simfinity.getModel(AuthorType).findById(parent.author);
976
+ }
977
+ },
978
+ }),
979
+ });
980
+
981
+ // Define Controllers
982
+ const bookController = {
983
+ onSaving: async (doc, args, session) => {
984
+ console.log(`Creating book: ${doc.title}`);
985
+ },
986
+
987
+ onSaved: async (doc, args, session) => {
988
+ console.log(`Book saved: ${doc.title}`);
989
+ }
990
+ };
991
+
992
+ // Connect Types
993
+ simfinity.connect(null, AuthorType, 'author', 'authors');
994
+ simfinity.connect(null, BookType, 'book', 'books', bookController);
995
+
996
+ // Create Schema
997
+ const schema = simfinity.createSchema();
998
+
999
+ // Setup Express Server
1000
+ const app = express();
1001
+
1002
+ app.use('/graphql', graphqlHTTP({
1003
+ schema,
1004
+ graphiql: true,
1005
+ formatError: simfinity.buildErrorFormatter((err) => {
1006
+ console.log(err);
1007
+ })
1008
+ }));
1009
+
1010
+ app.listen(4000, () => {
1011
+ console.log('Bookstore API running on http://localhost:4000/graphql');
1012
+ });
1013
+ ```
1014
+
1015
+ ## 🔗 Resources
1016
+
1017
+ - **[Samples Repository](https://github.com/simtlix/simfinity.js-samples)** - Complete examples and use cases
1018
+ - **[MongoDB Query Language](https://docs.mongodb.com/manual/tutorial/query-documents/)** - Learn about MongoDB querying
1019
+ - **[GraphQL Documentation](https://graphql.org/learn/)** - Learn about GraphQL
1020
+
1021
+ ## 📄 License
1022
+
1023
+ Apache-2.0 License - see the [LICENSE](LICENSE) file for details.
1024
+
1025
+ ## 🤝 Contributing
1026
+
1027
+ Contributions are welcome! Please feel free to submit a Pull Request.
1028
+
1029
+ ---
135
1030
 
136
- # Want to know more!
137
- Visit the [samples site](https://github.com/simtlix/simfinity.js-samples) and learn about SimfinityJS through different use cases
1031
+ *Built with ❤️ by [Simtlix](https://github.com/simtlix)*
138
1032
 
139
1033