@simtlix/simfinity-js 1.2.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,204 +1,222 @@
1
1
  # Simfinity.js
2
2
 
3
- Simfinity.js is a powerful library that automatically generates a GraphQL schema from your Mongoose models. It simplifies the process of creating a GraphQL API for your Node.js application by providing a set of conventions and helpers to handle common CRUD operations, filtering, pagination, and sorting.
3
+ A powerful Node.js framework that automatically generates GraphQL schemas from your data models, bringing all the power and flexibility of MongoDB query language to GraphQL interfaces.
4
4
 
5
- ## Installation
5
+ ## ✨ Features
6
6
 
7
- To use Simfinity.js in your project, you'll need to have `mongoose` and `graphql` installed as peer dependencies.
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
16
+
17
+ ## 📦 Installation
8
18
 
9
19
  ```bash
10
20
  npm install mongoose graphql @simtlix/simfinity-js
11
21
  ```
12
22
 
13
- ## Core Concepts
14
-
15
- The core of Simfinity.js revolves around two main concepts: connecting your Mongoose models to GraphQL types and creating a schema.
23
+ **Prerequisites**: Simfinity.js requires `mongoose` and `graphql` as peer dependencies.
16
24
 
17
- ### Connecting Models
25
+ ## 🚀 Quick Start
18
26
 
19
- The `simfinity.connect()` method is used to link a Mongoose model to a GraphQLObjectType. This tells Simfinity how to handle the data for that type.
27
+ ### 1. Basic Setup
20
28
 
21
29
  ```javascript
30
+ const express = require('express');
31
+ const { graphqlHTTP } = require('express-graphql');
22
32
  const mongoose = require('mongoose');
23
- const { GraphQLObjectType, GraphQLString, GraphQLNonNull } = require('graphql');
24
33
  const simfinity = require('@simtlix/simfinity-js');
25
34
 
26
- // 1. Define your GraphQL Type
35
+ // Connect to MongoDB
36
+ mongoose.connect('mongodb://localhost:27017/bookstore', {
37
+ useNewUrlParser: true,
38
+ useUnifiedTopology: true,
39
+ });
40
+
41
+ const app = express();
42
+ ```
43
+
44
+ ### 2. Define Your GraphQL Type
45
+
46
+ ```javascript
47
+ const { GraphQLObjectType, GraphQLString, GraphQLNonNull, GraphQLID } = require('graphql');
48
+
27
49
  const BookType = new GraphQLObjectType({
28
50
  name: 'Book',
29
51
  fields: () => ({
30
52
  id: { type: new GraphQLNonNull(GraphQLID) },
31
53
  title: { type: new GraphQLNonNull(GraphQLString) },
54
+ author: { type: GraphQLString },
32
55
  }),
33
56
  });
34
-
35
- // 2. Connect the type to Simfinity
36
- simfinity.connect(null, BookType, 'book', 'books');
37
57
  ```
38
58
 
39
- ### Creating a Schema
40
-
41
- Once you've connected your types, you can generate a GraphQL schema using `simfinity.createSchema()`. This will automatically create the queries and mutations for your connected types.
59
+ ### 3. Connect to Simfinity
42
60
 
43
61
  ```javascript
44
- const schema = simfinity.createSchema();
45
- ```
46
-
47
- ---
48
-
49
- # About SimfinityJS
50
- SimfinityJS is a Node.js framework that allows bringing all the power and flexibility of MongoDB query language to GraphQL interfaces.
51
-
52
- 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.
53
-
54
- As a result, developers can focus on model structure and object relationships.
55
-
56
- ## Features
57
- - Translation between GraphQL and MongoDB and viceversa
58
- - Implement business logic in a declarative way
59
- - Implement domain validations in a declarative way.
60
- - Supports declarative state machine. Business logic can be included in each state transition.
61
- - Powerful semantic API. Basically, any query that can be executed in mongocli can be executed in GraphQL, thanks to SimfinityJS.
62
-
63
-
62
+ // Connect the type to Simfinity
63
+ simfinity.connect(null, BookType, 'book', 'books');
64
64
 
