@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.
- package/README.md +445 -11
- package/package.json +1 -1
- 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
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
|
-
|
|
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;
|