@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 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
- ### Field-Level Validations
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
- Add validation logic directly to fields:
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.