@simtlix/simfinity-js 2.2.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.
Files changed (3) hide show
  1. package/README.md +445 -11
  2. package/package.json +1 -1
  3. package/src/index.js +99 -35
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
 
@@ -1439,6 +1553,317 @@ class NotFoundError extends SimfinityError {
1439
1553
  }
1440
1554
  ```
1441
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
+
1442
1867
  ## 🔧 Advanced Features
1443
1868
 
1444
1869
  ### Field Extensions
@@ -2881,7 +3306,7 @@ const BookInput = simfinity.getInputType(BookType);
2881
3306
  console.log(BookInput.getFields()); // Input fields for mutations
2882
3307
  ```
2883
3308
 
2884
- ### `saveObject(typeName, args, session?)`
3309
+ ### `saveObject(typeName, args, session?, context?)`
2885
3310
 
2886
3311
  Programmatically save an object outside of GraphQL mutations.
2887
3312
 
@@ -2889,6 +3314,7 @@ Programmatically save an object outside of GraphQL mutations.
2889
3314
  - `typeName` (string): The name of the GraphQL type
2890
3315
  - `args` (object): The data to save
2891
3316
  - `session` (MongooseSession, optional): Database session for transactions
3317
+ - `context` (object, optional): GraphQL context object (includes request info, user data, etc.)
2892
3318
 
2893
3319
  **Returns:**
2894
3320
  - `Promise<object>`: The saved object
@@ -2896,12 +3322,20 @@ Programmatically save an object outside of GraphQL mutations.
2896
3322
  **Example:**
2897
3323
 
2898
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)
2899
3331
  const newBook = await simfinity.saveObject('Book', {
2900
3332
  title: 'New Book',
2901
3333
  author: 'Author Name'
2902
3334
  }, session);
