@simtlix/simfinity-js 2.2.0 → 2.3.1
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 +502 -64
- package/package.json +1 -1
- package/src/index.js +100 -35
- package/src/plugins.js +50 -0
package/README.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
A powerful Node.js framework that automatically generates GraphQL schemas from your data models, bringing all the power and flexibility of MongoDB query language to GraphQL interfaces.
|
|
4
4
|
|
|
5
|
+
## 📑 Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#-features)
|
|
8
|
+
- [Installation](#-installation)
|
|
9
|
+
- [Quick Start](#-quick-start)
|
|
10
|
+
- [Core Concepts](#-core-concepts)
|
|
11
|
+
- [Connecting Models](#connecting-models)
|
|
12
|
+
- [Creating Schemas](#creating-schemas)
|
|
13
|
+
- [Global Configuration](#global-configuration)
|
|
14
|
+
- [Basic Usage](#-basic-usage)
|
|
15
|
+
- [Automatic Query Generation](#automatic-query-generation)
|
|
16
|
+
- [Automatic Mutation Generation](#automatic-mutation-generation)
|
|
17
|
+
- [Filtering and Querying](#filtering-and-querying)
|
|
18
|
+
- [Collection Field Filtering](#collection-field-filtering)
|
|
19
|
+
- [Middlewares](#-middlewares)
|
|
20
|
+
- [Adding Middlewares](#adding-middlewares)
|
|
21
|
+
- [Middleware Parameters](#middleware-parameters)
|
|
22
|
+
- [Common Use Cases](#common-use-cases)
|
|
23
|
+
- [Relationships](#-relationships)
|
|
24
|
+
- [Defining Relationships](#defining-relationships)
|
|
25
|
+
- [Auto-Generated Resolve Methods](#auto-generated-resolve-methods)
|
|
26
|
+
- [Adding Types Without Endpoints](#adding-types-without-endpoints)
|
|
27
|
+
- [Embedded vs Referenced Relationships](#embedded-vs-referenced-relationships)
|
|
28
|
+
- [Querying Relationships](#querying-relationships)
|
|
29
|
+
- [Controllers & Lifecycle Hooks](#️-controllers--lifecycle-hooks)
|
|
30
|
+
- [Hook Parameters](#hook-parameters)
|
|
31
|
+
- [State Machines](#-state-machines)
|
|
32
|
+
- [Validations](#-validations)
|
|
33
|
+
- [Field-Level Validations](#field-level-validations)
|
|
34
|
+
- [Type-Level Validations](#type-level-validations)
|
|
35
|
+
- [Custom Validated Scalar Types](#custom-validated-scalar-types)
|
|
36
|
+
- [Custom Error Classes](#custom-error-classes)
|
|
37
|
+
- [Query Scope](#-query-scope)
|
|
38
|
+
- [Overview](#overview)
|
|
39
|
+
- [Defining Scope](#defining-scope)
|
|
40
|
+
- [Scope for Find Operations](#scope-for-find-operations)
|
|
41
|
+
- [Scope for Aggregate Operations](#scope-for-aggregate-operations)
|
|
42
|
+
- [Scope for Get By ID Operations](#scope-for-get-by-id-operations)
|
|
43
|
+
- [Scope Function Parameters](#scope-function-parameters)
|
|
44
|
+
- [Advanced Features](#-advanced-features)
|
|
45
|
+
- [Field Extensions](#field-extensions)
|
|
46
|
+
- [Custom Mutations](#custom-mutations)
|
|
47
|
+
- [Working with Existing Mongoose Models](#working-with-existing-mongoose-models)
|
|
48
|
+
- [Programmatic Data Access](#programmatic-data-access)
|
|
49
|
+
- [Aggregation Queries](#-aggregation-queries)
|
|
50
|
+
- [Complete Example](#-complete-example)
|
|
51
|
+
- [Resources](#-resources)
|
|
52
|
+
- [License](#-license)
|
|
53
|
+
- [Contributing](#-contributing)
|
|
54
|
+
- [Query Examples from Series-Sample](#-query-examples-from-series-sample)
|
|
55
|
+
- [State Machine Example from Series-Sample](#-state-machine-example-from-series-sample)
|
|
56
|
+
- [Plugins for Count in Extensions](#-plugins-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
|
|
@@ -2213,28 +2638,30 @@ query {
|
|
|
2213
2638
|
- **count**: Optional boolean - if `true`, returns total count of matching records
|
|
2214
2639
|
|
|
2215
2640
|
#### Getting Total Count:
|
|
2216
|
-
When `count: true` is specified, the total count is available in the response extensions. You need to configure
|
|
2217
|
-
|
|
2218
|
-
```javascript
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
}
|
|
2641
|
+
When `count: true` is specified, the total count is available in the response extensions. You need to configure a plugin to expose it. Simfinity.js provides utility plugins for both Apollo Server and Envelop:
|
|
2642
|
+
|
|
2643
|
+
```javascript
|
|
2644
|
+
const simfinity = require('@simtlix/simfinity-js');
|
|
2645
|
+
|
|
2646
|
+
// For Envelop
|
|
2647
|
+
const getEnveloped = envelop({
|
|
2648
|
+
plugins: [
|
|
2649
|
+
useSchema(schema),
|
|
2650
|
+
simfinity.plugins.envelopCountPlugin(),
|
|
2651
|
+
],
|
|
2652
|
+
});
|
|
2653
|
+
|
|
2654
|
+
// For Apollo Server
|
|
2655
|
+
const server = new ApolloServer({
|
|
2656
|
+
schema,
|
|
2657
|
+
plugins: [
|
|
2658
|
+
simfinity.plugins.apolloCountPlugin(),
|
|
2659
|
+
],
|
|
2660
|
+
});
|
|
2236
2661
|
```
|
|
2237
2662
|
|
|
2663
|
+
See the [Plugins for Count in Extensions](#-plugins-for-count-in-extensions) section for complete examples.
|
|
2664
|
+
|
|
2238
2665
|
#### Example Response:
|
|
2239
2666
|
```json
|
|
2240
2667
|
{
|
|
@@ -2574,45 +3001,18 @@ mutation {
|
|
|
2574
3001
|
```
|
|
2575
3002
|
|
|
2576
3003
|
|
|
2577
|
-
## 📦
|
|
2578
|
-
|
|
2579
|
-
To include the total count in the extensions of your GraphQL response, you can use an Envelop plugin. This is particularly useful for pagination and analytics.
|
|
2580
|
-
|
|
2581
|
-
### Envelop Plugin Example
|
|
2582
|
-
|
|
2583
|
-
Here's how you can implement the plugin:
|
|
2584
|
-
|
|
2585
|
-
```javascript
|
|
2586
|
-
// Envelop plugin for count in extensions
|
|
2587
|
-
function useCountPlugin() {
|
|
2588
|
-
return {
|
|
2589
|
-
onExecute() {
|
|
2590
|
-
return {
|
|
2591
|
-
onExecuteDone({ result, args }) {
|
|
2592
|
-
if (args.contextValue?.count) {
|
|
2593
|
-
result.extensions = {
|
|
2594
|
-
...result.extensions,
|
|
2595
|
-
count: args.contextValue.count,
|
|
2596
|
-
};
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
};
|
|
2600
|
-
}
|
|
2601
|
-
};
|
|
2602
|
-
}
|
|
2603
|
-
```
|
|
3004
|
+
## 📦 Plugins for Count in Extensions
|
|
2604
3005
|
|
|
2605
|
-
|
|
3006
|
+
To include the total count in the extensions of your GraphQL response, Simfinity.js provides utility plugins for both Apollo Server and Envelop. This is particularly useful for pagination and analytics.
|
|
2606
3007
|
|
|
2607
|
-
|
|
2608
|
-
2. **Configure Context**: Ensure that your context includes the count value when executing queries.
|
|
2609
|
-
3. **Access Count**: The count will be available in the `extensions` field of the GraphQL response.
|
|
3008
|
+
### Envelop Plugin
|
|
2610
3009
|
|
|
2611
|
-
|
|
3010
|
+
Use `simfinity.plugins.envelopCountPlugin()` to add count to extensions when using Envelop:
|
|
2612
3011
|
|
|
2613
3012
|
```javascript
|
|
2614
3013
|
const { envelop, useSchema } = require('@envelop/core');
|
|
2615
3014
|
const { makeExecutableSchema } = require('@graphql-tools/schema');
|
|
3015
|
+
const simfinity = require('@simtlix/simfinity-js');
|
|
2616
3016
|
|
|
2617
3017
|
const schema = makeExecutableSchema({
|
|
2618
3018
|
typeDefs,
|
|
@@ -2622,13 +3022,42 @@ const schema = makeExecutableSchema({
|
|
|
2622
3022
|
const getEnveloped = envelop({
|
|
2623
3023
|
plugins: [
|
|
2624
3024
|
useSchema(schema),
|
|
2625
|
-
|
|
3025
|
+
simfinity.plugins.envelopCountPlugin(), // Add the count plugin here
|
|
2626
3026
|
],
|
|
2627
3027
|
});
|
|
2628
3028
|
|
|
2629
3029
|
// Use getEnveloped in your server setup
|
|
2630
3030
|
```
|
|
2631
3031
|
|
|
3032
|
+
### Apollo Server Plugin
|
|
3033
|
+
|
|
3034
|
+
Use `simfinity.plugins.apolloCountPlugin()` to add count to extensions when using Apollo Server:
|
|
3035
|
+
|
|
3036
|
+
```javascript
|
|
3037
|
+
const { ApolloServer } = require('apollo-server-express');
|
|
3038
|
+
const simfinity = require('@simtlix/simfinity-js');
|
|
3039
|
+
|
|
3040
|
+
const server = new ApolloServer({
|
|
3041
|
+
schema,
|
|
3042
|
+
plugins: [
|
|
3043
|
+
simfinity.plugins.apolloCountPlugin(), // Add the count plugin here
|
|
3044
|
+
],
|
|
3045
|
+
context: ({ req }) => {
|
|
3046
|
+
// Your context setup
|
|
3047
|
+
return {
|
|
3048
|
+
user: req.user,
|
|
3049
|
+
// count will be automatically added to extensions if present in context
|
|
3050
|
+
};
|
|
3051
|
+
},
|
|
3052
|
+
});
|
|
3053
|
+
```
|
|
3054
|
+
|
|
3055
|
+
### How to Use
|
|
3056
|
+
|
|
3057
|
+
1. **Import the Plugin**: Use `simfinity.plugins.envelopCountPlugin()` or `simfinity.plugins.apolloCountPlugin()` depending on your GraphQL server.
|
|
3058
|
+
2. **Configure Context**: Ensure that your context includes the count value when executing queries (Simfinity.js automatically sets `context.count` when `count: true` is specified in pagination).
|
|
3059
|
+
3. **Access Count**: The count will be available in the `extensions` field of the GraphQL response.
|
|
3060
|
+
|
|
2632
3061
|
### Example Response
|
|
2633
3062
|
|
|
2634
3063
|
When the plugin is correctly set up, your GraphQL response will include the count in the extensions:
|
|
@@ -2881,7 +3310,7 @@ const BookInput = simfinity.getInputType(BookType);
|
|
|
2881
3310
|
console.log(BookInput.getFields()); // Input fields for mutations
|
|
2882
3311
|
```
|
|
2883
3312
|
|
|
2884
|
-
### `saveObject(typeName, args, session?)`
|
|
3313
|
+
### `saveObject(typeName, args, session?, context?)`
|
|
2885
3314
|
|
|
2886
3315
|
Programmatically save an object outside of GraphQL mutations.
|
|
2887
3316
|
|
|
@@ -2889,6 +3318,7 @@ Programmatically save an object outside of GraphQL mutations.
|
|
|
2889
3318
|
- `typeName` (string): The name of the GraphQL type
|
|
2890
3319
|
- `args` (object): The data to save
|
|
2891
3320
|
- `session` (MongooseSession, optional): Database session for transactions
|
|
3321
|
+
- `context` (object, optional): GraphQL context object (includes request info, user data, etc.)
|
|
2892
3322
|
|
|
2893
3323
|
**Returns:**
|
|
2894
3324
|
- `Promise<object>`: The saved object
|
|
@@ -2896,12 +3326,20 @@ Programmatically save an object outside of GraphQL mutations.
|
|
|
2896
3326
|
**Example:**
|
|
2897
3327
|
|
|
2898
3328
|
```javascript
|
|
3329
|
+
const newBook = await simfinity.saveObject('Book', {
|
|
3330
|
+
title: 'New Book',
|
|
3331
|
+
author: 'Author Name'
|
|
3332
|
+
}, session, context);
|
|
3333
|
+
|
|
3334
|
+
// Without context (context will be undefined in controller hooks)
|
|
2899
3335
|
const newBook = await simfinity.saveObject('Book', {
|
|
2900
3336
|
title: 'New Book',
|
|
2901
3337
|
author: 'Author Name'
|
|
2902
3338
|
}, session);
|
|
2903
3339
|
```
|
|
2904
3340
|
|
|
3341
|
+
**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.
|
|
3342
|
+
|
|
2905
3343
|
### `createSchema(includedQueryTypes?, includedMutationTypes?, includedCustomMutations?)`
|
|
2906
3344
|
|
|
2907
3345
|
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;
|
|
@@ -2077,6 +2141,7 @@ export { createValidatedScalar };
|
|
|
2077
2141
|
|
|
2078
2142
|
export { default as validators } from './validators.js';
|
|
2079
2143
|
export { default as scalars } from './scalars.js';
|
|
2144
|
+
export { default as plugins } from './plugins.js';
|
|
2080
2145
|
|
|
2081
2146
|
const createArgsForQuery = (argTypes) => {
|
|
2082
2147
|
const argsObject = {};
|
package/src/plugins.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apollo Server plugin to add count to GraphQL response extensions
|
|
3
|
+
* @returns {Object} Apollo Server plugin
|
|
4
|
+
*/
|
|
5
|
+
export const apolloCountPlugin = () => {
|
|
6
|
+
return {
|
|
7
|
+
async requestDidStart() {
|
|
8
|
+
return {
|
|
9
|
+
async willSendResponse({ contextValue, response }) {
|
|
10
|
+
if (response.body.kind === 'single' && contextValue?.count) {
|
|
11
|
+
response.body.singleResult.extensions = {
|
|
12
|
+
...(response.body.singleResult.extensions || {}),
|
|
13
|
+
count: contextValue.count,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Envelop plugin to add count to GraphQL response extensions
|
|
24
|
+
* @returns {Object} Envelop plugin
|
|
25
|
+
*/
|
|
26
|
+
export const envelopCountPlugin = () => {
|
|
27
|
+
return {
|
|
28
|
+
onExecute() {
|
|
29
|
+
return {
|
|
30
|
+
onExecuteDone({ result, args }) {
|
|
31
|
+
if (args.contextValue?.count) {
|
|
32
|
+
result.extensions = {
|
|
33
|
+
...result.extensions,
|
|
34
|
+
count: args.contextValue.count
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Export all plugins as an object for convenience
|
|
44
|
+
const plugins = {
|
|
45
|
+
apolloCountPlugin,
|
|
46
|
+
envelopCountPlugin,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default plugins;
|
|
50
|
+
|