@memberjunction/sqlserver-dataprovider 2.43.0 → 2.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +261 -79
  2. package/package.json +9 -9
package/README.md CHANGED
@@ -17,6 +17,10 @@ The `@memberjunction/sqlserver-dataprovider` package implements MemberJunction's
17
17
  - **Entity Relationships**: Handle complex entity relationships automatically
18
18
  - **User/Role Management**: Integrated with MemberJunction's security model
19
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
20
24
 
21
25
  ## Installation
22
26
 
@@ -30,8 +34,13 @@ This package relies on the following key dependencies:
30
34
  - `@memberjunction/core`: Core MemberJunction functionality
31
35
  - `@memberjunction/core-entities`: Entity definitions
32
36
  - `@memberjunction/global`: Shared utilities and constants
33
- - `mssql`: SQL Server client for Node.js
34
- - `typeorm`: ORM for database operations
37
+ - `@memberjunction/actions`: Action execution framework
38
+ - `@memberjunction/ai`: AI integration capabilities
39
+ - `@memberjunction/ai-vector-dupe`: Duplicate detection using AI vectors
40
+ - `@memberjunction/aiengine`: AI engine integration
41
+ - `@memberjunction/queue`: Queue management for async operations
42
+ - `mssql`: SQL Server client for Node.js (v11+)
43
+ - `typeorm`: ORM for database operations (v0.3+)
35
44
 
36
45
  ## Usage
37
46
 
@@ -74,44 +83,50 @@ await dataProvider.initialize();
74
83
 
75
84
  ```typescript
76
85
  import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
77
- import { EntityInfo } from '@memberjunction/core';
86
+ import { Metadata, CompositeKey, UserInfo } from '@memberjunction/core';
87
+ import { UserEntity } from '@memberjunction/core-entities';
78
88
 
79
89
  // Setup data provider
80
90
  const dataProvider = new SQLServerDataProvider(/* config */);
81
91
  await dataProvider.initialize();
82
92
 
83
- // Load an entity
84
- const userResult = await dataProvider.loadEntity('User', 1);
85
- if (userResult.success) {
86
- const user = userResult.entity;
93
+ // Get entity metadata
94
+ const md = new Metadata();
95
+ const userEntity = md.EntityByName('User');
96
+
97
+ // Load an entity by ID
98
+ const userKey = new CompositeKey([{ FieldName: 'ID', Value: 1 }]);
99
+ const userResult = await dataProvider.Get(userEntity, userKey);
100
+
101
+ if (userResult.Success) {
102
+ const user = userResult.Entity;
87
103
  console.log(`Loaded user: ${user.FirstName} ${user.LastName}`);
88
104
 
89
105
  // Update the entity
90
106
  user.Email = 'new.email@example.com';
91
- const saveResult = await dataProvider.saveEntity('User', user);
107
+ const saveResult = await dataProvider.Save(user, contextUser);
92
108
 
93
- if (saveResult.success) {
94
- console.log(`User updated successfully, ID: ${saveResult.entity.ID}`);
109
+ if (saveResult.Success) {
110
+ console.log(`User updated successfully, ID: ${saveResult.Entity.ID}`);
95
111
  }
96
112
  }
97
113
 
98
114
  // Create a new entity
99
- const newUser = {
100
- ID: 0, // 0 indicates a new entity
101
- FirstName: 'John',
102
- LastName: 'Doe',
103
- Email: 'john.doe@example.com',
104
- // other required fields...
105
- };
106
-
107
- const createResult = await dataProvider.saveEntity('User', newUser);
108
- if (createResult.success) {
109
- console.log(`New user created with ID: ${createResult.entity.ID}`);
115
+ const newUserEntity = await md.GetEntityObject<UserEntity>('User');
116
+ newUserEntity.FirstName = 'John';
117
+ newUserEntity.LastName = 'Doe';
118
+ newUserEntity.Email = 'john.doe@example.com';
119
+ // set other required fields...
120
+
121
+ const createResult = await dataProvider.Save(newUserEntity, contextUser);
122
+ if (createResult.Success) {
123
+ console.log(`New user created with ID: ${createResult.Entity.ID}`);
110
124
  }
111
125
 
112
126
  // Delete an entity
113
- const deleteResult = await dataProvider.deleteEntity('User', 5);
114
- if (deleteResult.success) {
127
+ const deleteKey = new CompositeKey([{ FieldName: 'ID', Value: 5 }]);
128
+ const deleteResult = await dataProvider.Delete(userEntity, deleteKey, contextUser);
129
+ if (deleteResult.Success) {
115
130
  console.log('User deleted successfully');
116
131
  }
117
132
  ```