65
- # Quick Start
66
- ## Install
67
- ```bash
68
- npm install @simtlix/simfinity-js --save
65
+ // Create the GraphQL schema
66
+ const schema = simfinity.createSchema();
69
67
  ```
70
68
 
71
- ## Adding Simfinity to your application
69
+ ### 4. Setup GraphQL Endpoint
72
70
 
73
71
  ```javascript
74
- const express = require('express')
75
- const graphqlHTTP = require('express-graphql')
76
- const simfinity = require('@simtlix/simfinity-js')
77
- const app = express()
78
- const mongoose = require('mongoose')
79
-
80
- //Replace with your Mongo DB connection string
81
- mongoose.connect('mongodb://localhost:27017,localhost:27018,localhost:27019/example2', { replicaSet: 'rs', useNewUrlParser: true, useUnifiedTopology: true })
82
-
83
- mongoose.connection.once('open', () => {
84
- console.log('connected to database')
85
- })
86
-
87
- mongoose.set('debug', true);
88
-
89
- const type = require('./types')
90
- const includedTypes = [type.Book]
91
-
92
- const schema = simfinity.createSchema(includedTypes)
93
-
94
72
  app.use('/graphql', graphqlHTTP({
95
- // Directing express-graphql to use this schema to map out the graph
96
73
  schema,
97
- /* Directing express-graphql to use graphiql when goto '/graphql' address in the browser
98
- which provides an interface to make GraphQl queries */
99
74
  graphiql: true,
100
75
  formatError: simfinity.buildErrorFormatter((err) => {
101
- console.log(err)
76
+ console.log(err);
102
77
  })
78
+ }));
103
79
 
104
- }))
105
-
106
- app.listen(3000, () => {
107
- console.log('Listening on port 3000')
108
- })
80
+ app.listen(4000, () => {
81
+ console.log('Server is running on port 4000');
82
+ });
109
83
  ```
110
84
 
85
+ ### 5. Try It Out
111
86
 
112
- ### Defining the model
87
+ Open [http://localhost:4000/graphql](http://localhost:4000/graphql) and try these queries:
113
88
 
114
- ```javascript
115
- const graphql = require('graphql')
116
- const simfinity = require('@simtlix/simfinity-js')
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
+ ```
117
102
 
118
- const {
119
- GraphQLObjectType,GraphQLString,
120
- GraphQLID, GraphQLInt
121
- } = graphql
103
+ **List all books:**
104
+ ```graphql
105
+ query {
106
+ books {
107
+ id
108
+ title
109
+ author
110
+ }
111
+ }
112
+ ```
122
113
 
114
+ ## 🔧 Core Concepts
123
115
 
124
- const BookType = new GraphQLObjectType({
125
- name: 'Book',
126
- fields: () => ({
127
- id: {
128
- type: GraphQLID
129
- },
130
- name: { type: GraphQLString },
131
- pages: { type: GraphQLInt }
132
- })
133
- })
116
+ ### Connecting Models
134
117
 
135
- module.exports = BookType
118
+ The `simfinity.connect()` method links your GraphQL types to Simfinity's automatic schema generation:
136
119
 
137
- simfinity.connect(null, BookType, 'book', 'books', null, null, null)
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
+ );
138
130
  ```
139
131
 
132
+ ### Creating Schemas
140
133
 
141
- # Run
142
- Start replica set
134
+ Generate your complete GraphQL schema with optional type filtering:
143
135
 
144
- `run-rs`
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
+ ```
145
143
 
146
- Run the application
144
+ ### Global Configuration
147
145
 
148
- `node app.js`
146
+ ```javascript
147
+ // Prevent automatic MongoDB collection creation (useful for testing)
148
+ simfinity.preventCreatingCollection(true);
149
+
150
+ // Add middleware for all operations
151
+ simfinity.use((params, next) => {
152
+ // params contains: type, args, operation, context
153
+ console.log(`Executing ${params.operation} on ${params.type.name}`);
154
+ next();
155
+ });
156
+ ```
149
157
 
158
+ ## 📋 Basic Usage
150
159
 
