@simtlix/simfinity-js 2.1.0 → 2.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 +627 -14
- package/package.json +1 -1
- package/src/index.js +102 -35
- package/src/scalars.js +188 -0
- package/src/validators.js +250 -0
package/README.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
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
|
+
## 📑 Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#-features)
|
|
8
|
+
- [Installation](#-installation)
|
|
9
|
+
- [Quick Start](#-quick-start)
|
|
10
|
+
- [Core Concepts](#-core-concepts)
|
|
11
|
+
- [Connecting Models](#connecting-models)
|
|
12
|
+
- [Creating Schemas](#creating-schemas)
|
|
13
|
+
- [Global Configuration](#global-configuration)
|
|
14
|
+
- [Basic Usage](#-basic-usage)
|
|
15
|
+
- [Automatic Query Generation](#automatic-query-generation)
|
|
16
|
+
- [Automatic Mutation Generation](#automatic-mutation-generation)
|
|
17
|
+
- [Filtering and Querying](#filtering-and-querying)
|
|
18
|
+
- [Collection Field Filtering](#collection-field-filtering)
|
|
19
|
+
- [Middlewares](#-middlewares)
|
|
20
|
+
- [Adding Middlewares](#adding-middlewares)
|
|
21
|
+
- [Middleware Parameters](#middleware-parameters)
|
|
22
|
+
- [Common Use Cases](#common-use-cases)
|
|
23
|
+
- [Relationships](#-relationships)
|
|
24
|
+
- [Defining Relationships](#defining-relationships)
|
|
25
|
+
- [Auto-Generated Resolve Methods](#auto-generated-resolve-methods)
|
|
26
|
+
- [Adding Types Without Endpoints](#adding-types-without-endpoints)
|
|
27
|
+
- [Embedded vs Referenced Relationships](#embedded-vs-referenced-relationships)
|
|
28
|
+
- [Querying Relationships](#querying-relationships)
|
|
29
|
+
- [Controllers & Lifecycle Hooks](#️-controllers--lifecycle-hooks)
|
|
30
|
+
- [Hook Parameters](#hook-parameters)
|
|
31
|
+
- [State Machines](#-state-machines)
|
|
32
|
+
- [Validations](#-validations)
|
|
33
|
+
- [Field-Level Validations](#field-level-validations)
|
|
34
|
+
- [Type-Level Validations](#type-level-validations)
|
|
35
|
+
- [Custom Validated Scalar Types](#custom-validated-scalar-types)
|
|
36
|
+
- [Custom Error Classes](#custom-error-classes)
|
|
37
|
+
- [Query Scope](#-query-scope)
|
|
38
|
+
- [Overview](#overview)
|
|
39
|
+
- [Defining Scope](#defining-scope)
|
|
40
|
+
- [Scope for Find Operations](#scope-for-find-operations)
|
|
41
|
+
- [Scope for Aggregate Operations](#scope-for-aggregate-operations)
|
|
42
|
+
- [Scope for Get By ID Operations](#scope-for-get-by-id-operations)
|
|
43
|
+
- [Scope Function Parameters](#scope-function-parameters)
|
|
44
|
+
- [Advanced Features](#-advanced-features)
|
|
45
|
+
- [Field Extensions](#field-extensions)
|
|
46
|
+
- [Custom Mutations](#custom-mutations)
|
|
47
|
+
- [Working with Existing Mongoose Models](#working-with-existing-mongoose-models)
|
|
48
|
+
- [Programmatic Data Access](#programmatic-data-access)
|
|
49
|
+
- [Aggregation Queries](#-aggregation-queries)
|
|
50
|
+
- [Complete Example](#-complete-example)
|
|
51
|
+
- [Resources](#-resources)
|
|
52
|
+
- [License](#-license)
|
|
53
|
+
- [Contributing](#-contributing)
|
|
54
|
+
- [Query Examples from Series-Sample](#-query-examples-from-series-sample)
|
|
55
|
+
- [State Machine Example from Series-Sample](#-state-machine-example-from-series-sample)
|
|
56
|
+
- [Envelop Plugin for Count in Extensions](#-envelop-plugin-for-count-in-extensions)
|
|
57
|
+
- [API Reference](#-api-reference)
|
|
58
|
+
|
|
5
59
|
## ✨ Features
|
|
6
60
|
|
|
7
61
|
- **Automatic Schema Generation**: Define your object model, and Simfinity.js generates all queries and mutations
|
|
@@ -940,31 +994,44 @@ Controllers provide fine-grained control over operations with lifecycle hooks:
|
|
|
940
994
|
|
|
941
995
|
```javascript
|
|
942
996
|
const bookController = {
|
|
943
|
-
onSaving: async (doc, args, session) => {
|
|
997
|
+
onSaving: async (doc, args, session, context) => {
|
|
944
998
|
// Before saving - doc is a Mongoose document
|
|
945
999
|
if (!doc.title || doc.title.trim().length === 0) {
|
|
946
1000
|
throw new Error('Book title cannot be empty');
|
|
947
1001
|
}
|
|
1002
|
+
// Access user from context to set owner
|
|
1003
|
+
if (context && context.user) {
|
|
1004
|
+
doc.owner = context.user.id;
|
|
1005
|
+
}
|
|
948
1006
|
console.log(`Creating book: ${doc.title}`);
|
|
949
1007
|
},
|
|
950
1008
|
|
|
951
|
-
onSaved: async (doc, args, session) => {
|
|
1009
|
+
onSaved: async (doc, args, session, context) => {
|
|
952
1010
|
// After saving - doc is a plain object
|
|
953
1011
|
console.log(`Book saved: ${doc._id}`);
|
|
1012
|
+
// Can access context.user for post-save operations like notifications
|
|
954
1013
|
},
|
|
955
1014
|
|
|
956
|
-
onUpdating: async (id, doc, session) => {
|
|
1015
|
+
onUpdating: async (id, doc, session, context) => {
|
|
957
1016
|
// Before updating - doc contains only changed fields
|
|
1017
|
+
// Validate user has permission to update
|
|
1018
|
+
if (context && context.user && context.user.role !== 'admin') {
|
|
1019
|
+
throw new simfinity.SimfinityError('Only admins can update books', 'FORBIDDEN', 403);
|
|
1020
|
+
}
|
|
958
1021
|
console.log(`Updating book ${id}`);
|
|
959
1022
|
},
|
|
960
1023
|
|
|
961
|
-
onUpdated: async (doc, session) => {
|
|
1024
|
+
onUpdated: async (doc, session, context) => {
|
|
962
1025
|
// After updating - doc is the updated document
|
|
963
1026
|
console.log(`Book updated: ${doc.title}`);
|
|
964
1027
|
},
|
|
965
1028
|
|
|
966
|
-
onDelete: async (doc, session) => {
|
|
1029
|
+
onDelete: async (doc, session, context) => {
|
|
967
1030
|
// Before deleting - doc is the document to be deleted
|
|
1031
|
+
// Validate user has permission to delete
|
|
1032
|
+
if (context && context.user && context.user.role !== 'admin') {
|
|
1033
|
+
throw new simfinity.SimfinityError('Only admins can delete books', 'FORBIDDEN', 403);
|
|
1034
|
+
}
|
|
968
1035
|
console.log(`Deleting book: ${doc.title}`);
|
|
969
1036
|
}
|
|
970
1037
|
};
|
|
@@ -975,28 +1042,75 @@ simfinity.connect(null, BookType, 'book', 'books', bookController);
|
|
|
975
1042
|
|
|
976
1043
|
### Hook Parameters
|
|
977
1044
|
|
|
978
|
-
**`onSaving(doc, args, session)`**:
|
|
1045
|
+
**`onSaving(doc, args, session, context)`**:
|
|
979
1046
|
- `doc`: Mongoose Document instance (not yet saved)
|
|
980
1047
|
- `args`: Raw GraphQL mutation input
|
|
981
1048
|
- `session`: Mongoose session for transaction
|
|
1049
|
+
- `context`: GraphQL context object (includes request info, user data, etc.)
|
|
982
1050
|
|
|
983
|
-
**`onSaved(doc, args, session)`**:
|
|
1051
|
+
**`onSaved(doc, args, session, context)`**:
|
|
984
1052
|
- `doc`: Plain object of saved document
|
|
985
1053
|
- `args`: Raw GraphQL mutation input
|
|
986
1054
|
- `session`: Mongoose session for transaction
|
|
1055
|
+
- `context`: GraphQL context object (includes request info, user data, etc.)
|
|
987
1056
|
|
|
988
|
-
**`onUpdating(id, doc, session)`**:
|
|
1057
|
+
**`onUpdating(id, doc, session, context)`**:
|
|
989
1058
|
- `id`: Document ID being updated
|
|
990
1059
|
- `doc`: Plain object with only changed fields
|
|
991
1060
|
- `session`: Mongoose session for transaction
|
|
1061
|
+
- `context`: GraphQL context object (includes request info, user data, etc.)
|
|
992
1062
|
|
|
993
|
-
**`onUpdated(doc, session)`**:
|
|
1063
|
+
**`onUpdated(doc, session, context)`**:
|
|
994
1064
|
- `doc`: Full updated Mongoose document
|
|
995
1065
|
- `session`: Mongoose session for transaction
|
|
1066
|
+
- `context`: GraphQL context object (includes request info, user data, etc.)
|
|
996
1067
|
|
|
997
|
-
**`onDelete(doc, session)`**:
|
|
1068
|
+
**`onDelete(doc, session, context)`**:
|
|
998
1069
|
- `doc`: Plain object of document to be deleted
|
|
999
1070
|
- `session`: Mongoose session for transaction
|
|
1071
|
+
- `context`: GraphQL context object (includes request info, user data, etc.)
|
|
1072
|
+
|
|
1073
|
+
### Using Context in Controllers
|
|
1074
|
+
|
|
1075
|
+
The `context` parameter provides access to the GraphQL request context, which typically includes user information, request metadata, and other application-specific data. This is particularly useful for:
|
|
1076
|
+
|
|
1077
|
+
- **Setting ownership**: Automatically assign the current user as the owner of new entities
|
|
1078
|
+
- **Authorization checks**: Validate user permissions before allowing operations
|
|
1079
|
+
- **Audit logging**: Track who performed which operations
|
|
1080
|
+
- **User-specific business logic**: Apply different logic based on user roles or attributes
|
|
1081
|
+
|
|
1082
|
+
**Example: Setting Owner on Creation**
|
|
1083
|
+
|
|
1084
|
+
```javascript
|
|
1085
|
+
const documentController = {
|
|
1086
|
+
onSaving: async (doc, args, session, context) => {
|
|
1087
|
+
// Automatically set the owner to the current user
|
|
1088
|
+
if (context && context.user) {
|
|
1089
|
+
doc.owner = context.user.id;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
**Example: Role-Based Authorization**
|
|
1096
|
+
|
|
1097
|
+
```javascript
|
|
1098
|
+
const adminOnlyController = {
|
|
1099
|
+
onUpdating: async (id, doc, session, context) => {
|
|
1100
|
+
if (!context || !context.user || context.user.role !== 'admin') {
|
|
1101
|
+
throw new simfinity.SimfinityError('Admin access required', 'FORBIDDEN', 403);
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
|
|
1105
|
+
onDelete: async (doc, session, context) => {
|
|
1106
|
+
if (!context || !context.user || context.user.role !== 'admin') {
|
|
1107
|
+
throw new simfinity.SimfinityError('Admin access required', 'FORBIDDEN', 403);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
**Note**: When using `saveObject` programmatically (outside of GraphQL), the `context` parameter is optional and may be `undefined`. Always check for context existence before accessing its properties.
|
|
1000
1114
|
|
|
1001
1115
|
## 🔄 State Machines
|
|
1002
1116
|
|
|
@@ -1101,9 +1215,117 @@ mutation {
|
|
|
1101
1215
|
|
|
1102
1216
|
## ✅ Validations
|
|
1103
1217
|
|
|
1104
|
-
###
|
|
1218
|
+
### Declarative Validation Helpers
|
|
1219
|
+
|
|
1220
|
+
Simfinity.js provides built-in validation helpers to simplify common validation patterns, eliminating verbose boilerplate code.
|
|
1221
|
+
|
|
1222
|
+
#### Using Validators
|
|
1223
|
+
|
|
1224
|
+
```javascript
|
|
1225
|
+
const { validators } = require('@simtlix/simfinity-js');
|
|
1226
|
+
|
|
1227
|
+
const PersonType = new GraphQLObjectType({
|
|
1228
|
+
name: 'Person',
|
|
1229
|
+
fields: () => ({
|
|
1230
|
+
id: { type: GraphQLID },
|
|
1231
|
+
name: {
|
|
1232
|
+
type: GraphQLString,
|
|
1233
|
+
extensions: {
|
|
1234
|
+
validations: validators.stringLength('Name', 2, 100)
|
|
1235
|
+
}
|
|
1236
|
+
},
|
|
1237
|
+
email: {
|
|
1238
|
+
type: GraphQLString,
|
|
1239
|
+
extensions: {
|
|
1240
|
+
validations: validators.email()
|
|
1241
|
+
}
|
|
1242
|
+
},
|
|
1243
|
+
website: {
|
|
1244
|
+
type: GraphQLString,
|
|
1245
|
+
extensions: {
|
|
1246
|
+
validations: validators.url()
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
age: {
|
|
1250
|
+
type: GraphQLInt,
|
|
1251
|
+
extensions: {
|
|
1252
|
+
validations: validators.numberRange('Age', 0, 120)
|
|
1253
|
+
}
|
|
1254
|
+
},
|
|
1255
|
+
price: {
|
|
1256
|
+
type: GraphQLFloat,
|
|
1257
|
+
extensions: {
|
|
1258
|
+
validations: validators.positive('Price')
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
})
|
|
1262
|
+
});
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
#### Available Validators
|
|
1266
|
+
|
|
1267
|
+
**String Validators:**
|
|
1268
|
+
- `validators.stringLength(name, min, max)` - Validates string length with min/max bounds (required for CREATE)
|
|
1269
|
+
- `validators.maxLength(name, max)` - Validates maximum string length
|
|
1270
|
+
- `validators.pattern(name, regex, message)` - Validates against a regex pattern
|
|
1271
|
+
- `validators.email()` - Validates email format
|
|
1272
|
+
- `validators.url()` - Validates URL format
|
|
1273
|
+
|
|
1274
|
+
**Number Validators:**
|
|
1275
|
+
- `validators.numberRange(name, min, max)` - Validates number range
|
|
1276
|
+
- `validators.positive(name)` - Ensures number is positive
|
|
1277
|
+
|
|
1278
|
+
**Array Validators:**
|
|
1279
|
+
- `validators.arrayLength(name, maxItems, itemValidator)` - Validates array length and optionally each item
|
|
1280
|
+
|
|
1281
|
+
**Date Validators:**
|
|
1282
|
+
- `validators.dateFormat(name, format)` - Validates date format
|
|
1283
|
+
- `validators.futureDate(name)` - Ensures date is in the future
|
|
1284
|
+
|
|
1285
|
+
#### Validator Features
|
|
1286
|
+
|
|
1287
|
+
- **Automatic Operation Handling**: Validators work for both `CREATE` (save) and `UPDATE` operations
|
|
1288
|
+
- **Smart Validation**: For CREATE operations, values are required. For UPDATE operations, undefined/null values are allowed (field might not be updated)
|
|
1289
|
+
- **Consistent Error Messages**: All validators throw `SimfinityError` with appropriate messages
|
|
1105
1290
|
|
|
1106
|
-
|
|
1291
|
+
#### Example: Multiple Validators
|
|
1292
|
+
|
|
1293
|
+
```javascript
|
|
1294
|
+
const ProductType = new GraphQLObjectType({
|
|
1295
|
+
name: 'Product',
|
|
1296
|
+
fields: () => ({
|
|
1297
|
+
id: { type: GraphQLID },
|
|
1298
|
+
name: {
|
|
1299
|
+
type: GraphQLString,
|
|
1300
|
+
extensions: {
|
|
1301
|
+
validations: validators.stringLength('Product Name', 3, 200)
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
sku: {
|
|
1305
|
+
type: GraphQLString,
|
|
1306
|
+
extensions: {
|
|
1307
|
+
validations: validators.pattern('SKU', /^[A-Z0-9-]+$/, 'SKU must be uppercase alphanumeric with hyphens')
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
price: {
|
|
1311
|
+
type: GraphQLFloat,
|
|
1312
|
+
extensions: {
|
|
1313
|
+
validations: validators.positive('Price')
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
tags: {
|
|
1317
|
+
type: new GraphQLList(GraphQLString),
|
|
1318
|
+
extensions: {
|
|
1319
|
+
validations: validators.arrayLength('Tags', 10)
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
})
|
|
1323
|
+
});
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
### Field-Level Validations (Manual)
|
|
1327
|
+
|
|
1328
|
+
For custom validation logic, you can still write manual validators:
|
|
1107
1329
|
|
|
1108
1330
|
```javascript
|
|
1109
1331
|
const { SimfinityError } = require('@simtlix/simfinity-js');
|
|
@@ -1189,7 +1411,78 @@ const OrderType = new GraphQLObjectType({
|
|
|
1189
1411
|
|
|
1190
1412
|
### Custom Validated Scalar Types
|
|
1191
1413
|
|
|
1192
|
-
Create custom scalar types with built-in validation. The generated type names follow the pattern `{name}_{baseScalarTypeName}
|
|
1414
|
+
Create custom scalar types with built-in validation. The generated type names follow the pattern `{name}_{baseScalarTypeName}`.
|
|
1415
|
+
|
|
1416
|
+
#### Pre-built Scalars
|
|
1417
|
+
|
|
1418
|
+
Simfinity.js provides ready-to-use validated scalars for common patterns:
|
|
1419
|
+
|
|
1420
|
+
```javascript
|
|
1421
|
+
const { scalars } = require('@simtlix/simfinity-js');
|
|
1422
|
+
|
|
1423
|
+
const UserType = new GraphQLObjectType({
|
|
1424
|
+
name: 'User',
|
|
1425
|
+
fields: () => ({
|
|
1426
|
+
id: { type: GraphQLID },
|
|
1427
|
+
email: { type: scalars.EmailScalar }, // Type name: Email_String
|
|
1428
|
+
website: { type: scalars.URLScalar }, // Type name: URL_String
|
|
1429
|
+
age: { type: scalars.PositiveIntScalar }, // Type name: PositiveInt_Int
|
|
1430
|
+
price: { type: scalars.PositiveFloatScalar } // Type name: PositiveFloat_Float
|
|
1431
|
+
}),
|
|
1432
|
+
});
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
**Available Pre-built Scalars:**
|
|
1436
|
+
- `scalars.EmailScalar` - Validates email format (`Email_String`)
|
|
1437
|
+
- `scalars.URLScalar` - Validates URL format (`URL_String`)
|
|
1438
|
+
- `scalars.PositiveIntScalar` - Validates positive integers (`PositiveInt_Int`)
|
|
1439
|
+
- `scalars.PositiveFloatScalar` - Validates positive floats (`PositiveFloat_Float`)
|
|
1440
|
+
|
|
1441
|
+
#### Factory Functions for Custom Scalars
|
|
1442
|
+
|
|
1443
|
+
Create custom validated scalars with parameters:
|
|
1444
|
+
|
|
1445
|
+
```javascript
|
|
1446
|
+
const { scalars } = require('@simtlix/simfinity-js');
|
|
1447
|
+
|
|
1448
|
+
// Create a bounded string scalar (name length between 2-100 characters)
|
|
1449
|
+
const NameScalar = scalars.createBoundedStringScalar('Name', 2, 100);
|
|
1450
|
+
|
|
1451
|
+
// Create a bounded integer scalar (age between 0-120)
|
|
1452
|
+
const AgeScalar = scalars.createBoundedIntScalar('Age', 0, 120);
|
|
1453
|
+
|
|
1454
|
+
// Create a bounded float scalar (rating between 0-10)
|
|
1455
|
+
const RatingScalar = scalars.createBoundedFloatScalar('Rating', 0, 10);
|
|
1456
|
+
|
|
1457
|
+
// Create a pattern-based string scalar (phone number format)
|
|
1458
|
+
const PhoneScalar = scalars.createPatternStringScalar(
|
|
1459
|
+
'Phone',
|
|
1460
|
+
/^\+?[\d\s\-()]+$/,
|
|
1461
|
+
'Invalid phone number format'
|
|
1462
|
+
);
|
|
1463
|
+
|
|
1464
|
+
// Use in your types
|
|
1465
|
+
const PersonType = new GraphQLObjectType({
|
|
1466
|
+
name: 'Person',
|
|
1467
|
+
fields: () => ({
|
|
1468
|
+
id: { type: GraphQLID },
|
|
1469
|
+
name: { type: NameScalar }, // Type name: Name_String
|
|
1470
|
+
age: { type: AgeScalar }, // Type name: Age_Int
|
|
1471
|
+
rating: { type: RatingScalar }, // Type name: Rating_Float
|
|
1472
|
+
phone: { type: PhoneScalar } // Type name: Phone_String
|
|
1473
|
+
}),
|
|
1474
|
+
});
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
**Available Factory Functions:**
|
|
1478
|
+
- `scalars.createBoundedStringScalar(name, min, max)` - String with length bounds
|
|
1479
|
+
- `scalars.createBoundedIntScalar(name, min, max)` - Integer with range validation
|
|
1480
|
+
- `scalars.createBoundedFloatScalar(name, min, max)` - Float with range validation
|
|
1481
|
+
- `scalars.createPatternStringScalar(name, pattern, message)` - String with regex pattern validation
|
|
1482
|
+
|
|
1483
|
+
#### Creating Custom Scalars Manually
|
|
1484
|
+
|
|
1485
|
+
You can also create custom scalars using `createValidatedScalar` directly:
|
|
1193
1486
|
|
|
1194
1487
|
```javascript
|
|
1195
1488
|
const { GraphQLString, GraphQLInt } = require('graphql');
|
|
@@ -1260,6 +1553,317 @@ class NotFoundError extends SimfinityError {
|
|
|
1260
1553
|
}
|
|
1261
1554
|
```
|
|
1262
1555
|
|
|
1556
|
+
## 🔒 Query Scope
|
|
1557
|
+
|
|
1558
|
+
### Overview
|
|
1559
|
+
|
|
1560
|
+
Query scope allows you to automatically modify query arguments based on context (e.g., user permissions). This enables automatic filtering so that users can only see documents they're authorized to access. Scope functions are executed after middleware and before query execution, allowing you to append filter conditions to queries and aggregations.
|
|
1561
|
+
|
|
1562
|
+
### Defining Scope
|
|
1563
|
+
|
|
1564
|
+
Define scope in the type extensions, similar to how validations are defined:
|
|
1565
|
+
|
|
1566
|
+
```javascript
|
|
1567
|
+
const EpisodeType = new GraphQLObjectType({
|
|
1568
|
+
name: 'episode',
|
|
1569
|
+
extensions: {
|
|
1570
|
+
validations: {
|
|
1571
|
+
create: [validateEpisodeFields],
|
|
1572
|
+
update: [validateEpisodeBusinessRules]
|
|
1573
|
+
},
|
|
1574
|
+
scope: {
|
|
1575
|
+
find: async ({ type, args, operation, context }) => {
|
|
1576
|
+
// Modify args in place to add filter conditions
|
|
1577
|
+
args.owner = {
|
|
1578
|
+
terms: [
|
|
1579
|
+
{
|
|
1580
|
+
path: 'id',
|
|
1581
|
+
operator: 'EQ',
|
|
1582
|
+
value: context.user.id
|
|
1583
|
+
}
|
|
1584
|
+
]
|
|
1585
|
+
};
|
|
1586
|
+
},
|
|
1587
|
+
aggregate: async ({ type, args, operation, context }) => {
|
|
1588
|
+
// Apply same scope to aggregate queries
|
|
1589
|
+
args.owner = {
|
|
1590
|
+
terms: [
|
|
1591
|
+
{
|
|
1592
|
+
path: 'id',
|
|
1593
|
+
operator: 'EQ',
|
|
1594
|
+
value: context.user.id
|
|
1595
|
+
}
|
|
1596
|
+
]
|
|
1597
|
+
};
|
|
1598
|
+
},
|
|
1599
|
+
get_by_id: async ({ type, args, operation, context }) => {
|
|
1600
|
+
// For get_by_id, scope is automatically merged with id filter
|
|
1601
|
+
args.owner = {
|
|
1602
|
+
terms: [
|
|
1603
|
+
{
|
|
1604
|
+
path: 'id',
|
|
1605
|
+
operator: 'EQ',
|
|
1606
|
+
value: context.user.id
|
|
1607
|
+
}
|
|
1608
|
+
]
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
fields: () => ({
|
|
1614
|
+
id: { type: GraphQLID },
|
|
1615
|
+
name: { type: GraphQLString },
|
|
1616
|
+
owner: {
|
|
1617
|
+
type: new GraphQLNonNull(simfinity.getType('user')),
|
|
1618
|
+
extensions: {
|
|
1619
|
+
relation: {
|
|
1620
|
+
connectionField: 'owner',
|
|
1621
|
+
displayField: 'name'
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
})
|
|
1626
|
+
});
|
|
1627
|
+
```
|
|
1628
|
+
|
|
1629
|
+
### Scope for Find Operations
|
|
1630
|
+
|
|
1631
|
+
Scope functions for `find` operations modify the query arguments that are passed to `buildQuery`. The modified arguments are automatically used to filter results:
|
|
1632
|
+
|
|
1633
|
+
```javascript
|
|
1634
|
+
const DocumentType = new GraphQLObjectType({
|
|
1635
|
+
name: 'Document',
|
|
1636
|
+
extensions: {
|
|
1637
|
+
scope: {
|
|
1638
|
+
find: async ({ type, args, operation, context }) => {
|
|
1639
|
+
// Only show documents owned by the current user
|
|
1640
|
+
args.owner = {
|
|
1641
|
+
terms: [
|
|
1642
|
+
{
|
|
1643
|
+
path: 'id',
|
|
1644
|
+
operator: 'EQ',
|
|
1645
|
+
value: context.user.id
|
|
1646
|
+
}
|
|
1647
|
+
]
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
},
|
|
1652
|
+
fields: () => ({
|
|
1653
|
+
id: { type: GraphQLID },
|
|
1654
|
+
title: { type: GraphQLString },
|
|
1655
|
+
owner: {
|
|
1656
|
+
type: new GraphQLNonNull(simfinity.getType('user')),
|
|
1657
|
+
extensions: {
|
|
1658
|
+
relation: {
|
|
1659
|
+
connectionField: 'owner',
|
|
1660
|
+
displayField: 'name'
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
})
|
|
1665
|
+
});
|
|
1666
|
+
```
|
|
1667
|
+
|
|
1668
|
+
**Result**: All `documents` queries will automatically filter to only return documents where `owner.id` equals `context.user.id`.
|
|
1669
|
+
|
|
1670
|
+
### Scope for Aggregate Operations
|
|
1671
|
+
|
|
1672
|
+
Scope functions for `aggregate` operations work the same way, ensuring aggregation queries also respect the scope:
|
|
1673
|
+
|
|
1674
|
+
```javascript
|
|
1675
|
+
const OrderType = new GraphQLObjectType({
|
|
1676
|
+
name: 'Order',
|
|
1677
|
+
extensions: {
|
|
1678
|
+
scope: {
|
|
1679
|
+
aggregate: async ({ type, args, operation, context }) => {
|
|
1680
|
+
// Only aggregate orders for the current user's organization
|
|
1681
|
+
args.organization = {
|
|
1682
|
+
terms: [
|
|
1683
|
+
{
|
|
1684
|
+
path: 'id',
|
|
1685
|
+
operator: 'EQ',
|
|
1686
|
+
value: context.user.organizationId
|
|
1687
|
+
}
|
|
1688
|
+
]
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
},
|
|
1693
|
+
fields: () => ({
|
|
1694
|
+
// ... fields
|
|
1695
|
+
})
|
|
1696
|
+
});
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
**Result**: All `orders_aggregate` queries will automatically filter to only aggregate orders from the user's organization.
|
|
1700
|
+
|
|
1701
|
+
### Scope for Get By ID Operations
|
|
1702
|
+
|
|
1703
|
+
For `get_by_id` operations, scope functions modify a temporary query arguments object that includes the id filter. The system automatically combines the id filter with scope filters:
|
|
1704
|
+
|
|
1705
|
+
```javascript
|
|
1706
|
+
const PrivateDocumentType = new GraphQLObjectType({
|
|
1707
|
+
name: 'PrivateDocument',
|
|
1708
|
+
extensions: {
|
|
1709
|
+
scope: {
|
|
1710
|
+
get_by_id: async ({ type, args, operation, context }) => {
|
|
1711
|
+
// Ensure user can only access their own documents
|
|
1712
|
+
args.owner = {
|
|
1713
|
+
terms: [
|
|
1714
|
+
{
|
|
1715
|
+
path: 'id',
|
|
1716
|
+
operator: 'EQ',
|
|
1717
|
+
value: context.user.id
|
|
1718
|
+
}
|
|
1719
|
+
]
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
},
|
|
1724
|
+
fields: () => ({
|
|
1725
|
+
// ... fields
|
|
1726
|
+
})
|
|
1727
|
+
});
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
**Result**: When querying `privatedocument(id: "some_id")`, the system will:
|
|
1731
|
+
1. Create a query that includes both the id filter and the owner scope filter
|
|
1732
|
+
2. Only return the document if it matches both conditions
|
|
1733
|
+
3. Return `null` if the document exists but doesn't match the scope
|
|
1734
|
+
|
|
1735
|
+
### Scope Function Parameters
|
|
1736
|
+
|
|
1737
|
+
Scope functions receive the same parameters as middleware for consistency:
|
|
1738
|
+
|
|
1739
|
+
```javascript
|
|
1740
|
+
{
|
|
1741
|
+
type, // Type information (model, gqltype, controller, etc.)
|
|
1742
|
+
args, // GraphQL arguments passed to the operation (modify this object)
|
|
1743
|
+
operation, // Operation type: 'find', 'aggregate', or 'get_by_id'
|
|
1744
|
+
context // GraphQL context object (includes request info, user data, etc.)
|
|
1745
|
+
}
|
|
1746
|
+
```
|
|
1747
|
+
|
|
1748
|
+
### Filter Structure
|
|
1749
|
+
|
|
1750
|
+
When modifying `args` in scope functions, use the appropriate filter structure:
|
|
1751
|
+
|
|
1752
|
+
**For scalar fields:**
|
|
1753
|
+
```javascript
|
|
1754
|
+
args.fieldName = {
|
|
1755
|
+
operator: 'EQ',
|
|
1756
|
+
value: 'someValue'
|
|
1757
|
+
};
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1760
|
+
**For object/relation fields (QLTypeFilterExpression):**
|
|
1761
|
+
```javascript
|
|
1762
|
+
args.relationField = {
|
|
1763
|
+
terms: [
|
|
1764
|
+
{
|
|
1765
|
+
path: 'fieldName',
|
|
1766
|
+
operator: 'EQ',
|
|
1767
|
+
value: 'someValue'
|
|
1768
|
+
}
|
|
1769
|
+
]
|
|
1770
|
+
};
|
|
1771
|
+
```
|
|
1772
|
+
|
|
1773
|
+
### Complete Example
|
|
1774
|
+
|
|
1775
|
+
Here's a complete example showing scope for all query operations:
|
|
1776
|
+
|
|
1777
|
+
```javascript
|
|
1778
|
+
const EpisodeType = new GraphQLObjectType({
|
|
1779
|
+
name: 'episode',
|
|
1780
|
+
extensions: {
|
|
1781
|
+
validations: {
|
|
1782
|
+
save: [validateEpisodeFields],
|
|
1783
|
+
update: [validateEpisodeBusinessRules]
|
|
1784
|
+
},
|
|
1785
|
+
scope: {
|
|
1786
|
+
find: async ({ type, args, operation, context }) => {
|
|
1787
|
+
// Only show episodes from seasons the user has access to
|
|
1788
|
+
args.season = {
|
|
1789
|
+
terms: [
|
|
1790
|
+
{
|
|
1791
|
+
path: 'owner.id',
|
|
1792
|
+
operator: 'EQ',
|
|
1793
|
+
value: context.user.id
|
|
1794
|
+
}
|
|
1795
|
+
]
|
|
1796
|
+
};
|
|
1797
|
+
},
|
|
1798
|
+
aggregate: async ({ type, args, operation, context }) => {
|
|
1799
|
+
// Apply same scope to aggregations
|
|
1800
|
+
args.season = {
|
|
1801
|
+
terms: [
|
|
1802
|
+
{
|
|
1803
|
+
path: 'owner.id',
|
|
1804
|
+
operator: 'EQ',
|
|
1805
|
+
value: context.user.id
|
|
1806
|
+
}
|
|
1807
|
+
]
|
|
1808
|
+
};
|
|
1809
|
+
},
|
|
1810
|
+
get_by_id: async ({ type, args, operation, context }) => {
|
|
1811
|
+
// Ensure user can only access their own episodes
|
|
1812
|
+
args.owner = {
|
|
1813
|
+
terms: [
|
|
1814
|
+
{
|
|
1815
|
+
path: 'id',
|
|
1816
|
+
operator: 'EQ',
|
|
1817
|
+
value: context.user.id
|
|
1818
|
+
}
|
|
1819
|
+
]
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
},
|
|
1824
|
+
fields: () => ({
|
|
1825
|
+
id: { type: GraphQLID },
|
|
1826
|
+
number: { type: GraphQLInt },
|
|
1827
|
+
name: { type: GraphQLString },
|
|
1828
|
+
season: {
|
|
1829
|
+
type: new GraphQLNonNull(simfinity.getType('season')),
|
|
1830
|
+
extensions: {
|
|
1831
|
+
relation: {
|
|
1832
|
+
connectionField: 'season',
|
|
1833
|
+
displayField: 'number'
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
},
|
|
1837
|
+
owner: {
|
|
1838
|
+
type: new GraphQLNonNull(simfinity.getType('user')),
|
|
1839
|
+
extensions: {
|
|
1840
|
+
relation: {
|
|
1841
|
+
connectionField: 'owner',
|
|
1842
|
+
displayField: 'name'
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
})
|
|
1847
|
+
});
|
|
1848
|
+
```
|
|
1849
|
+
|
|
1850
|
+
### Important Notes
|
|
1851
|
+
|
|
1852
|
+
- **Execution Order**: Scope functions are executed **after** middleware, so middleware can set up context (e.g., user info) that scope functions can use
|
|
1853
|
+
- **Modify Args In Place**: Scope functions should modify the `args` object directly
|
|
1854
|
+
- **Filter Structure**: Use the correct filter structure (`QLFilter` for scalars, `QLTypeFilterExpression` for relations)
|
|
1855
|
+
- **All Query Operations**: Scope applies to `find`, `aggregate`, and `get_by_id` operations
|
|
1856
|
+
- **Automatic Merging**: For `get_by_id`, the id filter is automatically combined with scope filters
|
|
1857
|
+
- **Context Access**: Use `context.user`, `context.ip`, or other context properties to determine scope
|
|
1858
|
+
|
|
1859
|
+
### Use Cases
|
|
1860
|
+
|
|
1861
|
+
- **Multi-tenancy**: Filter documents by organization or tenant
|
|
1862
|
+
- **User-specific data**: Only show documents owned by the current user
|
|
1863
|
+
- **Role-based access**: Filter based on user roles or permissions
|
|
1864
|
+
- **Department/Team scoping**: Show only data relevant to user's department
|
|
1865
|
+
- **Geographic scoping**: Filter by user's location or region
|
|
1866
|
+
|
|
1263
1867
|
## 🔧 Advanced Features
|
|
1264
1868
|
|
|
1265
1869
|
### Field Extensions
|
|
@@ -2702,7 +3306,7 @@ const BookInput = simfinity.getInputType(BookType);
|
|
|
2702
3306
|
console.log(BookInput.getFields()); // Input fields for mutations
|
|
2703
3307
|
```
|
|
2704
3308
|
|
|
2705
|
-
### `saveObject(typeName, args, session?)`
|
|
3309
|
+
### `saveObject(typeName, args, session?, context?)`
|
|
2706
3310
|
|
|
2707
3311
|
Programmatically save an object outside of GraphQL mutations.
|
|
2708
3312
|
|
|
@@ -2710,6 +3314,7 @@ Programmatically save an object outside of GraphQL mutations.
|
|
|
2710
3314
|
- `typeName` (string): The name of the GraphQL type
|
|
2711
3315
|
- `args` (object): The data to save
|
|
2712
3316
|
- `session` (MongooseSession, optional): Database session for transactions
|
|
3317
|
+
- `context` (object, optional): GraphQL context object (includes request info, user data, etc.)
|
|
2713
3318
|
|
|
2714
3319
|
**Returns:**
|
|
2715
3320
|
- `Promise<object>`: The saved object
|
|
@@ -2717,12 +3322,20 @@ Programmatically save an object outside of GraphQL mutations.
|
|
|
2717
3322
|
**Example:**
|
|
2718
3323
|
|
|
2719
3324
|
```javascript
|
|
3325
|
+
const newBook = await simfinity.saveObject('Book', {
|
|
3326
|
+
title: 'New Book',
|
|
3327
|
+
author: 'Author Name'
|
|
3328
|
+
}, session, context);
|
|
3329
|
+
|
|
3330
|
+
// Without context (context will be undefined in controller hooks)
|
|
2720
3331
|
const newBook = await simfinity.saveObject('Book', {
|
|
2721
3332
|
title: 'New Book',
|
|
2722
3333
|
author: 'Author Name'
|
|
2723
3334
|
}, session);
|
|
2724
3335
|
```
|
|
2725
3336
|
|
|
3337
|
+
**Note**: When `context` is not provided, it will be `undefined` in controller hooks. This is acceptable for programmatic usage where context may not be available.
|
|
3338
|
+
|
|
2726
3339
|
### `createSchema(includedQueryTypes?, includedMutationTypes?, includedCustomMutations?)`
|
|
2727
3340
|
|
|
2728
3341
|
Creates the final GraphQL schema with all connected types.
|