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