160
+ ### Automatic Query Generation
151
161
 
152
- # Try it
162
+ Simfinity automatically generates queries for each connected type:
153
163
 
154
- Open http://localhost:3000/graphql endpoint defined on app.js
164
+ ```javascript
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
+ ```
155
169
 
170
+ ### Automatic Mutation Generation
156
171
 
157
- Create a book
158
- ```graphql
159
- mutation {
160
- addbook (
161
- input:{
162
- name: "Hello World Book",
163
- pages: 333
164
- }
165
- )
166
- }
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
167
179
  ```
168
180
 
181
+ ### Filtering and Querying
182
+
183
+ Query with powerful filtering options:
169
184
 
170
- List all books
171
185
  ```graphql
172
186
  query {
173
- books {
174
- id, name, pages
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
175
196
  }
176
197
  }
177
198
  ```
178
199
 
200
+ #### Available Operators
179
201
 
180
- # Want to know more!
181
- Visit the [samples site](https://github.com/simtlix/simfinity.js-samples) and learn about SimfinityJS through different use cases
182
-
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
183
212
 
213
+ ## 🔗 Relationships
184
214
 
185
- ## Bookstore Example
215
+ ### Defining Relationships
186
216
 
187
- Let's explore how to use Simfinity.js with a simple bookstore example. We'll have two main types: `Author` and `Book`.
188
-
189
- ### 1. Define Your GraphQL Types
190
-
191
- First, we'll define the `GraphQLObjectType` for our `Author` and `Book` models. Notice the `extensions` field on the `author` field of the `BookType`. This is how you define relationships in Simfinity.
217
+ Use the `extensions.relation` field to define relationships between types:
192
218
 
193
219
  ```javascript
194
- const {
195
- GraphQLObjectType,
196
- GraphQLString,
197
- GraphQLNonNull,
198
- GraphQLID,
199
- GraphQLList,
200
- } = require('graphql');
201
-
202
220
  const AuthorType = new GraphQLObjectType({
203
221
  name: 'Author',
204
222
  fields: () => ({
@@ -208,9 +226,11 @@ const AuthorType = new GraphQLObjectType({
208
226
  type: new GraphQLList(BookType),
209
227
  extensions: {
210
228
  relation: {
211
- connectionField: 'authorId',
229
+ connectionField: 'author',
230
+ displayField: 'title'
212
231
  },
213
232
  },
233
+ // resolve method automatically generated! 🎉
214
234
  },
215
235
  }),
216
236
  });
@@ -224,487 +244,790 @@ const BookType = new GraphQLObjectType({
224
244
  type: AuthorType,
225
245
  extensions: {
226
246
  relation: {
227
- connectionField: 'authorId',
247
+ displayField: 'name'
228
248
  },
229
249
  },
250
+ // resolve method automatically generated! 🎉
230
251
  },
231
252
  }),
232
253
  });
233
254
  ```
234
255
 
235
- ### Advanced Relationship Definition
256
+ ### Relationship Configuration
236
257
 
237
- For more control over your relationships, you can provide additional options in the `extensions.relation` object and add a custom `resolve` function.
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)
238
261
 
239
- * `connectionField`: (Required) The name of the field on the current model that stores the ID of the related object (e.g., `authorId` on the `Book` model).
240
- * `displayField`: (Optional) The name of the field on the related object that should be used as its display value. This can be useful for auto-generated UI components.
241
- * `resolve`: (Optional) A custom resolver function to fetch the related data. This gives you full control over how the relationship is resolved. If not provided, Simfinity.js will handle it automatically based on the `connectionField`.
262
+ ### Auto-Generated Resolve Methods
242
263
 
243
- Here's an example of an `Episode` type with a relationship to a `Season` type, using these advanced options. This demonstrates how to define which field to display from the related object and how to write a custom resolver.
264
+ 🎉 **NEW**: Simfinity.js automatically generates resolve methods for relationship fields when types are connected, eliminating the need for manual resolver boilerplate.
244
265
 
