@memberjunction/sqlserver-dataprovider 2.22.2 → 2.23.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.
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**************************************************************************************************************
|
|
3
|
-
* The SQLServerDataProvider provides a data provider for the entities framework that uses SQL Server directly
|
|
4
|
-
* In practice - this FILE will NOT exist in the entities library, we need to move to its own separate project
|
|
5
|
-
* so it is only included by the consumer of the entities library if they want to use it.
|
|
6
|
-
**************************************************************************************************************/
|
|
3
|
+
* The SQLServerDataProvider provides a data provider for the entities framework that uses SQL Server directly
|
|
4
|
+
* In practice - this FILE will NOT exist in the entities library, we need to move to its own separate project
|
|
5
|
+
* so it is only included by the consumer of the entities library if they want to use it.
|
|
6
|
+
**************************************************************************************************************/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.SQLServerDataProvider = exports.SQLServerProviderConfigData = void 0;
|
|
9
9
|
const core_1 = require("@memberjunction/core");
|
|
@@ -14,14 +14,20 @@ const SQLServerTransactionGroup_1 = require("./SQLServerTransactionGroup");
|
|
|
14
14
|
const UserCache_1 = require("./UserCache");
|
|
15
15
|
const actions_1 = require("@memberjunction/actions");
|
|
16
16
|
class SQLServerProviderConfigData extends core_1.ProviderConfigDataBase {
|
|
17
|
-
get DataSource() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
get DataSource() {
|
|
18
|
+
return this.Data.DataSource;
|
|
19
|
+
}
|
|
20
|
+
get CurrentUserEmail() {
|
|
21
|
+
return this.Data.CurrentUserEmail;
|
|
22
|
+
}
|
|
23
|
+
get CheckRefreshIntervalSeconds() {
|
|
24
|
+
return this.Data.CheckRefreshIntervalSeconds;
|
|
25
|
+
}
|
|
26
|
+
constructor(dataSource, currentUserEmail, MJCoreSchemaName, checkRefreshIntervalSeconds = 0 /*default to disabling auto refresh */, includeSchemas, excludeSchemas) {
|
|
21
27
|
super({
|
|
22
28
|
DataSource: dataSource,
|
|
23
29
|
CurrentUserEmail: currentUserEmail,
|
|
24
|
-
CheckRefreshIntervalSeconds: checkRefreshIntervalSeconds
|
|
30
|
+
CheckRefreshIntervalSeconds: checkRefreshIntervalSeconds,
|
|
25
31
|
}, MJCoreSchemaName, includeSchemas, excludeSchemas);
|
|
26
32
|
}
|
|
27
33
|
}
|
|
@@ -32,7 +38,9 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
32
38
|
super(...arguments);
|
|
33
39
|
this._bAllowRefresh = true;
|
|
34
40
|
}
|
|
35
|
-
get ConfigData() {
|
|
41
|
+
get ConfigData() {
|
|
42
|
+
return super.ConfigData;
|
|
43
|
+
}
|
|
36
44
|
async Config(configData) {
|
|
37
45
|
try {
|
|
38
46
|
this._dataSource = configData.DataSource;
|
|
@@ -41,7 +49,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
41
49
|
}
|
|
42
50
|
catch (e) {
|
|
43
51
|
(0, core_1.LogError)(e);
|
|
44
|
-
throw
|
|
52
|
+
throw e;
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
/**
|
|
@@ -62,7 +70,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
62
70
|
host: dbOptions.host || '',
|
|
63
71
|
port: dbOptions.port || '',
|
|
64
72
|
instanceName: dbOptions.instanceName ? '/' + dbOptions.instanceName : '',
|
|
65
|
-
database: dbOptions.database || ''
|
|
73
|
+
database: dbOptions.database || '',
|
|
66
74
|
};
|
|
67
75
|
return options.type + '://' + options.host + ':' + options.port + options.instanceName + '/' + options.database;
|
|
68
76
|
}
|
|
@@ -86,9 +94,23 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
86
94
|
const result = await this.ExecuteSQL(sql);
|
|
87
95
|
const end = new Date().getTime();
|
|
88
96
|
if (result)
|
|
89
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
Success: true,
|
|
99
|
+
ReportID: ReportID,
|
|
100
|
+
Results: result,
|
|
101
|
+
RowCount: result.length,
|
|
102
|
+
ExecutionTime: end - start,
|
|
103
|
+
ErrorMessage: '',
|
|
104
|
+
};
|
|
90
105
|
else
|
|
91
|
-
return {
|
|
106
|
+
return {
|
|
107
|
+
Success: false,
|
|
108
|
+
ReportID: ReportID,
|
|
109
|
+
Results: [],
|
|
110
|
+
RowCount: 0,
|
|
111
|
+
ExecutionTime: end - start,
|
|
112
|
+
ErrorMessage: 'Error running report SQL',
|
|
113
|
+
};
|
|
92
114
|
}
|
|
93
115
|
else
|
|
94
116
|
return { Success: false, ReportID: ReportID, Results: [], RowCount: 0, ExecutionTime: 0, ErrorMessage: 'Report not found' };
|
|
@@ -111,9 +133,23 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
111
133
|
const result = await this.ExecuteSQL(sql);
|
|
112
134
|
const end = new Date().getTime();
|
|
113
135
|
if (result)
|
|
114
|
-
return {
|
|
136
|
+
return {
|
|
137
|
+
Success: true,
|
|
138
|
+
QueryID: QueryID,
|
|
139
|
+
Results: result,
|
|
140
|
+
RowCount: result.length,
|
|
141
|
+
ExecutionTime: end - start,
|
|
142
|
+
ErrorMessage: '',
|
|
143
|
+
};
|
|
115
144
|
else
|
|
116
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
Success: false,
|
|
147
|
+
QueryID: QueryID,
|
|
148
|
+
Results: [],
|
|
149
|
+
RowCount: 0,
|
|
150
|
+
ExecutionTime: end - start,
|
|
151
|
+
ErrorMessage: 'Error running query SQL',
|
|
152
|
+
};
|
|
117
153
|
}
|
|
118
154
|
else
|
|
119
155
|
return { Success: false, QueryID: QueryID, Results: [], RowCount: 0, ExecutionTime: 0, ErrorMessage: 'Query not found' };
|
|
@@ -183,7 +219,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
183
219
|
}
|
|
184
220
|
catch (e) {
|
|
185
221
|
(0, core_1.LogError)(e);
|
|
186
|
-
throw
|
|
222
|
+
throw e;
|
|
187
223
|
}
|
|
188
224
|
}
|
|
189
225
|
/**************************************************************************/
|
|
@@ -243,7 +279,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
243
279
|
let countSQL = topSQL && topSQL.length > 0 ? `SELECT COUNT(*) AS TotalRowCount FROM [${entityInfo.SchemaName}].${entityInfo.BaseView}` : null;
|
|
244
280
|
let whereSQL = '';
|
|
245
281
|
let bHasWhere = false;
|
|
246
|
-
let userViewRunID =
|
|
282
|
+
let userViewRunID = '';
|
|
247
283
|
// The view may have a where clause that is part of the view definition. If so, we need to add it to the SQL
|
|
248
284
|
if (viewEntity?.WhereClause && viewEntity?.WhereClause.length > 0) {
|
|
249
285
|
const renderedWhere = await this.RenderViewWhereClause(viewEntity, contextUser);
|
|
@@ -280,8 +316,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
280
316
|
}
|
|
281
317
|
// now, check for an exclude UserViewRunID, or exclusion of ALL prior runs
|
|
282
318
|
// if provided, we need to exclude the records that were part of that run (or all prior runs)
|
|
283
|
-
if ((excludeUserViewRunID && excludeUserViewRunID.length > 0) ||
|
|
284
|
-
(params.ExcludeDataFromAllPriorViewRuns === true)) {
|
|
319
|
+
if ((excludeUserViewRunID && excludeUserViewRunID.length > 0) || params.ExcludeDataFromAllPriorViewRuns === true) {
|
|
285
320
|
let sExcludeSQL = `ID NOT IN (SELECT RecordID FROM [${this.MJCoreSchemaName}].vwUserViewRunDetails WHERE EntityID='${viewEntity.EntityID}' AND`;
|
|
286
321
|
if (params.ExcludeDataFromAllPriorViewRuns === true)
|
|
287
322
|
sExcludeSQL += ` UserViewID=${viewEntity.ID})`; // exclude ALL prior runs for this view, we do NOT need to also add the UserViewRunID even if it was provided because this will automatically filter that out too
|
|
@@ -302,7 +337,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
302
337
|
bHasWhere = true;
|
|
303
338
|
}
|
|
304
339
|
}
|
|
305
|
-
// NEXT, apply Row Level Security (RLS)
|
|
340
|
+
// NEXT, apply Row Level Security (RLS)
|
|
306
341
|
if (!entityInfo.UserExemptFromRowLevelSecurity(user, core_1.EntityPermissionType.Read)) {
|
|
307
342
|
// user is NOT exempt from RLS, so we need to apply it
|
|
308
343
|
const rlsWhereClause = entityInfo.GetUserRowLevelSecurityWhereClause(user, core_1.EntityPermissionType.Read, '');
|
|
@@ -325,7 +360,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
325
360
|
// first check params.OrderBy, that takes first priority
|
|
326
361
|
// if that's not provided, then we check the view definition for its SortState
|
|
327
362
|
// if that's not provided we do NOT sort
|
|
328
|
-
const orderBy = params.OrderBy ? params.OrderBy :
|
|
363
|
+
const orderBy = params.OrderBy ? params.OrderBy : viewEntity ? viewEntity.OrderByClause : '';
|
|
329
364
|
// if we're saving the view results, we need to wrap the entire SQL statement
|
|
330
365
|
if (viewEntity?.ID && viewEntity?.ID.length > 0 && saveViewResults && user) {
|
|
331
366
|
const { executeViewSQL, runID } = await this.executeSQLForUserViewRunLogging(viewEntity.ID, viewEntity.EntityBaseView, whereSQL, orderBy, user);
|
|
@@ -333,7 +368,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
333
368
|
userViewRunID = runID;
|
|
334
369
|
}
|
|
335
370
|
else if (orderBy && orderBy.length > 0) {
|
|
336
|
-
// we only add order by if we're not doing run logging. This is becuase the run logging will
|
|
371
|
+
// we only add order by if we're not doing run logging. This is becuase the run logging will
|
|
337
372
|
// add the order by to its SELECT query that pulls from the list of records that were returned
|
|
338
373
|
// there is no point in ordering the rows as they are saved into an audit list anyway so no order-by above
|
|
339
374
|
// just here for final step before we execute it.
|
|
@@ -341,9 +376,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
341
376
|
throw new Error(`Invalid Order By clause: ${orderBy}, contains one more for forbidden keywords`);
|
|
342
377
|
viewSQL += ` ORDER BY ${orderBy}`;
|
|
343
378
|
}
|
|
344
|
-
if (params.StartRow && params.StartRow > 0
|
|
345
|
-
&& params.MaxRows && params.MaxRows > 0
|
|
346
|
-
&& entityInfo.FirstPrimaryKey) {
|
|
379
|
+
if (params.StartRow && params.StartRow > 0 && params.MaxRows && params.MaxRows > 0 && entityInfo.FirstPrimaryKey) {
|
|
347
380
|
viewSQL += ` ORDER BY ${entityInfo.FirstPrimaryKey.Name} `;
|
|
348
381
|
viewSQL += ` OFFSET ${params.StartRow} ROWS FETCH NEXT ${params.MaxRows} ROWS ONLY`;
|
|
349
382
|
}
|
|
@@ -360,26 +393,31 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
360
393
|
}
|
|
361
394
|
}
|
|
362
395
|
const stopTime = new Date();
|
|
363
|
-
if (params.ForceAuditLog ||
|
|
396
|
+
if (params.ForceAuditLog ||
|
|
397
|
+
(viewEntity?.ID &&
|
|
398
|
+
(extraFilter === undefined || extraFilter === null || extraFilter?.trim().length === 0) &&
|
|
399
|
+
entityInfo.AuditViewRuns)) {
|
|
364
400
|
// ONLY LOG TOP LEVEL VIEW EXECUTION - this would be for views with an ID, and don't have ExtraFilter as ExtraFilter
|
|
365
401
|
// is only used in the system on a tab or just for ad hoc view execution
|
|
366
|
-
// we do NOT want to wait for this, so no await,
|
|
402
|
+
// we do NOT want to wait for this, so no await,
|
|
367
403
|
this.createAuditLogRecord(user, 'Run View', 'Run View', 'Success', JSON.stringify({
|
|
368
404
|
ViewID: viewEntity?.ID,
|
|
369
405
|
ViewName: viewEntity?.Name,
|
|
370
406
|
Description: params.AuditLogDescription,
|
|
371
407
|
RowCount: retData.length,
|
|
372
|
-
SQL: viewSQL
|
|
408
|
+
SQL: viewSQL,
|
|
373
409
|
}), entityInfo.ID, null, params.AuditLogDescription);
|
|
374
410
|
}
|
|
375
411
|
return {
|
|
376
|
-
RowCount: params.ResultType === 'count_only'
|
|
412
|
+
RowCount: params.ResultType === 'count_only'
|
|
413
|
+
? rowCount
|
|
414
|
+
: retData.length /*this property should be total row count if the ResultType='count_only' otherwise it should be the row count of the returned rows */,
|
|
377
415
|
TotalRowCount: rowCount ? rowCount : retData.length,
|
|
378
416
|
Results: retData,
|
|
379
417
|
UserViewRunID: userViewRunID,
|
|
380
418
|
ExecutionTime: stopTime.getTime() - startTime.getTime(),
|
|
381
419
|
Success: true,
|
|
382
|
-
ErrorMessage: null
|
|
420
|
+
ErrorMessage: null,
|
|
383
421
|
};
|
|
384
422
|
}
|
|
385
423
|
else
|
|
@@ -392,10 +430,10 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
392
430
|
RowCount: 0,
|
|
393
431
|
TotalRowCount: 0,
|
|
394
432
|
Results: [],
|
|
395
|
-
UserViewRunID:
|
|
433
|
+
UserViewRunID: '',
|
|
396
434
|
ExecutionTime: exceptionStopTime.getTime() - startTime.getTime(),
|
|
397
435
|
Success: false,
|
|
398
|
-
ErrorMessage: e.message
|
|
436
|
+
ErrorMessage: e.message,
|
|
399
437
|
};
|
|
400
438
|
}
|
|
401
439
|
}
|
|
@@ -424,7 +462,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
424
462
|
/\bunion\b/,
|
|
425
463
|
/\bcast\b/,
|
|
426
464
|
/\bxp_/,
|
|
427
|
-
|
|
465
|
+
/;/,
|
|
428
466
|
];
|
|
429
467
|
// Check for forbidden patterns
|
|
430
468
|
for (const pattern of forbiddenPatterns) {
|
|
@@ -440,10 +478,12 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
440
478
|
if (fieldList.length === 0)
|
|
441
479
|
return '*';
|
|
442
480
|
else
|
|
443
|
-
return fieldList
|
|
481
|
+
return fieldList
|
|
482
|
+
.map((f) => {
|
|
444
483
|
const asString = f.CodeName === f.Name ? '' : ` AS [${f.CodeName}]`;
|
|
445
484
|
return `[${f.Name}]${asString}`;
|
|
446
|
-
})
|
|
485
|
+
})
|
|
486
|
+
.join(',');
|
|
447
487
|
}
|
|
448
488
|
getRunTimeViewFieldArray(params, viewEntity) {
|
|
449
489
|
const fieldList = [];
|
|
@@ -460,7 +500,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
460
500
|
if (params.Fields) {
|
|
461
501
|
// fields provided, if primary key isn't included, add it first
|
|
462
502
|
for (const ef of entityInfo.PrimaryKeys) {
|
|
463
|
-
if (params.Fields.find(f => f.trim().toLowerCase() === ef.Name.toLowerCase()) === undefined)
|
|
503
|
+
if (params.Fields.find((f) => f.trim().toLowerCase() === ef.Name.toLowerCase()) === undefined)
|
|
464
504
|
fieldList.push(ef); // always include the primary key fields in view run time field list
|
|
465
505
|
}
|
|
466
506
|
// now add the rest of the param.Fields to fields
|
|
@@ -479,7 +519,8 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
479
519
|
if (viewEntity) {
|
|
480
520
|
// saved view, figure out it's field list
|
|
481
521
|
viewEntity.Columns.forEach((c) => {
|
|
482
|
-
if (!c.hidden) {
|
|
522
|
+
if (!c.hidden) {
|
|
523
|
+
// only return the non-hidden fields
|
|
483
524
|
if (c.EntityField) {
|
|
484
525
|
fieldList.push(c.EntityField);
|
|
485
526
|
}
|
|
@@ -490,7 +531,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
490
531
|
});
|
|
491
532
|
// the below shouldn't happen as the pkey fields should always be included by now, but make SURE...
|
|
492
533
|
for (const ef of entityInfo.PrimaryKeys) {
|
|
493
|
-
if (fieldList.find(f => f.Name?.trim().toLowerCase() === ef.Name?.toLowerCase()) === undefined)
|
|
534
|
+
if (fieldList.find((f) => f.Name?.trim().toLowerCase() === ef.Name?.toLowerCase()) === undefined)
|
|
494
535
|
fieldList.push(ef); // always include the primary key fields in view run time field list
|
|
495
536
|
}
|
|
496
537
|
}
|
|
@@ -512,13 +553,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
512
553
|
`;
|
|
513
554
|
const runIDResult = await this._dataSource.query(sSQL);
|
|
514
555
|
const runID = runIDResult[0].UserViewRunID;
|
|
515
|
-
const sRetSQL = `SELECT * FROM [${entityInfo.SchemaName}].${entityBaseView} WHERE ${entityInfo.FirstPrimaryKey.Name} IN
|
|
556
|
+
const sRetSQL = `SELECT * FROM [${entityInfo.SchemaName}].${entityBaseView} WHERE ${entityInfo.FirstPrimaryKey.Name} IN
|
|
516
557
|
(SELECT RecordID FROM [${this.MJCoreSchemaName}].vwUserViewRunDetails WHERE UserViewRunID=${runID})
|
|
517
558
|
${orderBySQL && orderBySQL.length > 0 ? ' ORDER BY ' + orderBySQL : ''}`;
|
|
518
559
|
return { executeViewSQL: sRetSQL, runID: runID };
|
|
519
560
|
}
|
|
520
561
|
createViewUserSearchSQL(entityInfo, userSearchString) {
|
|
521
|
-
// we have a user search string.
|
|
562
|
+
// we have a user search string.
|
|
522
563
|
// if we have full text search, we use that.
|
|
523
564
|
// Otherwise, we need to manually construct the additional filter associated with this. The user search string is just text from the user
|
|
524
565
|
// we need to apply it to one or more fields that are part of the entity that support being part of a user search.
|
|
@@ -535,11 +576,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
535
576
|
const uUpper = u.toUpperCase();
|
|
536
577
|
if (uUpper.includes(' AND ') || uUpper.includes(' OR ') || uUpper.includes(' NOT ')) {
|
|
537
578
|
//replace all spaces with %, but add spaces inbetween the original and, or and not keywords
|
|
538
|
-
u = uUpper.replace(/ /g,
|
|
579
|
+
u = uUpper.replace(/ /g, '%').replace(/%AND%/g, ' AND ').replace(/%OR%/g, ' OR ').replace(/%NOT%/g, ' NOT ');
|
|
539
580
|
}
|
|
540
581
|
else if (uUpper.includes('AND') || uUpper.includes('OR') || uUpper.includes('NOT')) {
|
|
541
582
|
//leave the string alone, except replacing spaces with %
|
|
542
|
-
u = u.replace(/ /g,
|
|
583
|
+
u = u.replace(/ /g, '%');
|
|
543
584
|
}
|
|
544
585
|
else if (u.includes(' ')) {
|
|
545
586
|
if (u.startsWith('"') && u.endsWith('"')) {
|
|
@@ -577,8 +618,12 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
577
618
|
}
|
|
578
619
|
async createAuditLogRecord(user, authorizationName, auditLogTypeName, status, details, entityId, recordId, auditLogDescription) {
|
|
579
620
|
try {
|
|
580
|
-
const authorization = authorizationName
|
|
581
|
-
|
|
621
|
+
const authorization = authorizationName
|
|
622
|
+
? this.Authorizations.find((a) => a?.Name?.trim().toLowerCase() === authorizationName.trim().toLowerCase())
|
|
623
|
+
: null;
|
|
624
|
+
const auditLogType = auditLogTypeName
|
|
625
|
+
? this.AuditLogTypes.find((a) => a?.Name?.trim().toLowerCase() === auditLogTypeName.trim().toLowerCase())
|
|
626
|
+
: null;
|
|
582
627
|
if (!user)
|
|
583
628
|
throw new Error(`User is a required parameter`);
|
|
584
629
|
if (!auditLogType)
|
|
@@ -646,14 +691,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
646
691
|
}
|
|
647
692
|
catch (e) {
|
|
648
693
|
(0, core_1.LogError)(e);
|
|
649
|
-
throw
|
|
694
|
+
throw e;
|
|
650
695
|
}
|
|
651
696
|
}
|
|
652
697
|
async SetRecordFavoriteStatus(userId, entityName, CompositeKey, isFavorite, contextUser) {
|
|
653
698
|
try {
|
|
654
699
|
const currentFavoriteId = await this.GetRecordFavoriteID(userId, entityName, CompositeKey);
|
|
655
|
-
if ((currentFavoriteId === null && isFavorite === false) ||
|
|
656
|
-
(currentFavoriteId !== null && isFavorite === true))
|
|
700
|
+
if ((currentFavoriteId === null && isFavorite === false) || (currentFavoriteId !== null && isFavorite === true))
|
|
657
701
|
return; // no change
|
|
658
702
|
// if we're here that means we need to invert the status, which either means creating a record or deleting a record
|
|
659
703
|
const e = this.Entities.find((e) => e.Name === entityName);
|
|
@@ -680,7 +724,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
680
724
|
}
|
|
681
725
|
catch (e) {
|
|
682
726
|
(0, core_1.LogError)(e);
|
|
683
|
-
throw
|
|
727
|
+
throw e;
|
|
684
728
|
}
|
|
685
729
|
}
|
|
686
730
|
async GetRecordChanges(entityName, compositeKey) {
|
|
@@ -690,7 +734,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
690
734
|
}
|
|
691
735
|
catch (e) {
|
|
692
736
|
(0, core_1.LogError)(e);
|
|
693
|
-
throw
|
|
737
|
+
throw e;
|
|
694
738
|
}
|
|
695
739
|
}
|
|
696
740
|
/**
|
|
@@ -709,24 +753,24 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
709
753
|
// we do this in SQL by combining the pirmary key name and value for each row using the default separator defined by the CompositeKey class
|
|
710
754
|
// the output of this should be like the following 'Field1|Value1||Field2|Value2||Field3|Value3' where the || is the CompositeKey.DefaultFieldDelimiter and the | is the CompositeKey.DefaultValueDelimiter
|
|
711
755
|
const quotes = entity.FirstPrimaryKey.NeedsQuotes ? "'" : '';
|
|
712
|
-
const primaryKeySelectString = `CONCAT(${entity.PrimaryKeys.map(pk => `'${pk.Name}|', CAST(${pk.Name} AS NVARCHAR(MAX))`).join(`,'${core_1.CompositeKey.DefaultFieldDelimiter}',`)})`;
|
|
756
|
+
const primaryKeySelectString = `CONCAT(${entity.PrimaryKeys.map((pk) => `'${pk.Name}|', CAST(${pk.Name} AS NVARCHAR(MAX))`).join(`,'${core_1.CompositeKey.DefaultFieldDelimiter}',`)})`;
|
|
713
757
|
// for this entity, check to see if it has any fields that are soft links, and for each of those, generate the SQL
|
|
714
758
|
entity.Fields.filter((f) => f.EntityIDFieldName && f.EntityIDFieldName.length > 0).forEach((f) => {
|
|
715
759
|
// each field in f must be processed
|
|
716
760
|
if (sSQL.length > 0)
|
|
717
761
|
sSQL += ' UNION ALL ';
|
|
718
|
-
// there is a layer of indirection here because each ROW in each of the entity records for this entity/field combination could point to a DIFFERENT
|
|
762
|
+
// there is a layer of indirection here because each ROW in each of the entity records for this entity/field combination could point to a DIFFERENT
|
|
719
763
|
// entity. We find out which entity it is pointed to via the EntityIDFieldName in the field definition, so we have to filter the rows in the entity
|
|
720
764
|
// based on that.
|
|
721
|
-
sSQL += `SELECT
|
|
722
|
-
'${entityName}' AS EntityName,
|
|
723
|
-
'${entity.Name}' AS RelatedEntityName,
|
|
724
|
-
${primaryKeySelectString} AS PrimaryKeyValue,
|
|
725
|
-
'${f.Name}' AS FieldName
|
|
726
|
-
FROM
|
|
765
|
+
sSQL += `SELECT
|
|
766
|
+
'${entityName}' AS EntityName,
|
|
767
|
+
'${entity.Name}' AS RelatedEntityName,
|
|
768
|
+
${primaryKeySelectString} AS PrimaryKeyValue,
|
|
769
|
+
'${f.Name}' AS FieldName
|
|
770
|
+
FROM
|
|
727
771
|
[${entity.SchemaName}].[${entity.BaseView}]
|
|
728
|
-
WHERE
|
|
729
|
-
[${f.EntityIDFieldName}] = ${quotes}${entity.ID}${quotes} AND
|
|
772
|
+
WHERE
|
|
773
|
+
[${f.EntityIDFieldName}] = ${quotes}${entity.ID}${quotes} AND
|
|
730
774
|
[${f.Name}] = ${quotes}${compositeKey.GetValueByIndex(0)}${quotes}`; // we only use the first primary key value, this is because we don't yet support composite primary keys
|
|
731
775
|
});
|
|
732
776
|
});
|
|
@@ -738,17 +782,17 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
738
782
|
const entityInfo = this.Entities.find((e) => e.Name.trim().toLowerCase() === entityDependency.EntityName?.trim().toLowerCase());
|
|
739
783
|
const quotes = entityInfo.FirstPrimaryKey.NeedsQuotes ? "'" : '';
|
|
740
784
|
const relatedEntityInfo = this.Entities.find((e) => e.Name.trim().toLowerCase() === entityDependency.RelatedEntityName?.trim().toLowerCase());
|
|
741
|
-
const primaryKeySelectString = `CONCAT(${entityInfo.PrimaryKeys.map(pk => `'${pk.Name}|', CAST(${pk.Name} AS NVARCHAR(MAX))`).join(`,'${core_1.CompositeKey.DefaultFieldDelimiter}',`)})`;
|
|
785
|
+
const primaryKeySelectString = `CONCAT(${entityInfo.PrimaryKeys.map((pk) => `'${pk.Name}|', CAST(${pk.Name} AS NVARCHAR(MAX))`).join(`,'${core_1.CompositeKey.DefaultFieldDelimiter}',`)})`;
|
|
742
786
|
if (sSQL.length > 0)
|
|
743
787
|
sSQL += ' UNION ALL ';
|
|
744
|
-
sSQL += `SELECT
|
|
745
|
-
'${entityDependency.EntityName}' AS EntityName,
|
|
746
|
-
'${entityDependency.RelatedEntityName}' AS RelatedEntityName,
|
|
747
|
-
${primaryKeySelectString} AS PrimaryKeyValue,
|
|
748
|
-
'${entityDependency.FieldName}' AS FieldName
|
|
749
|
-
FROM
|
|
788
|
+
sSQL += `SELECT
|
|
789
|
+
'${entityDependency.EntityName}' AS EntityName,
|
|
790
|
+
'${entityDependency.RelatedEntityName}' AS RelatedEntityName,
|
|
791
|
+
${primaryKeySelectString} AS PrimaryKeyValue,
|
|
792
|
+
'${entityDependency.FieldName}' AS FieldName
|
|
793
|
+
FROM
|
|
750
794
|
[${relatedEntityInfo.SchemaName}].[${relatedEntityInfo.BaseView}]
|
|
751
|
-
WHERE
|
|
795
|
+
WHERE
|
|
752
796
|
[${entityDependency.FieldName}] = ${this.GetRecordDependencyLinkSQL(entityDependency, entityInfo, relatedEntityInfo, compositeKey)}`;
|
|
753
797
|
}
|
|
754
798
|
return sSQL;
|
|
@@ -771,8 +815,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
771
815
|
return recordDependencies;
|
|
772
816
|
}
|
|
773
817
|
// now, we have to construct a query that will return the dependencies for this record, both hard and soft links
|
|
774
|
-
const sSQL = this.GetHardLinkDependencySQL(entityDependencies, compositeKey) + '\n' +
|
|
775
|
-
this.GetSoftLinkDependencySQL(entityName, compositeKey);
|
|
818
|
+
const sSQL = this.GetHardLinkDependencySQL(entityDependencies, compositeKey) + '\n' + this.GetSoftLinkDependencySQL(entityName, compositeKey);
|
|
776
819
|
// now, execute the query
|
|
777
820
|
const result = await this.ExecuteSQL(sSQL);
|
|
778
821
|
if (!result || result.length === 0) {
|
|
@@ -803,7 +846,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
803
846
|
EntityName: r.EntityName,
|
|
804
847
|
RelatedEntityName: r.RelatedEntityName,
|
|
805
848
|
FieldName: r.FieldName,
|
|
806
|
-
PrimaryKey: compositeKey
|
|
849
|
+
PrimaryKey: compositeKey,
|
|
807
850
|
};
|
|
808
851
|
recordDependencies.push(recordDependency);
|
|
809
852
|
}
|
|
@@ -812,7 +855,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
812
855
|
catch (e) {
|
|
813
856
|
// log and throw
|
|
814
857
|
(0, core_1.LogError)(e);
|
|
815
|
-
throw
|
|
858
|
+
throw e;
|
|
816
859
|
}
|
|
817
860
|
}
|
|
818
861
|
GetRecordDependencyLinkSQL(dep, entity, relatedEntity, CompositeKey) {
|
|
@@ -834,7 +877,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
834
877
|
}
|
|
835
878
|
async GetRecordDuplicates(params, contextUser) {
|
|
836
879
|
if (!contextUser) {
|
|
837
|
-
throw new Error(
|
|
880
|
+
throw new Error('User context is required to get record duplicates.');
|
|
838
881
|
}
|
|
839
882
|
const listEntity = await this.GetEntityObject('Lists');
|
|
840
883
|
listEntity.ContextCurrentUser = contextUser;
|
|
@@ -857,12 +900,12 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
857
900
|
}
|
|
858
901
|
let response = {
|
|
859
902
|
Status: 'Inprogress',
|
|
860
|
-
PotentialDuplicateResult: []
|
|
903
|
+
PotentialDuplicateResult: [],
|
|
861
904
|
};
|
|
862
905
|
return response;
|
|
863
906
|
}
|
|
864
907
|
async MergeRecords(request, contextUser) {
|
|
865
|
-
const e = this.Entities.find(e => e.Name.trim().toLowerCase() === request.EntityName.trim().toLowerCase());
|
|
908
|
+
const e = this.Entities.find((e) => e.Name.trim().toLowerCase() === request.EntityName.trim().toLowerCase());
|
|
866
909
|
if (!e || !e.AllowRecordMerge)
|
|
867
910
|
throw new Error(`Entity ${request.EntityName} does not allow record merging, check the AllowRecordMerge property in the entity metadata`);
|
|
868
911
|
const result = {
|
|
@@ -875,13 +918,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
875
918
|
const mergeRecordLog = await this.StartMergeLogging(request, result, contextUser);
|
|
876
919
|
try {
|
|
877
920
|
/*
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
921
|
+
we will follow this process...
|
|
922
|
+
* 1. Begin Transaction
|
|
923
|
+
* 2. The surviving record is loaded and fields are updated from the field map, if provided, and the record is saved. If a FieldMap not provided within the request object, this step is skipped.
|
|
924
|
+
* 3. For each of the records that will be merged INTO the surviving record, we call the GetEntityDependencies() method and get a list of all other records in the database are linked to the record to be deleted. We then go through each of those dependencies and update the link to point to the SurvivingRecordID and save the record.
|
|
925
|
+
* 4. The record to be deleted is then deleted.
|
|
926
|
+
* 5. Commit or Rollback Transaction
|
|
927
|
+
*/
|
|
885
928
|
// Step 1 - begin transaction
|
|
886
929
|
await this.BeginTransaction();
|
|
887
930
|
// Step 2 - update the surviving record, but only do this if we were provided a field map
|
|
@@ -891,7 +934,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
891
934
|
for (const fieldMap of request.FieldMap) {
|
|
892
935
|
survivor.Set(fieldMap.FieldName, fieldMap.Value);
|
|
893
936
|
}
|
|
894
|
-
if (!await survivor.Save()) {
|
|
937
|
+
if (!(await survivor.Save())) {
|
|
895
938
|
result.OverallStatus = 'Error saving survivor record with values from provided field map.';
|
|
896
939
|
throw new Error(result.OverallStatus);
|
|
897
940
|
}
|
|
@@ -913,13 +956,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
913
956
|
await relatedEntity.InnerLoad(dependency.PrimaryKey);
|
|
914
957
|
relatedEntity.Set(dependency.FieldName, request.SurvivingRecordCompositeKey.GetValueByIndex(0)); // only support single field foreign keys for now
|
|
915
958
|
/*
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if (!await relatedEntity.Save()) {
|
|
959
|
+
if we later support composite foreign keys, we'll need to do this instead, at the moment this code will break as dependency.KeyValuePair is a single value, not an array
|
|
960
|
+
|
|
961
|
+
for (let pkv of dependency.KeyValuePairs) {
|
|
962
|
+
relatedEntity.Set(dependency.FieldName, pkv.Value);
|
|
963
|
+
}
|
|
964
|
+
*/
|
|
965
|
+
if (!(await relatedEntity.Save())) {
|
|
923
966
|
newRecStatus.Success = false;
|
|
924
967
|
newRecStatus.Message = `Error updating dependency record ${dependency.PrimaryKey.ToString} for entity ${dependency.RelatedEntityName} to point to surviving record ${request.SurvivingRecordCompositeKey.ToString()}`;
|
|
925
968
|
throw new Error(newRecStatus.Message);
|
|
@@ -928,7 +971,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
928
971
|
// if we get here, that means that all of the dependencies were updated successfully, so we can now delete the records to be merged
|
|
929
972
|
const recordToDelete = await this.GetEntityObject(request.EntityName, contextUser);
|
|
930
973
|
await recordToDelete.InnerLoad(pksToDelete);
|
|
931
|
-
if (!await recordToDelete.Delete()) {
|
|
974
|
+
if (!(await recordToDelete.Delete())) {
|
|
932
975
|
newRecStatus.Message = `Error deleting record ${pksToDelete.ToString()} for entity ${request.EntityName}`;
|
|
933
976
|
throw new Error(newRecStatus.Message);
|
|
934
977
|
}
|
|
@@ -947,7 +990,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
947
990
|
await this.RollbackTransaction();
|
|
948
991
|
// attempt to persist the status to the DB, although that might fail
|
|
949
992
|
await this.CompleteMergeLogging(mergeRecordLog, result, contextUser);
|
|
950
|
-
throw
|
|
993
|
+
throw e;
|
|
951
994
|
}
|
|
952
995
|
}
|
|
953
996
|
async StartMergeLogging(request, result, contextUser) {
|
|
@@ -961,7 +1004,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
961
1004
|
throw new Error(`contextUser is null and no CurrentUser is set`);
|
|
962
1005
|
recordMergeLog.NewRecord();
|
|
963
1006
|
recordMergeLog.EntityID = entity.ID;
|
|
964
|
-
recordMergeLog.SurvivingRecordID = request.SurvivingRecordCompositeKey.Values(); // this would join together all of the primary key values, which is fine as the primary key is a string
|
|
1007
|
+
recordMergeLog.SurvivingRecordID = request.SurvivingRecordCompositeKey.Values(); // this would join together all of the primary key values, which is fine as the primary key is a string
|
|
965
1008
|
recordMergeLog.InitiatedByUserID = contextUser ? contextUser.ID : this.CurrentUser?.ID;
|
|
966
1009
|
recordMergeLog.ApprovalStatus = 'Approved';
|
|
967
1010
|
recordMergeLog.ApprovedByUserID = contextUser ? contextUser.ID : this.CurrentUser?.ID;
|
|
@@ -976,7 +1019,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
976
1019
|
}
|
|
977
1020
|
catch (e) {
|
|
978
1021
|
(0, core_1.LogError)(e);
|
|
979
|
-
throw
|
|
1022
|
+
throw e;
|
|
980
1023
|
}
|
|
981
1024
|
}
|
|
982
1025
|
async CompleteMergeLogging(recordMergeLog, result, contextUser) {
|
|
@@ -986,18 +1029,19 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
986
1029
|
throw new Error(`contextUser is null and no CurrentUser is set`);
|
|
987
1030
|
recordMergeLog.ProcessingStatus = result.Success ? 'Complete' : 'Error';
|
|
988
1031
|
recordMergeLog.ProcessingEndedAt = new Date();
|
|
989
|
-
if (!result.Success)
|
|
1032
|
+
if (!result.Success)
|
|
1033
|
+
// only create the log record if the merge failed, otherwise it is wasted space
|
|
990
1034
|
recordMergeLog.ProcessingLog = result.OverallStatus;
|
|
991
1035
|
if (await recordMergeLog.Save()) {
|
|
992
1036
|
// top level saved, now let's create the deletion detail records for each of the records that were merged
|
|
993
1037
|
for (const d of result.RecordStatus) {
|
|
994
|
-
const recordMergeDeletionLog = await this.GetEntityObject('Record Merge Deletion Logs', contextUser);
|
|
1038
|
+
const recordMergeDeletionLog = (await this.GetEntityObject('Record Merge Deletion Logs', contextUser));
|
|
995
1039
|
recordMergeDeletionLog.NewRecord();
|
|
996
1040
|
recordMergeDeletionLog.RecordMergeLogID = recordMergeLog.ID;
|
|
997
1041
|
recordMergeDeletionLog.DeletedRecordID = d.CompositeKey.Values(); // this would join together all of the primary key values, which is fine as the primary key is a string
|
|
998
1042
|
recordMergeDeletionLog.Status = d.Success ? 'Complete' : 'Error';
|
|
999
1043
|
recordMergeDeletionLog.ProcessingLog = d.Success ? null : d.Message; // only save the message if it failed
|
|
1000
|
-
if (!await recordMergeDeletionLog.Save())
|
|
1044
|
+
if (!(await recordMergeDeletionLog.Save()))
|
|
1001
1045
|
throw new Error(`Error saving record merge deletion log`);
|
|
1002
1046
|
}
|
|
1003
1047
|
}
|
|
@@ -1012,13 +1056,14 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1012
1056
|
}
|
|
1013
1057
|
GetSaveSQL(entity, bNewRecord, spName, user) {
|
|
1014
1058
|
const sSimpleSQL = `EXEC [${entity.EntityInfo.SchemaName}].${spName} ${this.generateSPParams(entity, !bNewRecord)}`;
|
|
1015
|
-
const recordChangesEntityInfo = this.Entities.find(e => e.Name === 'Record Changes');
|
|
1059
|
+
const recordChangesEntityInfo = this.Entities.find((e) => e.Name === 'Record Changes');
|
|
1016
1060
|
let sSQL = '';
|
|
1017
|
-
if (entity.EntityInfo.TrackRecordChanges && entity.EntityInfo.Name.trim().toLowerCase() !== 'record changes') {
|
|
1061
|
+
if (entity.EntityInfo.TrackRecordChanges && entity.EntityInfo.Name.trim().toLowerCase() !== 'record changes') {
|
|
1062
|
+
// don't track changes for the record changes entity
|
|
1018
1063
|
let oldData = null;
|
|
1019
1064
|
// use SQL Server CONCAT function to combine all of the primary key values and then combine them together
|
|
1020
1065
|
// using the default field delimiter and default value delimiter as defined in the CompositeKey class
|
|
1021
|
-
const concatPKIDString = `CONCAT(${entity.EntityInfo.PrimaryKeys.map(pk => `'${pk.CodeName}','${core_1.CompositeKey.DefaultValueDelimiter}',${pk.Name}`).join(`,'${core_1.CompositeKey.DefaultFieldDelimiter}',`)})`;
|
|
1066
|
+
const concatPKIDString = `CONCAT(${entity.EntityInfo.PrimaryKeys.map((pk) => `'${pk.CodeName}','${core_1.CompositeKey.DefaultValueDelimiter}',${pk.Name}`).join(`,'${core_1.CompositeKey.DefaultFieldDelimiter}',`)})`;
|
|
1022
1067
|
if (!bNewRecord)
|
|
1023
1068
|
oldData = entity.GetAll(true); // get all the OLD values, only do for existing records, for new records, not relevant
|
|
1024
1069
|
sSQL = `
|
|
@@ -1029,13 +1074,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1029
1074
|
INSERT INTO @ResultTable
|
|
1030
1075
|
${sSimpleSQL}
|
|
1031
1076
|
|
|
1032
|
-
DECLARE @ID NVARCHAR(MAX)
|
|
1077
|
+
DECLARE @ID NVARCHAR(MAX)
|
|
1033
1078
|
SELECT @ID = ${concatPKIDString} FROM @ResultTable
|
|
1034
|
-
IF @ID IS NOT NULL
|
|
1079
|
+
IF @ID IS NOT NULL
|
|
1035
1080
|
BEGIN
|
|
1036
1081
|
DECLARE @ResultChangesTable TABLE (
|
|
1037
|
-
${this.getAllEntityColumnsSQL(recordChangesEntityInfo)}
|
|
1038
|
-
)
|
|
1082
|
+
${this.getAllEntityColumnsSQL(recordChangesEntityInfo)}
|
|
1083
|
+
)
|
|
1039
1084
|
|
|
1040
1085
|
INSERT INTO @ResultChangesTable
|
|
1041
1086
|
${this.GetLogRecordChangeSQL(entity.GetAll(false), oldData, entity.EntityInfo.Name, '@ID', entity.EntityInfo, bNewRecord ? 'Create' : 'Update', user, false)}
|
|
@@ -1050,8 +1095,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1050
1095
|
return sSQL;
|
|
1051
1096
|
}
|
|
1052
1097
|
GetEntityAIActions(entityInfo, before) {
|
|
1053
|
-
return aiengine_1.AIEngine.Instance.EntityAIActions.filter((a) => a.EntityID === entityInfo.ID &&
|
|
1054
|
-
a.TriggerEvent.toLowerCase().trim() === (before ? 'before save' : 'after save'));
|
|
1098
|
+
return aiengine_1.AIEngine.Instance.EntityAIActions.filter((a) => a.EntityID === entityInfo.ID && a.TriggerEvent.toLowerCase().trim() === (before ? 'before save' : 'after save'));
|
|
1055
1099
|
}
|
|
1056
1100
|
async HandleEntityActions(entity, baseType, before, user) {
|
|
1057
1101
|
// use the EntityActionEngine for this
|
|
@@ -1060,7 +1104,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1060
1104
|
await engine.Config(false, user);
|
|
1061
1105
|
const newRecord = entity.IsSaved ? false : true;
|
|
1062
1106
|
const baseTypeType = baseType === 'save' ? (newRecord ? 'Create' : 'Update') : 'Delete';
|
|
1063
|
-
const invocationType = baseType === 'validate' ? 'Validate' :
|
|
1107
|
+
const invocationType = baseType === 'validate' ? 'Validate' : before ? 'Before' + baseTypeType : 'After' + baseTypeType;
|
|
1064
1108
|
const invocationTypeEntity = engine.InvocationTypes.find((i) => i.Name === invocationType);
|
|
1065
1109
|
if (!invocationTypeEntity) {
|
|
1066
1110
|
(0, core_1.LogError)(`Invocation Type ${invocationType} not found in metadata`);
|
|
@@ -1074,7 +1118,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1074
1118
|
EntityAction: a,
|
|
1075
1119
|
EntityObject: entity,
|
|
1076
1120
|
InvocationType: invocationTypeEntity,
|
|
1077
|
-
ContextUser: user
|
|
1121
|
+
ContextUser: user,
|
|
1078
1122
|
});
|
|
1079
1123
|
results.push(result);
|
|
1080
1124
|
}
|
|
@@ -1106,13 +1150,12 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1106
1150
|
const ai = aiengine_1.AIEngine.Instance;
|
|
1107
1151
|
for (let i = 0; i < actions.length; i++) {
|
|
1108
1152
|
const a = actions[i];
|
|
1109
|
-
if (a.TriggerEvent === 'before save' && before ||
|
|
1110
|
-
a.TriggerEvent === 'after save' && !before) {
|
|
1153
|
+
if ((a.TriggerEvent === 'before save' && before) || (a.TriggerEvent === 'after save' && !before)) {
|
|
1111
1154
|
const p = {
|
|
1112
1155
|
entityAIActionId: a.ID,
|
|
1113
1156
|
entityRecord: entity,
|
|
1114
1157
|
actionId: a.AIActionID,
|
|
1115
|
-
modelId: a.AIModelID
|
|
1158
|
+
modelId: a.AIModelID,
|
|
1116
1159
|
};
|
|
1117
1160
|
if (before) {
|
|
1118
1161
|
// do it with await so we're blocking, as it needs to complete before the record save continues
|
|
@@ -1154,11 +1197,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1154
1197
|
else {
|
|
1155
1198
|
// getting here means we are good to save, now check to see if we're dirty and need to save
|
|
1156
1199
|
// REMEMBER - this is the provider and the BaseEntity/subclasses handle user-level permission checking already, we just make sure API was turned on for the operation
|
|
1157
|
-
if (entity.Dirty ||
|
|
1200
|
+
if (entity.Dirty || options.IgnoreDirtyState || options.ReplayOnly) {
|
|
1158
1201
|
entityResult.StartedAt = new Date();
|
|
1159
1202
|
entityResult.Type = bNewRecord ? 'create' : 'update';
|
|
1160
|
-
entityResult.OriginalValues = entity.Fields.map(f => {
|
|
1161
|
-
|
|
1203
|
+
entityResult.OriginalValues = entity.Fields.map((f) => {
|
|
1204
|
+
return { FieldName: f.Name, Value: f.Value };
|
|
1205
|
+
}); // save the original values before we start the process
|
|
1206
|
+
entity.ResultHistory.push(entityResult); // push the new result as we have started a process
|
|
1162
1207
|
// The assumption is that Validate() has already been called by the BaseEntity object that is invoking this provider.
|
|
1163
1208
|
// However, we have an extra responsibility in this situation which is to fire off the EntityActions for the Validate invocation type and
|
|
1164
1209
|
// make sure they clear. If they don't clear we throw an exception with the message provided.
|
|
@@ -1166,7 +1211,10 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1166
1211
|
const validationResult = await this.HandleEntityActions(entity, 'validate', false, user);
|
|
1167
1212
|
if (validationResult && validationResult.length > 0) {
|
|
1168
1213
|
// one or more actions executed, see the reults and if any failed, concat their messages and return as exception being thrown
|
|
1169
|
-
const message = validationResult
|
|
1214
|
+
const message = validationResult
|
|
1215
|
+
.filter((v) => !v.Success)
|
|
1216
|
+
.map((v) => v.Message)
|
|
1217
|
+
.join('\n\n');
|
|
1170
1218
|
if (message) {
|
|
1171
1219
|
entityResult.Success = false;
|
|
1172
1220
|
entityResult.EndedAt = new Date();
|
|
@@ -1178,8 +1226,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1178
1226
|
else {
|
|
1179
1227
|
// we are in replay mode we so do NOT need to do the validation stuff, skipping it...
|
|
1180
1228
|
}
|
|
1181
|
-
const spName = bNewRecord
|
|
1182
|
-
|
|
1229
|
+
const spName = bNewRecord
|
|
1230
|
+
? entity.EntityInfo.spCreate && entity.EntityInfo.spCreate.length > 0
|
|
1231
|
+
? entity.EntityInfo.spCreate
|
|
1232
|
+
: 'spCreate' + entity.EntityInfo.BaseTable
|
|
1233
|
+
: entity.EntityInfo.spUpdate && entity.EntityInfo.spUpdate.length > 0
|
|
1234
|
+
? entity.EntityInfo.spUpdate
|
|
1235
|
+
: 'spUpdate' + entity.EntityInfo.BaseTable;
|
|
1183
1236
|
if (options.SkipEntityActions !== true /*options set, but not set to skip entity actions*/) {
|
|
1184
1237
|
await this.HandleEntityActions(entity, 'save', true, user);
|
|
1185
1238
|
}
|
|
@@ -1198,7 +1251,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1198
1251
|
this._bAllowRefresh = false; // stop refreshes of metadata while we're doing work
|
|
1199
1252
|
entity.TransactionGroup.AddTransaction(new core_1.TransactionItem(entity, sSQL, null, { dataSource: this._dataSource }, (results, success) => {
|
|
1200
1253
|
// we get here whenever the transaction group does gets around to committing
|
|
1201
|
-
// our query.
|
|
1254
|
+
// our query.
|
|
1202
1255
|
this._bAllowRefresh = true; // allow refreshes again
|
|
1203
1256
|
entityResult.EndedAt = new Date();
|
|
1204
1257
|
if (success && results) {
|
|
@@ -1214,7 +1267,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1214
1267
|
resolve(results[0]);
|
|
1215
1268
|
}
|
|
1216
1269
|
else {
|
|
1217
|
-
// the transaction failed, nothing to update, but we need to call Reject so the
|
|
1270
|
+
// the transaction failed, nothing to update, but we need to call Reject so the
|
|
1218
1271
|
// promise resolves with a rejection so our outer caller knows
|
|
1219
1272
|
entityResult.Success = false;
|
|
1220
1273
|
entityResult.Message = 'Transaction Failed';
|
|
@@ -1276,18 +1329,18 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1276
1329
|
const f = entity.EntityInfo.Fields[i];
|
|
1277
1330
|
if (f.AllowUpdateAPI) {
|
|
1278
1331
|
if (!f.SkipValidation) {
|
|
1279
|
-
// DO NOT INCLUDE any fields where we skip validation, these are fields that are not editable by the user/object
|
|
1332
|
+
// DO NOT INCLUDE any fields where we skip validation, these are fields that are not editable by the user/object
|
|
1280
1333
|
// model/api because they're special fields like ID, CreatedAt, etc. or they're virtual or auto-increment, etc.
|
|
1281
1334
|
let value = entity.Get(f.Name);
|
|
1282
1335
|
if (f.Type.trim().toLowerCase() === 'datetimeoffset') {
|
|
1283
1336
|
value = new Date(value).toISOString();
|
|
1284
1337
|
}
|
|
1285
1338
|
else if (!isUpdate && f.Type.trim().toLowerCase() === 'uniqueidentifier') {
|
|
1286
|
-
// in the case of unique identifiers, for CREATE procs only,
|
|
1339
|
+
// in the case of unique identifiers, for CREATE procs only,
|
|
1287
1340
|
// we need to check to see if the value we have in the entity object is a function like newid() or newsquentialid()
|
|
1288
|
-
// in those cases we should just skip the parameter entirely because that means there is a default value that should be used
|
|
1341
|
+
// in those cases we should just skip the parameter entirely because that means there is a default value that should be used
|
|
1289
1342
|
// and that will be handled by the database not by us
|
|
1290
|
-
// instead of just checking for specific functions like newid(), we can instead check for any string that includes ()
|
|
1343
|
+
// instead of just checking for specific functions like newid(), we can instead check for any string that includes ()
|
|
1291
1344
|
// this way we can handle any function that the database might support in the future
|
|
1292
1345
|
if (typeof value === 'string' && value.includes('()')) {
|
|
1293
1346
|
continue; // skip this field entirely by going to the next iteration of the loop
|
|
@@ -1362,7 +1415,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1362
1415
|
}
|
|
1363
1416
|
else
|
|
1364
1417
|
pVal = paramValue;
|
|
1365
|
-
return
|
|
1418
|
+
return paramValue === null || paramValue === undefined ? 'NULL' : quoteString + pVal + quoteString;
|
|
1366
1419
|
}
|
|
1367
1420
|
GetLogRecordChangeSQL(newData, oldData, entityName, recordID, entityInfo, type, user, wrapRecordIdInQuotes) {
|
|
1368
1421
|
const fullRecordJSON = JSON.stringify(this.escapeQuotesInProperties(newData ? newData : oldData, "'")); // stringify old data if we don't have new - means we are DELETING A RECORD
|
|
@@ -1371,14 +1424,14 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1371
1424
|
if (changesKeys.length > 0 || oldData === null /*new record*/ || newData === null /*deleted record*/) {
|
|
1372
1425
|
const changesJSON = changes !== null ? JSON.stringify(changes) : '';
|
|
1373
1426
|
const quotes = wrapRecordIdInQuotes ? "'" : '';
|
|
1374
|
-
const sSQL = `EXEC [${this.MJCoreSchemaName}].spCreateRecordChange_Internal @EntityName='${entityName}',
|
|
1375
|
-
@RecordID=${quotes}${recordID}${quotes},
|
|
1427
|
+
const sSQL = `EXEC [${this.MJCoreSchemaName}].spCreateRecordChange_Internal @EntityName='${entityName}',
|
|
1428
|
+
@RecordID=${quotes}${recordID}${quotes},
|
|
1376
1429
|
@UserID='${user.ID}',
|
|
1377
1430
|
@Type='${type}',
|
|
1378
|
-
@ChangesJSON='${changesJSON}',
|
|
1379
|
-
@ChangesDescription='${oldData && newData ? this.CreateUserDescriptionOfChanges(changes) : !oldData ? 'Record Created' : 'Record Deleted'}',
|
|
1380
|
-
@FullRecordJSON='${fullRecordJSON}',
|
|
1381
|
-
@Status='Complete',
|
|
1431
|
+
@ChangesJSON='${changesJSON}',
|
|
1432
|
+
@ChangesDescription='${oldData && newData ? this.CreateUserDescriptionOfChanges(changes) : !oldData ? 'Record Created' : 'Record Deleted'}',
|
|
1433
|
+
@FullRecordJSON='${fullRecordJSON}',
|
|
1434
|
+
@Status='Complete',
|
|
1382
1435
|
@Comments=null`;
|
|
1383
1436
|
return sSQL;
|
|
1384
1437
|
}
|
|
@@ -1407,11 +1460,14 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1407
1460
|
if (sRet.length > 0) {
|
|
1408
1461
|
sRet += '\n';
|
|
1409
1462
|
}
|
|
1410
|
-
if (change.oldValue && change.newValue)
|
|
1463
|
+
if (change.oldValue && change.newValue)
|
|
1464
|
+
// both old and new values set, show change
|
|
1411
1465
|
sRet += `${change.field} changed from ${this.trimString(change.oldValue, maxValueLength, cutOffText)} to ${this.trimString(change.newValue, maxValueLength, cutOffText)}`;
|
|
1412
|
-
else if (change.newValue)
|
|
1466
|
+
else if (change.newValue)
|
|
1467
|
+
// old value was blank, new value isn't
|
|
1413
1468
|
sRet += `${change.field} set to ${this.trimString(change.newValue, maxValueLength, cutOffText)}`;
|
|
1414
|
-
else if (change.oldValue)
|
|
1469
|
+
else if (change.oldValue)
|
|
1470
|
+
// new value is blank, old value wasn't
|
|
1415
1471
|
sRet += `${change.field} cleared from ${this.trimString(change.oldValue, maxValueLength, cutOffText)}`;
|
|
1416
1472
|
}
|
|
1417
1473
|
return sRet.replace(/'/g, "''");
|
|
@@ -1451,12 +1507,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1451
1507
|
else {
|
|
1452
1508
|
const changes = {};
|
|
1453
1509
|
for (const key in newData) {
|
|
1454
|
-
const f = entityInfo.Fields.find(f => f.Name.toLowerCase() === key.toLowerCase());
|
|
1510
|
+
const f = entityInfo.Fields.find((f) => f.Name.toLowerCase() === key.toLowerCase());
|
|
1455
1511
|
let bDiff = false;
|
|
1456
1512
|
if (f.ReadOnly)
|
|
1457
1513
|
bDiff = false; // read only fields are never different, they can change in the database, but we don't consider them to be a change for record changes purposes.
|
|
1458
|
-
else if ((oldData[key] == undefined || oldData[key] == null) &&
|
|
1459
|
-
(newData[key] == undefined || newData[key] == null))
|
|
1514
|
+
else if ((oldData[key] == undefined || oldData[key] == null) && (newData[key] == undefined || newData[key] == null))
|
|
1460
1515
|
bDiff = false; // this branch of logic ensures that undefined and null are treated the same
|
|
1461
1516
|
else {
|
|
1462
1517
|
switch (f.TSType) {
|
|
@@ -1464,7 +1519,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1464
1519
|
bDiff = oldData[key] !== newData[key];
|
|
1465
1520
|
break;
|
|
1466
1521
|
case core_1.EntityFieldTSType.Date:
|
|
1467
|
-
bDiff =
|
|
1522
|
+
bDiff = new Date(oldData[key]).getTime() !== new Date(newData[key]).getTime();
|
|
1468
1523
|
break;
|
|
1469
1524
|
case core_1.EntityFieldTSType.Number:
|
|
1470
1525
|
case core_1.EntityFieldTSType.Boolean:
|
|
@@ -1475,14 +1530,12 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1475
1530
|
if (bDiff) {
|
|
1476
1531
|
// make sure we escape things properly
|
|
1477
1532
|
const r = new RegExp(quoteToEscape, 'g');
|
|
1478
|
-
const o =
|
|
1479
|
-
|
|
1480
|
-
const n = (newData[key] && typeof newData[key] === 'string') ?
|
|
1481
|
-
newData[key].replace(r, quoteToEscape + quoteToEscape) : newData[key];
|
|
1533
|
+
const o = oldData[key] && typeof oldData[key] === 'string' ? oldData[key].replace(r, quoteToEscape + quoteToEscape) : oldData[key];
|
|
1534
|
+
const n = newData[key] && typeof newData[key] === 'string' ? newData[key].replace(r, quoteToEscape + quoteToEscape) : newData[key];
|
|
1482
1535
|
changes[key] = {
|
|
1483
1536
|
field: key,
|
|
1484
1537
|
oldValue: o,
|
|
1485
|
-
newValue: n
|
|
1538
|
+
newValue: n,
|
|
1486
1539
|
};
|
|
1487
1540
|
}
|
|
1488
1541
|
}
|
|
@@ -1505,28 +1558,29 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1505
1558
|
if (EntityRelationshipsToLoad && EntityRelationshipsToLoad.length > 0) {
|
|
1506
1559
|
for (let i = 0; i < EntityRelationshipsToLoad.length; i++) {
|
|
1507
1560
|
const rel = EntityRelationshipsToLoad[i];
|
|
1508
|
-
const relInfo = entity.EntityInfo.RelatedEntities.find(r => r.RelatedEntity == rel);
|
|
1561
|
+
const relInfo = entity.EntityInfo.RelatedEntities.find((r) => r.RelatedEntity == rel);
|
|
1509
1562
|
if (relInfo) {
|
|
1510
1563
|
let relSql = '';
|
|
1511
|
-
const relEntitySchemaName = this.Entities.find(e => e.Name.trim().toLowerCase() === relInfo.RelatedEntity.trim().toLowerCase())?.SchemaName;
|
|
1564
|
+
const relEntitySchemaName = this.Entities.find((e) => e.Name.trim().toLowerCase() === relInfo.RelatedEntity.trim().toLowerCase())?.SchemaName;
|
|
1512
1565
|
const quotes = entity.FirstPrimaryKey.NeedsQuotes ? "'" : '';
|
|
1513
1566
|
if (relInfo.Type.trim().toLowerCase() === 'one to many')
|
|
1514
1567
|
// one to many - simple query
|
|
1515
|
-
relSql = ` SELECT
|
|
1516
|
-
*
|
|
1517
|
-
FROM
|
|
1518
|
-
[${relEntitySchemaName}].[${relInfo.RelatedEntityBaseView}]
|
|
1519
|
-
WHERE
|
|
1520
|
-
[${relInfo.RelatedEntityJoinField}] = ${quotes}${ret[entity.FirstPrimaryKey.Name]}${quotes}`;
|
|
1568
|
+
relSql = ` SELECT
|
|
1569
|
+
*
|
|
1570
|
+
FROM
|
|
1571
|
+
[${relEntitySchemaName}].[${relInfo.RelatedEntityBaseView}]
|
|
1572
|
+
WHERE
|
|
1573
|
+
[${relInfo.RelatedEntityJoinField}] = ${quotes}${ret[entity.FirstPrimaryKey.Name]}${quotes}`;
|
|
1574
|
+
// don't yet support composite foreign keys
|
|
1575
|
+
// many to many - need to use join view
|
|
1521
1576
|
else
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
WHERE
|
|
1577
|
+
relSql = ` SELECT
|
|
1578
|
+
_theview.*
|
|
1579
|
+
FROM
|
|
1580
|
+
[${relEntitySchemaName}].[${relInfo.RelatedEntityBaseView}] _theview
|
|
1581
|
+
INNER JOIN
|
|
1582
|
+
[${relEntitySchemaName}].[${relInfo.JoinView}] _jv ON _theview.[${relInfo.RelatedEntityJoinField}] = _jv.[${relInfo.JoinEntityInverseJoinField}]
|
|
1583
|
+
WHERE
|
|
1530
1584
|
_jv.${relInfo.JoinEntityJoinField} = ${quotes}${ret[entity.FirstPrimaryKey.Name]}${quotes}`; // don't yet support composite foreign keys
|
|
1531
1585
|
const relData = await this.ExecuteSQL(relSql);
|
|
1532
1586
|
if (relData && relData.length > 0) {
|
|
@@ -1549,8 +1603,9 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1549
1603
|
return `@${f.CodeName}=${quotes}${kv.Value}${quotes}`;
|
|
1550
1604
|
}).join(', ');
|
|
1551
1605
|
const sSimpleSQL = `EXEC [${entity.EntityInfo.SchemaName}].[${spName}] ${sParams}`;
|
|
1552
|
-
const recordChangesEntityInfo = this.Entities.find(e => e.Name === 'Record Changes');
|
|
1553
|
-
if (entity.EntityInfo.TrackRecordChanges && entity.EntityInfo.Name.trim().toLowerCase() !== 'record changes') {
|
|
1606
|
+
const recordChangesEntityInfo = this.Entities.find((e) => e.Name === 'Record Changes');
|
|
1607
|
+
if (entity.EntityInfo.TrackRecordChanges && entity.EntityInfo.Name.trim().toLowerCase() !== 'record changes') {
|
|
1608
|
+
// don't track changes for the record changes entity
|
|
1554
1609
|
const oldData = entity.GetAll(true); // get all the OLD values
|
|
1555
1610
|
const sTableDeclare = entity.PrimaryKeys.map((pk) => {
|
|
1556
1611
|
return `${pk.CodeName} ${pk.EntityFieldInfo.SQLFullType}`;
|
|
@@ -1581,11 +1636,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1581
1636
|
|
|
1582
1637
|
DECLARE ${sVariableDeclare}
|
|
1583
1638
|
SELECT ${sSelectDeclare} FROM @ResultTable
|
|
1584
|
-
IF ${sIF}
|
|
1639
|
+
IF ${sIF}
|
|
1585
1640
|
BEGIN
|
|
1586
1641
|
DECLARE @ResultChangesTable TABLE (
|
|
1587
|
-
${this.getAllEntityColumnsSQL(recordChangesEntityInfo)}
|
|
1588
|
-
)
|
|
1642
|
+
${this.getAllEntityColumnsSQL(recordChangesEntityInfo)}
|
|
1643
|
+
)
|
|
1589
1644
|
|
|
1590
1645
|
INSERT INTO @ResultChangesTable
|
|
1591
1646
|
${this.GetLogRecordChangeSQL(null /*pass in null for new data for deleted records*/, oldData, entity.EntityInfo.Name, sCombinedPrimaryKey, entity.EntityInfo, 'Delete', user, true)}
|
|
@@ -1615,7 +1670,9 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1615
1670
|
throw new Error(`Delete() isn't callable for ${entity.EntityInfo.Name} as AllowDeleteAPI is false`);
|
|
1616
1671
|
result.StartedAt = new Date();
|
|
1617
1672
|
result.Type = 'delete';
|
|
1618
|
-
result.OriginalValues = entity.Fields.map(f => {
|
|
1673
|
+
result.OriginalValues = entity.Fields.map((f) => {
|
|
1674
|
+
return { FieldName: f.Name, Value: f.Value };
|
|
1675
|
+
}); // save the original values before we start the process
|
|
1619
1676
|
entity.ResultHistory.push(result); // push the new result as we have started a process
|
|
1620
1677
|
// REMEMBER - this is the provider and the BaseEntity/subclasses handle user-level permission checking already, we just make sure API was turned on for the operation
|
|
1621
1678
|
// if we get here we can delete, so build the SQL and then handle appropriately either as part of TransGroup or directly...
|
|
@@ -1633,7 +1690,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1633
1690
|
// and when the transaction is committed, we will send all the queries at once
|
|
1634
1691
|
entity.TransactionGroup.AddTransaction(new core_1.TransactionItem(entity, sSQL, null, { dataSource: this._dataSource }, (results, success) => {
|
|
1635
1692
|
// we get here whenever the transaction group does gets around to committing
|
|
1636
|
-
// our query.
|
|
1693
|
+
// our query.
|
|
1637
1694
|
result.EndedAt = new Date();
|
|
1638
1695
|
if (success && results) {
|
|
1639
1696
|
// Entity AI Actions and Actions - fired off async, NO await on purpose
|
|
@@ -1652,9 +1709,9 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1652
1709
|
result.Success = true;
|
|
1653
1710
|
resolve(true);
|
|
1654
1711
|
}
|
|
1712
|
+
// the transaction failed, nothing to update, but we need to call Reject so the
|
|
1713
|
+
// promise resolves with a rejection so our outer caller knows
|
|
1655
1714
|
else
|
|
1656
|
-
// the transaction failed, nothing to update, but we need to call Reject so the
|
|
1657
|
-
// promise resolves with a rejection so our outer caller knows
|
|
1658
1715
|
result.Success = false;
|
|
1659
1716
|
result.Message = 'Transaction failed to commit';
|
|
1660
1717
|
reject(results);
|
|
@@ -1701,23 +1758,23 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1701
1758
|
// START ---- IMetadataProvider
|
|
1702
1759
|
/**************************************************************************/
|
|
1703
1760
|
async GetDatasetByName(datasetName, itemFilters) {
|
|
1704
|
-
const sSQL = `SELECT
|
|
1761
|
+
const sSQL = `SELECT
|
|
1705
1762
|
di.*,
|
|
1706
1763
|
e.BaseView EntityBaseView,
|
|
1707
1764
|
e.SchemaName EntitySchemaName,
|
|
1708
1765
|
di.__mj_UpdatedAt AS DatasetItemUpdatedAt,
|
|
1709
1766
|
d.__mj_UpdatedAt AS DatasetUpdatedAt
|
|
1710
|
-
FROM
|
|
1711
|
-
[${this.MJCoreSchemaName}].vwDatasets d
|
|
1712
|
-
INNER JOIN
|
|
1713
|
-
[${this.MJCoreSchemaName}].vwDatasetItems di
|
|
1767
|
+
FROM
|
|
1768
|
+
[${this.MJCoreSchemaName}].vwDatasets d
|
|
1769
|
+
INNER JOIN
|
|
1770
|
+
[${this.MJCoreSchemaName}].vwDatasetItems di
|
|
1714
1771
|
ON
|
|
1715
1772
|
d.ID = di.DatasetID
|
|
1716
|
-
INNER JOIN
|
|
1717
|
-
[${this.MJCoreSchemaName}].vwEntities e
|
|
1773
|
+
INNER JOIN
|
|
1774
|
+
[${this.MJCoreSchemaName}].vwEntities e
|
|
1718
1775
|
ON
|
|
1719
1776
|
di.EntityID = e.ID
|
|
1720
|
-
WHERE
|
|
1777
|
+
WHERE
|
|
1721
1778
|
d.Name = @0`;
|
|
1722
1779
|
const items = await this.ExecuteSQL(sSQL, [datasetName]);
|
|
1723
1780
|
// now we have the dataset and the items, we need to get the update date from the items underlying entities
|
|
@@ -1729,7 +1786,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1729
1786
|
// execute all promises in parallel
|
|
1730
1787
|
const results = await Promise.all(promises);
|
|
1731
1788
|
// determine overall success
|
|
1732
|
-
const bSuccess = results.every(result => result.Success);
|
|
1789
|
+
const bSuccess = results.every((result) => result.Success);
|
|
1733
1790
|
// get the latest update date from all the results
|
|
1734
1791
|
const latestUpdateDate = results.reduce((acc, result) => {
|
|
1735
1792
|
if (result.LatestUpdatedDate && result.LatestUpdatedDate > acc) {
|
|
@@ -1743,12 +1800,12 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1743
1800
|
Success: bSuccess,
|
|
1744
1801
|
Status: '',
|
|
1745
1802
|
LatestUpdateDate: latestUpdateDate,
|
|
1746
|
-
Results: results
|
|
1803
|
+
Results: results,
|
|
1747
1804
|
};
|
|
1748
1805
|
}
|
|
1749
1806
|
else {
|
|
1750
1807
|
return {
|
|
1751
|
-
DatasetID:
|
|
1808
|
+
DatasetID: '',
|
|
1752
1809
|
DatasetName: datasetName,
|
|
1753
1810
|
Success: false,
|
|
1754
1811
|
Status: 'No Dataset or Items found for DatasetName: ' + datasetName,
|
|
@@ -1760,7 +1817,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1760
1817
|
async GetDatasetItem(item, itemFilters, datasetName) {
|
|
1761
1818
|
let filterSQL = '';
|
|
1762
1819
|
if (itemFilters && itemFilters.length > 0) {
|
|
1763
|
-
const filter = itemFilters.find(f => f.ItemCode === item.Code);
|
|
1820
|
+
const filter = itemFilters.find((f) => f.ItemCode === item.Code);
|
|
1764
1821
|
if (filter)
|
|
1765
1822
|
filterSQL = (item.WhereClause ? ' AND ' : ' WHERE ') + '(' + filter.Filter + ')';
|
|
1766
1823
|
}
|
|
@@ -1777,7 +1834,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1777
1834
|
Results: null,
|
|
1778
1835
|
LatestUpdateDate: null,
|
|
1779
1836
|
Status: 'Invalid columns specified for dataset item',
|
|
1780
|
-
Success: false
|
|
1837
|
+
Success: false,
|
|
1781
1838
|
};
|
|
1782
1839
|
}
|
|
1783
1840
|
const itemSQL = `SELECT ${columns} FROM [${item.EntitySchemaName}].[${item.EntityBaseView}] ${item.WhereClause ? 'WHERE ' + item.WhereClause : ''}${filterSQL}`;
|
|
@@ -1801,7 +1858,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1801
1858
|
Code: item.Code,
|
|
1802
1859
|
Results: itemData,
|
|
1803
1860
|
LatestUpdateDate: latestUpdateDate,
|
|
1804
|
-
Success: itemData !== null && itemData !== undefined
|
|
1861
|
+
Success: itemData !== null && itemData !== undefined,
|
|
1805
1862
|
};
|
|
1806
1863
|
}
|
|
1807
1864
|
/**
|
|
@@ -1812,10 +1869,10 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1812
1869
|
* @returns
|
|
1813
1870
|
*/
|
|
1814
1871
|
GetColumnsForDatasetItem(item, datasetName) {
|
|
1815
|
-
const specifiedColumns = item.Columns ? item.Columns.split(',').map(col => col.trim()) : [];
|
|
1872
|
+
const specifiedColumns = item.Columns ? item.Columns.split(',').map((col) => col.trim()) : [];
|
|
1816
1873
|
if (specifiedColumns.length > 0) {
|
|
1817
|
-
// validate that the columns specified are valid within the entity metadata
|
|
1818
|
-
const entity = this.Entities.find(e => e.ID === item.EntityID);
|
|
1874
|
+
// validate that the columns specified are valid within the entity metadata
|
|
1875
|
+
const entity = this.Entities.find((e) => e.ID === item.EntityID);
|
|
1819
1876
|
if (!entity && this.Entities.length > 0) {
|
|
1820
1877
|
// we have loaded entities (e.g. Entites.length > 0) but the entity wasn't found, log an error and return a failed result
|
|
1821
1878
|
// the reason we continue below if we have NOT loaded Entities is that when the system first bootstraps, DATASET gets loaded
|
|
@@ -1826,11 +1883,11 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1826
1883
|
}
|
|
1827
1884
|
else {
|
|
1828
1885
|
if (entity) {
|
|
1829
|
-
// have a valid entity, now make sure that all of the columns specified are valid
|
|
1886
|
+
// have a valid entity, now make sure that all of the columns specified are valid
|
|
1830
1887
|
// only do the column validity check if we have an entity, we can get here if the entity wasn't found IF we haven't loaded entities yet per above comment
|
|
1831
1888
|
const invalidColumns = [];
|
|
1832
1889
|
specifiedColumns.forEach((col) => {
|
|
1833
|
-
if (!entity.Fields.find(f => f.Name.trim().toLowerCase() === col.trim().toLowerCase())) {
|
|
1890
|
+
if (!entity.Fields.find((f) => f.Name.trim().toLowerCase() === col.trim().toLowerCase())) {
|
|
1834
1891
|
invalidColumns.push(col);
|
|
1835
1892
|
}
|
|
1836
1893
|
});
|
|
@@ -1841,35 +1898,34 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1841
1898
|
}
|
|
1842
1899
|
// check to see if the specified columns include the DateFieldToCheck
|
|
1843
1900
|
// in the below we only check entity metadata if we have it, if we don't have it, we just add the special fields back in
|
|
1844
|
-
if (item.DateFieldToCheck && item.DateFieldToCheck.trim().length > 0 &&
|
|
1845
|
-
specifiedColumns.indexOf(item.DateFieldToCheck) === -1) {
|
|
1901
|
+
if (item.DateFieldToCheck && item.DateFieldToCheck.trim().length > 0 && specifiedColumns.indexOf(item.DateFieldToCheck) === -1) {
|
|
1846
1902
|
// we only check the entity if we have it, otherwise we just add it back in
|
|
1847
|
-
if (!entity || entity.Fields.find(f => f.Name.trim().toLowerCase() === item.DateFieldToCheck.trim().toLowerCase()))
|
|
1903
|
+
if (!entity || entity.Fields.find((f) => f.Name.trim().toLowerCase() === item.DateFieldToCheck.trim().toLowerCase()))
|
|
1848
1904
|
specifiedColumns.push(item.DateFieldToCheck);
|
|
1849
1905
|
}
|
|
1850
1906
|
}
|
|
1851
1907
|
}
|
|
1852
|
-
return specifiedColumns.length > 0 ? specifiedColumns.map(colName => `[${colName.trim()}]`).join(',') : '*';
|
|
1908
|
+
return specifiedColumns.length > 0 ? specifiedColumns.map((colName) => `[${colName.trim()}]`).join(',') : '*';
|
|
1853
1909
|
}
|
|
1854
1910
|
async GetDatasetStatusByName(datasetName, itemFilters) {
|
|
1855
1911
|
const sSQL = `
|
|
1856
|
-
SELECT
|
|
1912
|
+
SELECT
|
|
1857
1913
|
di.*,
|
|
1858
1914
|
e.BaseView EntityBaseView,
|
|
1859
1915
|
e.SchemaName EntitySchemaName,
|
|
1860
1916
|
d.__mj_UpdatedAt AS DatasetUpdatedAt,
|
|
1861
|
-
di.__mj_UpdatedAt AS DatasetItemUpdatedAt
|
|
1862
|
-
FROM
|
|
1863
|
-
[${this.MJCoreSchemaName}].vwDatasets d
|
|
1864
|
-
INNER JOIN
|
|
1865
|
-
[${this.MJCoreSchemaName}].vwDatasetItems di
|
|
1917
|
+
di.__mj_UpdatedAt AS DatasetItemUpdatedAt
|
|
1918
|
+
FROM
|
|
1919
|
+
[${this.MJCoreSchemaName}].vwDatasets d
|
|
1920
|
+
INNER JOIN
|
|
1921
|
+
[${this.MJCoreSchemaName}].vwDatasetItems di
|
|
1866
1922
|
ON
|
|
1867
1923
|
d.ID = di.DatasetID
|
|
1868
1924
|
INNER JOIN
|
|
1869
1925
|
[${this.MJCoreSchemaName}].vwEntities e
|
|
1870
1926
|
ON
|
|
1871
1927
|
di.EntityID = e.ID
|
|
1872
|
-
WHERE
|
|
1928
|
+
WHERE
|
|
1873
1929
|
d.Name = @0`;
|
|
1874
1930
|
const items = await this.ExecuteSQL(sSQL, [datasetName]);
|
|
1875
1931
|
// now we have the dataset and the items, we need to get the update date from the items underlying entities
|
|
@@ -1880,22 +1936,22 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1880
1936
|
items.forEach((item, index) => {
|
|
1881
1937
|
let filterSQL = '';
|
|
1882
1938
|
if (itemFilters && itemFilters.length > 0) {
|
|
1883
|
-
const filter = itemFilters.find(f => f.ItemCode === item.Code);
|
|
1939
|
+
const filter = itemFilters.find((f) => f.ItemCode === item.Code);
|
|
1884
1940
|
if (filter)
|
|
1885
1941
|
filterSQL = ' WHERE ' + filter.Filter;
|
|
1886
1942
|
}
|
|
1887
1943
|
const itemUpdatedAt = new Date(item.DatasetItemUpdatedAt);
|
|
1888
1944
|
const datasetUpdatedAt = new Date(item.DatasetUpdatedAt);
|
|
1889
1945
|
const datasetMaxUpdatedAt = new Date(Math.max(itemUpdatedAt.getTime(), datasetUpdatedAt.getTime())).toISOString();
|
|
1890
|
-
const itemSQL = `SELECT
|
|
1891
|
-
CASE
|
|
1892
|
-
WHEN MAX(${item.DateFieldToCheck}) > '${datasetMaxUpdatedAt}' THEN MAX(${item.DateFieldToCheck})
|
|
1893
|
-
ELSE '${datasetMaxUpdatedAt}'
|
|
1946
|
+
const itemSQL = `SELECT
|
|
1947
|
+
CASE
|
|
1948
|
+
WHEN MAX(${item.DateFieldToCheck}) > '${datasetMaxUpdatedAt}' THEN MAX(${item.DateFieldToCheck})
|
|
1949
|
+
ELSE '${datasetMaxUpdatedAt}'
|
|
1894
1950
|
END AS UpdateDate,
|
|
1895
|
-
COUNT(*) AS TheRowCount,
|
|
1896
|
-
'${item.EntityID}' AS EntityID,
|
|
1897
|
-
'${item.Entity}' AS EntityName
|
|
1898
|
-
FROM
|
|
1951
|
+
COUNT(*) AS TheRowCount,
|
|
1952
|
+
'${item.EntityID}' AS EntityID,
|
|
1953
|
+
'${item.Entity}' AS EntityName
|
|
1954
|
+
FROM
|
|
1899
1955
|
[${item.EntitySchemaName}].[${item.EntityBaseView}]${filterSQL}`;
|
|
1900
1956
|
combinedSQL += itemSQL;
|
|
1901
1957
|
if (index < items.length - 1) {
|
|
@@ -1905,13 +1961,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1905
1961
|
const itemUpdateDates = await this.ExecuteSQL(combinedSQL);
|
|
1906
1962
|
if (itemUpdateDates && itemUpdateDates.length > 0) {
|
|
1907
1963
|
let latestUpdateDate = new Date(1900, 1, 1);
|
|
1908
|
-
itemUpdateDates.forEach(itemUpdate => {
|
|
1964
|
+
itemUpdateDates.forEach((itemUpdate) => {
|
|
1909
1965
|
const updateDate = new Date(itemUpdate.UpdateDate);
|
|
1910
1966
|
updateDates.push({
|
|
1911
1967
|
EntityID: itemUpdate.EntityID,
|
|
1912
1968
|
EntityName: itemUpdate.EntityName,
|
|
1913
1969
|
RowCount: itemUpdate.TheRowCount,
|
|
1914
|
-
UpdateDate: updateDate
|
|
1970
|
+
UpdateDate: updateDate,
|
|
1915
1971
|
});
|
|
1916
1972
|
if (updateDate > latestUpdateDate) {
|
|
1917
1973
|
latestUpdateDate = updateDate;
|
|
@@ -1923,7 +1979,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1923
1979
|
Success: true,
|
|
1924
1980
|
Status: '',
|
|
1925
1981
|
LatestUpdateDate: latestUpdateDate,
|
|
1926
|
-
EntityUpdateDates: updateDates
|
|
1982
|
+
EntityUpdateDates: updateDates,
|
|
1927
1983
|
};
|
|
1928
1984
|
}
|
|
1929
1985
|
else {
|
|
@@ -1933,18 +1989,18 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1933
1989
|
Success: false,
|
|
1934
1990
|
Status: 'No update dates found for DatasetName: ' + datasetName,
|
|
1935
1991
|
LatestUpdateDate: null,
|
|
1936
|
-
EntityUpdateDates: null
|
|
1992
|
+
EntityUpdateDates: null,
|
|
1937
1993
|
};
|
|
1938
1994
|
}
|
|
1939
1995
|
}
|
|
1940
1996
|
else {
|
|
1941
1997
|
return {
|
|
1942
|
-
DatasetID:
|
|
1998
|
+
DatasetID: '',
|
|
1943
1999
|
DatasetName: datasetName,
|
|
1944
2000
|
Success: false,
|
|
1945
2001
|
Status: 'No Dataset or Items found for DatasetName: ' + datasetName,
|
|
1946
2002
|
EntityUpdateDates: null,
|
|
1947
|
-
LatestUpdateDate: null
|
|
2003
|
+
LatestUpdateDate: null,
|
|
1948
2004
|
};
|
|
1949
2005
|
}
|
|
1950
2006
|
}
|
|
@@ -1955,7 +2011,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1955
2011
|
for (let i = 0; i < apps.length; i++) {
|
|
1956
2012
|
ret.push(new core_1.ApplicationInfo(this, {
|
|
1957
2013
|
...apps[i],
|
|
1958
|
-
ApplicationEntities: appEntities.filter(ae => ae.ApplicationName.trim().toLowerCase() === apps[i].Name.trim().toLowerCase())
|
|
2014
|
+
ApplicationEntities: appEntities.filter((ae) => ae.ApplicationName.trim().toLowerCase() === apps[i].Name.trim().toLowerCase()),
|
|
1959
2015
|
}));
|
|
1960
2016
|
}
|
|
1961
2017
|
return ret;
|
|
@@ -1976,7 +2032,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1976
2032
|
for (let i = 0; i < users.length; i++) {
|
|
1977
2033
|
ret.push(new core_1.UserInfo(this, {
|
|
1978
2034
|
...users[i],
|
|
1979
|
-
UserRoles: userRoles.filter(ur => ur.UserID === users[i].ID)
|
|
2035
|
+
UserRoles: userRoles.filter((ur) => ur.UserID === users[i].ID),
|
|
1980
2036
|
}));
|
|
1981
2037
|
}
|
|
1982
2038
|
return ret;
|
|
@@ -1988,7 +2044,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1988
2044
|
for (let i = 0; i < auths.length; i++) {
|
|
1989
2045
|
ret.push(new core_1.AuthorizationInfo(this, {
|
|
1990
2046
|
...auths[i],
|
|
1991
|
-
AuthorizationRoles: authRoles.filter(ar => ar.AuthorizationName.trim().toLowerCase() === auths[i].Name.trim().toLowerCase())
|
|
2047
|
+
AuthorizationRoles: authRoles.filter((ar) => ar.AuthorizationName.trim().toLowerCase() === auths[i].Name.trim().toLowerCase()),
|
|
1992
2048
|
}));
|
|
1993
2049
|
}
|
|
1994
2050
|
return ret;
|
|
@@ -1999,7 +2055,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
1999
2055
|
else if (this._currentUserEmail && this._currentUserEmail.length > 0) {
|
|
2000
2056
|
// attempt to lookup current user from email since this.CurrentUser is null for some reason (unexpected)
|
|
2001
2057
|
if (UserCache_1.UserCache && UserCache_1.UserCache.Users)
|
|
2002
|
-
return UserCache_1.UserCache.Users.find(u => u.Email.trim().toLowerCase() === this._currentUserEmail.trim().toLowerCase());
|
|
2058
|
+
return UserCache_1.UserCache.Users.find((u) => u.Email.trim().toLowerCase() === this._currentUserEmail.trim().toLowerCase());
|
|
2003
2059
|
}
|
|
2004
2060
|
// if we get here we can't get the current user
|
|
2005
2061
|
return null;
|
|
@@ -2010,7 +2066,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2010
2066
|
const userRoles = await this.ExecuteSQL(`SELECT * FROM [${this.MJCoreSchemaName}].vwUserRoles WHERE UserID='${user[0].ID}'`);
|
|
2011
2067
|
return new core_1.UserInfo(this, {
|
|
2012
2068
|
...user[0],
|
|
2013
|
-
UserRoles: userRoles ? userRoles : []
|
|
2069
|
+
UserRoles: userRoles ? userRoles : [],
|
|
2014
2070
|
});
|
|
2015
2071
|
}
|
|
2016
2072
|
else
|
|
@@ -2104,7 +2160,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2104
2160
|
const result = [];
|
|
2105
2161
|
for (let i = 0; i < info.length; i++) {
|
|
2106
2162
|
const r = await this.GetEntityRecordName(info[i].EntityName, info[i].CompositeKey);
|
|
2107
|
-
result.push({
|
|
2163
|
+
result.push({
|
|
2164
|
+
EntityName: info[i].EntityName,
|
|
2165
|
+
CompositeKey: info[i].CompositeKey,
|
|
2166
|
+
RecordName: r,
|
|
2167
|
+
Success: r ? true : false,
|
|
2168
|
+
Status: r ? 'Success' : 'Error',
|
|
2169
|
+
});
|
|
2108
2170
|
}
|
|
2109
2171
|
return result;
|
|
2110
2172
|
}
|
|
@@ -2129,13 +2191,13 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2129
2191
|
}
|
|
2130
2192
|
}
|
|
2131
2193
|
GetEntityRecordNameSQL(entityName, CompositeKey) {
|
|
2132
|
-
const e = this.Entities.find(e => e.Name === entityName);
|
|
2194
|
+
const e = this.Entities.find((e) => e.Name === entityName);
|
|
2133
2195
|
if (!e)
|
|
2134
2196
|
throw new Error(`Entity ${entityName} not found`);
|
|
2135
2197
|
else {
|
|
2136
|
-
let f = e.Fields.find(f => f.IsNameField);
|
|
2198
|
+
let f = e.Fields.find((f) => f.IsNameField);
|
|
2137
2199
|
if (!f)
|
|
2138
|
-
f = e.Fields.find(f => f.Name === 'Name');
|
|
2200
|
+
f = e.Fields.find((f) => f.Name === 'Name');
|
|
2139
2201
|
if (!f) {
|
|
2140
2202
|
(0, core_1.LogError)(`Entity ${entityName} does not have an IsNameField or a field with the column name of Name, returning null, use recordId`);
|
|
2141
2203
|
return null;
|
|
@@ -2145,7 +2207,7 @@ class SQLServerDataProvider extends core_1.ProviderBase {
|
|
|
2145
2207
|
let sql = `SELECT [${f.Name}] FROM [${e.SchemaName}].[${e.BaseView}] WHERE `;
|
|
2146
2208
|
let where = '';
|
|
2147
2209
|
for (let pkv of CompositeKey.KeyValuePairs) {
|
|
2148
|
-
const pk = e.PrimaryKeys.find(pk => pk.Name === pkv.FieldName);
|
|
2210
|
+
const pk = e.PrimaryKeys.find((pk) => pk.Name === pkv.FieldName);
|
|
2149
2211
|
const quotes = pk.NeedsQuotes ? "'" : '';
|
|
2150
2212
|
if (where.length > 0)
|
|
2151
2213
|
where += ' AND ';
|