2903
3335
  ```
2904
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
+
2905
3339
  ### `createSchema(includedQueryTypes?, includedMutationTypes?, includedCustomMutations?)`
2906
3340
 
2907
3341
  Creates the final GraphQL schema with all connected types.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -614,53 +614,53 @@ const executeRegisteredMutation = async (args, callback, session) => {
614
614
  }
615
615
  };
616
616
 
617
- const iterateonCollectionFields = async (materializedModel, gqltype, objectId, session) => {
617
+ const iterateonCollectionFields = async (materializedModel, gqltype, objectId, session, context) => {
618
618
  for (const [collectionFieldKey, collectionField] of
619
619
  Object.entries(materializedModel.collectionFields)) {
620
620
  if (collectionField.added) {
621
621
 
622
622
  await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
623
- collectionField.added, operations.SAVE);
623
+ collectionField.added, operations.SAVE, context);
624
624
  }
625
625
  if (collectionField.updated) {
626
626
 
627
627
  await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
628
- collectionField.updated, operations.UPDATE);
628
+ collectionField.updated, operations.UPDATE, context);
629
629
  }
630
630
  if (collectionField.deleted) {
631
631
 
632
632
  await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
633
- collectionField.deleted, operations.DELETE);
633
+ collectionField.deleted, operations.DELETE, context);
634
634
  }
635
635
  }
636
636
  };
637
637
 
638
- const onDeleteObject = async (Model, gqltype, controller, args, session) => {
638
+ const onDeleteObject = async (Model, gqltype, controller, args, session, context) => {
639
639
  const deletedObject = await Model.findById({ _id: args }).session(session).lean();
640
640
 
641
641
  if (controller && controller.onDelete) {
642
- await controller.onDelete(deletedObject, session);
642
+ await controller.onDelete(deletedObject, session, context);
643
643
  }
644
644
 
645
645
  return Model.findByIdAndDelete({ _id: args }).session(session);
646
646
  };
647
647
 
648
- const onDeleteSubject = async (Model, controller, id, session) => {
648
+ const onDeleteSubject = async (Model, controller, id, session, context) => {
649
649
  const currentObject = await Model.findById({ _id: id }).session(session).lean();
650
650
 
651
651
  if (controller && controller.onDelete) {
652
- await controller.onDelete(currentObject, session);
652
+ await controller.onDelete(currentObject, session, context);
653
653
  }
654
654
 
655
655
  return Model.findByIdAndDelete({ _id: id }).session(session);
656
656
  };
657
657
 
658
- const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent) => {
658
+ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
659
659
  const materializedModel = await materializeModel(args, gqltype, linkToParent, 'UPDATE', session);
660
660
  const objectId = args.id;
661
661
 
662
662
  if (materializedModel.collectionFields) {
663
- await iterateonCollectionFields(materializedModel, gqltype, objectId, session);
663
+ await iterateonCollectionFields(materializedModel, gqltype, objectId, session, context);
664
664
  }
665
665
 
666
666
  const currentObject = await Model.findById({ _id: objectId }).lean();
@@ -688,7 +688,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
688
688
  });
689
689
 
690
690
  if (controller && controller.onUpdating) {
691
- await controller.onUpdating(objectId, materializedModel.modelArgs, session);
691
+ await controller.onUpdating(objectId, materializedModel.modelArgs, session, context);
692
692
  }
693
693
 
694
694
  const result = Model.findByIdAndUpdate(
@@ -696,13 +696,13 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
696
696
  ).session(session);
697
697
 
698
698
  if (controller && controller.onUpdated) {
699
- await controller.onUpdated(result, session);
699
+ await controller.onUpdated(result, session, context);
700
700
  }
701
701
 
702
702
  return result;
703
703
  };
704
704
 
705
- const onStateChanged = async (Model, gqltype, controller, args, session, actionField) => {
705
+ const onStateChanged = async (Model, gqltype, controller, args, session, actionField, context) => {
706
706
  const storedModel = await Model.findById(args.id);
707
707
  if (!storedModel) {
708
708
  throw new SimfinityError(`${gqltype.name} ${args.id} is not valid`, 'NOT_VALID_ID', 404);
@@ -713,7 +713,7 @@ const onStateChanged = async (Model, gqltype, controller, args, session, actionF
713
713
  }
714
714
 
715
715
  args.state = actionField.to.name;
716
- let result = await onUpdateSubject(Model, gqltype, controller, args, session);
716
+ let result = await onUpdateSubject(Model, gqltype, controller, args, session, null, context);
717
717
  result = result.toObject();
718
718
  result.state = actionField.to.value;
719
719
  return result;
@@ -721,7 +721,7 @@ const onStateChanged = async (Model, gqltype, controller, args, session, actionF
721
721
  throw new SimfinityError(`Action is not allowed from state ${storedModel.state}`, 'BAD_REQUEST', 400);
722
722
  };
723
723
 
724
- const onSaveObject = async (Model, gqltype, controller, args, session, linkToParent) => {
724
+ const onSaveObject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
725
725
  const materializedModel = await materializeModel(args, gqltype, linkToParent, 'CREATE', session);
726
726
  if (typesDict.types[gqltype.name].stateMachine) {
727
727
  materializedModel.modelArgs.state = typesDict.types[gqltype.name]
@@ -732,17 +732,17 @@ const onSaveObject = async (Model, gqltype, controller, args, session, linkToPar
732
732
  newObject.$session(session);
733
733
 
734
734
  if (controller && controller.onSaving) {
735
- await controller.onSaving(newObject, args, session);
735
+ await controller.onSaving(newObject, args, session, context);
736
736
  }
737
737
 
738
738
  if (materializedModel.collectionFields) {
739
- await iterateonCollectionFields(materializedModel, gqltype, newObject._id, session);
739
+ await iterateonCollectionFields(materializedModel, gqltype, newObject._id, session, context);
740
740
  }
741
741
 
742
742
  let result = await newObject.save();
743
743
  result = result.toObject();
744
744
  if (controller && controller.onSaved) {
745
- await controller.onSaved(result, args, session);
745
+ await controller.onSaved(result, args, session, context);
746
746
  }
747
747
  if (typesDict.types[gqltype.name].stateMachine) {
748
748
  result.state = typesDict.types[gqltype.name].stateMachine.initialState.value;
@@ -750,29 +750,29 @@ const onSaveObject = async (Model, gqltype, controller, args, session, linkToPar
750
750
  return result;
751
751
  };
752
752
 
753
- export const saveObject = async (typeName, args, session) => {
753
+ export const saveObject = async (typeName, args, session, context) => {
754
754
  const type = typesDict.types[typeName];
755
- return onSaveObject(type.model, type.gqltype, type.controller, args, session);
755
+ return onSaveObject(type.model, type.gqltype, type.controller, args, session, null, context);
756
756
  };
757
757
 
758
758
  const executeOperation = async (Model, gqltype, controller,
759
- args, operation, actionField, session) => {
759
+ args, operation, actionField, session, context) => {
760
760
  const mySession = session || await mongoose.startSession();
761
761
  await mySession.startTransaction();
762
762
  try {
763
763
  let newObject = null;
764
764
  switch (operation) {
765
765
  case operations.SAVE:
766
- newObject = await onSaveObject(Model, gqltype, controller, args, mySession);
766
+ newObject = await onSaveObject(Model, gqltype, controller, args, mySession, null, context);
767
767
  break;
768
768
  case operations.UPDATE:
769
- newObject = await onUpdateSubject(Model, gqltype, controller, args, mySession);
769
+ newObject = await onUpdateSubject(Model, gqltype, controller, args, mySession, null, context);
770
770
  break;
771
771
  case operations.DELETE:
772
- newObject = await onDeleteObject(Model, gqltype, controller, args, mySession);
772
+ newObject = await onDeleteObject(Model, gqltype, controller, args, mySession, context);
773
773
  break;
774
774
  case operations.STATE_CHANGED:
775
- newObject = await onStateChanged(Model, gqltype, controller, args, mySession, actionField);
775
+ newObject = await onStateChanged(Model, gqltype, controller, args, mySession, actionField, context);
776
776
  break;
777
777
  }
778
778
  await mySession.commitTransaction();
@@ -781,7 +781,7 @@ const executeOperation = async (Model, gqltype, controller,
781
781
  } catch (error) {
782
782
  await mySession.abortTransaction();
783
783
  if (error.errorLabels && error.errorLabels.includes('TransientTransactionError')) {
784
- return executeOperation(Model, gqltype, controller, args, operation, actionField, mySession);
784
+ return executeOperation(Model, gqltype, controller, args, operation, actionField, mySession, context);
785
785
  }
786
786
  mySession.endSession();
787
787
  throw error;
@@ -789,7 +789,7 @@ const executeOperation = async (Model, gqltype, controller,
789
789
  };
790
790
 
791
791
  const executeItemFunction = async (gqltype, collectionField, objectId, session,
792
- collectionFieldsList, operationType) => {
792
+ collectionFieldsList, operationType, context) => {
793
793
  const argTypes = gqltype.getFields();
794
794
  const collectionGQLType = argTypes[collectionField].type.ofType;
795
795
  const { connectionField } = argTypes[collectionField].extensions.relation;
@@ -802,7 +802,7 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
802
802
  await onSaveObject(typesDict.types[collectionGQLType.name].model, collectionGQLType,
803
803
  typesDict.types[collectionGQLType.name].controller, collectionItem, session, (item) => {
804
804
  item[connectionField] = objectId;
805
- });
805
+ }, context);
806
806
  };
807
807
  break;
808
808
  case operations.UPDATE:
@@ -810,13 +810,13 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
810
810
  await onUpdateSubject(typesDict.types[collectionGQLType.name].model, collectionGQLType,
811
811
  typesDict.types[collectionGQLType.name].controller, collectionItem, session, (item) => {
812
812
  item[connectionField] = objectId;
813
- });
813
+ }, context);
814
814
  };
815
815
  break;
816
816
  case operations.DELETE:
817
817
  operationFunction = async (collectionItem) => {
818
818
  await onDeleteSubject(typesDict.types[collectionGQLType.name].model,
819
- typesDict.types[collectionGQLType.name].controller, collectionItem, session);
819
+ typesDict.types[collectionGQLType.name].controller, collectionItem, session, context);
820
820
  };
821
821
  }
822
822
 
@@ -846,6 +846,31 @@ const excecuteMiddleware = (context) => {
846
846
  middleware();
847
847
  };
848
848
 
849
+ const executeScope = async (params) => {
850
+ const { type, args, operation, context } = params;
851
+
852
+ if (!type || !type.gqltype || !type.gqltype.extensions) {
853
+ return null;
854
+ }
855
+
856
+ const extensions = type.gqltype.extensions;
857
+ if (!extensions.scope || !extensions.scope[operation]) {
858
+ return null;
859
+ }
860
+
861
+ const scopeFunction = extensions.scope[operation];
862
+ if (typeof scopeFunction !== 'function') {
863
+ return null;
864
+ }
865
+
866
+ // Call the scope function with the same params as middleware
867
+ const result = await scopeFunction({ type, args, operation, context });
868
+
869
+ // For get_by_id, the scope function returns additional filters to merge
870
+ // For find and aggregate, it modifies args in place
871
+ return result;
872
+ };
873
+
849
874
  const buildMutation = (name, includedMutationTypes, includedCustomMutations) => {
850
875
  const rootQueryArgs = {};
851
876
  rootQueryArgs.name = name;
@@ -872,7 +897,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
872
897
 
873
898
  excecuteMiddleware(params);
874
899
  return executeOperation(type.model, type.gqltype, type.controller,
875
- args.input, operations.SAVE);
900
+ args.input, operations.SAVE, null, null, context);
876
901
  },
877
902
  };
878
903
  rootQueryArgs.fields[`delete${type.simpleEntityEndpointName}`] = {
@@ -889,7 +914,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
889
914
 
890
915
  excecuteMiddleware(params);
891
916
  return executeOperation(type.model, type.gqltype, type.controller,
892
- args.id, operations.DELETE);
917
+ args.id, operations.DELETE, null, null, context);
893
918
  },
894
919
  };
895
920
  }
@@ -914,7 +939,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
914
939
 
915
940
  excecuteMiddleware(params);
916
941
  return executeOperation(type.model, type.gqltype, type.controller,
917
- args.input, operations.UPDATE);
942
+ args.input, operations.UPDATE, null, null, context);
918
943
  },
919
944
  };
920
945
  if (type.stateMachine) {
@@ -936,7 +961,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
936
961
 
937
962
  excecuteMiddleware(params);
938
963
  return executeOperation(type.model, type.gqltype, type.controller,
939
- args.input, operations.STATE_CHANGED, actionField);
964
+ args.input, operations.STATE_CHANGED, actionField, null, context);
940
965
  },
941
966
  };
942
967
  }
@@ -1829,7 +1854,44 @@ const buildRootQuery = (name, includedTypes) => {
1829
1854
  context,
1830
1855
  };
1831
1856
  excecuteMiddleware(params);
1832
- return await type.model.findById(args.id);
1857
+
1858
+ // Check if scope is defined for get_by_id
1859
+ const hasScope = type.gqltype.extensions && type.gqltype.extensions.scope && type.gqltype.extensions.scope.get_by_id;
1860
+
1861
+ if (hasScope) {
1862
+ // Build query args with id filter - scope function will modify this
1863
+ const queryArgs = {
1864
+ id: { operator: 'EQ', value: args.id },
1865
+ };
1866
+
1867
+ // Create temporary params with queryArgs for scope function
1868
+ const scopeParams = {
1869
+ type,
1870
+ args: queryArgs,
1871
+ operation: 'get_by_id',
1872
+ context,
1873
+ };
1874
+
1875
+ // Execute scope which will modify queryArgs in place
1876
+ await executeScope(scopeParams);
1877
+
1878
+ // Build aggregation pipeline from the combined filters
1879
+ const aggregateClauses = await buildQuery(queryArgs, type.gqltype);
1880
+
1881
+ // Execute the query and get the first result
1882
+ let result;
1883
+ if (aggregateClauses.length === 0) {
1884
+ result = await type.model.findOne({ _id: args.id });
1885
+ } else {
1886
+ const results = await type.model.aggregate(aggregateClauses);
1887
+ result = results.length > 0 ? results[0] : null;
1888
+ }
1889
+
1890
+ return result;
1891
+ } else {
1892
+ // No scope defined, use the original findById
1893
+ return await type.model.findById(args.id);
1894
+ }
1833
1895
  },
1834
1896
  };
1835
1897
 
@@ -1848,6 +1910,7 @@ const buildRootQuery = (name, includedTypes) => {
1848
1910
  context,
1849
1911
  };
1850
1912
  excecuteMiddleware(params);
1913
+ await executeScope(params);
1851
1914
  const aggregateClauses = await buildQuery(args, type.gqltype);
1852
1915
  if (args.pagination && args.pagination.count) {
1853
1916
  const aggregateClausesForCount = await buildQuery(args, type.gqltype, true);
@@ -1882,6 +1945,7 @@ const buildRootQuery = (name, includedTypes) => {
1882
1945
  context,
1883
1946
  };
1884
1947
  excecuteMiddleware(params);
1948
+ await executeScope(params);
1885
1949
  const aggregateClauses = await buildAggregationQuery(args, type.gqltype, args.aggregation);
1886
1950
  const result = await type.model.aggregate(aggregateClauses);
1887
1951
  return result;