245
- ```javascript
246
- const { GraphQLID, GraphQLObjectType, GraphQLString, GraphQLInt } = require('graphql');
247
- const GraphQLDateTime = require('graphql-iso-date').GraphQLDateTime;
248
- const simfinity = require('@simtlix/simfinity-js');
249
- const seasonType = require('./season'); // Assuming seasonType is defined elsewhere
266
+ #### Before (Manual Resolvers)
250
267
 
251
- const episodeType = new GraphQLObjectType({
252
- name: 'episode',
268
+ ```javascript
269
+ const BookType = new GraphQLObjectType({
270
+ name: 'Book',
253
271
  fields: () => ({
254
- id: { type: GraphQLID },
255
- number: { type: GraphQLInt },
256
- name: { type: GraphQLString },
257
- date: { type: GraphQLDateTime },
258
- season: {
259
- type: seasonType,
272
+ id: { type: new GraphQLNonNull(GraphQLID) },
273
+ title: { type: new GraphQLNonNull(GraphQLString) },
274
+ author: {
275
+ type: AuthorType,
260
276
  extensions: {
261
277
  relation: {
262
- connectionField: 'seasonID',
263
- displayField: 'number'
264
- }
278
+ displayField: 'name'
279
+ },
265
280
  },
281
+ // You had to manually write this
266
282
  resolve(parent) {
267
- // Use simfinity.getModel() to get the Mongoose model for a GraphQL type
268
- return simfinity.getModel(seasonType).findById(parent.seasonID);
283
+ return simfinity.getModel(AuthorType).findById(parent.author);
269
284
  }
270
285
  },
271
- })
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
+ }),
272
300
  });
273
301
  ```
274
302
 
275
- In this example:
276
- - The `season` field on `episodeType` is linked to `seasonType`.
277
- - `connectionField: 'seasonID'` tells Simfinity that the `seasonID` field in the episode document holds the ID of the related season.
278
- - `displayField: 'number'` suggests that the `number` field of a season (e.g., season 1, season 2) should be used to represent it.
279
- - The `resolve` function manually fetches the season document using its ID from the parent episode. This is useful for custom logic, but often not necessary, as Simfinity can resolve it automatically.
280
-
281
- ### 2. Connect Your Types
282
-
283
- Next, we'll connect these types to Simfinity. This will automatically generate the Mongoose models and the necessary queries and mutations.
303
+ #### After (Auto-Generated Resolvers)
284
304
 
285
305
  ```javascript
286
- const simfinity = require('@simtlix/simfinity-js');
287
-
288
- simfinity.connect(null, AuthorType, 'author', 'authors');
289
- simfinity.connect(null, BookType, 'book', 'books');
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
+ });
290
332
  ```
291
333
 
292
- ### 3. Create the Server
334
+ #### How It Works
293
335
 
294
- Finally, we'll create a simple Express server with `express-graphql` to serve our schema.
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
295
341
 
296
- ```javascript
297
- const express = require('express');
298
- const { graphqlHTTP } = require('express-graphql');
299
- const mongoose = require('mongoose');
300
- const simfinity = require('@simtlix/simfinity-js');
342
+ #### Connect Your Types
301
343
 
302
- // ... (AuthorType and BookType definitions)
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');
303
349
 
304
- // Connect to MongoDB
305
- mongoose.connect('mongodb://localhost/bookstore', {
306
- useNewUrlParser: true,
307
- useUnifiedTopology: true,
308
- });
350
+ // Or use addNoEndpointType for types that don't need direct queries/mutations
351
+ simfinity.addNoEndpointType(AuthorType);
352
+ ```
309
353
 
310
- const app = express();
354
+ That's it! All relationship resolvers are automatically generated when you connect your types.
311
355
 
312
- app.use('/graphql', graphqlHTTP({
313
- schema: simfinity.createSchema(),
314
- graphiql: true,
315
- }));
356
+ ### Embedded vs Referenced Relationships
316
357
 
317
- app.listen(4000, () => {
318
- console.log('Server is running on port 4000');
319
- });
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
+ }
320
370
  ```
321
371
 
