@memberjunction/sqlserver-dataprovider 3.4.0 → 4.1.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 +573 -758
- package/dist/NodeFileSystemProvider.d.ts +16 -0
- package/dist/NodeFileSystemProvider.d.ts.map +1 -0
- package/dist/NodeFileSystemProvider.js +35 -0
- package/dist/NodeFileSystemProvider.js.map +1 -0
- package/dist/SQLServerDataProvider.d.ts +23 -4
- package/dist/SQLServerDataProvider.d.ts.map +1 -1
- package/dist/SQLServerDataProvider.js +189 -177
- package/dist/SQLServerDataProvider.js.map +1 -1
- package/dist/SQLServerTransactionGroup.js +16 -43
- package/dist/SQLServerTransactionGroup.js.map +1 -1
- package/dist/SqlLogger.js +11 -43
- package/dist/SqlLogger.js.map +1 -1
- package/dist/UserCache.d.ts +1 -1
- package/dist/UserCache.d.ts.map +1 -1
- package/dist/UserCache.js +9 -38
- package/dist/UserCache.js.map +1 -1
- package/dist/__tests__/setup.d.ts +5 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +17 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -17
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -17
- package/dist/index.js.map +1 -1
- package/dist/queryParameterProcessor.js +4 -32
- package/dist/queryParameterProcessor.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -6
- package/dist/types.js.map +1 -1
- package/package.json +23 -22
package/README.md
CHANGED
|
@@ -1,29 +1,78 @@
|
|
|
1
1
|
# MemberJunction SQL Server Data Provider
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
A comprehensive SQL Server data provider implementation for the MemberJunction framework, serving as the primary bridge between MemberJunction applications and Microsoft SQL Server databases. This package implements all core data provider interfaces -- entity CRUD, metadata management, view/query/report execution, transaction handling, and SQL logging.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
```mermaid
|
|
8
|
+
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888' }}}%%
|
|
9
|
+
graph TB
|
|
10
|
+
subgraph Application["Application Layer"]
|
|
11
|
+
MJS["MJServer / MJAPI"]
|
|
12
|
+
MS["MetadataSync"]
|
|
13
|
+
Custom["Custom Applications"]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
subgraph Provider["SQLServerDataProvider"]
|
|
17
|
+
style Provider fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
18
|
+
SDP["SQLServerDataProvider"]
|
|
19
|
+
TXG["SQLServerTransactionGroup"]
|
|
20
|
+
UC["UserCache"]
|
|
21
|
+
QPP["QueryParameterProcessor"]
|
|
22
|
+
SL["SqlLoggingSessionImpl"]
|
|
23
|
+
NFS["NodeFileSystemProvider"]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
subgraph Interfaces["MJ Core Interfaces"]
|
|
27
|
+
style Interfaces fill:#7c5295,stroke:#563a6b,color:#fff
|
|
28
|
+
IED["IEntityDataProvider"]
|
|
29
|
+
IMP["IMetadataProvider"]
|
|
30
|
+
IRV["IRunViewProvider"]
|
|
31
|
+
IRR["IRunReportProvider"]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
subgraph Database["SQL Server"]
|
|
35
|
+
style Database fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
36
|
+
Pool["Connection Pool"]
|
|
37
|
+
SP["Stored Procedures"]
|
|
38
|
+
Views["Database Views"]
|
|
39
|
+
Tables["Entity Tables"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
MJS --> SDP
|
|
43
|
+
MS --> SDP
|
|
44
|
+
Custom --> SDP
|
|
45
|
+
SDP --> IED
|
|
46
|
+
SDP --> IMP
|
|
47
|
+
SDP --> IRV
|
|
48
|
+
SDP --> IRR
|
|
49
|
+
SDP --> TXG
|
|
50
|
+
SDP --> UC
|
|
51
|
+
SDP --> QPP
|
|
52
|
+
SDP --> SL
|
|
53
|
+
SDP --> NFS
|
|
54
|
+
SDP --> Pool
|
|
55
|
+
Pool --> SP
|
|
56
|
+
Pool --> Views
|
|
57
|
+
Pool --> Tables
|
|
58
|
+
```
|
|
8
59
|
|
|
9
60
|
## Key Features
|
|
10
61
|
|
|
11
|
-
- **Full CRUD Operations
|
|
12
|
-
- **Transaction Support
|
|
13
|
-
- **View Execution
|
|
14
|
-
- **Report
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **Session Management**: Multiple concurrent SQL logging sessions with user filtering
|
|
26
|
-
- **Pattern Filtering**: Include/exclude SQL statements using simple wildcards or regex patterns ([details](#pattern-filtering))
|
|
62
|
+
- **Full CRUD Operations** -- Create, Read, Update, Delete for all MemberJunction entities via generated stored procedures
|
|
63
|
+
- **Transaction Support** -- Both transaction groups (multi-entity atomic operations) and instance-level transactions with nested savepoint support
|
|
64
|
+
- **View Execution** -- Run database views with filtering, sorting, pagination, and aggregation
|
|
65
|
+
- **Report and Query Execution** -- Execute reports and parameterized queries with Nunjucks template processing
|
|
66
|
+
- **Connection Pooling** -- Efficient shared connection pool management with configurable sizing
|
|
67
|
+
- **SQL Logging** -- Real-time SQL statement capture to files with session management, pattern filtering, and Flyway migration formatting
|
|
68
|
+
- **User and Role Caching** -- Server-side singleton cache for user information and role assignments
|
|
69
|
+
- **Record Change Tracking** -- Integrated audit trail for entity modifications
|
|
70
|
+
- **Duplicate Detection** -- AI-powered duplicate record detection via vector similarity
|
|
71
|
+
- **Record Merging** -- Merge duplicate records with dependency resolution
|
|
72
|
+
- **Row-Level Security** -- Enforced data access controls at the database level
|
|
73
|
+
- **Metadata Refresh** -- Automatic and on-demand metadata refresh with configurable intervals
|
|
74
|
+
- **Field Encryption** -- Transparent encryption and decryption of sensitive entity fields
|
|
75
|
+
- **DateTime Offset Handling** -- Automatic detection and adjustment for SQL Server timezone behavior
|
|
27
76
|
|
|
28
77
|
## Installation
|
|
29
78
|
|
|
@@ -33,898 +82,664 @@ npm install @memberjunction/sqlserver-dataprovider
|
|
|
33
82
|
|
|
34
83
|
## Dependencies
|
|
35
84
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
|
|
85
|
+
| Package | Purpose |
|
|
86
|
+
|---------|---------|
|
|
87
|
+
| `@memberjunction/core` | Core MJ framework: base entities, metadata, providers |
|
|
88
|
+
| `@memberjunction/core-entities` | Generated entity subclasses and type definitions |
|
|
89
|
+
| `@memberjunction/global` | Shared utilities, global object store, SQL validation |
|
|
90
|
+
| `@memberjunction/actions` | Server-side entity action execution |
|
|
91
|
+
| `@memberjunction/actions-base` | Action result types |
|
|
92
|
+
| `@memberjunction/ai` | AI integration capabilities |
|
|
93
|
+
| `@memberjunction/ai-provider-bundle` | AI provider bundle |
|
|
94
|
+
| `@memberjunction/ai-vector-dupe` | AI-powered duplicate detection |
|
|
95
|
+
| `@memberjunction/aiengine` | AI engine for entity AI actions |
|
|
96
|
+
| `@memberjunction/encryption` | Field-level encryption engine |
|
|
97
|
+
| `@memberjunction/queue` | Queue management for async operations |
|
|
98
|
+
| `mssql` | SQL Server client for Node.js |
|
|
99
|
+
| `nunjucks` | Template engine for parameterized queries |
|
|
100
|
+
| `sql-formatter` | SQL pretty-printing for log output |
|
|
101
|
+
| `rxjs` | Reactive extensions for transaction queue processing |
|
|
102
|
+
|
|
103
|
+
## Exported API
|
|
104
|
+
|
|
105
|
+
The package exports the following public symbols from its entry point:
|
|
106
|
+
|
|
107
|
+
| Export | Type | Description |
|
|
108
|
+
|--------|------|-------------|
|
|
109
|
+
| `SQLServerDataProvider` | Class | Main data provider implementing all MJ provider interfaces |
|
|
110
|
+
| `SQLServerProviderConfigData` | Class | Configuration data for provider initialization |
|
|
111
|
+
| `SQLServerTransactionGroup` | Class | Transaction group for atomic multi-entity operations |
|
|
112
|
+
| `UserCache` | Class | Singleton server-side user and role cache |
|
|
113
|
+
| `QueryParameterProcessor` | Class | Parameter validation and Nunjucks query template processor |
|
|
114
|
+
| `NodeFileSystemProvider` | Class | Node.js `fs`-based implementation of `IFileSystemProvider` |
|
|
115
|
+
| `SqlLoggingSessionImpl` | Class | Internal SQL logging session implementation |
|
|
116
|
+
| `SqlLoggingSession` | Interface | Public interface for a logging session |
|
|
117
|
+
| `SqlLoggingOptions` | Interface | Configuration options for SQL logging sessions |
|
|
118
|
+
| `ExecuteSQLOptions` | Interface | Options for SQL execution with logging support |
|
|
119
|
+
| `ExecuteSQLBatchOptions` | Interface | Options for batch SQL execution |
|
|
120
|
+
| `setupSQLServerClient` | Function | Helper to initialize provider, set global provider, start user cache |
|
|
121
|
+
|
|
122
|
+
## Data Flow
|
|
123
|
+
|
|
124
|
+
```mermaid
|
|
125
|
+
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888' }}}%%
|
|
126
|
+
sequenceDiagram
|
|
127
|
+
participant App as Application
|
|
128
|
+
participant SDP as SQLServerDataProvider
|
|
129
|
+
participant Queue as SQL Queue (RxJS)
|
|
130
|
+
participant Logger as SqlLoggingSessions
|
|
131
|
+
participant Pool as Connection Pool
|
|
132
|
+
participant DB as SQL Server
|
|
133
|
+
|
|
134
|
+
App->>SDP: Save(entity, user, options)
|
|
135
|
+
SDP->>SDP: Generate SP call SQL
|
|
136
|
+
SDP->>SDP: Encrypt sensitive fields
|
|
137
|
+
|
|
138
|
+
alt Transaction Active
|
|
139
|
+
SDP->>Queue: Enqueue (sequential)
|
|
140
|
+
Queue->>Pool: Execute via Transaction
|
|
141
|
+
else No Transaction
|
|
142
|
+
SDP->>Pool: Execute directly (parallel)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
SDP-->>Logger: Log SQL (parallel, non-blocking)
|
|
146
|
+
Pool->>DB: Execute stored procedure
|
|
147
|
+
DB-->>Pool: Return result set
|
|
148
|
+
Pool-->>SDP: Raw result
|
|
149
|
+
SDP->>SDP: Process rows (decrypt, timezone adjust)
|
|
150
|
+
SDP-->>App: BaseEntityResult
|
|
151
|
+
```
|
|
47
152
|
|
|
48
153
|
## Usage
|
|
49
154
|
|
|
50
|
-
###
|
|
155
|
+
### Initialization with setupSQLServerClient
|
|
156
|
+
|
|
157
|
+
The `setupSQLServerClient` helper handles full provider initialization: connecting to the pool, configuring the provider, loading the user cache, setting up the global MJ provider, and running the startup manager.
|
|
51
158
|
|
|
52
159
|
```typescript
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
160
|
+
import { setupSQLServerClient } from '@memberjunction/sqlserver-dataprovider';
|
|
161
|
+
import { SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
|
|
162
|
+
import sql from 'mssql';
|
|
55
163
|
|
|
56
|
-
//
|
|
57
|
-
const
|
|
58
|
-
|
|
164
|
+
// Create and connect a connection pool
|
|
165
|
+
const pool = new sql.ConnectionPool({
|
|
166
|
+
server: 'your-server.database.windows.net',
|
|
59
167
|
port: 1433,
|
|
60
168
|
database: 'YourMJDatabase',
|
|
61
169
|
user: 'your-username',
|
|
62
170
|
password: 'your-password',
|
|
63
171
|
options: {
|
|
64
172
|
encrypt: true,
|
|
65
|
-
trustServerCertificate: false
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// Or using environment variables
|
|
73
|
-
const dataProvider = new SQLServerDataProvider({
|
|
74
|
-
host: ConfigHelper.getConfigValue('MJ_HOST'),
|
|
75
|
-
port: ConfigHelper.getConfigValue('MJ_PORT', 1433),
|
|
76
|
-
database: ConfigHelper.getConfigValue('MJ_DATABASE'),
|
|
77
|
-
user: ConfigHelper.getConfigValue('MJ_USER'),
|
|
78
|
-
password: ConfigHelper.getConfigValue('MJ_PASSWORD')
|
|
173
|
+
trustServerCertificate: false,
|
|
174
|
+
},
|
|
175
|
+
pool: {
|
|
176
|
+
max: 50,
|
|
177
|
+
min: 5,
|
|
178
|
+
idleTimeoutMillis: 30000,
|
|
179
|
+
},
|
|
79
180
|
});
|
|
181
|
+
await pool.connect();
|
|
182
|
+
|
|
183
|
+
// Initialize the provider (sets global MJ provider, loads user cache, runs startup)
|
|
184
|
+
const config = new SQLServerProviderConfigData(
|
|
185
|
+
pool,
|
|
186
|
+
'__mj', // MJ core schema name
|
|
187
|
+
60, // metadata refresh interval in seconds (0 to disable)
|
|
188
|
+
undefined, // includeSchemas (undefined = all)
|
|
189
|
+
undefined, // excludeSchemas
|
|
190
|
+
true // ignoreExistingMetadata (true for first instance)
|
|
191
|
+
);
|
|
80
192
|
|
|
81
|
-
|
|
82
|
-
await dataProvider.initialize();
|
|
193
|
+
const provider = await setupSQLServerClient(config);
|
|
83
194
|
```
|
|
84
195
|
|
|
85
196
|
### Working with Entities
|
|
86
197
|
|
|
87
198
|
```typescript
|
|
88
|
-
import {
|
|
89
|
-
import { Metadata, CompositeKey, UserInfo } from '@memberjunction/core';
|
|
199
|
+
import { Metadata, CompositeKey } from '@memberjunction/core';
|
|
90
200
|
import { UserEntity } from '@memberjunction/core-entities';
|
|
91
201
|
|
|
92
|
-
// Setup data provider
|
|
93
|
-
const dataProvider = new SQLServerDataProvider(/* config */);
|
|
94
|
-
await dataProvider.initialize();
|
|
95
|
-
|
|
96
|
-
// Get entity metadata
|
|
97
202
|
const md = new Metadata();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (userResult.Success) {
|
|
105
|
-
const user = userResult.Entity;
|
|
106
|
-
console.log(`Loaded user: ${user.FirstName} ${user.LastName}`);
|
|
107
|
-
|
|
108
|
-
// Update the entity
|
|
109
|
-
user.Email = 'new.email@example.com';
|
|
110
|
-
const saveResult = await dataProvider.Save(user, contextUser);
|
|
111
|
-
|
|
112
|
-
if (saveResult.Success) {
|
|
113
|
-
console.log(`User updated successfully, ID: ${saveResult.Entity.ID}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
203
|
+
|
|
204
|
+
// Load an entity by primary key
|
|
205
|
+
const user = await md.GetEntityObject<UserEntity>('Users', contextUser);
|
|
206
|
+
const key = new CompositeKey([{ FieldName: 'ID', Value: userId }]);
|
|
207
|
+
await user.Load(key);
|
|
208
|
+
console.log(`Loaded: ${user.Name}`);
|
|
116
209
|
|
|
117
210
|
// Create a new entity
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const createResult = await dataProvider.Save(newUserEntity, contextUser);
|
|
125
|
-
if (createResult.Success) {
|
|
126
|
-
console.log(`New user created with ID: ${createResult.Entity.ID}`);
|
|
211
|
+
const newUser = await md.GetEntityObject<UserEntity>('Users', contextUser);
|
|
212
|
+
newUser.Name = 'John Doe';
|
|
213
|
+
newUser.Email = 'john@example.com';
|
|
214
|
+
const saved = await newUser.Save();
|
|
215
|
+
if (saved) {
|
|
216
|
+
console.log(`Created user with ID: ${newUser.ID}`);
|
|
127
217
|
}
|
|
128
218
|
|
|
129
219
|
// Delete an entity
|
|
130
|
-
|
|
131
|
-
const deleteResult = await dataProvider.Delete(userEntity, deleteKey, contextUser);
|
|
132
|
-
if (deleteResult.Success) {
|
|
133
|
-
console.log('User deleted successfully');
|
|
134
|
-
}
|
|
220
|
+
await newUser.Delete();
|
|
135
221
|
```
|
|
136
222
|
|
|
137
|
-
### Transaction
|
|
138
|
-
|
|
139
|
-
The SQL Server Data Provider supports comprehensive transaction management through both transaction groups and instance-level transactions.
|
|
223
|
+
### Transaction Groups
|
|
140
224
|
|
|
141
|
-
|
|
225
|
+
Transaction groups execute multiple entity operations within a single database transaction, with automatic rollback on failure.
|
|
142
226
|
|
|
143
227
|
```typescript
|
|
144
|
-
import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
|
|
145
228
|
import { SQLServerTransactionGroup } from '@memberjunction/sqlserver-dataprovider';
|
|
146
|
-
import { Metadata } from '@memberjunction/core';
|
|
147
229
|
|
|
148
|
-
|
|
149
|
-
const dataProvider = new SQLServerDataProvider(/* config */);
|
|
150
|
-
await dataProvider.initialize();
|
|
230
|
+
const transaction = await provider.CreateTransactionGroup();
|
|
151
231
|
|
|
152
|
-
|
|
153
|
-
|
|
232
|
+
const order = await md.GetEntityObject('Orders', contextUser);
|
|
233
|
+
order.CustomerID = customerId;
|
|
234
|
+
order.Status = 'New';
|
|
235
|
+
order.TransactionGroup = transaction;
|
|
154
236
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const orderItemEntity2 = await md.GetEntityObject('Order Item');
|
|
160
|
-
|
|
161
|
-
// Set up the order
|
|
162
|
-
orderEntity.CustomerID = 123;
|
|
163
|
-
orderEntity.OrderDate = new Date();
|
|
164
|
-
orderEntity.Status = 'New';
|
|
165
|
-
|
|
166
|
-
// Add to transaction - this will get ID after save
|
|
167
|
-
await transaction.AddTransaction(orderEntity);
|
|
168
|
-
|
|
169
|
-
// Set up order items with references to the order
|
|
170
|
-
orderItemEntity1.OrderID = '@Order.1'; // Reference to the first Order in this transaction
|
|
171
|
-
orderItemEntity1.ProductID = 456;
|
|
172
|
-
orderItemEntity1.Quantity = 2;
|
|
173
|
-
orderItemEntity1.Price = 29.99;
|
|
174
|
-
|
|
175
|
-
orderItemEntity2.OrderID = '@Order.1'; // Same order reference
|
|
176
|
-
orderItemEntity2.ProductID = 789;
|
|
177
|
-
orderItemEntity2.Quantity = 1;
|
|
178
|
-
orderItemEntity2.Price = 49.99;
|
|
179
|
-
|
|
180
|
-
// Add items to transaction
|
|
181
|
-
await transaction.AddTransaction(orderItemEntity1);
|
|
182
|
-
await transaction.AddTransaction(orderItemEntity2);
|
|
183
|
-
|
|
184
|
-
// Execute the transaction group
|
|
185
|
-
const results = await transaction.Submit();
|
|
237
|
+
const item = await md.GetEntityObject('Order Items', contextUser);
|
|
238
|
+
item.ProductID = productId;
|
|
239
|
+
item.Quantity = 2;
|
|
240
|
+
item.TransactionGroup = transaction;
|
|
186
241
|
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const orderResult = results.find(r => r.Entity.EntityInfo.Name === 'Order');
|
|
192
|
-
console.log('Order ID:', orderResult?.Entity.ID);
|
|
193
|
-
} else {
|
|
194
|
-
console.error('Transaction failed');
|
|
195
|
-
results.filter(r => !r.Success).forEach(r => {
|
|
196
|
-
console.error(`Failed: ${r.Entity.EntityInfo.Name}`, r.Message);
|
|
197
|
-
});
|
|
198
|
-
}
|
|
242
|
+
// Both saves are queued, then executed atomically on Submit
|
|
243
|
+
await order.Save();
|
|
244
|
+
await item.Save();
|
|
245
|
+
const results = await transaction.Submit();
|
|
199
246
|
```
|
|
200
247
|
|
|
201
|
-
|
|
248
|
+
### Instance-Level Transactions
|
|
202
249
|
|
|
203
|
-
|
|
250
|
+
For multi-user server environments, each provider instance supports isolated transaction state with nested savepoints.
|
|
204
251
|
|
|
205
252
|
```typescript
|
|
206
|
-
// Each request gets its own provider instance
|
|
207
|
-
const dataProvider = new SQLServerDataProvider(connectionPool);
|
|
208
|
-
await dataProvider.Config(config);
|
|
209
|
-
|
|
210
253
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
await
|
|
217
|
-
|
|
218
|
-
// Delete operations also participate in the transaction
|
|
219
|
-
await dataProvider.Delete(entity3, deleteOptions, contextUser);
|
|
220
|
-
|
|
221
|
-
// Commit the transaction
|
|
222
|
-
await dataProvider.CommitTransaction();
|
|
254
|
+
await provider.BeginTransaction();
|
|
255
|
+
|
|
256
|
+
await provider.Save(entity1, contextUser, {});
|
|
257
|
+
await provider.Save(entity2, contextUser, {});
|
|
258
|
+
|
|
259
|
+
await provider.CommitTransaction();
|
|
223
260
|
} catch (error) {
|
|
224
|
-
|
|
225
|
-
await dataProvider.RollbackTransaction();
|
|
261
|
+
await provider.RollbackTransaction();
|
|
226
262
|
throw error;
|
|
227
263
|
}
|
|
228
264
|
```
|
|
229
265
|
|
|
230
|
-
|
|
231
|
-
- Each provider instance maintains its own transaction state
|
|
232
|
-
- No transaction scope IDs needed - simpler API
|
|
233
|
-
- Automatic isolation between concurrent requests (each has its own instance)
|
|
234
|
-
- Supports nested transactions with SQL Server savepoints
|
|
235
|
-
- Used automatically by MJServer for all GraphQL mutations
|
|
236
|
-
|
|
237
|
-
**Best Practices for Multi-User Environments:**
|
|
238
|
-
1. Create a new SQLServerDataProvider instance per request
|
|
239
|
-
2. Configure with `ignoreExistingMetadata: false` to reuse cached metadata
|
|
240
|
-
3. Let the instance be garbage collected after the request completes
|
|
241
|
-
4. No need for explicit cleanup - transaction state is instance-scoped
|
|
242
|
-
|
|
243
|
-
### Running Views and Reports
|
|
266
|
+
### Running Views
|
|
244
267
|
|
|
245
268
|
```typescript
|
|
246
|
-
import {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
PageNumber: 1
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const viewResult = await dataProvider.RunView(viewOptions);
|
|
263
|
-
|
|
264
|
-
if (viewResult.success) {
|
|
265
|
-
console.log(`Found ${viewResult.Results.length} users`);
|
|
266
|
-
console.log(`Total matching records: ${viewResult.TotalRowCount}`);
|
|
267
|
-
|
|
268
|
-
viewResult.Results.forEach(user => {
|
|
269
|
-
console.log(`${user.FirstName} ${user.LastName} (${user.Email})`);
|
|
270
|
-
});
|
|
269
|
+
import { RunView } from '@memberjunction/core';
|
|
270
|
+
|
|
271
|
+
const rv = new RunView();
|
|
272
|
+
const result = await rv.RunView({
|
|
273
|
+
EntityName: 'Users',
|
|
274
|
+
ExtraFilter: "Status = 'Active'",
|
|
275
|
+
OrderBy: 'Name',
|
|
276
|
+
MaxRows: 100,
|
|
277
|
+
ResultType: 'entity_object',
|
|
278
|
+
}, contextUser);
|
|
279
|
+
|
|
280
|
+
if (result.Success) {
|
|
281
|
+
console.log(`Found ${result.Results.length} active users`);
|
|
271
282
|
}
|
|
283
|
+
```
|
|
272
284
|
|
|
273
|
-
|
|
274
|
-
const reportParams: RunReportParams = {
|
|
275
|
-
ReportID: 'report-id-here',
|
|
276
|
-
// Other parameters as needed
|
|
277
|
-
};
|
|
285
|
+
### Running Parameterized Queries
|
|
278
286
|
|
|
279
|
-
|
|
287
|
+
The `QueryParameterProcessor` validates parameters and processes Nunjucks templates for parameterized queries.
|
|
280
288
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
289
|
+
```typescript
|
|
290
|
+
import { RunQuery } from '@memberjunction/core';
|
|
291
|
+
|
|
292
|
+
const rq = new RunQuery();
|
|
293
|
+
const result = await rq.RunQuery({
|
|
294
|
+
QueryName: 'ActiveUsersByDepartment',
|
|
295
|
+
CategoryPath: '/Reports/Users/',
|
|
296
|
+
Parameters: {
|
|
297
|
+
department: 'Engineering',
|
|
298
|
+
minHireDate: '2023-01-01',
|
|
299
|
+
},
|
|
300
|
+
}, contextUser);
|
|
301
|
+
|
|
302
|
+
if (result.Success) {
|
|
303
|
+
console.log('Query results:', result.Results);
|
|
285
304
|
}
|
|
286
305
|
```
|
|
287
306
|
|
|
288
|
-
### Executing Raw
|
|
307
|
+
### Executing Raw SQL
|
|
289
308
|
|
|
290
309
|
```typescript
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const dataProvider = new SQLServerDataProvider(/* config */);
|
|
296
|
-
await dataProvider.initialize();
|
|
297
|
-
|
|
298
|
-
// Execute raw SQL with parameters
|
|
299
|
-
const sqlResult = await dataProvider.ExecuteSQL(
|
|
300
|
-
'SELECT * FROM Users WHERE Department = @dept AND HireDate > @date',
|
|
301
|
-
{
|
|
302
|
-
dept: 'Engineering',
|
|
303
|
-
date: '2022-01-01'
|
|
304
|
-
}
|
|
310
|
+
// Instance method
|
|
311
|
+
const rows = await provider.ExecuteSQL(
|
|
312
|
+
'SELECT * FROM Users WHERE Department = @dept',
|
|
313
|
+
{ dept: 'Engineering' }
|
|
305
314
|
);
|
|
306
315
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// Execute a stored procedure
|
|
313
|
-
const spResult = await dataProvider.ExecuteSQL(
|
|
314
|
-
'EXEC sp_GetUserPermissions @UserID',
|
|
315
|
-
{
|
|
316
|
-
UserID: 123
|
|
317
|
-
}
|
|
316
|
+
// Static method (useful when you have a pool but not a provider)
|
|
317
|
+
const rows2 = await SQLServerDataProvider.ExecuteSQLWithPool(
|
|
318
|
+
pool,
|
|
319
|
+
'SELECT TOP 10 * FROM Users ORDER BY Name'
|
|
318
320
|
);
|
|
319
|
-
|
|
320
|
-
console.log('User permissions:', spResult);
|
|
321
|
-
|
|
322
|
-
// Using RunQuery for pre-defined queries
|
|
323
|
-
const queryParams: RunQueryParams = {
|
|
324
|
-
QueryID: 'query-id-here', // or use QueryName + Category identification
|
|
325
|
-
// Alternative: use QueryName with hierarchical CategoryPath
|
|
326
|
-
// QueryName: 'CalculateCost',
|
|
327
|
-
// CategoryPath: '/MJ/AI/Agents/' // Hierarchical path notation
|
|
328
|
-
// CategoryID: 'optional-direct-category-id',
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
const queryResult = await dataProvider.RunQuery(queryParams);
|
|
332
|
-
|
|
333
|
-
if (queryResult.Success) {
|
|
334
|
-
console.log('Query results:', queryResult.Results);
|
|
335
|
-
console.log('Execution time:', queryResult.ExecutionTime, 'ms');
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Query lookup supports hierarchical category paths
|
|
339
|
-
// Example: Query with name "CalculateCost" in category hierarchy "MJ" -> "AI" -> "Agents"
|
|
340
|
-
const hierarchicalQueryParams: RunQueryParams = {
|
|
341
|
-
QueryName: 'CalculateCost',
|
|
342
|
-
CategoryPath: '/MJ/AI/Agents/' // Full hierarchical path with leading/trailing slashes
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
// The CategoryPath is parsed as a path where:
|
|
346
|
-
// - "/" separates category levels
|
|
347
|
-
// - Each segment is matched case-insensitively against category names
|
|
348
|
-
// - The path walks from root to leaf through the ParentID relationships
|
|
349
|
-
// - Falls back to simple category name matching for backward compatibility
|
|
350
321
|
```
|
|
351
322
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
|
|
356
|
-
|
|
357
|
-
// Setup data provider
|
|
358
|
-
const dataProvider = new SQLServerDataProvider(/* config */);
|
|
359
|
-
await dataProvider.initialize();
|
|
360
|
-
|
|
361
|
-
// Set current user context
|
|
362
|
-
dataProvider.setCurrentUser(123); // User ID
|
|
363
|
-
|
|
364
|
-
// Get current user
|
|
365
|
-
const currentUser = dataProvider.getCurrentUser();
|
|
366
|
-
console.log(`Current user: ${currentUser.FirstName} ${currentUser.LastName}`);
|
|
323
|
+
## SQL Logging
|
|
367
324
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
325
|
+
The provider includes a comprehensive SQL logging subsystem for capturing executed SQL statements to files. Logging sessions run in parallel with query execution and do not impact performance.
|
|
326
|
+
|
|
327
|
+
### Logging Architecture
|
|
328
|
+
|
|
329
|
+
```mermaid
|
|
330
|
+
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888' }}}%%
|
|
331
|
+
graph LR
|
|
332
|
+
subgraph Execution["SQL Execution"]
|
|
333
|
+
style Execution fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
334
|
+
EX["ExecuteSQL"]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
subgraph Sessions["Active Logging Sessions"]
|
|
338
|
+
style Sessions fill:#b8762f,stroke:#8a5722,color:#fff
|
|
339
|
+
S1["Session 1: mutations only"]
|
|
340
|
+
S2["Session 2: migration format"]
|
|
341
|
+
S3["Session 3: user-filtered"]
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
subgraph Output["Output Files"]
|
|
345
|
+
style Output fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
346
|
+
F1["operations.sql"]
|
|
347
|
+
F2["migration.sql"]
|
|
348
|
+
F3["user-audit.sql"]
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
EX -->|parallel, non-blocking| S1
|
|
352
|
+
EX -->|parallel, non-blocking| S2
|
|
353
|
+
EX -->|parallel, non-blocking| S3
|
|
354
|
+
S1 -->|filtered + formatted| F1
|
|
355
|
+
S2 -->|schema placeholders| F2
|
|
356
|
+
S3 -->|user-scoped| F3
|
|
371
357
|
```
|
|
372
358
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
The SQL Server data provider accepts the following configuration options:
|
|
376
|
-
|
|
377
|
-
| Option | Description | Default |
|
|
378
|
-
|--------|-------------|---------|
|
|
379
|
-
| `host` | SQL Server hostname or IP | required |
|
|
380
|
-
| `port` | SQL Server port | 1433 |
|
|
381
|
-
| `database` | Database name | required |
|
|
382
|
-
| `user` | Username | required |
|
|
383
|
-
| `password` | Password | required |
|
|
384
|
-
| `connectionTimeout` | Connection timeout in ms | 15000 |
|
|
385
|
-
| `requestTimeout` | Request timeout in ms | 15000 |
|
|
386
|
-
| `pool.max` | Maximum pool size | 10 |
|
|
387
|
-
| `pool.min` | Minimum pool size | 0 |
|
|
388
|
-
| `pool.idleTimeoutMillis` | Pool idle timeout | 30000 |
|
|
389
|
-
| `options.encrypt` | Use encryption | true |
|
|
390
|
-
| `options.trustServerCertificate` | Trust server certificate | false |
|
|
391
|
-
| `options.enableArithAbort` | Enable arithmetic abort | true |
|
|
392
|
-
|
|
393
|
-
## Advanced Usage
|
|
394
|
-
|
|
395
|
-
### Custom SQL Execution Hooks
|
|
359
|
+
### Creating a Logging Session
|
|
396
360
|
|
|
397
361
|
```typescript
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const duration = Date.now() - startTime;
|
|
410
|
-
|
|
411
|
-
console.log(`Query executed in ${duration}ms`);
|
|
412
|
-
console.log(`Rows returned: ${result?.length || 0}`);
|
|
413
|
-
|
|
414
|
-
return result;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Custom error handling
|
|
418
|
-
protected async HandleExecuteSQLError(error: any, sql: string): Promise<void> {
|
|
419
|
-
console.error('SQL Error:', error);
|
|
420
|
-
console.error('Failed SQL:', sql);
|
|
421
|
-
// Add custom error handling logic here
|
|
422
|
-
await super.HandleExecuteSQLError(error, sql);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
### Error Handling
|
|
428
|
-
|
|
429
|
-
The SQL Server Data Provider includes comprehensive error handling:
|
|
362
|
+
const session = await provider.CreateSqlLogger('./logs/operations.sql', {
|
|
363
|
+
sessionName: 'Debug session',
|
|
364
|
+
statementTypes: 'both', // 'queries', 'mutations', or 'both'
|
|
365
|
+
prettyPrint: true, // Format SQL with sql-formatter
|
|
366
|
+
formatAsMigration: false, // Replace schema names with Flyway placeholders
|
|
367
|
+
logRecordChangeMetadata: false, // Log only core SP calls, not change tracking wrapper
|
|
368
|
+
retainEmptyLogFiles: false, // Delete file if no statements were logged
|
|
369
|
+
filterByUserId: 'user@example.com', // Only capture this user's SQL
|
|
370
|
+
filterPatterns: [/spCreateAIPromptRun/i], // Exclude matching patterns
|
|
371
|
+
filterType: 'exclude', // 'exclude' or 'include'
|
|
372
|
+
});
|
|
430
373
|
|
|
431
|
-
```typescript
|
|
432
374
|
try {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
} catch (error) {
|
|
439
|
-
console.error('Unexpected error:', error);
|
|
440
|
-
// Handle system-level errors
|
|
375
|
+
// All SQL operations are automatically captured
|
|
376
|
+
await provider.ExecuteSQL('INSERT INTO ...');
|
|
377
|
+
console.log(`Captured ${session.statementCount} statements`);
|
|
378
|
+
} finally {
|
|
379
|
+
await session.dispose(); // Stop logging, close file, clean up
|
|
441
380
|
}
|
|
442
381
|
```
|
|
443
382
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
### Building the Package
|
|
447
|
-
|
|
448
|
-
```bash
|
|
449
|
-
# From the package directory
|
|
450
|
-
npm run build
|
|
451
|
-
|
|
452
|
-
# Or from the repository root
|
|
453
|
-
turbo build --filter="@memberjunction/sqlserver-dataprovider"
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
### Development Scripts
|
|
457
|
-
|
|
458
|
-
- `npm run build` - Compile TypeScript to JavaScript
|
|
459
|
-
- `npm run start` - Run the package with ts-node-dev for development
|
|
460
|
-
|
|
461
|
-
### TypeScript Configuration
|
|
462
|
-
|
|
463
|
-
This package is configured with TypeScript strict mode enabled. The compiled output is placed in the `dist/` directory with declaration files for type support.
|
|
464
|
-
|
|
465
|
-
## API Reference
|
|
466
|
-
|
|
467
|
-
### SQLServerDataProvider
|
|
468
|
-
|
|
469
|
-
The main class that implements IEntityDataProvider, IMetadataProvider, IRunViewProvider, IRunReportProvider, and IRunQueryProvider interfaces.
|
|
470
|
-
|
|
471
|
-
#### Key Methods
|
|
472
|
-
|
|
473
|
-
- `Config(configData: SQLServerProviderConfigData): Promise<boolean>` - Configure the provider with connection details
|
|
474
|
-
- `Get(entity: EntityInfo, CompositeKey: CompositeKey, user?: UserInfo): Promise<BaseEntityResult>` - Load an entity by primary key
|
|
475
|
-
- `Save(entity: BaseEntity, user: UserInfo, options?: EntitySaveOptions): Promise<BaseEntityResult>` - Save (create/update) an entity
|
|
476
|
-
- `Delete(entity: EntityInfo, CompositeKey: CompositeKey, user?: UserInfo, options?: EntityDeleteOptions): Promise<BaseEntityResult>` - Delete an entity
|
|
477
|
-
- `RunView(params: RunViewParams, contextUser?: UserInfo): Promise<RunViewResult>` - Execute a database view
|
|
478
|
-
- `RunReport(params: RunReportParams, contextUser?: UserInfo): Promise<RunReportResult>` - Execute a report
|
|
479
|
-
- `RunQuery(params: RunQueryParams, contextUser?: UserInfo): Promise<RunQueryResult>` - Execute a query
|
|
480
|
-
- `ExecuteSQL(sql: string, params?: any, maxRows?: number): Promise<any[]>` - Execute raw SQL
|
|
481
|
-
- `createSqlLogger(filePath: string, options?: SqlLoggingOptions): Promise<SqlLoggingSession>` - Create a new SQL logging session
|
|
482
|
-
- `getActiveSqlLoggingSessions(): SqlLoggingSession[]` - Get all active logging sessions
|
|
483
|
-
- `disposeAllSqlLoggingSessions(): Promise<void>` - Stop and clean up all logging sessions
|
|
484
|
-
- `isSqlLoggingEnabled(): boolean` - Check if SQL logging is available
|
|
485
|
-
|
|
486
|
-
### SQLServerProviderConfigData
|
|
487
|
-
|
|
488
|
-
Configuration class for the SQL Server provider.
|
|
489
|
-
|
|
490
|
-
#### Constructor Parameters
|
|
491
|
-
|
|
492
|
-
```typescript
|
|
493
|
-
constructor(
|
|
494
|
-
connectionPool: sql.ConnectionPool,
|
|
495
|
-
MJCoreSchemaName?: string,
|
|
496
|
-
checkRefreshIntervalSeconds: number = 0,
|
|
497
|
-
includeSchemas?: string[],
|
|
498
|
-
excludeSchemas?: string[],
|
|
499
|
-
ignoreExistingMetadata: boolean = true
|
|
500
|
-
)
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
#### Properties
|
|
504
|
-
|
|
505
|
-
- `ConnectionPool: sql.ConnectionPool` - SQL Server connection pool instance
|
|
506
|
-
- `CheckRefreshIntervalSeconds: number` - Interval for checking metadata refresh (0 to disable)
|
|
507
|
-
- `MJCoreSchemaName: string` - Schema name for MJ core tables (default: '__mj')
|
|
508
|
-
- `IncludeSchemas?: string[]` - List of schemas to include
|
|
509
|
-
- `ExcludeSchemas?: string[]` - List of schemas to exclude
|
|
510
|
-
- `IgnoreExistingMetadata: boolean` - Whether to ignore cached metadata and force a reload (default: true)
|
|
511
|
-
|
|
512
|
-
**Important Note on `ignoreExistingMetadata`:**
|
|
513
|
-
- Set to `false` in multi-user environments to reuse cached metadata across provider instances
|
|
514
|
-
- This significantly improves performance when creating provider instances per request
|
|
515
|
-
- The first instance loads metadata from the database, subsequent instances reuse it
|
|
516
|
-
|
|
517
|
-
### SQLServerTransactionGroup
|
|
518
|
-
|
|
519
|
-
SQL Server implementation of TransactionGroupBase for managing database transactions.
|
|
520
|
-
|
|
521
|
-
#### Methods
|
|
522
|
-
|
|
523
|
-
- `HandleSubmit(): Promise<TransactionResult[]>` - Execute all pending transactions in the group
|
|
524
|
-
|
|
525
|
-
### UserCache
|
|
526
|
-
|
|
527
|
-
Server-side cache for user and role information.
|
|
528
|
-
|
|
529
|
-
#### Static Methods
|
|
530
|
-
|
|
531
|
-
- `Instance: UserCache` - Get singleton instance
|
|
532
|
-
- `Users: UserInfo[]` - Get all cached users
|
|
533
|
-
|
|
534
|
-
#### Instance Methods
|
|
535
|
-
|
|
536
|
-
- `Refresh(dataSource: DataSource, autoRefreshIntervalMS?: number): Promise<void>` - Refresh user cache
|
|
537
|
-
- `UserByName(name: string, caseSensitive?: boolean): UserInfo | undefined` - Find user by name
|
|
538
|
-
|
|
539
|
-
### setupSQLServerClient
|
|
540
|
-
|
|
541
|
-
Helper function to initialize and configure the SQL Server data provider.
|
|
542
|
-
|
|
543
|
-
```typescript
|
|
544
|
-
setupSQLServerClient(config: SQLServerProviderConfigData): Promise<SQLServerDataProvider>
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
## SQL Logging
|
|
548
|
-
|
|
549
|
-
The SQL Server Data Provider includes comprehensive SQL logging capabilities that allow you to capture SQL statements in real-time. This feature supports both programmatic access and runtime control through the MemberJunction UI.
|
|
550
|
-
|
|
551
|
-
### Key Features
|
|
552
|
-
|
|
553
|
-
- **Real-time SQL capture** - Monitor SQL statements as they execute
|
|
554
|
-
- **Session-based logging** with unique identifiers and names
|
|
555
|
-
- **User filtering** - Capture SQL from specific users only
|
|
556
|
-
- **Multiple output formats** - Standard SQL logs or migration-ready files
|
|
557
|
-
- **Runtime control** - Start/stop sessions through GraphQL API and UI
|
|
558
|
-
- **Owner-level security** - Only users with Owner privileges can access SQL logging
|
|
559
|
-
- **Automatic cleanup** - Sessions auto-expire and clean up empty files
|
|
560
|
-
- **Concurrent sessions** - Support multiple active logging sessions
|
|
561
|
-
- **Parameter capture** - Logs both SQL statements and their parameters
|
|
562
|
-
- **Pattern filtering** - Include/exclude statements using simple wildcards or regex
|
|
383
|
+
### Migration-Ready Format
|
|
563
384
|
|
|
564
|
-
|
|
385
|
+
When `formatAsMigration: true`, the logger automatically:
|
|
386
|
+
- Replaces schema names with `${flyway:defaultSchema}` placeholders
|
|
387
|
+
- Escapes `${...}` patterns within SQL string literals to prevent Flyway interpretation
|
|
388
|
+
- Splits string literals exceeding SQL Server's 4000-character NVARCHAR limit into concatenated chunks with `CAST(... AS NVARCHAR(MAX))`
|
|
565
389
|
|
|
566
390
|
```typescript
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
sessionName: 'User registration operations',
|
|
576
|
-
filterByUserId: 'user@example.com', // Only log SQL from this user
|
|
577
|
-
prettyPrint: true,
|
|
578
|
-
statementTypes: 'both' // Log both queries and mutations
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
// Perform your database operations - they will be automatically logged
|
|
582
|
-
await dataProvider.ExecuteSQL('INSERT INTO Users (Name, Email) VALUES (@name, @email)', {
|
|
583
|
-
name: 'John Doe',
|
|
584
|
-
email: 'john@example.com'
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
// Check session status
|
|
588
|
-
console.log(`Session ${logger.id} has captured ${logger.statementCount} statements`);
|
|
589
|
-
|
|
590
|
-
// Clean up the logging session
|
|
591
|
-
await logger.dispose();
|
|
391
|
+
const session = await provider.CreateSqlLogger(
|
|
392
|
+
'./migrations/V20250207120000__entity_updates.sql',
|
|
393
|
+
{
|
|
394
|
+
formatAsMigration: true,
|
|
395
|
+
batchSeparator: 'GO',
|
|
396
|
+
description: 'Entity schema updates',
|
|
397
|
+
}
|
|
398
|
+
);
|
|
592
399
|
```
|
|
593
400
|
|
|
594
401
|
### Pattern Filtering
|
|
595
402
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
#### Pattern Types
|
|
599
|
-
|
|
600
|
-
1. **Simple Wildcard Patterns** (Recommended for most users):
|
|
601
|
-
- Use `*` as a wildcard character
|
|
602
|
-
- Case-insensitive by default
|
|
603
|
-
- Examples:
|
|
604
|
-
- `*AIPrompt*` - Matches anything containing "AIPrompt"
|
|
605
|
-
- `spCreate*` - Matches anything starting with "spCreate"
|
|
606
|
-
- `*Run` - Matches anything ending with "Run"
|
|
607
|
-
- `UserTable` - Exact match only
|
|
403
|
+
Filter which SQL statements are logged using simple wildcard patterns or full regular expressions.
|
|
608
404
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
- Examples:
|
|
613
|
-
- `/spCreate.*Run/i` - Case-insensitive regex
|
|
614
|
-
- `/^SELECT.*FROM.*vw/` - Queries from views
|
|
615
|
-
- `/INSERT INTO (Users|Roles)/i` - Insert into Users or Roles
|
|
405
|
+
**Simple wildcards** use `*` as a wildcard character:
|
|
406
|
+
- `*AIPrompt*` -- matches anything containing "AIPrompt"
|
|
407
|
+
- `spCreate*` -- matches anything starting with "spCreate"
|
|
616
408
|
|
|
617
|
-
|
|
409
|
+
**Regex patterns** provide full regular expression support:
|
|
410
|
+
- `/spCreate.*Run/i` -- case-insensitive regex
|
|
411
|
+
- `/^SELECT.*FROM.*vw/` -- queries from views
|
|
618
412
|
|
|
619
413
|
```typescript
|
|
620
|
-
// Exclude
|
|
621
|
-
const
|
|
622
|
-
sessionName: 'Production Operations',
|
|
414
|
+
// Exclude noisy patterns
|
|
415
|
+
const session = await provider.CreateSqlLogger('./logs/filtered.sql', {
|
|
623
416
|
filterPatterns: [
|
|
624
|
-
/spCreateAIPromptRun/i,
|
|
625
|
-
/
|
|
626
|
-
|
|
627
|
-
/INSERT INTO EntityFieldValue/i // Regex: Exclude field value inserts
|
|
417
|
+
/spCreateAIPromptRun/i,
|
|
418
|
+
/^SELECT.*FROM.*vw.*Metadata/i,
|
|
419
|
+
'*EntityFieldValue*',
|
|
628
420
|
],
|
|
629
|
-
filterType: 'exclude'
|
|
421
|
+
filterType: 'exclude', // Default: skip matching statements
|
|
630
422
|
});
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
#### Include Mode
|
|
634
423
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
filterPatterns: [
|
|
640
|
-
/INSERT INTO Users/i,
|
|
641
|
-
/UPDATE Users/i,
|
|
642
|
-
/DELETE FROM Users/i,
|
|
643
|
-
/sp_ChangePassword/i
|
|
644
|
-
],
|
|
645
|
-
filterType: 'include' // Only log statements matching these patterns
|
|
424
|
+
// Include only specific patterns
|
|
425
|
+
const auditSession = await provider.CreateSqlLogger('./logs/audit.sql', {
|
|
426
|
+
filterPatterns: [/INSERT INTO Users/i, /UPDATE Users/i, /DELETE FROM Users/i],
|
|
427
|
+
filterType: 'include', // Only log matching statements
|
|
646
428
|
});
|
|
647
429
|
```
|
|
648
430
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
When configuring SQL logging in MetadataSync's `.mj-sync.json`, you can use string patterns that support both formats:
|
|
652
|
-
|
|
653
|
-
```json
|
|
654
|
-
{
|
|
655
|
-
"sqlLogging": {
|
|
656
|
-
"enabled": true,
|
|
657
|
-
"filterPatterns": [
|
|
658
|
-
"*AIPrompt*", // Simple: Exclude anything with "AIPrompt"
|
|
659
|
-
"/^EXEC sp_/i", // Regex: Exclude stored procedures
|
|
660
|
-
"*EntityFieldValue*", // Simple: Exclude EntityFieldValue operations
|
|
661
|
-
"/INSERT INTO (__mj|mj)/i" // Regex: Exclude system table inserts
|
|
662
|
-
],
|
|
663
|
-
"filterType": "exclude"
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
#### Filter Pattern Options
|
|
669
|
-
- `filterPatterns`: Array of patterns (RegExp objects in code, strings in config)
|
|
670
|
-
- `filterType`:
|
|
671
|
-
- `'exclude'` (default): Skip logging if ANY pattern matches
|
|
672
|
-
- `'include'`: Only log if ANY pattern matches
|
|
673
|
-
- If no patterns are specified, all SQL is logged (backward compatible)
|
|
431
|
+
### Session Management
|
|
674
432
|
|
|
675
|
-
|
|
433
|
+
```typescript
|
|
434
|
+
// List all active sessions
|
|
435
|
+
const active = provider.GetActiveSqlLoggingSessions();
|
|
436
|
+
console.log(`${active.length} sessions active`);
|
|
676
437
|
|
|
677
|
-
|
|
438
|
+
// Get a specific session
|
|
439
|
+
const session = provider.GetSqlLoggingSessionById(sessionId);
|
|
678
440
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
mutation {
|
|
683
|
-
startSqlLogging(input: {
|
|
684
|
-
fileName: "debug-session.sql"
|
|
685
|
-
filterToCurrentUser: true
|
|
686
|
-
options: {
|
|
687
|
-
sessionName: "Debug Session"
|
|
688
|
-
prettyPrint: true
|
|
689
|
-
statementTypes: "both"
|
|
690
|
-
formatAsMigration: false
|
|
691
|
-
}
|
|
692
|
-
}) {
|
|
693
|
-
id
|
|
694
|
-
filePath
|
|
695
|
-
sessionName
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
`;
|
|
699
|
-
|
|
700
|
-
// List active sessions
|
|
701
|
-
const query = `
|
|
702
|
-
query {
|
|
703
|
-
activeSqlLoggingSessions {
|
|
704
|
-
id
|
|
705
|
-
sessionName
|
|
706
|
-
startTime
|
|
707
|
-
statementCount
|
|
708
|
-
filterByUserId
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
`;
|
|
441
|
+
// Dispose all sessions (cleanup on shutdown)
|
|
442
|
+
await provider.DisposeAllSqlLoggingSessions();
|
|
443
|
+
```
|
|
712
444
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
445
|
+
## Transaction Processing
|
|
446
|
+
|
|
447
|
+
```mermaid
|
|
448
|
+
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888' }}}%%
|
|
449
|
+
graph TD
|
|
450
|
+
subgraph TransactionGroup["Transaction Group Flow"]
|
|
451
|
+
style TransactionGroup fill:#7c5295,stroke:#563a6b,color:#fff
|
|
452
|
+
TG1["AddTransaction(entity)"]
|
|
453
|
+
TG2["Submit()"]
|
|
454
|
+
TG3["Begin SQL Transaction"]
|
|
455
|
+
TG4["Execute items sequentially"]
|
|
456
|
+
TG5{"All succeeded?"}
|
|
457
|
+
TG6["Commit"]
|
|
458
|
+
TG7["Rollback"]
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
subgraph InstanceTx["Instance Transaction Flow"]
|
|
462
|
+
style InstanceTx fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
463
|
+
IT1["BeginTransaction()"]
|
|
464
|
+
IT2["Queue serializes queries"]
|
|
465
|
+
IT3["Save / Delete / ExecuteSQL"]
|
|
466
|
+
IT4["CommitTransaction()"]
|
|
467
|
+
IT5["RollbackTransaction()"]
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
TG1 --> TG2
|
|
471
|
+
TG2 --> TG3
|
|
472
|
+
TG3 --> TG4
|
|
473
|
+
TG4 --> TG5
|
|
474
|
+
TG5 -->|Yes| TG6
|
|
475
|
+
TG5 -->|No| TG7
|
|
476
|
+
|
|
477
|
+
IT1 --> IT2
|
|
478
|
+
IT2 --> IT3
|
|
479
|
+
IT3 --> IT4
|
|
480
|
+
IT3 -->|Error| IT5
|
|
719
481
|
```
|
|
720
482
|
|
|
721
|
-
|
|
483
|
+
The provider supports two transaction mechanisms:
|
|
722
484
|
|
|
723
|
-
SQL
|
|
485
|
+
**Transaction Groups** (`SQLServerTransactionGroup`) -- bundle multiple entity save/delete operations and execute them within a single SQL Server transaction. If any operation fails, the entire group is rolled back. Transaction groups also support inter-entity variable references, allowing a newly created entity's ID to be passed to dependent entities in the same batch.
|
|
724
486
|
|
|
725
|
-
|
|
726
|
-
2. **Session Management**: Start/stop sessions with custom options
|
|
727
|
-
3. **Real-time Monitoring**: View active sessions and statement counts
|
|
728
|
-
4. **User Filtering**: Option to capture only your SQL statements
|
|
729
|
-
5. **Log Viewing**: Preview log file contents (implementation dependent)
|
|
487
|
+
**Instance-Level Transactions** -- each `SQLServerDataProvider` instance maintains its own transaction state. When a transaction is active, all SQL queries from that instance are serialized through an RxJS queue (`concatMap`) and executed against the same `sql.Transaction` object. Non-transactional queries bypass the queue for maximum parallelism. Nested transactions use SQL Server savepoints.
|
|
730
488
|
|
|
731
|
-
|
|
489
|
+
## UserCache
|
|
732
490
|
|
|
733
|
-
|
|
734
|
-
// Create logger with migration formatting
|
|
735
|
-
const migrationLogger = await dataProvider.createSqlLogger('./migrations/V20241215120000__User_Operations.sql', {
|
|
736
|
-
formatAsMigration: true,
|
|
737
|
-
sessionName: 'User management operations for deployment',
|
|
738
|
-
batchSeparator: 'GO',
|
|
739
|
-
logRecordChangeMetadata: true
|
|
740
|
-
});
|
|
491
|
+
The `UserCache` is a singleton that loads all users and their role assignments from the database and keeps them in memory. It is used for user lookups during authentication and authorization.
|
|
741
492
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
```
|
|
493
|
+
```typescript
|
|
494
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
745
495
|
|
|
746
|
-
|
|
496
|
+
// Access the singleton
|
|
497
|
+
const cache = UserCache.Instance;
|
|
747
498
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
499
|
+
// Look up users
|
|
500
|
+
const user = cache.UserByName('john@example.com');
|
|
501
|
+
const systemUser = cache.GetSystemUser();
|
|
502
|
+
const allUsers = cache.Users;
|
|
752
503
|
|
|
753
|
-
//
|
|
754
|
-
await
|
|
504
|
+
// Refresh from database (with optional auto-refresh interval in ms)
|
|
505
|
+
await cache.Refresh(pool, 60000);
|
|
506
|
+
```
|
|
755
507
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
508
|
+
## QueryParameterProcessor
|
|
509
|
+
|
|
510
|
+
Handles parameter validation and Nunjucks template rendering for parameterized queries.
|
|
511
|
+
|
|
512
|
+
```mermaid
|
|
513
|
+
%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#888' }}}%%
|
|
514
|
+
graph LR
|
|
515
|
+
subgraph Input["Query Input"]
|
|
516
|
+
style Input fill:#2d6a9f,stroke:#1a4971,color:#fff
|
|
517
|
+
QI["QueryInfo with SQL template"]
|
|
518
|
+
P["Parameters"]
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
subgraph Processing["QueryParameterProcessor"]
|
|
522
|
+
style Processing fill:#b8762f,stroke:#8a5722,color:#fff
|
|
523
|
+
V["Validate parameters"]
|
|
524
|
+
T["Type conversion"]
|
|
525
|
+
R["Nunjucks render"]
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
subgraph Output["Result"]
|
|
529
|
+
style Output fill:#2d8659,stroke:#1a5c3a,color:#fff
|
|
530
|
+
SQL["Processed SQL"]
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
QI --> V
|
|
534
|
+
P --> V
|
|
535
|
+
V --> T
|
|
536
|
+
T --> R
|
|
537
|
+
R --> SQL
|
|
760
538
|
```
|
|
761
539
|
|
|
762
|
-
|
|
540
|
+
- Validates required parameters, applies defaults, and performs type conversion (string, number, date, boolean, array)
|
|
541
|
+
- Renders parameterized SQL using Nunjucks with custom SQL-safe filters registered through `RunQuerySQLFilterManager`
|
|
542
|
+
- Rejects unknown parameters to prevent injection
|
|
763
543
|
|
|
764
|
-
##
|
|
544
|
+
## Configuration Reference
|
|
765
545
|
|
|
766
|
-
###
|
|
546
|
+
### SQLServerProviderConfigData
|
|
767
547
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
548
|
+
| Parameter | Type | Default | Description |
|
|
549
|
+
|-----------|------|---------|-------------|
|
|
550
|
+
| `connectionPool` | `sql.ConnectionPool` | required | Connected mssql connection pool |
|
|
551
|
+
| `MJCoreSchemaName` | `string` | `'__mj'` | Database schema for MJ core tables |
|
|
552
|
+
| `checkRefreshIntervalSeconds` | `number` | `0` | Interval for automatic metadata refresh (0 = disabled) |
|
|
553
|
+
| `includeSchemas` | `string[]` | `undefined` | Restrict metadata loading to these schemas |
|
|
554
|
+
| `excludeSchemas` | `string[]` | `undefined` | Exclude these schemas from metadata loading |
|
|
555
|
+
| `ignoreExistingMetadata` | `boolean` | `true` | Force full metadata reload; set `false` for per-request instances to reuse cache |
|
|
772
556
|
|
|
773
|
-
|
|
774
|
-
- Ensure correct username/password or Windows authentication
|
|
775
|
-
- Verify user has appropriate database permissions
|
|
776
|
-
- Check if encryption settings match server requirements
|
|
557
|
+
### Connection Pool Settings
|
|
777
558
|
|
|
778
|
-
|
|
779
|
-
- Verify `MJCoreSchemaName` matches your database schema (default: `__mj`)
|
|
780
|
-
- Ensure user has access to the schema
|
|
781
|
-
- Check if MemberJunction tables are properly installed
|
|
559
|
+
Configure via `mj.config.cjs` at the repository root:
|
|
782
560
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
561
|
+
```javascript
|
|
562
|
+
module.exports = {
|
|
563
|
+
databaseSettings: {
|
|
564
|
+
connectionPool: {
|
|
565
|
+
max: 50, // Maximum connections
|
|
566
|
+
min: 5, // Minimum connections
|
|
567
|
+
idleTimeoutMillis: 30000, // Idle timeout in ms
|
|
568
|
+
acquireTimeoutMillis: 30000, // Acquire timeout in ms
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
```
|
|
787
573
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
574
|
+
**Recommended pool sizes:**
|
|
575
|
+
|
|
576
|
+
| Environment | max | min | Notes |
|
|
577
|
+
|-------------|-----|-----|-------|
|
|
578
|
+
| Development | 10 | 2 | Low concurrency |
|
|
579
|
+
| Production Standard | 50 | 5 | 2-4x CPU cores of API server |
|
|
580
|
+
| Production High Load | 100 | 10 | Monitor SQL Server RESOURCE_SEMAPHORE and THREADPOOL wait types |
|
|
581
|
+
|
|
582
|
+
### SqlLoggingOptions
|
|
583
|
+
|
|
584
|
+
| Option | Type | Default | Description |
|
|
585
|
+
|--------|------|---------|-------------|
|
|
586
|
+
| `formatAsMigration` | `boolean` | `false` | Replace schema names with Flyway `${flyway:defaultSchema}` placeholders |
|
|
587
|
+
| `defaultSchemaName` | `string` | MJ core schema | Schema name to replace with Flyway placeholder |
|
|
588
|
+
| `description` | `string` | `undefined` | Comment written at the start of the log file |
|
|
589
|
+
| `statementTypes` | `'queries' \| 'mutations' \| 'both'` | `'both'` | Which statement types to log |
|
|
590
|
+
| `batchSeparator` | `string` | `undefined` | Separator emitted after each statement (e.g., `'GO'`) |
|
|
591
|
+
| `prettyPrint` | `boolean` | `false` | Format SQL using sql-formatter with T-SQL dialect |
|
|
592
|
+
| `logRecordChangeMetadata` | `boolean` | `false` | Log full change-tracking wrapper SQL vs. core SP calls only |
|
|
593
|
+
| `retainEmptyLogFiles` | `boolean` | `false` | Keep log files that contain zero statements |
|
|
594
|
+
| `filterByUserId` | `string` | `undefined` | Only log SQL executed by this user |
|
|
595
|
+
| `sessionName` | `string` | `undefined` | Friendly name for UI display |
|
|
596
|
+
| `verboseOutput` | `boolean` | `false` | Output debug information to console |
|
|
597
|
+
| `filterPatterns` | `(string \| RegExp)[]` | `undefined` | Patterns for filtering SQL statements |
|
|
598
|
+
| `filterType` | `'include' \| 'exclude'` | `'exclude'` | How `filterPatterns` are applied |
|
|
599
|
+
|
|
600
|
+
## Build and Development
|
|
792
601
|
|
|
793
|
-
|
|
602
|
+
```bash
|
|
603
|
+
# Build the package
|
|
604
|
+
cd packages/SQLServerDataProvider && npm run build
|
|
794
605
|
|
|
795
|
-
|
|
606
|
+
# Run tests
|
|
607
|
+
cd packages/SQLServerDataProvider && npm test
|
|
796
608
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
export MJ_LOG_SQL=true
|
|
609
|
+
# Run tests with coverage
|
|
610
|
+
cd packages/SQLServerDataProvider && npm run test:coverage
|
|
800
611
|
|
|
801
|
-
#
|
|
802
|
-
|
|
612
|
+
# Run tests in watch mode
|
|
613
|
+
cd packages/SQLServerDataProvider && npm run test:watch
|
|
803
614
|
```
|
|
804
615
|
|
|
805
|
-
##
|
|
616
|
+
## Key Implementation Details
|
|
806
617
|
|
|
807
|
-
|
|
618
|
+
### Connection Pool Best Practices
|
|
808
619
|
|
|
809
|
-
|
|
620
|
+
The provider follows SQL Server connection pool best practices:
|
|
810
621
|
|
|
811
|
-
|
|
622
|
+
1. **Single shared pool** -- one `sql.ConnectionPool` is created at server startup and reused for the application's lifetime
|
|
623
|
+
2. **Fresh request per query** -- each `ExecuteSQL` call creates a new `sql.Request` from the pool, enabling safe parallel execution
|
|
624
|
+
3. **No pool close in handlers** -- the pool remains open; the caller is responsible for closing it on shutdown
|
|
625
|
+
4. **Configurable pool sizing** -- pool `max`/`min` are tunable through `mj.config.cjs`
|
|
812
626
|
|
|
813
|
-
|
|
627
|
+
### DateTime Offset Adjustment
|
|
814
628
|
|
|
815
|
-
|
|
629
|
+
The provider automatically detects whether the SQL Server + mssql driver combination produces incorrect DATETIMEOFFSET values. On first query, it runs a diagnostic test and caches the result. If adjustment is needed, all DATETIMEOFFSET fields are corrected during row processing.
|
|
816
630
|
|
|
817
|
-
|
|
631
|
+
### Metadata Refresh
|
|
818
632
|
|
|
819
|
-
|
|
633
|
+
When `checkRefreshIntervalSeconds > 0`, the provider periodically checks whether database metadata has changed (new entities, field modifications, etc.) and reloads if needed. The `RefreshIfNeeded()` method can also be called on demand.
|
|
820
634
|
|
|
821
|
-
|
|
635
|
+
### Multi-Instance Pattern
|
|
822
636
|
|
|
823
|
-
|
|
824
|
-
// In your mj.config.cjs at the root level
|
|
825
|
-
module.exports = {
|
|
826
|
-
databaseSettings: {
|
|
827
|
-
connectionPool: {
|
|
828
|
-
max: 50, // Maximum connections (default: 50)
|
|
829
|
-
min: 5, // Minimum connections (default: 5)
|
|
830
|
-
idleTimeoutMillis: 30000, // Idle timeout (default: 30s)
|
|
831
|
-
acquireTimeoutMillis: 30000 // Acquire timeout (default: 30s)
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
|
-
```
|
|
637
|
+
In server environments like MJAPI, a new `SQLServerDataProvider` instance is created per request. Setting `ignoreExistingMetadata: false` on subsequent instances allows them to reuse the metadata loaded by the first instance, avoiding redundant database queries.
|
|
836
638
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
MemberJunction follows SQL Server connection best practices:
|
|
840
|
-
|
|
841
|
-
#### ✅ What We Do Right
|
|
639
|
+
## Troubleshooting
|
|
842
640
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
641
|
+
| Symptom | Likely Cause | Solution |
|
|
642
|
+
|---------|-------------|----------|
|
|
643
|
+
| Connection timeout | Network or firewall issue | Increase `connectionTimeout`; verify SQL Server firewall rules |
|
|
644
|
+
| Authentication failure | Wrong credentials or permissions | Verify credentials; check encryption settings match server |
|
|
645
|
+
| Schema not found | Wrong `MJCoreSchemaName` | Verify schema exists (default is `__mj`); check user schema access |
|
|
646
|
+
| Transaction rollback | Constraint violation in entity save | Check required fields, foreign key references, unique constraints |
|
|
647
|
+
| Pool exhausted | Too many concurrent connections | Increase `pool.max`; check for leaked connections or long-running queries |
|
|
648
|
+
| EREQINPROG error | Request reuse during transaction | This is handled automatically; the provider clears stale transaction references |
|
|
848
649
|
|
|
849
|
-
|
|
650
|
+
## IS-A Type Relationship Transaction Support
|
|
850
651
|
|
|
851
|
-
|
|
852
|
-
2. We don't open/close the pool repeatedly
|
|
853
|
-
3. We don't share Request objects between queries
|
|
854
|
-
4. We don't use hardcoded pool limits
|
|
652
|
+
MemberJunction supports IS-A type relationships where child entities inherit from parent entities (e.g., `MeetingEntity IS-A ProductEntity`). The SQLServerDataProvider manages SQL transactions to ensure atomic save and delete operations across the entire entity hierarchy.
|
|
855
653
|
|
|
856
|
-
###
|
|
654
|
+
### How IS-A Transactions Work
|
|
857
655
|
|
|
858
|
-
|
|
656
|
+
When you save or delete an entity that participates in an IS-A hierarchy, SQLServerDataProvider automatically:
|
|
859
657
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
acquireTimeoutMillis: 15000
|
|
867
|
-
}
|
|
868
|
-
```
|
|
658
|
+
1. **Creates a SQL Transaction**: The initiating (leaf) entity calls `BeginISATransaction()` to create a new `sql.Transaction` on the connection pool
|
|
659
|
+
2. **Propagates the Transaction**: The transaction is stored in `BaseEntity.ProviderTransaction` and shared across all entities in the parent chain
|
|
660
|
+
3. **Executes Operations in Order**:
|
|
661
|
+
- For **saves**: Parent entities are saved first, then the child entity uses the parent's ID
|
|
662
|
+
- For **deletes**: The child entity is deleted first, then parents are deleted in reverse order
|
|
663
|
+
4. **Commits or Rolls Back**: `CommitISATransaction()` commits all changes, or `RollbackISATransaction()` reverts everything on failure
|
|
869
664
|
|
|
870
|
-
|
|
871
|
-
```javascript
|
|
872
|
-
connectionPool: {
|
|
873
|
-
max: 50, // 2-4× CPU cores of your API server
|
|
874
|
-
min: 5,
|
|
875
|
-
idleTimeoutMillis: 30000,
|
|
876
|
-
acquireTimeoutMillis: 30000
|
|
877
|
-
}
|
|
878
|
-
```
|
|
665
|
+
### Transaction Lifecycle
|
|
879
666
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
667
|
+
```typescript
|
|
668
|
+
// Example: Saving a MeetingEntity (which IS-A ProductEntity)
|
|
669
|
+
const meeting = await md.GetEntityObject<MeetingEntity>('Meetings');
|
|
670
|
+
meeting.Name = 'Project Planning';
|
|
671
|
+
meeting.MeetingDate = new Date();
|
|
672
|
+
// ... set other fields
|
|
673
|
+
|
|
674
|
+
// When you call Save(), the provider automatically:
|
|
675
|
+
// 1. Begins a SQL transaction
|
|
676
|
+
// 2. Saves the Product parent entity first
|
|
677
|
+
// 3. Uses the Product ID to save the Meeting child entity
|
|
678
|
+
// 4. Commits the transaction
|
|
679
|
+
const result = await meeting.Save();
|
|
680
|
+
|
|
681
|
+
// If any step fails, the entire transaction is rolled back
|
|
888
682
|
```
|
|
889
683
|
|
|
890
|
-
###
|
|
684
|
+
### Key Methods
|
|
891
685
|
|
|
892
|
-
|
|
686
|
+
- `BeginISATransaction()`: Creates a new `sql.Transaction` on the connection pool and stores it in `BaseEntity.ProviderTransaction`
|
|
687
|
+
- `CommitISATransaction()`: Commits the shared transaction across the entire IS-A chain
|
|
688
|
+
- `RollbackISATransaction()`: Rolls back all changes if any operation in the chain fails
|
|
893
689
|
|
|
894
|
-
|
|
690
|
+
### Benefits
|
|
895
691
|
|
|
896
|
-
|
|
692
|
+
- **Atomicity**: All saves/deletes in the hierarchy succeed or fail together
|
|
693
|
+
- **Consistency**: No orphaned child records or missing parent data
|
|
694
|
+
- **Transparent**: The transaction management is automatic - no manual transaction handling required
|
|
695
|
+
- **Shared State**: All entities in the chain use the same `sql.Transaction` instance via `BaseEntity.ProviderTransaction`
|
|
897
696
|
|
|
898
|
-
|
|
899
|
-
- `RESOURCE_SEMAPHORE`: Memory pressure
|
|
900
|
-
- `THREADPOOL`: Worker thread exhaustion
|
|
697
|
+
For more details on IS-A relationships and how they work across MemberJunction, see [MJCore IS-A Relationships Documentation](../../MJCore/docs/isa-relationships.md).
|
|
901
698
|
|
|
902
|
-
|
|
699
|
+
## Virtual Entity Support
|
|
903
700
|
|
|
904
|
-
|
|
701
|
+
MemberJunction supports virtual entities that are backed by SQL views instead of physical tables. Virtual entities provide read-only access to data and are commonly used for reporting, aggregations, and denormalized views.
|
|
905
702
|
|
|
906
|
-
|
|
907
|
-
2. **Check for Leaks**: Ensure all queries complete properly
|
|
908
|
-
3. **Monitor Long Queries**: Identify and optimize slow queries that hold connections
|
|
909
|
-
4. **Review Concurrent Load**: Ensure pool size matches your peak concurrency needs
|
|
703
|
+
### Read Operations
|
|
910
704
|
|
|
911
|
-
|
|
705
|
+
Virtual entities work seamlessly with the SQLServerDataProvider for all read operations:
|
|
912
706
|
|
|
913
|
-
|
|
707
|
+
- **RunView**: Execute queries against the virtual entity's underlying SQL view
|
|
708
|
+
- **Get**: Load individual records by primary key (if the view supports it)
|
|
709
|
+
- **Filtering, Sorting, Pagination**: All standard query operations work as expected
|
|
914
710
|
|
|
915
711
|
```typescript
|
|
916
|
-
|
|
917
|
-
|
|
712
|
+
// Example: Querying a virtual entity backed by a view
|
|
713
|
+
const rv = new RunView();
|
|
714
|
+
const result = await rv.RunView<UserSummaryEntity>({
|
|
715
|
+
EntityName: 'User Summary', // Virtual entity backed by vwUserSummary
|
|
716
|
+
ExtraFilter: "Department = 'Engineering'",
|
|
717
|
+
OrderBy: 'LastLoginDate DESC',
|
|
718
|
+
ResultType: 'entity_object'
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// result.Results contains fully-typed UserSummaryEntity objects
|
|
722
|
+
const users = result.Results;
|
|
918
723
|
```
|
|
919
724
|
|
|
920
|
-
|
|
725
|
+
### Write Operations
|
|
921
726
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
727
|
+
Write operations (Save, Delete) are **automatically blocked** for virtual entities at the BaseEntity level before they reach the data provider:
|
|
728
|
+
|
|
729
|
+
- **BaseEntity.Save()**: Returns an error if called on a virtual entity
|
|
730
|
+
- **BaseEntity.Delete()**: Returns an error if called on a virtual entity
|
|
731
|
+
- **Why**: Virtual entities represent read-only views and cannot be modified directly
|
|
926
732
|
|
|
927
|
-
|
|
928
|
-
If you are using `SQLServerDataProvider` outside of the context of MJServer/MJAPI it is your responsibility to create connection pool in alignment with whatever practices make sense for your project and pass that along to the SQLServerDataProvider configuration process.
|
|
733
|
+
### Use Cases for Virtual Entities
|
|
929
734
|
|
|
930
|
-
|
|
735
|
+
- **Aggregated Data**: Summary views that combine data from multiple tables
|
|
736
|
+
- **Denormalized Views**: Flattened representations of complex relationships
|
|
737
|
+
- **Calculated Fields**: Views that include computed columns or transformations
|
|
738
|
+
- **Security Views**: Row-level filtering applied at the database view level
|
|
739
|
+
- **Reporting**: Pre-joined data optimized for reporting queries
|
|
740
|
+
|
|
741
|
+
For comprehensive documentation on virtual entities, their configuration, and advanced usage patterns, see [MJCore Virtual Entities Documentation](../../MJCore/docs/virtual-entities.md).
|
|
742
|
+
|
|
743
|
+
## License
|
|
744
|
+
|
|
745
|
+
ISC
|