@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 +765 -442
- package/eslint.config.mjs +63 -0
- package/package.json +26 -6
- package/src/index.js +242 -43
- package/tests/prevent-collection-creation.test.js +63 -0
- package/tests/validated-scalar.test.js +167 -0
- package/.eslintignore +0 -2
- package/.eslintrc.json +0 -38
package/README.md
CHANGED
|
@@ -1,204 +1,222 @@
|
|
|
1
1
|
# Simfinity.js
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
5
|
+
## ✨ Features
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
+
## 🚀 Quick Start
|
|
18
26
|
|
|
19
|
-
|
|
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
|
-
//
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
npm install @simtlix/simfinity-js --save
|
|
65
|
+
// Create the GraphQL schema
|
|
66
|
+
const schema = simfinity.createSchema();
|
|
69
67
|
```
|
|
70
68
|
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
Open [http://localhost:4000/graphql](http://localhost:4000/graphql) and try these queries:
|
|
113
88
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
+
The `simfinity.connect()` method links your GraphQL types to Simfinity's automatic schema generation:
|
|
136
119
|
|
|
137
|
-
|
|
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
|
-
|
|
142
|
-
Start replica set
|
|
134
|
+
Generate your complete GraphQL schema with optional type filtering:
|
|
143
135
|
|
|
144
|
-
|
|
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
|
-
|
|
144
|
+
### Global Configuration
|
|
147
145
|
|
|
148
|
-
|
|
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
|
-
|
|
162
|
+
Simfinity automatically generates queries for each connected type:
|
|
153
163
|
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
215
|
+
### Defining Relationships
|
|
186
216
|
|
|
187
|
-
|
|
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: '
|
|
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
|
-
|
|
247
|
+
displayField: 'name'
|
|
228
248
|
},
|
|
229
249
|
},
|
|
250
|
+
// resolve method automatically generated! 🎉
|
|
230
251
|
},
|
|
231
252
|
}),
|
|
232
253
|
});
|
|
233
254
|
```
|
|
234
255
|
|
|
235
|
-
###
|
|
256
|
+
### Relationship Configuration
|
|
236
257
|
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
268
|
+
```javascript
|
|
269
|
+
const BookType = new GraphQLObjectType({
|
|
270
|
+
name: 'Book',
|
|
253
271
|
fields: () => ({
|
|
254
|
-
id: { type: GraphQLID },
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
278
|
+
displayField: 'name'
|
|
279
|
+
},
|
|
265
280
|
},
|
|
281
|
+
// You had to manually write this
|
|
266
282
|
resolve(parent) {
|
|
267
|
-
|
|
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
|
-
|
|
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
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
334
|
+
#### How It Works
|
|
293
335
|
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
354
|
+
That's it! All relationship resolvers are automatically generated when you connect your types.
|
|
311
355
|
|
|
312
|
-
|
|
313
|
-
schema: simfinity.createSchema(),
|
|
314
|
-
graphiql: true,
|
|
315
|
-
}));
|
|
356
|
+
### Embedded vs Referenced Relationships
|
|
316
357
|
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
###
|
|
385
|
+
### Querying Relationships
|
|
329
386
|
|
|
330
|
-
|
|
387
|
+
Query nested relationships with dot notation:
|
|
331
388
|
|
|
332
389
|
```graphql
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
372
|
-
|
|
432
|
+
**Create embedded objects:**
|
|
373
433
|
```graphql
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
379
|
-
id
|
|
444
|
+
publisher {
|
|
380
445
|
name
|
|
446
|
+
location
|
|
381
447
|
}
|
|
382
448
|
}
|
|
383
449
|
}
|
|
384
450
|
```
|
|
385
451
|
|
|
386
|
-
###
|
|
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
|
-
|
|
454
|
+
Work with arrays of related objects:
|
|
393
455
|
|
|
394
456
|
```graphql
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
481
|
+
## 🎛️ Controllers & Lifecycle Hooks
|
|
407
482
|
|
|
408
|
-
|
|
483
|
+
Controllers provide fine-grained control over operations with lifecycle hooks:
|
|
409
484
|
|
|
410
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
###
|
|
520
|
+
### Hook Parameters
|
|
425
521
|
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
566
|
+
### 2. Define Type with State Field
|
|
451
567
|
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
###
|
|
618
|
+
### 4. Connect with State Machine
|
|
476
619
|
|
|
477
|
-
|
|
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
|
-
###
|
|
624
|
+
### 5. Use State Machine Mutations
|
|
493
625
|
|
|
494
|
-
|
|
626
|
+
The state machine automatically generates mutations for each action:
|
|
495
627
|
|
|
496
628
|
```graphql
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
634
|
+
state
|
|
635
|
+
customer
|
|
508
636
|
}
|
|
509
637
|
}
|
|
510
638
|
```
|
|
511
639
|
|
|
512
|
-
|
|
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
|
-
|
|
646
|
+
## ✅ Validations
|
|
515
647
|
|
|
516
|
-
###
|
|
648
|
+
### Field-Level Validations
|
|
517
649
|
|
|
518
|
-
|
|
650
|
+
Add validation logic directly to fields:
|
|
519
651
|
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
###
|
|
701
|
+
### Type-Level Validations
|
|
532
702
|
|
|
533
|
-
|
|
703
|
+
Validate objects as a whole:
|
|
534
704
|
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
###
|
|
734
|
+
### Custom Validated Scalar Types
|
|
548
735
|
|
|
549
|
-
|
|
736
|
+
Create custom scalar types with built-in validation:
|
|
550
737
|
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
-
|
|
778
|
+
### Custom Error Classes
|
|
560
779
|
|
|
561
|
-
|
|
780
|
+
Create domain-specific error classes:
|
|
562
781
|
|
|
563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
809
|
+
### Field Extensions
|
|
572
810
|
|
|
573
|
-
|
|
811
|
+
Control field behavior with extensions:
|
|
574
812
|
|
|
575
813
|
```javascript
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
831
|
+
})
|
|
832
|
+
});
|
|
833
|
+
```
|
|
585
834
|
|
|
586
|
-
|
|
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
|
-
|
|
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
|
-
|
|
600
|
-
simfinity.
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
854
|
+
### Adding Types Without Endpoints
|
|
610
855
|
|
|
611
|
-
|
|
856
|
+
Include types in the schema without generating endpoints:
|
|
612
857
|
|
|
613
|
-
|
|
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
|
-
|
|
867
|
+
```javascript
|
|
868
|
+
const mongoose = require('mongoose');
|
|
616
869
|
|
|
617
|
-
|
|
870
|
+
const BookSchema = new mongoose.Schema({
|
|
871
|
+
title: String,
|
|
872
|
+
author: String,
|
|
873
|
+
publishedDate: Date
|
|
874
|
+
});
|
|
618
875
|
|
|
619
|
-
|
|
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
|
-
|
|
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
|
-
|
|
628
|
-
const
|
|
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
|
-
|
|
901
|
+
## 📚 Complete Example
|
|
631
902
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
//
|
|
643
|
-
const
|
|
644
|
-
name: '
|
|
926
|
+
// Define Types
|
|
927
|
+
const AuthorType = new GraphQLObjectType({
|
|
928
|
+
name: 'Author',
|
|
645
929
|
fields: () => ({
|
|
646
|
-
id: { type: GraphQLID },
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
-
//
|
|
678
|
-
simfinity.connect(
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
1015
|
+
## 🔗 Resources
|
|
690
1016
|
|
|
691
|
-
|
|
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
|
-
|
|
1021
|
+
## 📄 License
|
|
694
1022
|
|
|
695
|
-
|
|
1023
|
+
Apache-2.0 License - see the [LICENSE](LICENSE) file for details.
|
|
696
1024
|
|
|
697
|
-
|
|
1025
|
+
## 🤝 Contributing
|
|
698
1026
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
id
|
|
703
|
-
state
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
```
|
|
1027
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
1028
|
+
|
|
1029
|
+
---
|
|
707
1030
|
|
|
708
|
-
|
|
1031
|
+
*Built with ❤️ by [Simtlix](https://github.com/simtlix)*
|
|
709
1032
|
|
|
710
1033
|
|