322
- ## Creating Complex Objects
323
-
324
- Simfinity.js makes it easy to create and connect objects in a single mutation. When you define a relationship, the input type for the parent object will automatically include a field for the child's ID.
325
-
326
- For our bookstore example, the `addBook` mutation will accept an `author` field, which is an object containing the `id` of the author.
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
+ ```
327
384
 
328
- ### Creating a Book for an Existing Author
385
+ ### Querying Relationships
329
386
 
330
- To create a new book and link it to an author that already exists, you can use the `addBook` mutation and provide the author's ID.
387
+ Query nested relationships with dot notation:
331
388
 
332
389
  ```graphql
333
- mutation {
334
- addBook(input: {
335
- title: "The Hitchhiker's Guide to the Galaxy",
336
- author: {
337
- id: "author_id_here"
338
- }
390
+ query {
391
+ books(author: {
392
+ terms: [
393
+ {
394
+ path: "country.name",
395
+ operator: EQ,
396
+ value: "England"
397
+ }
398
+ ]
339
399
  }) {
340
400
  id
341
401
  title
342
402
  author {
343
- id
344
403
  name
404
+ country {
405
+ name
406
+ }
345
407
  }
346
408
  }
347
409
  }
348
410
  ```
349
411
 
350
- This will create a new book and set its `authorId` field to the provided author ID.
351
-
352
- ---
353
-
354
- ## Querying Data
355
-
356
- Simfinity.js provides a rich set of querying capabilities that are automatically added to your schema. You can filter, paginate, and sort your data with ease.
357
-
358
- ### Basic Queries
359
-
360
- To get a list of all books, you can use the `books` query:
412
+ ### Creating Objects with Relationships
361
413
 
414
+ **Link to existing objects:**
362
415
  ```graphql
363
- query {
364
- books {
416
+ mutation {
417
+ addBook(input: {
418
+ title: "New Book"
419
+ author: {
420
+ id: "existing_author_id"
421
+ }
422
+ }) {
365
423
  id
366
424
  title
425
+ author {
426
+ name
427
+ }
367
428
  }
368
429
  }
369
430
  ```
370
431
 
371
- To get a single book by its ID, you can use the `book` query:
372
-
432
+ **Create embedded objects:**
373
433
  ```graphql
374
- query {
375
- book(id: "book_id_here") {
434
+ mutation {
435
+ addBook(input: {
436
+ title: "New Book"
437
+ publisher: {
438
+ name: "Penguin Books"
439
+ location: "London"
440
+ }
441
+ }) {
376
442
  id
377
443
  title
378
- author {
379
- id
444
+ publisher {
380
445
  name
446
+ location
381
447
  }
382
448
  }
383
449
  }
384
450
  ```
385
451
 
386
- ### Filtering
387
-
388
- You can filter your queries using the `filter` argument. The filter object takes an `operator` and a `value`.
389
-
390
- #### Simple Equality Filter
452
+ ### Collection Fields
391
453
 
392
- To find all books with a specific title:
454
+ Work with arrays of related objects:
393
455
 
394
456
  ```graphql
395
- query {
396
- books(title: {
397
- operator: EQ,
398
- value: "The Hitchhiker's Guide to the Galaxy"
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
+ }
399
470
  }) {
400
471
  id
401
472
  title
473
+ reviews {
474
+ rating
475
+ comment
476
+ }
402
477
  }
403
478
  }
404
479
  ```
405
480
 
406
- #### Using Other Operators
481
+ ## 🎛️ Controllers & Lifecycle Hooks
407
482
 
408
- Simfinity.js supports a variety of operators: `EQ`, `NE`, `GT`, `GTE`, `LT`, `LTE`, `LIKE`, `IN`, `NIN`, and `BTW`.
483
+ Controllers provide fine-grained control over operations with lifecycle hooks:
409
484
 
410
- To find all books with "Guide" in the title:
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
+ },
411
494
 
412
- ```graphql
413
- query {
414
- books(title: {
415
- operator: LIKE,
416
- value: "Guide"
417
- }) {
418
- id
419
- title
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}`);
420
513
  }
421
- }
514
+ };
515
+
516
+ // Connect with controller
517
+ simfinity.connect(null, BookType, 'book', 'books', bookController);
422
518
  ```
423
519
 
424
- ### Filtering on Nested Objects
520
+ ### Hook Parameters
425
521
 
426
- You can also filter based on the fields of a related object. To do this, you provide a `terms` array to the filter argument, where each term specifies a `path` to the nested field.
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
427
526
 
428
- To find all books by a specific author:
527
+ **`onSaved(doc, args, session)`**:
528
+ - `doc`: Plain object of saved document
529
+ - `args`: Raw GraphQL mutation input
530
+ - `session`: Mongoose session for transaction
429
531
 
430
- ```graphql
431
- query {
432
- books(author: {
433
- terms: [
434
- {
435
- path: "name",
436
- operator: EQ,
437
- value: "Douglas Adams"
438
- }
439
- ]
440
- }) {
441
- id
442
- title
443
- author {
444
- name
445
- }
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' }
446
562
  }
447
- }
563
+ });
448
564
  ```
449
565
 
450
- You can also use a deeper path to filter on nested relations. For example, if our `Author` type had a `country` relation, we could find all books by authors from a specific country:
566
+ ### 2. Define Type with State Field
451
567
 
452
- ```graphql
453
- query {
454
- books(author: {
455
- terms: [
456
- {
457
- path: "country.name",
458
- operator: EQ,
459
- value: "England"
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
460
593
  }
461
- ]
462
- }) {
463
- id
464
- title
465
- author {
466
- name
467
- country {
468
- name
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}`);
469
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'
470
613
  }
471
614
  }
472
- }
615
+ };
473
616
  ```
474
617
 
475
- ### Pagination
618
+ ### 4. Connect with State Machine
476
619
 
477
- To paginate your results, you can use the `pagination` argument. You can also get a `count` of the total number of documents that match the query.
478
-
479
- ```graphql
480
- query {
481
- books(pagination: {
482
- page: 1,
483
- size: 10,
484
- count: true
485
- }) {
486
- id
487
- title
488
- }
489
- }
620
+ ```javascript
621
+ simfinity.connect(null, OrderType, 'order', 'orders', null, null, stateMachine);
490
622
  ```
491
623
 
492
- ### Sorting
624
+ ### 5. Use State Machine Mutations
493
625
 
494
- To sort your results, you can use the `sort` argument.
626
+ The state machine automatically generates mutations for each action:
495
627
 
496
628
  ```graphql
497
- query {
498
- books(sort: {
499
- terms: [
500
- {
501
- field: "title",
502
- order: ASC
503
- }
504
- ]
629
+ mutation {
630
+ process_order(input: {
631
+ id: "order_id"
505
632
  }) {
506
633
  id
507
- title
634
+ state
635
+ customer
508
636
  }
509
637
  }
510
638
  ```
511
639
 
512
- ## Mutations
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
513
645
 
514
- Simfinity.js automatically generates `add`, `update`, and `delete` mutations for each type you connect.
646
+ ## Validations
515
647
 
516
- ### Add Mutation
648
+ ### Field-Level Validations
517
649
 
518
- To create a new author:
650
+ Add validation logic directly to fields:
519
651
 
520
- ```graphql
521
- mutation {
522
- addAuthor(input: {
523
- name: "J.R.R. Tolkien"
524
- }) {
525
- id
526
- name
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
+ }
527
660
  }
528
- }
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
+ });
529
699
  ```
530
700
 
531
- ### Update Mutation
701
+ ### Type-Level Validations
532
702
 
533
- To update an existing author's name:
703
+ Validate objects as a whole:
534
704
 
535
- ```graphql
536
- mutation {
537
- updateAuthor(input: {
538
- id: "author_id_here",
539
- name: "John Ronald Reuel Tolkien"
540
- }) {
541
- id
542
- name
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
+ }
543
717
  }
544
- }
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
+ });
545
732
  ```
546
733
 
547
- ### Delete Mutation
734
+ ### Custom Validated Scalar Types
548
735
 
549
- To delete an author:
736
+ Create custom scalar types with built-in validation:
550
737
 
551
- ```graphql
552
- mutation {
553
- deleteAuthor(id: "author_id_here") {
554
- id
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
+ }
555
752
  }
556
- }
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
+ });
557
776
  ```
558
777
 
559
- ## Lifecycle Hooks with Controllers
778
+ ### Custom Error Classes
560
779
 
561
- For more granular control over the automatically generated mutations (`add`, `update`, `delete`), you can provide a controller object to Simfinity.js. This controller can contain methods that are executed as lifecycle hooks during these operations, allowing you to run validation, perform modifications, or trigger side effects.
780
+ Create domain-specific error classes:
562
781
 
563
- The controller is passed as the fifth argument to the `simfinity.connect()` method.
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
+ }
564
791
 
565
- ### Controller Methods
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
+ ```
566
806
 
567
- * `onCreating({ doc })`: Executed just before a new document is created. You can modify the `doc` or throw an error to prevent creation.
568
- * `onUpdating({ doc, originalDoc })`: Executed before a document is updated. It receives the new `doc` with the pending changes and the `originalDoc` as it exists in the database.
569
- * `onDeleting({ doc })`: Executed before a document is deleted. It receives the document that is about to be removed.
807
+ ## 🔧 Advanced Features
570
808
 
571
- ### Example
809
+ ### Field Extensions
572
810
 
573
- Here's how you can define a controller for our `Book` type to add custom validation and logging:
811
+ Control field behavior with extensions:
574
812
 
575
813
  ```javascript
576
- const bookController = {
577
- onCreating: async ({ doc }) => {
578
- // Validate that a book has a title before saving.
579
- if (!doc.title || doc.title.trim().length === 0) {
580
- throw new Error('Book title cannot be empty.');
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
+ }
581
830
  }
582
- console.log(`A new book titled "${doc.title}" is being created.`);
583
- // You can also modify the document before it's saved, e.g., to add a timestamp.
584
- },
831
+ })
832
+ });
833
+ ```
585
834
 
586
- onUpdating: async ({ doc, originalDoc }) => {
587
- // Log the update operation.
588
- console.log(`The book "${originalDoc.title}" is being updated.`);
589
- // 'doc' contains the new values, while 'originalDoc' has the old ones.
590
- },
835
+ ### Custom Mutations
591
836
 
592
- onDeleting: async ({ doc }) => {
593
- // Perform a final check or logging before deletion.
594
- console.log(`The book "${doc.title}" is being deleted.`);
595
- // This is a good place to perform related cleanup operations.
596
- }
597
- };
837
+ Register custom mutations beyond the automatic CRUD operations:
598
838
 
599
- // Connect the BookType with its controller
600
- simfinity.connect(
601
- null, // mongooseModel
602
- BookType, // graphQLType
603
- 'book', // singularName
604
- 'books', // pluralName
605
- bookController // controller
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
+ }
606
851
  );
607
852
  ```
