@memberjunction/sqlserver-dataprovider 4.0.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 CHANGED
@@ -1,29 +1,78 @@
1
1
  # MemberJunction SQL Server Data Provider
2
2
 
3
- A robust SQL Server data provider implementation for MemberJunction applications, providing seamless database connectivity, query execution, and entity management.
4
-
5
- ## Overview
6
-
7
- The `@memberjunction/sqlserver-dataprovider` package implements MemberJunction's data provider interface specifically for Microsoft SQL Server databases. It serves as the bridge between your MemberJunction application and SQL Server, handling data access, entity operations, view execution, and more.
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**: Complete Create, Read, Update, Delete operations for all entities
12
- - **Transaction Support**: Manage atomic operations with transaction groups
13
- - **View Execution**: Run database views with filtering, sorting, and pagination
14
- - **Report Generation**: Execute reports with parameters
15
- - **Query Execution**: Run raw SQL queries with parameter support
16
- - **Connection Pooling**: Efficient database connection management
17
- - **Entity Relationships**: Handle complex entity relationships automatically
18
- - **User/Role Management**: Integrated with MemberJunction's security model
19
- - **Type-Safe Operations**: Fully TypeScript compatible
20
- - **AI Integration**: Support for AI-powered features through entity actions
21
- - **Duplicate Detection**: Built-in support for duplicate record detection
22
- - **Audit Logging**: Comprehensive audit trail capabilities
23
- - **Row-Level Security**: Enforce data access controls at the database level
24
- - **SQL Logging**: Real-time SQL statement capture for debugging and migration generation
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
- This package relies on the following key dependencies:
37
- - `@memberjunction/core`: Core MemberJunction functionality
38
- - `@memberjunction/core-entities`: Entity definitions
39
- - `@memberjunction/global`: Shared utilities and constants
40
- - `@memberjunction/actions`: Action execution framework
41
- - `@memberjunction/ai`: AI integration capabilities
42
- - `@memberjunction/ai-vector-dupe`: Duplicate detection using AI vectors
43
- - `@memberjunction/aiengine`: AI engine integration
44
- - `@memberjunction/queue`: Queue management for async operations
45
- - `mssql`: SQL Server client for Node.js (v11+)
46
- - `typeorm`: ORM for database operations (v0.3+)
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
- ### Basic Setup
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 { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
54
- import { ConfigHelper } from '@memberjunction/global';
160
+ import { setupSQLServerClient } from '@memberjunction/sqlserver-dataprovider';
161
+ import { SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
162
+ import sql from 'mssql';
55
163
 
56
- // Configure database connection
57
- const config = {
58
- host: 'your-server.database.windows.net',
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
- // Create data provider instance
70
- const dataProvider = new SQLServerDataProvider(config);
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
- // Initialize the data provider (connects to the database)
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 { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
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
- const userEntity = md.EntityByName('User');
99
-
100
- // Load an entity by ID
101
- const userKey = new CompositeKey([{ FieldName: 'ID', Value: 1 }]);
102
- const userResult = await dataProvider.Get(userEntity, userKey);
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 newUserEntity = await md.GetEntityObject<UserEntity>('User');
119
- newUserEntity.FirstName = 'John';
120
- newUserEntity.LastName = 'Doe';
121
- newUserEntity.Email = 'john.doe@example.com';
122
- // set other required fields...
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
- const deleteKey = new CompositeKey([{ FieldName: 'ID', Value: 5 }]);
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 Management
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
- #### Transaction Groups
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
- // Setup data provider
149
- const dataProvider = new SQLServerDataProvider(/* config */);
150
- await dataProvider.initialize();
230
+ const transaction = await provider.CreateTransactionGroup();
151
231
 
152
- // Create a transaction group
153
- const transaction = new SQLServerTransactionGroup('CreateOrderWithItems');
232
+ const order = await md.GetEntityObject('Orders', contextUser);
233
+ order.CustomerID = customerId;
234
+ order.Status = 'New';
235
+ order.TransactionGroup = transaction;
154
236
 
155
- // Get entity objects
156
- const md = new Metadata();
157
- const orderEntity = await md.GetEntityObject('Order');
158
- const orderItemEntity1 = await md.GetEntityObject('Order Item');
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
- // Check results
188
- const success = results.every(r => r.Success);
189
- if (success) {
190
- console.log('Transaction completed successfully');
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
- #### Instance-Level Transactions (Multi-User Environments)
248
+ ### Instance-Level Transactions
202
249
 
203
- In multi-user server environments like MJServer, each request gets its own SQLServerDataProvider instance with isolated transaction state. This provides automatic transaction isolation without requiring transaction scope IDs:
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
- // Begin a transaction on this instance
212
- await dataProvider.BeginTransaction();
213
-
214
- // Perform operations - all use this instance's transaction
215
- await dataProvider.Save(entity1, contextUser);
216
- await dataProvider.Save(entity2, contextUser);
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
- // Rollback on error
225
- await dataProvider.RollbackTransaction();
261
+ await provider.RollbackTransaction();
226
262
  throw error;
227
263
  }
228
264
  ```
229
265
 
230
- **Key Features of Instance-Level Transactions:**
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 { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
247
- import { RunViewParams, RunReportParams } from '@memberjunction/core';
248
-
249
- // Setup data provider
250
- const dataProvider = new SQLServerDataProvider(/* config */);
251
- await dataProvider.initialize();
252
-
253
- // Run a view with filtering and pagination
254
- const viewOptions: RunViewParams = {
255
- EntityName: 'vwActiveUsers',
256
- ExtraFilter: "Role = 'Administrator'",
257
- OrderBy: 'LastName, FirstName',
258
- PageSize: 10,
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
- // Run a report
274
- const reportParams: RunReportParams = {
275
- ReportID: 'report-id-here',
276
- // Other parameters as needed
277
- };
285
+ ### Running Parameterized Queries
278
286
 
279
- const reportResult = await dataProvider.RunReport(reportParams);
287
+ The `QueryParameterProcessor` validates parameters and processes Nunjucks templates for parameterized queries.
280
288
 
281
- if (reportResult.Success) {
282
- console.log('Report data:', reportResult.Results);
283
- console.log('Row count:', reportResult.RowCount);
284
- console.log('Execution time:', reportResult.ExecutionTime, 'ms');
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 Queries
307
+ ### Executing Raw SQL
289
308
 
290
309
  ```typescript
291
- import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
292
- import { RunQueryParams } from '@memberjunction/core';
293
-
294
- // Setup data provider
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
- console.log(`Query returned ${sqlResult.length} rows`);
308
- sqlResult.forEach(row => {
309
- console.log(row);
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
- ### User Management and Caching
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
- // User caching is handled automatically by the provider
369
- // but you can clear the cache if needed
370
- dataProvider.clearUserCache();
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
- ## Configuration Options
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
- import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
399
-
400
- class CustomSQLProvider extends SQLServerDataProvider {
401
- // Override to add custom logging or modifications
402
- async ExecuteSQL(sql: string, params?: any, maxRows?: number): Promise<any> {
403
- console.log(`Executing SQL: ${sql}`);
404
- console.log('Parameters:', params);
405
-
406
- // Add timing
407
- const startTime = Date.now();
408
- const result = await super.ExecuteSQL(sql, params, maxRows);
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
- const result = await dataProvider.Save(entity, user);
434
- if (!result.Success) {
435
- console.error('Save failed:', result.ErrorMessage);
436
- // Handle validation or business logic errors
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
- ## Build & Development
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
- ### Programmatic Usage
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
- import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
568
-
569
- const dataProvider = new SQLServerDataProvider(/* config */);
570
- await dataProvider.initialize();
571
-
572
- // Create a SQL logging session
573
- const logger = await dataProvider.createSqlLogger('./logs/sql/operations.sql', {
574
- formatAsMigration: false,
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
- SQL logging sessions support pattern-based filtering to include or exclude specific SQL statements. You can use either **regex patterns** for advanced matching or **simple wildcard patterns** for ease of use.
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
- 2. **Regular Expression Patterns** (For advanced users):
610
- - Full regex support with flags
611
- - More powerful but requires regex knowledge
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
- #### Exclude Mode (Default)
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 specific patterns from logging
621
- const logger = await dataProvider.createSqlLogger('./logs/sql/filtered.sql', {
622
- sessionName: 'Production Operations',
414
+ // Exclude noisy patterns
415
+ const session = await provider.CreateSqlLogger('./logs/filtered.sql', {
623
416
  filterPatterns: [
624
- /spCreateAIPromptRun/i, // Regex: Exclude AI prompt runs
625
- /spUpdateAIPromptRun/i, // Regex: Exclude AI prompt updates
626
- /^SELECT.*FROM.*vw.*Metadata/i, // Regex: Exclude metadata view queries
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' // Default - exclude matching patterns
421
+ filterType: 'exclude', // Default: skip matching statements
630
422
  });
631
- ```
632
-
633
- #### Include Mode
634
423
 
635
- ```typescript
636
- // Only log specific patterns
637
- const auditLogger = await dataProvider.createSqlLogger('./logs/sql/audit.sql', {
638
- sessionName: 'User Audit Trail',
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
- #### Using with MetadataSync
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
- > **Note**: Filtering is applied to the actual SQL that will be logged. If `logRecordChangeMetadata` is false and a simplified SQL fallback is provided, the filtering tests against the simplified version.
433
+ ```typescript
434
+ // List all active sessions
435
+ const active = provider.GetActiveSqlLoggingSessions();
436
+ console.log(`${active.length} sessions active`);
676
437
 
677
- ### Runtime Control via GraphQL
438
+ // Get a specific session
439
+ const session = provider.GetSqlLoggingSessionById(sessionId);
678
440
 
679
- ```typescript
680
- // Start a new logging session
681
- const mutation = `
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
- // Stop a session
714
- const stopMutation = `
715
- mutation {
716
- stopSqlLogging(sessionId: "session-id-here")
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
- ### UI Integration
483
+ The provider supports two transaction mechanisms:
722
484
 
723
- SQL logging can be controlled through the MemberJunction Explorer UI:
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
- 1. **Settings Panel**: Navigate to Settings > SQL Logging (Owner access required)
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
- ### Migration-Ready Format
489
+ ## UserCache
732
490
 
733
- ```typescript
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
- // Your operations are logged in Flyway-compatible format
743
- // with proper headers and schema placeholders
744
- ```
493
+ ```typescript
494
+ import { UserCache } from '@memberjunction/sqlserver-dataprovider';
745
495
 
746
- ### Session Management Methods
496
+ // Access the singleton
497
+ const cache = UserCache.Instance;
747
498
 
748
- ```typescript
749
- // Get all active sessions
750
- const activeSessions = dataProvider.getActiveSqlLoggingSessions();
751
- console.log(`${activeSessions.length} sessions currently active`);
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
- // Dispose all sessions
754
- await dataProvider.disposeAllSqlLoggingSessions();
504
+ // Refresh from database (with optional auto-refresh interval in ms)
505
+ await cache.Refresh(pool, 60000);
506
+ ```
755
507
 
756
- // Check if logging is enabled
757
- if (dataProvider.isSqlLoggingEnabled()) {
758
- console.log('SQL logging is available');
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
- > **Security Note**: SQL logging requires Owner-level privileges in the MemberJunction system. Only users with `Type = 'Owner'` can create, manage, or access SQL logging sessions.
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
- ## Troubleshooting
544
+ ## Configuration Reference
765
545
 
766
- ### Common Issues
546
+ ### SQLServerProviderConfigData
767
547
 
768
- 1. **Connection Timeout Errors**
769
- - Increase `connectionTimeout` and `requestTimeout` in configuration
770
- - Verify network connectivity to SQL Server
771
- - Check SQL Server firewall rules
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
- 2. **Authentication Failures**
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
- 3. **Schema Not Found**
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
- 4. **Transaction Rollback Issues**
784
- - Check for constraint violations in related entities
785
- - Verify all required fields are populated
786
- - Review transaction logs for specific error details
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
- 5. **Performance Issues**
789
- - Adjust connection pool settings (`pool.max`, `pool.min`)
790
- - Enable query logging to identify slow queries
791
- - Consider adding database indexes for frequently queried fields
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
- ### Debug Logging
602
+ ```bash
603
+ # Build the package
604
+ cd packages/SQLServerDataProvider && npm run build
794
605
 
795
- Enable detailed logging by setting environment variables:
606
+ # Run tests
607
+ cd packages/SQLServerDataProvider && npm test
796
608
 
797
- ```bash
798
- # Enable SQL query logging
799
- export MJ_LOG_SQL=true
609
+ # Run tests with coverage
610
+ cd packages/SQLServerDataProvider && npm run test:coverage
800
611
 
801
- # Enable detailed error logging
802
- export MJ_LOG_LEVEL=debug
612
+ # Run tests in watch mode
613
+ cd packages/SQLServerDataProvider && npm run test:watch
803
614
  ```
804
615
 
805
- ## License
616
+ ## Key Implementation Details
806
617
 
807
- ISC
618
+ ### Connection Pool Best Practices
808
619
 
809
- ## SQL Server Connection Pooling and Best Practices
620
+ The provider follows SQL Server connection pool best practices:
810
621
 
811
- ### Overview
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
- The MemberJunction SQL Server Data Provider is designed to support high-performance parallel database operations through proper connection pool management. The underlying `mssql` driver (node-mssql) is expressly designed to handle many concurrent database calls efficiently.
627
+ ### DateTime Offset Adjustment
814
628
 
815
- ### How MemberJunction Handles Parallelism
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
- 1. **Single Shared Connection Pool**: MemberJunction creates one connection pool at server startup and reuses it throughout the application lifecycle. This pool is passed to the SQLServerDataProvider and used for all database operations.
631
+ ### Metadata Refresh
818
632
 
819
- 2. **Request-Per-Query Pattern**: Each database operation creates a new `sql.Request` object from the shared pool, allowing multiple queries to execute in parallel without blocking each other.
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
- 3. **Configurable Pool Size**: The connection pool can be configured via `mj.config.cjs` to support your specific concurrency needs:
635
+ ### Multi-Instance Pattern
822
636
 
823
- ```javascript
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
- ### Best Practices Implementation
838
-
839
- MemberJunction follows SQL Server connection best practices:
840
-
841
- #### ✅ What We Do Right
639
+ ## Troubleshooting
842
640
 
843
- 1. **Create Pool Once**: The pool is created during server initialization and never recreated
844
- 2. **Never Close Pool in Handlers**: The pool remains open for the server's lifetime
845
- 3. **Fresh Request Per Query**: Each query gets its own `sql.Request` object
846
- 4. **Proper Error Handling**: Connection failures are caught and logged appropriately
847
- 5. **Read-Only Pool Option**: Separate pool for read operations if configured
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
- #### ❌ Anti-Patterns We Avoid
650
+ ## IS-A Type Relationship Transaction Support
850
651
 
851
- 1. We don't create new connections for each request
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
- ### Recommended Pool Settings
654
+ ### How IS-A Transactions Work
857
655
 
858
- Based on your environment and load:
656
+ When you save or delete an entity that participates in an IS-A hierarchy, SQLServerDataProvider automatically:
859
657
 
860
- #### Development Environment
861
- ```javascript
862
- connectionPool: {
863
- max: 10,
864
- min: 2,
865
- idleTimeoutMillis: 60000,
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
- #### Production - Standard Load
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
- #### Production - High Load
881
- ```javascript
882
- connectionPool: {
883
- max: 100, // Monitor SQL Server wait types to tune
884
- min: 10,
885
- idleTimeoutMillis: 30000,
886
- acquireTimeoutMillis: 30000
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
- ### Performance Considerations
684
+ ### Key Methods
891
685
 
892
- 1. **Pool Size**: The practical concurrency limit equals your pool size. With default settings (max: 50), you can have up to 50 concurrent SQL operations.
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
- 2. **Connection Reuse**: The mssql driver efficiently reuses connections from the pool, minimizing connection overhead.
690
+ ### Benefits
895
691
 
896
- 3. **Queue Management**: When all connections are busy, additional requests queue in FIFO order until a connection becomes available.
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
- 4. **Monitoring**: Watch for these SQL Server wait types to identify if pool size is too large:
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
- ### Troubleshooting Connection Pool Issues
699
+ ## Virtual Entity Support
903
700
 
904
- If you experience "connection pool exhausted" errors:
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
- 1. **Increase Pool Size**: Adjust `max` in your configuration
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
- ### Technical Implementation Details
705
+ Virtual entities work seamlessly with the SQLServerDataProvider for all read operations:
912
706
 
913
- The connection pool is created in `/packages/MJServer/src/index.ts`:
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
- const pool = new sql.ConnectionPool(createMSSQLConfig());
917
- await pool.connect();
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
- And used in SQLServerDataProvider for each query:
725
+ ### Write Operations
921
726
 
922
- ```typescript
923
- const request = new sql.Request(this._pool);
924
- const result = await request.query(sql);
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
- #### **Important**
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
- This pattern ensures maximum parallelism while maintaining connection efficiency, allowing MemberJunction applications to scale to handle hundreds of concurrent database operations without blocking the Node.js event loop.
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