@simtlix/simfinity-js 1.1.0 → 1.2.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/.eslintrc.json +2 -1
- package/.github/workflows/master.yml +1 -1
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/release.yml +1 -1
- package/README.md +572 -1
- package/package.json +6 -2
- package/src/index.js +24 -12
package/.eslintrc.json
CHANGED
|
@@ -7,7 +7,7 @@ on:
|
|
|
7
7
|
|
|
8
8
|
jobs:
|
|
9
9
|
build:
|
|
10
|
-
runs-on: ubuntu-
|
|
10
|
+
runs-on: ubuntu-24.04
|
|
11
11
|
steps:
|
|
12
12
|
- uses: actions/checkout@v2.3.4
|
|
13
13
|
- uses: actions/setup-node@v1.4.4
|
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
|
|
20
20
|
publish-npm:
|
|
21
21
|
needs: build
|
|
22
|
-
runs-on: ubuntu-
|
|
22
|
+
runs-on: ubuntu-24.04
|
|
23
23
|
steps:
|
|
24
24
|
- uses: actions/checkout@v2.3.4
|
|
25
25
|
- uses: actions/setup-node@v1.4.4
|
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
NODE_AUTH_TOKEN: ${{secrets.NPMJS_TOKEN}}
|
|
33
33
|
publish-gpr:
|
|
34
34
|
needs: build
|
|
35
|
-
runs-on: ubuntu-
|
|
35
|
+
runs-on: ubuntu-24.04
|
|
36
36
|
steps:
|
|
37
37
|
- uses: actions/checkout@v2.3.4
|
|
38
38
|
- uses: actions/setup-node@v1.4.4
|
package/README.md
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
|
+
# Simfinity.js
|
|
1
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.
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
To use Simfinity.js in your project, you'll need to have `mongoose` and `graphql` installed as peer dependencies.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install mongoose graphql @simtlix/simfinity-js
|
|
11
|
+
```
|
|
12
|
+
|
|
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.
|
|
16
|
+
|
|
17
|
+
### Connecting Models
|
|
18
|
+
|
|
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.
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
const mongoose = require('mongoose');
|
|
23
|
+
const { GraphQLObjectType, GraphQLString, GraphQLNonNull } = require('graphql');
|
|
24
|
+
const simfinity = require('@simtlix/simfinity-js');
|
|
25
|
+
|
|
26
|
+
// 1. Define your GraphQL Type
|
|
27
|
+
const BookType = new GraphQLObjectType({
|
|
28
|
+
name: 'Book',
|
|
29
|
+
fields: () => ({
|
|
30
|
+
id: { type: new GraphQLNonNull(GraphQLID) },
|
|
31
|
+
title: { type: new GraphQLNonNull(GraphQLString) },
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 2. Connect the type to Simfinity
|
|
36
|
+
simfinity.connect(null, BookType, 'book', 'books');
|
|
37
|
+
```
|
|
38
|
+
|
|
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.
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
const schema = simfinity.createSchema();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
4
48
|
|
|
5
49
|
# About SimfinityJS
|
|
6
50
|
SimfinityJS is a Node.js framework that allows bringing all the power and flexibility of MongoDB query language to GraphQL interfaces.
|
|
@@ -137,3 +181,530 @@ query {
|
|
|
137
181
|
Visit the [samples site](https://github.com/simtlix/simfinity.js-samples) and learn about SimfinityJS through different use cases
|
|
138
182
|
|
|
139
183
|
|
|
184
|
+
|
|
185
|
+
## Bookstore Example
|
|
186
|
+
|
|
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.
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
const {
|
|
195
|
+
GraphQLObjectType,
|
|
196
|
+
GraphQLString,
|
|
197
|
+
GraphQLNonNull,
|
|
198
|
+
GraphQLID,
|
|
199
|
+
GraphQLList,
|
|
200
|
+
} = require('graphql');
|
|
201
|
+
|
|
202
|
+
const AuthorType = new GraphQLObjectType({
|
|
203
|
+
name: 'Author',
|
|
204
|
+
fields: () => ({
|
|
205
|
+
id: { type: new GraphQLNonNull(GraphQLID) },
|
|
206
|
+
name: { type: new GraphQLNonNull(GraphQLString) },
|
|
207
|
+
books: {
|
|
208
|
+
type: new GraphQLList(BookType),
|
|
209
|
+
extensions: {
|
|
210
|
+
relation: {
|
|
211
|
+
connectionField: 'authorId',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const BookType = new GraphQLObjectType({
|
|
219
|
+
name: 'Book',
|
|
220
|
+
fields: () => ({
|
|
221
|
+
id: { type: new GraphQLNonNull(GraphQLID) },
|
|
222
|
+
title: { type: new GraphQLNonNull(GraphQLString) },
|
|
223
|
+
author: {
|
|
224
|
+
type: AuthorType,
|
|
225
|
+
extensions: {
|
|
226
|
+
relation: {
|
|
227
|
+
connectionField: 'authorId',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Advanced Relationship Definition
|
|
236
|
+
|
|
237
|
+
For more control over your relationships, you can provide additional options in the `extensions.relation` object and add a custom `resolve` function.
|
|
238
|
+
|
|
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`.
|
|
242
|
+
|
|
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.
|
|
244
|
+
|
|
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
|
|
250
|
+
|
|
251
|
+
const episodeType = new GraphQLObjectType({
|
|
252
|
+
name: 'episode',
|
|
253
|
+
fields: () => ({
|
|
254
|
+
id: { type: GraphQLID },
|
|
255
|
+
number: { type: GraphQLInt },
|
|
256
|
+
name: { type: GraphQLString },
|
|
257
|
+
date: { type: GraphQLDateTime },
|
|
258
|
+
season: {
|
|
259
|
+
type: seasonType,
|
|
260
|
+
extensions: {
|
|
261
|
+
relation: {
|
|
262
|
+
connectionField: 'seasonID',
|
|
263
|
+
displayField: 'number'
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
resolve(parent) {
|
|
267
|
+
// Use simfinity.getModel() to get the Mongoose model for a GraphQL type
|
|
268
|
+
return simfinity.getModel(seasonType).findById(parent.seasonID);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
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.
|
|
284
|
+
|
|
285
|
+
```javascript
|
|
286
|
+
const simfinity = require('@simtlix/simfinity-js');
|
|
287
|
+
|
|
288
|
+
simfinity.connect(null, AuthorType, 'author', 'authors');
|
|
289
|
+
simfinity.connect(null, BookType, 'book', 'books');
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 3. Create the Server
|
|
293
|
+
|
|
294
|
+
Finally, we'll create a simple Express server with `express-graphql` to serve our schema.
|
|
295
|
+
|
|
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');
|
|
301
|
+
|
|
302
|
+
// ... (AuthorType and BookType definitions)
|
|
303
|
+
|
|
304
|
+
// Connect to MongoDB
|
|
305
|
+
mongoose.connect('mongodb://localhost/bookstore', {
|
|
306
|
+
useNewUrlParser: true,
|
|
307
|
+
useUnifiedTopology: true,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const app = express();
|
|
311
|
+
|
|
312
|
+
app.use('/graphql', graphqlHTTP({
|
|
313
|
+
schema: simfinity.createSchema(),
|
|
314
|
+
graphiql: true,
|
|
315
|
+
}));
|
|
316
|
+
|
|
317
|
+
app.listen(4000, () => {
|
|
318
|
+
console.log('Server is running on port 4000');
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
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.
|
|
327
|
+
|
|
328
|
+
### Creating a Book for an Existing Author
|
|
329
|
+
|
|
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.
|
|
331
|
+
|
|
332
|
+
```graphql
|
|
333
|
+
mutation {
|
|
334
|
+
addBook(input: {
|
|
335
|
+
title: "The Hitchhiker's Guide to the Galaxy",
|
|
336
|
+
author: {
|
|
337
|
+
id: "author_id_here"
|
|
338
|
+
}
|
|
339
|
+
}) {
|
|
340
|
+
id
|
|
341
|
+
title
|
|
342
|
+
author {
|
|
343
|
+
id
|
|
344
|
+
name
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
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:
|
|
361
|
+
|
|
362
|
+
```graphql
|
|
363
|
+
query {
|
|
364
|
+
books {
|
|
365
|
+
id
|
|
366
|
+
title
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
To get a single book by its ID, you can use the `book` query:
|
|
372
|
+
|
|
373
|
+
```graphql
|
|
374
|
+
query {
|
|
375
|
+
book(id: "book_id_here") {
|
|
376
|
+
id
|
|
377
|
+
title
|
|
378
|
+
author {
|
|
379
|
+
id
|
|
380
|
+
name
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
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
|
|
391
|
+
|
|
392
|
+
To find all books with a specific title:
|
|
393
|
+
|
|
394
|
+
```graphql
|
|
395
|
+
query {
|
|
396
|
+
books(title: {
|
|
397
|
+
operator: EQ,
|
|
398
|
+
value: "The Hitchhiker's Guide to the Galaxy"
|
|
399
|
+
}) {
|
|
400
|
+
id
|
|
401
|
+
title
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
#### Using Other Operators
|
|
407
|
+
|
|
408
|
+
Simfinity.js supports a variety of operators: `EQ`, `NE`, `GT`, `GTE`, `LT`, `LTE`, `LIKE`, `IN`, `NIN`, and `BTW`.
|
|
409
|
+
|
|
410
|
+
To find all books with "Guide" in the title:
|
|
411
|
+
|
|
412
|
+
```graphql
|
|
413
|
+
query {
|
|
414
|
+
books(title: {
|
|
415
|
+
operator: LIKE,
|
|
416
|
+
value: "Guide"
|
|
417
|
+
}) {
|
|
418
|
+
id
|
|
419
|
+
title
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Filtering on Nested Objects
|
|
425
|
+
|
|
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.
|
|
427
|
+
|
|
428
|
+
To find all books by a specific author:
|
|
429
|
+
|
|
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
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
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:
|
|
451
|
+
|
|
452
|
+
```graphql
|
|
453
|
+
query {
|
|
454
|
+
books(author: {
|
|
455
|
+
terms: [
|
|
456
|
+
{
|
|
457
|
+
path: "country.name",
|
|
458
|
+
operator: EQ,
|
|
459
|
+
value: "England"
|
|
460
|
+
}
|
|
461
|
+
]
|
|
462
|
+
}) {
|
|
463
|
+
id
|
|
464
|
+
title
|
|
465
|
+
author {
|
|
466
|
+
name
|
|
467
|
+
country {
|
|
468
|
+
name
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### Pagination
|
|
476
|
+
|
|
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
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Sorting
|
|
493
|
+
|
|
494
|
+
To sort your results, you can use the `sort` argument.
|
|
495
|
+
|
|
496
|
+
```graphql
|
|
497
|
+
query {
|
|
498
|
+
books(sort: {
|
|
499
|
+
terms: [
|
|
500
|
+
{
|
|
501
|
+
field: "title",
|
|
502
|
+
order: ASC
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
}) {
|
|
506
|
+
id
|
|
507
|
+
title
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## Mutations
|
|
513
|
+
|
|
514
|
+
Simfinity.js automatically generates `add`, `update`, and `delete` mutations for each type you connect.
|
|
515
|
+
|
|
516
|
+
### Add Mutation
|
|
517
|
+
|
|
518
|
+
To create a new author:
|
|
519
|
+
|
|
520
|
+
```graphql
|
|
521
|
+
mutation {
|
|
522
|
+
addAuthor(input: {
|
|
523
|
+
name: "J.R.R. Tolkien"
|
|
524
|
+
}) {
|
|
525
|
+
id
|
|
526
|
+
name
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Update Mutation
|
|
532
|
+
|
|
533
|
+
To update an existing author's name:
|
|
534
|
+
|
|
535
|
+
```graphql
|
|
536
|
+
mutation {
|
|
537
|
+
updateAuthor(input: {
|
|
538
|
+
id: "author_id_here",
|
|
539
|
+
name: "John Ronald Reuel Tolkien"
|
|
540
|
+
}) {
|
|
541
|
+
id
|
|
542
|
+
name
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Delete Mutation
|
|
548
|
+
|
|
549
|
+
To delete an author:
|
|
550
|
+
|
|
551
|
+
```graphql
|
|
552
|
+
mutation {
|
|
553
|
+
deleteAuthor(id: "author_id_here") {
|
|
554
|
+
id
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## Lifecycle Hooks with Controllers
|
|
560
|
+
|
|
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.
|
|
562
|
+
|
|
563
|
+
The controller is passed as the fifth argument to the `simfinity.connect()` method.
|
|
564
|
+
|
|
565
|
+
### Controller Methods
|
|
566
|
+
|
|
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.
|
|
570
|
+
|
|
571
|
+
### Example
|
|
572
|
+
|
|
573
|
+
Here's how you can define a controller for our `Book` type to add custom validation and logging:
|
|
574
|
+
|
|
575
|
+
```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.');
|
|
581
|
+
}
|
|
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
|
+
},
|
|
585
|
+
|
|
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
|
+
},
|
|
591
|
+
|
|
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
|
+
};
|
|
598
|
+
|
|
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
|
|
606
|
+
);
|
|
607
|
+
```
|
|
608
|
+
|
|
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.
|
|
610
|
+
|
|
611
|
+
## State Machines
|
|
612
|
+
|
|
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.
|
|
614
|
+
|
|
615
|
+
### Defining a State Machine
|
|
616
|
+
|
|
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:
|
|
618
|
+
|
|
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.
|
|
623
|
+
|
|
624
|
+
Here is the complete example:
|
|
625
|
+
|
|
626
|
+
```javascript
|
|
627
|
+
const graphql = require('graphql');
|
|
628
|
+
const simfinity = require('@simtlix/simfinity-js');
|
|
629
|
+
|
|
630
|
+
const { GraphQLObjectType, GraphQLID, GraphQLInt, GraphQLEnumType } = graphql;
|
|
631
|
+
|
|
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
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// 2. Define the GraphQLObjectType
|
|
643
|
+
const seasonType = new GraphQLObjectType({
|
|
644
|
+
name: 'season',
|
|
645
|
+
fields: () => ({
|
|
646
|
+
id: { type: GraphQLID },
|
|
647
|
+
number: { type: GraphQLInt },
|
|
648
|
+
year: { type: GraphQLInt },
|
|
649
|
+
state: { type: seasonState }
|
|
650
|
+
})
|
|
651
|
+
});
|
|
652
|
+
|
|
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}`);
|
|
664
|
+
}
|
|
665
|
+
},
|
|
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}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
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
|
+
);
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
When a new `season` is created, its `state` field will automatically be set to `SCHEDULED`.
|
|
690
|
+
|
|
691
|
+
### Triggering State Transitions
|
|
692
|
+
|
|
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`).
|
|
694
|
+
|
|
695
|
+
For our `season` example, Simfinity.js will generate `activateSeason` and `finalizeSeason` mutations.
|
|
696
|
+
|
|
697
|
+
To activate a season, you would call the `activateSeason` mutation with the ID of the season:
|
|
698
|
+
|
|
699
|
+
```graphql
|
|
700
|
+
mutation {
|
|
701
|
+
activateSeason(id: "season_id_here") {
|
|
702
|
+
id
|
|
703
|
+
state
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
This will change the season's state from `SCHEDULED` to `ACTIVE` and execute the `action` function defined for the `activate` transition.
|
|
709
|
+
|
|
710
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simtlix/simfinity-js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"graphql": "^14.7.0",
|
|
19
|
-
"mongoose": "^
|
|
19
|
+
"mongoose": "^8.15.1"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"eslint": "^7.7.0",
|
|
@@ -29,5 +29,9 @@
|
|
|
29
29
|
"pre-commit": "npm run lint || npm run lint-fix"
|
|
30
30
|
},
|
|
31
31
|
"owner": "simtlix"
|
|
32
|
+
},
|
|
33
|
+
"optionalDependencies": {
|
|
34
|
+
"graphql": "^14.7.0",
|
|
35
|
+
"mongoose": "^8.15.1"
|
|
32
36
|
}
|
|
33
37
|
}
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@ const QLOperator = require('./const/QLOperator');
|
|
|
7
7
|
const QLValue = require('./const/QLValue');
|
|
8
8
|
const QLSort = require('./const/QLSort');
|
|
9
9
|
|
|
10
|
-
mongoose.set('
|
|
10
|
+
mongoose.set('strictQuery', false);
|
|
11
11
|
|
|
12
12
|
const {
|
|
13
13
|
GraphQLObjectType, GraphQLString, GraphQLID, GraphQLSchema, GraphQLList,
|
|
@@ -470,7 +470,17 @@ const onDeleteObject = async (Model, gqltype, controller, args, session, linkToP
|
|
|
470
470
|
await controller.onDelete(deletedObject, session);
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
return Model.findByIdAndDelete(args
|
|
473
|
+
return Model.findByIdAndDelete({ _id: args.id }).session(session);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const onDeleteSubject = async (Model, controller, id, session) => {
|
|
477
|
+
const currentObject = await Model.findById({ _id: id }).lean();
|
|
478
|
+
|
|
479
|
+
if (controller && controller.onDelete) {
|
|
480
|
+
await controller.onDelete(currentObject, session);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return Model.findByIdAndDelete(id, { session });
|
|
474
484
|
};
|
|
475
485
|
|
|
476
486
|
const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent) => {
|
|
@@ -481,7 +491,6 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
481
491
|
await iterateonCollectionFields(materializedModel, gqltype, objectId, session);
|
|
482
492
|
}
|
|
483
493
|
|
|
484
|
-
let modifiedObject = materializedModel.modelArgs;
|
|
485
494
|
const currentObject = await Model.findById({ _id: objectId }).lean();
|
|
486
495
|
|
|
487
496
|
const argTypes = gqltype.getFields();
|
|
@@ -490,32 +499,32 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
490
499
|
if (fieldEntry.extensions && fieldEntry.extensions.relation
|
|
491
500
|
&& fieldEntry.extensions.relation.embedded) {
|
|
492
501
|
const oldObjectData = currentObject[fieldEntryName];
|
|
493
|
-
const newObjectData =
|
|
502
|
+
const newObjectData = materializedModel.modelArgs[fieldEntryName];
|
|
494
503
|
if (newObjectData) {
|
|
495
504
|
if (Array.isArray(oldObjectData) && Array.isArray(newObjectData)) {
|
|
496
|
-
|
|
505
|
+
materializedModel.modelArgs[fieldEntryName] = newObjectData;
|
|
497
506
|
} else {
|
|
498
|
-
|
|
507
|
+
materializedModel.modelArgs[fieldEntryName] = { ...oldObjectData, ...newObjectData };
|
|
499
508
|
}
|
|
500
509
|
}
|
|
501
510
|
}
|
|
502
511
|
|
|
503
512
|
if (args[fieldEntryName] === null
|
|
504
513
|
&& !(fieldEntry.type instanceof GraphQLNonNull)) {
|
|
505
|
-
|
|
514
|
+
materializedModel.modelArgs = { ...materializedModel.modelArgs, $unset: { [fieldEntryName]: '' } };
|
|
506
515
|
}
|
|
507
516
|
});
|
|
508
517
|
|
|
509
518
|
if (controller && controller.onUpdating) {
|
|
510
|
-
await controller.onUpdating(objectId,
|
|
519
|
+
await controller.onUpdating(objectId, materializedModel.modelArgs, session);
|
|
511
520
|
}
|
|
512
521
|
|
|
513
522
|
const result = Model.findByIdAndUpdate(
|
|
514
|
-
objectId,
|
|
515
|
-
);
|
|
523
|
+
objectId, materializedModel.modelArgs, { new: true },
|
|
524
|
+
).session(session);
|
|
516
525
|
|
|
517
526
|
if (controller && controller.onUpdated) {
|
|
518
|
-
await controller.onUpdated(result,
|
|
527
|
+
await controller.onUpdated(result, session);
|
|
519
528
|
}
|
|
520
529
|
|
|
521
530
|
return result;
|
|
@@ -633,7 +642,10 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
633
642
|
};
|
|
634
643
|
break;
|
|
635
644
|
case operations.DELETE:
|
|
636
|
-
|
|
645
|
+
operationFunction = async (collectionItem) => {
|
|
646
|
+
await onDeleteSubject(typesDict.types[collectionGQLType.name].model,
|
|
647
|
+
typesDict.types[collectionGQLType.name].controller, collectionItem, session);
|
|
648
|
+
};
|
|
637
649
|
}
|
|
638
650
|
|
|
639
651
|
for (const element of collectionFieldsList) {
|