608
853
 
609
- When you now use the `addBook`, `updateBook`, or `deleteBook` mutations, the corresponding controller methods will be executed. For example, trying to create a book with an empty title would now fail with the custom error message.
854
+ ### Adding Types Without Endpoints
610
855
 
611
- ## State Machines
856
+ Include types in the schema without generating endpoints:
612
857
 
613
- Simfinity.js has built-in support for state machines, allowing you to manage the lifecycle of your objects in a declarative way. You can define states and actions that transition an object from one state to another. For each action, you can also specify business logic that gets executed during the transition.
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:
614
866
 
615
- ### Defining a State Machine
867
+ ```javascript
868
+ const mongoose = require('mongoose');
616
869
 
617
- Let's look at an example of a `Season` type that has a lifecycle managed by a state machine. The process involves four main steps:
870
+ const BookSchema = new mongoose.Schema({
871
+ title: String,
872
+ author: String,
873
+ publishedDate: Date
874
+ });
618
875
 
619
- 1. **Define States**: Create a `GraphQLEnumType` to represent the possible states.
620
- 2. **Define Type**: Create the `GraphQLObjectType` that will have a state field.
621
- 3. **Configure State Machine**: Define an object with the `initialState` and the `actions` that govern transitions.
622
- 4. **Connect**: Use `simfinity.connect()` to link the type with its state machine.
876
+ const BookModel = mongoose.model('Book', BookSchema);
623
877
 
624
- Here is the complete example:
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:
625
885
 
626
886
  ```javascript