@@ -120,54 +135,59 @@ if (deleteResult.success) {
120
135
 
121
136
  ```typescript
122
137
  import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
123
- import { TransactionGroupBase } from '@memberjunction/core';
138
+ import { SQLServerTransactionGroup } from '@memberjunction/sqlserver-dataprovider';
139
+ import { Metadata } from '@memberjunction/core';
124
140
 
125
141
  // Setup data provider
126
142
  const dataProvider = new SQLServerDataProvider(/* config */);
127
143
  await dataProvider.initialize();
128
144
 
129
145
  // Create a transaction group
130
- class OrderTransactionGroup extends TransactionGroupBase {
131
- constructor() {
132
- super('CreateOrderWithItems');
133
- }
134
- }
146
+ const transaction = new SQLServerTransactionGroup('CreateOrderWithItems');
135
147
 
136
- const transaction = new OrderTransactionGroup();
148
+ // Get entity objects
149
+ const md = new Metadata();
150
+ const orderEntity = await md.GetEntityObject('Order');
151
+ const orderItemEntity1 = await md.GetEntityObject('Order Item');
152
+ const orderItemEntity2 = await md.GetEntityObject('Order Item');
137
153
 
138
- // Add multiple entities to the transaction
139
- transaction.addEntity('Order', {
140
- ID: 0,
141
- CustomerID: 123,
142
- OrderDate: new Date(),
143
- Status: 'New'
144
- });
154
+ // Set up the order
155
+ orderEntity.CustomerID = 123;
156
+ orderEntity.OrderDate = new Date();
157
+ orderEntity.Status = 'New';
145
158
 
146
- // Reference previous entities in the same transaction
147
- transaction.addEntity('OrderItem', {
148
- ID: 0,
149
- OrderID: '@Order.1', // Reference to the first Order in this transaction
150
- ProductID: 456,
151
- Quantity: 2,
152
- Price: 29.99
153
- });
159
+ // Add to transaction - this will get ID after save
160
+ await transaction.AddTransaction(orderEntity);
154
161
 
155
- transaction.addEntity('OrderItem', {
156
- ID: 0,
157
- OrderID: '@Order.1',
158
- ProductID: 789,
159
- Quantity: 1,
160
- Price: 49.99
161
- });
162
+ // Set up order items with references to the order
163
+ orderItemEntity1.OrderID = '@Order.1'; // Reference to the first Order in this transaction
164
+ orderItemEntity1.ProductID = 456;
165
+ orderItemEntity1.Quantity = 2;
166
+ orderItemEntity1.Price = 29.99;
167
+
168
+ orderItemEntity2.OrderID = '@Order.1'; // Same order reference
169
+ orderItemEntity2.ProductID = 789;
170
+ orderItemEntity2.Quantity = 1;
171
+ orderItemEntity2.Price = 49.99;
172
+
173
+ // Add items to transaction
174
+ await transaction.AddTransaction(orderItemEntity1);
175
+ await transaction.AddTransaction(orderItemEntity2);
162
176
 
163
177
  // Execute the transaction group
164
- const result = await dataProvider.executeTransactionGroup(transaction);
178
+ const results = await transaction.Submit();
165
179
 
166
- if (result.success) {
180
+ // Check results
181
+ const success = results.every(r => r.Success);
182
+ if (success) {
167
183
  console.log('Transaction completed successfully');
168
- console.log('Order ID:', result.results.find(r => r.entityName === 'Order')?.entity.ID);
184
+ const orderResult = results.find(r => r.Entity.EntityInfo.Name === 'Order');
185
+ console.log('Order ID:', orderResult?.Entity.ID);
169
186
  } else {
170
- console.error('Transaction failed:', result.error);
187
+ console.error('Transaction failed');
188
+ results.filter(r => !r.Success).forEach(r => {
189
+ console.error(`Failed: ${r.Entity.EntityInfo.Name}`, r.Message);
190
+ });
171
191
  }
172
192
  ```
173
193
 
@@ -175,14 +195,14 @@ if (result.success) {
175
195
 
176
196
  ```typescript
177
197
  import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
178
- import { RunViewOptions } from '@memberjunction/core';
198
+ import { RunViewParams, RunReportParams } from '@memberjunction/core';
179
199
 
180
200
  // Setup data provider
181
201
  const dataProvider = new SQLServerDataProvider(/* config */);
182
202
  await dataProvider.initialize();
183
203
 
184
204
  // Run a view with filtering and pagination
185
- const viewOptions: RunViewOptions = {
205
+ const viewOptions: RunViewParams = {
186
206
  EntityName: 'vwActiveUsers',
187
207
  ExtraFilter: "Role = 'Administrator'",
188
208
  OrderBy: 'LastName, FirstName',
@@ -190,7 +210,7 @@ const viewOptions: RunViewOptions = {
190
210
  PageNumber: 1
191
211
  };
192
212
 
193
- const viewResult = await dataProvider.runView(viewOptions);
213
+ const viewResult = await dataProvider.RunView(viewOptions);
194
214
 
195
215
  if (viewResult.success) {
196
216
  console.log(`Found ${viewResult.Results.length} users`);
@@ -202,14 +222,17 @@ if (viewResult.success) {
202
222
  }
203
223
 
204
224
  // Run a report
205
- const reportResult = await dataProvider.runReport('SalesReport', {
206
- StartDate: '2023-01-01',
207
- EndDate: '2023-12-31',
208
- Format: 'JSON'
209
- });
225
+ const reportParams: RunReportParams = {
226
+ ReportID: 'report-id-here',
227
+ // Other parameters as needed
228
+ };
229
+
230
+ const reportResult = await dataProvider.RunReport(reportParams);
210
231
 
211
- if (reportResult.success) {
212
- console.log('Report data:', reportResult.results);
232
+ if (reportResult.Success) {
233
+ console.log('Report data:', reportResult.Results);
234
+ console.log('Row count:', reportResult.RowCount);
235
+ console.log('Execution time:', reportResult.ExecutionTime, 'ms');
213
236
  }
214
237
  ```
215
238
 
@@ -217,13 +240,14 @@ if (reportResult.success) {
217
240
 
218
241
  ```typescript
219
242
  import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
243
+ import { RunQueryParams } from '@memberjunction/core';
220
244
 
221
245
  // Setup data provider
222
246
  const dataProvider = new SQLServerDataProvider(/* config */);
223
247
  await dataProvider.initialize();
224
248
 
225
- // Execute a parameterized query
226
- const queryResult = await dataProvider.executeQuery(
249
+ // Execute raw SQL with parameters
250
+ const sqlResult = await dataProvider.ExecuteSQL(
227
251
  'SELECT * FROM Users WHERE Department = @dept AND HireDate > @date',
228
252
  {
229
253
  dept: 'Engineering',
@@ -231,23 +255,33 @@ const queryResult = await dataProvider.executeQuery(
231
255
  }
232
256
  );
233
257
 
234
- if (queryResult.success) {
235
- console.log(`Query returned ${queryResult.results.length} rows`);
236
- queryResult.results.forEach(row => {
237
- console.log(row);
238
- });
239
- }
258
+ console.log(`Query returned ${sqlResult.length} rows`);
259
+ sqlResult.forEach(row => {
260
+ console.log(row);
261
+ });
240
262
 
241
263
  // Execute a stored procedure
242
- const spResult = await dataProvider.executeQuery(
264
+ const spResult = await dataProvider.ExecuteSQL(
243
265
  'EXEC sp_GetUserPermissions @UserID',
244
266
  {
245
267
  UserID: 123
246
268
  }
247
269
  );
248
270
 
249
- if (spResult.success) {
250
- console.log('User permissions:', spResult.results);
271
+ console.log('User permissions:', spResult);
272
+
273
+ // Using RunQuery for pre-defined queries
274
+ const queryParams: RunQueryParams = {
275
+ QueryID: 'query-id-here', // or use QueryName
276
+ // CategoryID: 'optional-category-id',
277
+ // CategoryName: 'optional-category-name'
278
+ };
279
+
280
+ const queryResult = await dataProvider.RunQuery(queryParams);
281
+
282
+ if (queryResult.Success) {
283
+ console.log('Query results:', queryResult.Results);
284
+ console.log('Execution time:', queryResult.ExecutionTime, 'ms');
251
285
  }
252
286
  ```
253
287
 
@@ -301,22 +335,170 @@ import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
301
335
 
302
336
  class CustomSQLProvider extends SQLServerDataProvider {
303
337
  // Override to add custom logging or modifications
304
- async executeQuery(sql: string, params?: any): Promise<any> {
338
+ async ExecuteSQL(sql: string, params?: any, maxRows?: number): Promise<any> {
305
339
  console.log(`Executing SQL: ${sql}`);
306
340
  console.log('Parameters:', params);
307
341
 
308
342
  // Add timing
309
343
  const startTime = Date.now();
310
- const result = await super.executeQuery(sql, params);
344
+ const result = await super.ExecuteSQL(sql, params, maxRows);
311
345
  const duration = Date.now() - startTime;
312
346
 
313
347
  console.log(`Query executed in ${duration}ms`);
348
+ console.log(`Rows returned: ${result?.length || 0}`);
314
349
 
315
350
  return result;
316
351
  }
352
+
353
+ // Custom error handling
354
+ protected async HandleExecuteSQLError(error: any, sql: string): Promise<void> {
355
+ console.error('SQL Error:', error);
356
+ console.error('Failed SQL:', sql);
357
+ // Add custom error handling logic here
358
+ await super.HandleExecuteSQLError(error, sql);
359
+ }
317
360
  }
318
361
  ```
319
362
 
363
+ ### Error Handling
364
+
365
+ The SQL Server Data Provider includes comprehensive error handling:
366
+
367
+ ```typescript
368
+ try {
369
+ const result = await dataProvider.Save(entity, user);
370
+ if (!result.Success) {
371
+ console.error('Save failed:', result.ErrorMessage);
372
+ // Handle validation or business logic errors
373
+ }
374
+ } catch (error) {
375
+ console.error('Unexpected error:', error);
376
+ // Handle system-level errors
377
+ }
378
+ ```
379
+
380
+ ## Build & Development
381
+
382
+ ### Building the Package
383
+
384
+ ```bash
385
+ # From the package directory
386
+ npm run build
387
+
388
+ # Or from the repository root
389
+ turbo build --filter="@memberjunction/sqlserver-dataprovider"
390
+ ```
391
+
392
+ ### Development Scripts
393
+
394
+ - `npm run build` - Compile TypeScript to JavaScript
395
+ - `npm run start` - Run the package with ts-node-dev for development
396
+
397
+ ### TypeScript Configuration
398
+
399
+ This package is configured with TypeScript strict mode enabled. The compiled output is placed in the `dist/` directory with declaration files for type support.
400
+
401
+ ## API Reference
402
+
403
+ ### SQLServerDataProvider
404
+
405
+ The main class that implements IEntityDataProvider, IMetadataProvider, IRunViewProvider, IRunReportProvider, and IRunQueryProvider interfaces.
406
+
407
+ #### Key Methods
408
+
409
+ - `Config(configData: SQLServerProviderConfigData): Promise<boolean>` - Configure the provider with connection details
410
+ - `Get(entity: EntityInfo, CompositeKey: CompositeKey, user?: UserInfo): Promise<BaseEntityResult>` - Load an entity by primary key
411
+ - `Save(entity: BaseEntity, user: UserInfo, options?: EntitySaveOptions): Promise<BaseEntityResult>` - Save (create/update) an entity
412
+ - `Delete(entity: EntityInfo, CompositeKey: CompositeKey, user?: UserInfo, options?: EntityDeleteOptions): Promise<BaseEntityResult>` - Delete an entity
413
+ - `RunView(params: RunViewParams, contextUser?: UserInfo): Promise<RunViewResult>` - Execute a database view
414
+ - `RunReport(params: RunReportParams, contextUser?: UserInfo): Promise<RunReportResult>` - Execute a report
415
+ - `RunQuery(params: RunQueryParams, contextUser?: UserInfo): Promise<RunQueryResult>` - Execute a query
416
+ - `ExecuteSQL(sql: string, params?: any, maxRows?: number): Promise<any[]>` - Execute raw SQL
417
+
418
+ ### SQLServerProviderConfigData
419
+
420
+ Configuration class for the SQL Server provider.
421
+
422
+ #### Properties
423
+
424
+ - `DataSource: DataSource` - TypeORM DataSource instance
425
+ - `CurrentUserEmail: string` - Email of the current user
426
+ - `CheckRefreshIntervalSeconds: number` - Interval for checking metadata refresh (0 to disable)
427
+ - `MJCoreSchemaName: string` - Schema name for MJ core tables (default: '__mj')
428
+ - `IncludeSchemas?: string[]` - List of schemas to include
429
+ - `ExcludeSchemas?: string[]` - List of schemas to exclude
430
+
431
+ ### SQLServerTransactionGroup
432
+
433
+ SQL Server implementation of TransactionGroupBase for managing database transactions.
434
+
435
+ #### Methods
436
+
437
+ - `HandleSubmit(): Promise<TransactionResult[]>` - Execute all pending transactions in the group
438
+
439
+ ### UserCache
440
+
441
+ Server-side cache for user and role information.
442
+
443
+ #### Static Methods
444
+
445
+ - `Instance: UserCache` - Get singleton instance
446
+ - `Users: UserInfo[]` - Get all cached users
447
+
448
+ #### Instance Methods
449
+
450
+ - `Refresh(dataSource: DataSource, autoRefreshIntervalMS?: number): Promise<void>` - Refresh user cache
451
+ - `UserByName(name: string, caseSensitive?: boolean): UserInfo | undefined` - Find user by name
452
+
453
+ ### setupSQLServerClient
454
+
455
+ Helper function to initialize and configure the SQL Server data provider.
456
+
457
+ ```typescript
458
+ setupSQLServerClient(config: SQLServerProviderConfigData): Promise<SQLServerDataProvider>
459
+ ```
460
+
461
+ ## Troubleshooting
462
+
463
+ ### Common Issues
464
+
465
+ 1. **Connection Timeout Errors**
466
+ - Increase `connectionTimeout` and `requestTimeout` in configuration
467
+ - Verify network connectivity to SQL Server
468
+ - Check SQL Server firewall rules
469
+
470
+ 2. **Authentication Failures**
471
+ - Ensure correct username/password or Windows authentication
472
+ - Verify user has appropriate database permissions
473
+ - Check if encryption settings match server requirements
474
+
475
+ 3. **Schema Not Found**
476
+ - Verify `MJCoreSchemaName` matches your database schema (default: `__mj`)
477
+ - Ensure user has access to the schema
478
+ - Check if MemberJunction tables are properly installed
479
+
480
+ 4. **Transaction Rollback Issues**
481
+ - Check for constraint violations in related entities
482
+ - Verify all required fields are populated
483
+ - Review transaction logs for specific error details
484
+
485
+ 5. **Performance Issues**
486
+ - Adjust connection pool settings (`pool.max`, `pool.min`)
487
+ - Enable query logging to identify slow queries
488
+ - Consider adding database indexes for frequently queried fields
489
+
490
+ ### Debug Logging
491
+
492
+ Enable detailed logging by setting environment variables:
493
+
494
+ ```bash
495
+ # Enable SQL query logging
496
+ export MJ_LOG_SQL=true
497
+
498
+ # Enable detailed error logging
499
+ export MJ_LOG_LEVEL=debug
500
+ ```
501
+
320
502
  ## License
321
503
 
322
504
  ISC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/sqlserver-dataprovider",
3
- "version": "2.43.0",
3
+ "version": "2.45.0",
4
4
  "description": "MemberJunction: SQL Server Client Data Provider",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,14 +19,14 @@
19
19
  "typescript": "^5.4.5"
20
20
  },
21
21
  "dependencies": {
22
- "@memberjunction/actions": "2.43.0",
23
- "@memberjunction/ai": "2.43.0",
24
- "@memberjunction/ai-vector-dupe": "2.43.0",
25
- "@memberjunction/aiengine": "2.43.0",
26
- "@memberjunction/core": "2.43.0",
27
- "@memberjunction/core-entities": "2.43.0",
28
- "@memberjunction/global": "2.43.0",
29
- "@memberjunction/queue": "2.43.0",
22
+ "@memberjunction/actions": "2.45.0",
23
+ "@memberjunction/ai": "2.45.0",
24
+ "@memberjunction/ai-vector-dupe": "2.45.0",
25
+ "@memberjunction/aiengine": "2.45.0",
26
+ "@memberjunction/core": "2.45.0",
27
+ "@memberjunction/core-entities": "2.45.0",
28
+ "@memberjunction/global": "2.45.0",
29
+ "@memberjunction/queue": "2.45.0",
30
30
  "mssql": "^11.0.1",
31
31
  "typeorm": "^0.3.20"
32
32
  }