627
- const graphql = require('graphql');
628
- const simfinity = require('@simtlix/simfinity-js');
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
+ ```
629
900
 
630
- const { GraphQLObjectType, GraphQLID, GraphQLInt, GraphQLEnumType } = graphql;
901
+ ## 📚 Complete Example
631
902
 
632
- // 1. Define the states using a GraphQLEnumType
633
- const seasonState = new GraphQLEnumType({
634
- name: 'seasonState',
635
- values: {
636
- SCHEDULED: { value: 'SCHEDULED' },
637
- ACTIVE: { value: 'ACTIVE' },
638
- FINISHED: { value: 'FINISHED' }
639
- }
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,
640
924
  });
641
925
 
642
- // 2. Define the GraphQLObjectType
643
- const seasonType = new GraphQLObjectType({
644
- name: 'season',
926
+ // Define Types
927
+ const AuthorType = new GraphQLObjectType({
928
+ name: 'Author',
645
929
  fields: () => ({
646
- id: { type: GraphQLID },
647
- number: { type: GraphQLInt },
648
- year: { type: GraphQLInt },
649
- state: { type: seasonState }
650
- })
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
+ }),
651
946
  });
652
947
 
653
- // 3. Define the state machine configuration
654
- const stateMachine = {
655
- initialState: 'SCHEDULED', // The value of the initial state
656
- actions: {
657
- activate: {
658
- from: 'SCHEDULED',
659
- to: 'ACTIVE',
660
- action: async ({ doc }) => {
661
- // Business logic to run on activation
662
- // The 'doc' parameter contains the document being transitioned
663
- console.log(`Activating season ${doc._id} of year ${doc.year}`);
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
+ }
664
964
  }
665
965
  },
666
- finalize: {
667
- from: 'ACTIVE',
668
- to: 'FINISHED',
669
- action: async ({ doc }) => {
670
- // Business logic to run on finalization
671
- console.log(`Finalizing season ${doc._id} of year ${doc.year}`);
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);
672
976
  }
673
- }
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}`);
674
989
  }
675
990
  };
676
991
 
677
- // 4. Connect the type and its state machine to Simfinity
678
- simfinity.connect(
679
- null,
680
- seasonType,
681
- 'season',
682
- 'seasons',
683
- null,
684
- null,
685
- stateMachine
686
- );
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
+ });
687
1013
  ```
688
1014
 
689
- When a new `season` is created, its `state` field will automatically be set to `SCHEDULED`.
1015
+ ## 🔗 Resources
690
1016
 
691
- ### Triggering State Transitions
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
692
1020
 
693
- When you connect a type with a state machine, Simfinity.js automatically creates a GraphQL mutation for each action. The mutation name is a combination of the action name and the type name (e.g., `actionName` + `TypeName`).
1021
+ ## 📄 License
694
1022
 
695
- For our `season` example, Simfinity.js will generate `activateSeason` and `finalizeSeason` mutations.
1023
+ Apache-2.0 License - see the [LICENSE](LICENSE) file for details.
696
1024
 
697
- To activate a season, you would call the `activateSeason` mutation with the ID of the season:
1025
+ ## 🤝 Contributing
698
1026
 
699
- ```graphql
700
- mutation {
701
- activateSeason(id: "season_id_here") {
702
- id
703
- state
704
- }
705
- }
706
- ```
1027
+ Contributions are welcome! Please feel free to submit a Pull Request.
1028
+
1029
+ ---
707
1030
 
708
- This will change the season's state from `SCHEDULED` to `ACTIVE` and execute the `action` function defined for the `activate` transition.
1031
+ *Built with ❤️ by [Simtlix](https://github.com/simtlix)*
709
1032
 
710
1033