@memberjunction/sqlserver-dataprovider 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1293 @@
1
+ "use strict";
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
+ **************************************************************************************************************/
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.SQLServerDataProvider = exports.SQLServerProviderConfigData = void 0;
9
+ const core_1 = require("@memberjunction/core");
10
+ const SQLServerTransactionGroup_1 = require("./SQLServerTransactionGroup");
11
+ const ai_1 = require("@memberjunction/ai");
12
+ const queue_1 = require("@memberjunction/queue");
13
+ const UserCache_1 = require("./UserCache");
14
+ class SQLServerProviderConfigData extends core_1.ProviderConfigDataBase {
15
+ get DataSource() { return this.Data.DataSource; }
16
+ get CurrentUserEmail() { return this.Data.CurrentUserEmail; }
17
+ get CheckRefreshIntervalSeconds() { return this.Data.CheckRefreshIntervalSeconds; }
18
+ constructor(dataSource, currentUserEmail, MJCoreSchemaName, checkRefreshIntervalSeconds = 90 /*default to 90 seconds between checks*/, includeSchemas, excludeSchemas) {
19
+ super({
20
+ DataSource: dataSource,
21
+ CurrentUserEmail: currentUserEmail,
22
+ CheckRefreshIntervalSeconds: checkRefreshIntervalSeconds
23
+ }, MJCoreSchemaName, includeSchemas, excludeSchemas);
24
+ }
25
+ }
26
+ exports.SQLServerProviderConfigData = SQLServerProviderConfigData;
27
+ // Implements both the IEntityDataProvider and IMetadataProvider interfaces.
28
+ class SQLServerDataProvider extends core_1.ProviderBase {
29
+ constructor() {
30
+ super(...arguments);
31
+ this._bAllowRefresh = true;
32
+ }
33
+ get ConfigData() { return super.ConfigData; }
34
+ async Config(configData) {
35
+ try {
36
+ this._dataSource = configData.DataSource;
37
+ this._currentUserEmail = configData.CurrentUserEmail;
38
+ return super.Config(configData); // now parent class can do it's config
39
+ }
40
+ catch (e) {
41
+ (0, core_1.LogError)(e);
42
+ throw (e);
43
+ }
44
+ }
45
+ AllowRefresh() {
46
+ return this._bAllowRefresh;
47
+ }
48
+ /**************************************************************************/
49
+ // START ---- IRunReportProvider
50
+ /**************************************************************************/
51
+ async RunReport(params, contextUser) {
52
+ const ReportID = params.ReportID;
53
+ // run the sql and return the data
54
+ const sqlReport = "SELECT ReportSQL FROM vwReports WHERE ID = " + ReportID;
55
+ const reportInfo = await this.ExecuteSQL(sqlReport);
56
+ if (reportInfo && reportInfo.length > 0) {
57
+ const start = new Date().getTime();
58
+ const sql = reportInfo[0].ReportSQL;
59
+ const result = await this.ExecuteSQL(sql);
60
+ const end = new Date().getTime();
61
+ if (result)
62
+ return { Success: true, ReportID: ReportID, Results: result, RowCount: result.length, ExecutionTime: end - start, ErrorMessage: '' };
63
+ else
64
+ return { Success: false, ReportID: ReportID, Results: [], RowCount: 0, ExecutionTime: end - start, ErrorMessage: 'Error running report SQL' };
65
+ }
66
+ else
67
+ return { Success: false, ReportID: ReportID, Results: [], RowCount: 0, ExecutionTime: 0, ErrorMessage: 'Report not found' };
68
+ }
69
+ /**************************************************************************/
70
+ // END ---- IRunReportProvider
71
+ /**************************************************************************/
72
+ /**************************************************************************/
73
+ // START ---- IRunViewProvider
74
+ /**************************************************************************/
75
+ async RunView(params, contextUser) {
76
+ const startTime = new Date();
77
+ try {
78
+ if (params) {
79
+ const user = contextUser ? contextUser : this.CurrentUser;
80
+ if (!user)
81
+ throw new Error(`User ${this._currentUserEmail} not found in metadata and no contextUser provided to RunView()`);
82
+ let viewEntity = null, entityInfo = null;
83
+ if (params.ViewEntity)
84
+ viewEntity = params.ViewEntity;
85
+ else if (params.ViewID && params.ViewID > 0)
86
+ viewEntity = await core_1.ViewInfo.GetViewEntity(params.ViewID, contextUser);
87
+ else if (params.ViewName && params.ViewName.length > 0)
88
+ viewEntity = await core_1.ViewInfo.GetViewEntityByName(params.ViewName, contextUser);
89
+ if (!viewEntity) {
90
+ // if we don't have viewEntity, that means it is a dynamic view, so we need EntityName at a minimum
91
+ if (!params.EntityName || params.EntityName.length === 0)
92
+ throw new Error(`EntityName is required when ViewID or ViewName is not provided`);
93
+ entityInfo = this.Entities.find((e) => e.Name === params.EntityName);
94
+ if (!entityInfo)
95
+ throw new Error(`Entity ${params.EntityName} not found in metadata`);
96
+ }
97
+ else {
98
+ entityInfo = this.Entities.find((e) => e.ID === viewEntity.EntityID);
99
+ if (!entityInfo)
100
+ throw new Error(`Entity ID: ${viewEntity.EntityID} not found in metadata`);
101
+ }
102
+ // check permissions now, this call will throw an error if the user doesn't have permission
103
+ this.CheckUserReadPermissions(entityInfo.Name, user);
104
+ // get other variaables from params
105
+ const extraFilter = params.ExtraFilter;
106
+ const userSearchString = params.UserSearchString;
107
+ const excludeUserViewRunID = params.ExcludeUserViewRunID;
108
+ const overrideExcludeFilter = params.OverrideExcludeFilter;
109
+ const saveViewResults = params.SaveViewResults;
110
+ let topSQL = '';
111
+ if (params.IgnoreMaxRows === true) {
112
+ // do nothing, leave it blank, this structure is here to make the code easier to read
113
+ }
114
+ else if (entityInfo.UserViewMaxRows && entityInfo.UserViewMaxRows > 0) {
115
+ topSQL = 'TOP ' + entityInfo.UserViewMaxRows;
116
+ }
117
+ const fields = this.getRunTimeViewFieldString(params, viewEntity);
118
+ let viewSQL = `SELECT ${topSQL} ${fields} FROM ${entityInfo.BaseView}`;
119
+ let countSQL = topSQL && topSQL.length > 0 ? `SELECT COUNT(*) AS TotalRowCount FROM ${entityInfo.BaseView}` : null;
120
+ let whereSQL = '';
121
+ let bHasWhere = false;
122
+ let userViewRunID = 0;
123
+ // The view may have a where clause that is part of the view definition. If so, we need to add it to the SQL
124
+ if (viewEntity?.WhereClause && viewEntity?.WhereClause.length > 0) {
125
+ whereSQL = `(${viewEntity.WhereClause})`;
126
+ bHasWhere = true;
127
+ }
128
+ // a developer calling the function can provide an additional Extra Filter which is any valid SQL exprssion that can be added to the WHERE clause
129
+ if (extraFilter && extraFilter.length > 0) {
130
+ // extra filter is simple- we just AND it to the where clause if it exists, or we add it as a where clause if there was no prior WHERE
131
+ if (!this.validateUserProvidedSQLClause(extraFilter))
132
+ throw new Error(`Invalid Extra Filter: ${extraFilter}, contains one more for forbidden keywords`);
133
+ if (bHasWhere) {
134
+ whereSQL += ` AND (${extraFilter})`;
135
+ }
136
+ else {
137
+ whereSQL = `(${extraFilter})`;
138
+ bHasWhere = true;
139
+ }
140
+ }
141
+ // check for a user provided search string and generate SQL as needed if provided
142
+ if (userSearchString && userSearchString.length > 0) {
143
+ if (!this.validateUserProvidedSQLClause(userSearchString))
144
+ throw new Error(`Invalid User Search SQL clause: ${userSearchString}, contains one more for forbidden keywords`);
145
+ const sUserSearchSQL = this.createViewUserSearchSQL(entityInfo, userSearchString);
146
+ if (sUserSearchSQL.length > 0) {
147
+ if (bHasWhere) {
148
+ whereSQL += ` AND (${sUserSearchSQL})`;
149
+ }
150
+ else {
151
+ whereSQL = `(${sUserSearchSQL})`;
152
+ bHasWhere = true;
153
+ }
154
+ }
155
+ }
156
+ // now, check for an exclude UserViewRunID, or exclusion of ALL prior runs
157
+ // if provided, we need to exclude the records that were part of that run (or all prior runs)
158
+ if ((excludeUserViewRunID && excludeUserViewRunID > 0) ||
159
+ (params.ExcludeDataFromAllPriorViewRuns === true)) {
160
+ let sExcludeSQL = `ID NOT IN (SELECT RecordID FROM vwUserViewRunDetails WHERE EntityID=${viewEntity.EntityID} AND`;
161
+ if (params.ExcludeDataFromAllPriorViewRuns === true)
162
+ 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
163
+ else
164
+ sExcludeSQL += `UserViewRunID=${excludeUserViewRunID})`; // exclude just the run that was provided
165
+ if (overrideExcludeFilter && overrideExcludeFilter.length > 0) {
166
+ if (!this.validateUserProvidedSQLClause(overrideExcludeFilter))
167
+ throw new Error(`Invalid OverrideExcludeFilter: ${overrideExcludeFilter}, contains one more for forbidden keywords`);
168
+ // add in the OVERRIDE filter with an OR statement, this results in those rows that match the Exclude filter to be included
169
+ // even if they're in the UserViewRunID that we're excluding
170
+ sExcludeSQL += ' OR (' + overrideExcludeFilter + ')';
171
+ }
172
+ if (bHasWhere) {
173
+ whereSQL += ` AND (${sExcludeSQL})`;
174
+ }
175
+ else {
176
+ whereSQL = `(${sExcludeSQL})`;
177
+ bHasWhere = true;
178
+ }
179
+ }
180
+ // NEXT, apply Row Level Security (RLS)
181
+ if (!entityInfo.UserExemptFromRowLevelSecurity(user, core_1.EntityPermissionType.Read)) {
182
+ // user is NOT exempt from RLS, so we need to apply it
183
+ const rlsWhereClause = entityInfo.GetUserRowLevelSecurityWhereClause(user, core_1.EntityPermissionType.Read, '');
184
+ if (rlsWhereClause && rlsWhereClause.length > 0) {
185
+ if (bHasWhere) {
186
+ whereSQL += ` AND (${rlsWhereClause})`;
187
+ }
188
+ else {
189
+ whereSQL = `(${rlsWhereClause})`;
190
+ bHasWhere = true;
191
+ }
192
+ }
193
+ }
194
+ if (bHasWhere) {
195
+ viewSQL += ` WHERE ${whereSQL}`;
196
+ if (countSQL)
197
+ countSQL += ` WHERE ${whereSQL}`;
198
+ }
199
+ // figure out the sorting for the view
200
+ // first check params.OrderBy, that takes first priority
201
+ // if that's not provided, then we check the view definition for its SortState
202
+ // if that's not provided we do NOT sort
203
+ const orderBy = params.OrderBy ? params.OrderBy : (viewEntity ? viewEntity.OrderByClause : '');
204
+ // if we're saving the view results, we need to wrap the entire SQL statement
205
+ if (viewEntity?.ID && viewEntity?.ID > 0 && saveViewResults && user) {
206
+ const { executeViewSQL, runID } = await this.executeSQLForUserViewRunLogging(viewEntity.ID, viewEntity.EntityBaseView, whereSQL, orderBy, user);
207
+ viewSQL = executeViewSQL;
208
+ userViewRunID = runID;
209
+ }
210
+ else if (orderBy && orderBy.length > 0) {
211
+ // we only add order by if we're not doing run logging. This is becuase the run logging will
212
+ // add the order by to its SELECT query that pulls from the list of records that were returned
213
+ // there is no point in ordering the rows as they are saved into an audit list anyway so no order-by above
214
+ // just here for final step before we execute it.
215
+ if (!this.validateUserProvidedSQLClause(orderBy))
216
+ throw new Error(`Invalid Order By clause: ${orderBy}, contains one more for forbidden keywords`);
217
+ viewSQL += ` ORDER BY ${orderBy}`;
218
+ }
219
+ // now we can run the viewSQL
220
+ const retData = await this._dataSource.query(viewSQL);
221
+ // finally, if we have a countSQL, we need to run that first to get the row count
222
+ // but only do that if the # of rows returned is equal to the max rows, otherwise we know we have all the rows
223
+ let rowCount = null;
224
+ if (countSQL && retData.length === entityInfo.UserViewMaxRows) {
225
+ const countResult = await this._dataSource.query(countSQL);
226
+ if (countResult && countResult.length > 0) {
227
+ rowCount = countResult[0].TotalRowCount;
228
+ }
229
+ }
230
+ const stopTime = new Date();
231
+ if (params.ForceAuditLog || (viewEntity?.ID && (extraFilter === null || extraFilter.trim().length === 0) && entityInfo.AuditViewRuns)) {
232
+ // ONLY LOG TOP LEVEL VIEW EXECUTION - this would be for views with an ID, and don't have ExtraFilter as ExtraFilter
233
+ // is only used in the system on a tab or just for ad hoc view execution
234
+ // we do NOT want to wait for this, so no await,
235
+ this.createAuditLogRecord(user, 'Run View', 'Run View', 'Success', JSON.stringify({
236
+ ViewID: viewEntity?.ID,
237
+ ViewName: viewEntity?.Name,
238
+ Description: params.AuditLogDescription,
239
+ RowCount: retData.length,
240
+ SQL: viewSQL
241
+ }), entityInfo.ID, null, params.AuditLogDescription);
242
+ }
243
+ return {
244
+ RowCount: retData.length,
245
+ TotalRowCount: rowCount ? rowCount : retData.length,
246
+ Results: retData,
247
+ UserViewRunID: userViewRunID,
248
+ ExecutionTime: stopTime.getTime() - startTime.getTime(),
249
+ Success: true,
250
+ ErrorMessage: null
251
+ };
252
+ }
253
+ else
254
+ return null;
255
+ }
256
+ catch (e) {
257
+ const exceptionStopTime = new Date();
258
+ (0, core_1.LogError)(e);
259
+ return {
260
+ RowCount: 0,
261
+ TotalRowCount: 0,
262
+ Results: [],
263
+ UserViewRunID: 0,
264
+ ExecutionTime: exceptionStopTime.getTime() - startTime.getTime(),
265
+ Success: false,
266
+ ErrorMessage: e.message
267
+ };
268
+ }
269
+ }
270
+ validateUserProvidedSQLClause(clause) {
271
+ // convert the clause to lower case to make the keyword search case-insensitive
272
+ const lowerClause = clause.toLowerCase();
273
+ // check for forbidden keywords and characters
274
+ const forbiddenPatterns = [
275
+ 'insert',
276
+ 'update',
277
+ 'delete',
278
+ 'exec',
279
+ 'execute',
280
+ 'drop',
281
+ '--',
282
+ '/*',
283
+ '*/',
284
+ 'union',
285
+ 'cast',
286
+ 'xp_',
287
+ ';'
288
+ ];
289
+ for (const pattern of forbiddenPatterns) {
290
+ if (lowerClause.includes(pattern)) {
291
+ return false;
292
+ }
293
+ }
294
+ return true;
295
+ }
296
+ getRunTimeViewFieldString(params, viewEntity) {
297
+ const fieldList = this.getRunTimeViewFieldArray(params, viewEntity);
298
+ // pass this back as a comma separated list
299
+ return fieldList.join(',');
300
+ }
301
+ getRunTimeViewFieldArray(params, viewEntity) {
302
+ const fieldList = [];
303
+ if (params.Fields) {
304
+ // fields provided, if ID isn't included, add it first
305
+ if (params.Fields.find((f) => f.trim().toLowerCase() === 'id') === undefined)
306
+ fieldList.push('ID');
307
+ // now add the rest of the param.Fields to fields
308
+ params.Fields.forEach((f) => {
309
+ fieldList.push(f);
310
+ });
311
+ }
312
+ else {
313
+ // fields weren't provided by the caller. So, let's do the following
314
+ // * if this is a defined view, using a View Name or View ID, we use the fields that are used wtihin the View and always return the ID
315
+ // * if this is an dynamic view, we return ALL fields in the entity using *
316
+ if (viewEntity) {
317
+ // saved view, figure out it's field list
318
+ viewEntity.Columns.forEach((c) => {
319
+ if (!c.hidden) // only return the non-hidden fields
320
+ fieldList.push(c.EntityField.Name);
321
+ });
322
+ if (fieldList.find((f) => f.trim().toLowerCase() === 'id') === undefined)
323
+ fieldList.push('ID'); // this should never happen, all views should always have ID in them, but just in case we do this here to ensure it
324
+ }
325
+ else // dynamic view
326
+ fieldList.push('*'); // no fields provided, include everything
327
+ }
328
+ return fieldList;
329
+ }
330
+ async executeSQLForUserViewRunLogging(viewId, entityBaseView, whereSQL, orderBySQL, user) {
331
+ const sSQL = `
332
+ DECLARE @ViewIDList TABLE ( ID INT );
333
+ INSERT INTO @ViewIDList (ID) (SELECT ID FROM ${entityBaseView} WHERE (${whereSQL}))
334
+ EXEC spCreateUserViewRunWithDetail(${viewId},${user.Email}, @ViewIDLIst)
335
+ `;
336
+ const runIDResult = await this._dataSource.query(sSQL);
337
+ const runID = runIDResult[0].UserViewRunID;
338
+ const sRetSQL = `SELECT * FROM ${entityBaseView} WHERE ID IN
339
+ (SELECT RecordID FROM vwUserViewRunDetails WHERE UserViewRunID=${runID})
340
+ ${orderBySQL && orderBySQL.length > 0 ? ' ORDER BY ' + orderBySQL : ''}`;
341
+ return { executeViewSQL: sRetSQL, runID: runID };
342
+ }
343
+ createViewUserSearchSQL(entityInfo, userSearchString) {
344
+ // we have a user search string.
345
+ // if we have full text search, we use that.
346
+ // Otherwise, we need to manually construct the additional filter associated with this. The user search string is just text from the user
347
+ // we need to apply it to one or more fields that are part of the entity that support being part of a user search.
348
+ // we need to get the list of fields that are part of the entity that support being part of a user search.
349
+ let sUserSearchSQL = '';
350
+ if (entityInfo.FullTextSearchEnabled) {
351
+ // we have full text search, so we use that, do as subquery but that gets optimized into JOIN by SQL Server, so we can keep our situation logially simpler
352
+ // in the context of overall filtering by doing as a SUBQUERY here.
353
+ // if we have a user search string that includes AND, OR, or NOT, we leave it alone
354
+ // otherwise, we check to see if the userSearchString is a single word, if so, we also leave it alone
355
+ // if the userSearchString doesn't have AND OR or NOT in it, and has multiple words, we convert the spaces to
356
+ // AND so that we can do a full text search on all the words
357
+ let u = userSearchString;
358
+ const uUpper = u.toUpperCase();
359
+ if (uUpper.includes('AND') || uUpper.includes('OR') || uUpper.includes('NOT')) {
360
+ // do nothing, leave it alone, this structure is here to make the code easier to read
361
+ }
362
+ else if (u.includes(' ')) {
363
+ // we have multiple words, so we need to convert the spaces to AND
364
+ u = userSearchString.replace(/ /g, ' AND ');
365
+ }
366
+ sUserSearchSQL = `ID IN (SELECT ID FROM ${entityInfo.SchemaName}.${entityInfo.FullTextSearchFunction}('${u}'))`;
367
+ }
368
+ else {
369
+ const entityFields = entityInfo.Fields;
370
+ for (const field of entityFields) {
371
+ if (field.IncludeInUserSearchAPI) {
372
+ let sParam = '';
373
+ if (sUserSearchSQL.length > 0)
374
+ sUserSearchSQL += ' OR ';
375
+ if (field.UserSearchParamFormatAPI && field.UserSearchParamFormatAPI.length > 0)
376
+ // we have a search param format. we need to apply it to the user search string
377
+ sParam = field.UserSearchParamFormatAPI.replace('{0}', userSearchString);
378
+ else
379
+ sParam = ` LIKE '%${userSearchString}%'`;
380
+ sUserSearchSQL += `(${field.Name} ${sParam})`;
381
+ }
382
+ }
383
+ if (sUserSearchSQL.length > 0)
384
+ sUserSearchSQL = '(' + sUserSearchSQL + ')'; // wrap the entire search string in parens
385
+ }
386
+ return sUserSearchSQL;
387
+ }
388
+ async createAuditLogRecord(user, authorizationName, auditLogTypeName, status, details, entityId, recordId, auditLogDescription) {
389
+ try {
390
+ const authorization = authorizationName ? this.Authorizations.find((a) => a.Name.trim().toLowerCase() === authorizationName.trim().toLowerCase()) : null;
391
+ const auditLogType = auditLogTypeName ? this.AuditLogTypes.find((a) => a.Name.trim().toLowerCase() === auditLogTypeName.trim().toLowerCase()) : null;
392
+ if (!user)
393
+ throw new Error(`User is a required parameter`);
394
+ if (!auditLogType)
395
+ throw new Error(`Audit Log Type ${auditLogTypeName} not found in metadata`);
396
+ const auditLog = await this.GetEntityObject('Audit Logs', user); // must pass user context on back end as we're not authenticated the same way as the front end
397
+ auditLog.NewRecord();
398
+ auditLog.Set('UserID', user.ID);
399
+ auditLog.Set('AuditLogTypeName', auditLogType.Name);
400
+ auditLog.Set('Status', status);
401
+ auditLog.Set('EntityID', entityId);
402
+ auditLog.Set('RecordID', recordId);
403
+ if (authorization)
404
+ auditLog.Set('AuthorizationName', authorization.Name);
405
+ if (details)
406
+ auditLog.Set('Details', details);
407
+ if (auditLogDescription)
408
+ auditLog.Set('Description', auditLogDescription);
409
+ if (await auditLog.Save())
410
+ return auditLog;
411
+ else
412
+ throw new Error(`Error saving audit log record`);
413
+ }
414
+ catch (err) {
415
+ (0, core_1.LogError)(err);
416
+ return null;
417
+ }
418
+ }
419
+ CheckUserReadPermissions(entityName, contextUser) {
420
+ const entityInfo = this.Entities.find((e) => e.Name === entityName);
421
+ if (!contextUser)
422
+ throw new Error(`contextUser is null`);
423
+ // first check permissions, the logged in user must have read permissions on the entity to run the view
424
+ if (entityInfo) {
425
+ const userPermissions = entityInfo.GetUserPermisions(contextUser);
426
+ if (!userPermissions.CanRead)
427
+ throw new Error(`User ${contextUser.Email} does not have read permissions on ${entityInfo.Name}`);
428
+ }
429
+ else
430
+ throw new Error(`Entity not found in metadata`);
431
+ }
432
+ /**************************************************************************/
433
+ // END ---- IRunViewProvider
434
+ /**************************************************************************/
435
+ /**************************************************************************/
436
+ // START ---- IEntityDataProvider
437
+ /**************************************************************************/
438
+ get ProviderType() {
439
+ return core_1.EntityDataProviderType.Database;
440
+ }
441
+ async GetRecordFavoriteStatus(userId, entityName, recordId) {
442
+ const id = await this.GetRecordFavoriteID(userId, entityName, recordId);
443
+ return id !== null;
444
+ }
445
+ async GetRecordFavoriteID(userId, entityName, recordId) {
446
+ try {
447
+ const sSQL = `SELECT ID FROM vwUserFavorites WHERE UserID = ${userId} AND Entity='${entityName}' AND RecordID=${recordId}`;
448
+ const result = await this.ExecuteSQL(sSQL);
449
+ if (result && result.length > 0)
450
+ return result[0].ID;
451
+ else
452
+ return null;
453
+ }
454
+ catch (e) {
455
+ (0, core_1.LogError)(e);
456
+ throw (e);
457
+ }
458
+ }
459
+ async SetRecordFavoriteStatus(userId, entityName, recordId, isFavorite, contextUser) {
460
+ try {
461
+ const currentFavoriteId = await this.GetRecordFavoriteID(userId, entityName, recordId);
462
+ if ((currentFavoriteId === null && isFavorite === false) ||
463
+ (currentFavoriteId !== null && isFavorite === true))
464
+ return; // no change
465
+ // if we're here that means we need to invert the status, which either means creating a record or deleting a record
466
+ const e = this.Entities.find((e) => e.Name === entityName);
467
+ const ufEntity = await this.GetEntityObject('User Favorites', contextUser || this.CurrentUser);
468
+ if (currentFavoriteId !== null) {
469
+ // delete the record since we are setting isFavorite to FALSE
470
+ await ufEntity.Load(currentFavoriteId);
471
+ if (await ufEntity.Delete())
472
+ return;
473
+ else
474
+ throw new Error(`Error deleting user favorite`);
475
+ }
476
+ else {
477
+ // create the record since we are setting isFavorite to TRUE
478
+ ufEntity.NewRecord();
479
+ ufEntity.Set('EntityID', e.ID);
480
+ ufEntity.Set('RecordID', recordId);
481
+ ufEntity.Set('UserID', userId);
482
+ if (await ufEntity.Save())
483
+ return;
484
+ else
485
+ throw new Error(`Error saving user favorite`);
486
+ }
487
+ }
488
+ catch (e) {
489
+ (0, core_1.LogError)(e);
490
+ throw (e);
491
+ }
492
+ }
493
+ async GetRecordChanges(entityName, recordId) {
494
+ try {
495
+ const sSQL = `SELECT * FROM vwRecordChanges WHERE Entity='${entityName}' AND RecordID=${recordId} ORDER BY ChangedAt DESC`;
496
+ return this.ExecuteSQL(sSQL);
497
+ }
498
+ catch (e) {
499
+ (0, core_1.LogError)(e);
500
+ throw (e);
501
+ }
502
+ }
503
+ GetSaveSQL(entity, bNewRecord, spName, user) {
504
+ const sSimpleSQL = 'EXEC dbo.' + spName + ' ' + this.generateSPParams(entity, !bNewRecord);
505
+ const recordChangesEntityInfo = this.Entities.find(e => e.Name === 'Record Changes');
506
+ let sSQL = '';
507
+ if (entity.EntityInfo.TrackRecordChanges) {
508
+ let oldData = null;
509
+ if (!bNewRecord)
510
+ oldData = entity.GetAll(true); // get all the OLD values, only do for existing records, for new records, not relevant
511
+ sSQL = `
512
+ DECLARE @ResultTable TABLE (
513
+ ${this.getAllEntityColumnsSQL(entity.EntityInfo)}
514
+ )
515
+
516
+ INSERT INTO @ResultTable
517
+ ${sSimpleSQL}
518
+
519
+ DECLARE @ID INT
520
+ SELECT @ID = ID FROM @ResultTable
521
+ IF @ID IS NOT NULL AND @ID > 0
522
+ BEGIN
523
+ DECLARE @ResultChangesTable TABLE (
524
+ ${this.getAllEntityColumnsSQL(recordChangesEntityInfo)}
525
+ )
526
+
527
+ INSERT INTO @ResultChangesTable
528
+ ${this.GetLogRecordChangeSQL(entity.GetAll(false), oldData, entity.EntityInfo.Name, '@ID', entity.EntityInfo, user)}
529
+ END
530
+
531
+ SELECT * FROM @ResultTable`;
532
+ }
533
+ else {
534
+ // not doing track changes for this entity, keep it simple
535
+ sSQL = sSimpleSQL;
536
+ }
537
+ return sSQL;
538
+ }
539
+ GetEntityAIActions(entityInfo, before) {
540
+ return ai_1.AIEngine.EntityAIActions.filter((a) => a.EntityID === entityInfo.ID &&
541
+ a.TriggerEvent.toLowerCase().trim() === (before ? 'before save' : 'after save'));
542
+ }
543
+ async HandleEntityAIActions(entity, before, user) {
544
+ // Make sure AI Metadata is loaded here...
545
+ await ai_1.AIEngine.LoadAIMetadata(user);
546
+ const actions = this.GetEntityAIActions(entity.EntityInfo, before); // get the actions we need to do for this entity
547
+ if (actions && actions.length > 0) {
548
+ const ai = new ai_1.AIEngine();
549
+ for (let i = 0; i < actions.length; i++) {
550
+ const a = actions[i];
551
+ const p = {
552
+ entityAIActionId: a.ID,
553
+ entityRecord: entity,
554
+ actionId: a.AIActionID,
555
+ modelId: a.AIModelID
556
+ };
557
+ if (before) {
558
+ // do it with await so we're blocking, as it needs to complete before the record save continues
559
+ await ai.ExecuteEntityAIAction(p);
560
+ }
561
+ else {
562
+ // just add a task and move on, we are doing 'after save' so we don't wait
563
+ try {
564
+ queue_1.QueueManager.AddTask('Entity AI Action', p, null, user);
565
+ }
566
+ catch (e) {
567
+ (0, core_1.LogError)(e.message);
568
+ }
569
+ }
570
+ }
571
+ }
572
+ }
573
+ async Save(entity, user, options) {
574
+ try {
575
+ if (entity.ID && entity.ID > 0 && !entity.EntityInfo.AllowUpdateAPI) {
576
+ // existing record and not allowed to update
577
+ throw new Error(`UPDATE not allowed for entity ${entity.EntityInfo.Name}`);
578
+ }
579
+ else if ((!entity.ID || entity.ID <= 0) && !entity.EntityInfo.AllowCreateAPI) {
580
+ // new record and not allowed to create
581
+ throw new Error(`CREATE not allowed for entity ${entity.EntityInfo.Name}`);
582
+ }
583
+ else {
584
+ // getting here means we are good to save, now check to see if we're dirty and need to save
585
+ // 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
586
+ if (entity.Dirty || (options && options.IgnoreDirtyState)) {
587
+ const bNewRecord = (entity.ID && entity.ID > 0) ? false : true;
588
+ const spName = bNewRecord ? (entity.EntityInfo.spCreate && entity.EntityInfo.spCreate.length > 0 ? entity.EntityInfo.spCreate : 'spCreate' + entity.EntityInfo.BaseTable) :
589
+ (entity.EntityInfo.spUpdate && entity.EntityInfo.spUpdate.length > 0 ? entity.EntityInfo.spUpdate : 'spUpdate' + entity.EntityInfo.BaseTable);
590
+ if (!options /*no options set*/ ||
591
+ options.SkipEntityAIActions !== true /*options set, but not set to skip entity AI actions*/) {
592
+ // process any Entity AI actions that are set to trigger BEFORE the save, these are generally a really bad idea to do before save
593
+ // but they are supported (for now)
594
+ await this.HandleEntityAIActions(entity, true, user);
595
+ }
596
+ const sSQL = this.GetSaveSQL(entity, bNewRecord, spName, user);
597
+ if (entity.TransactionGroup) {
598
+ // we have a transaction group, need to play nice and be part of it
599
+ return new Promise((resolve, reject) => {
600
+ // we are part of a transaction group, so just add our query to the list
601
+ // and when the transaction is committed, we will send all the queries at once
602
+ this._bAllowRefresh = false; // stop refreshes of metadata while we're doing work
603
+ entity.TransactionGroup.AddTransaction(new core_1.TransactionItem(sSQL, null, { dataSource: this._dataSource }, (results, success) => {
604
+ // we get here whenever the transaction group does gets around to committing
605
+ // our query.
606
+ this._bAllowRefresh = true; // allow refreshes again
607
+ if (success && results) {
608
+ // process any Entity AI actions that are set to trigger AFTER the save
609
+ // these are fired off but are NOT part of the transaction group, so if they fail,
610
+ // the transaction group will still commit, but the AI action will not be executed
611
+ if (!options /*no options set*/ ||
612
+ options.SkipEntityAIActions !== true /*options set, but not set to skip entity AI actions*/)
613
+ this.HandleEntityAIActions(entity, false, user);
614
+ resolve(results[0]);
615
+ }
616
+ else
617
+ // the transaction failed, nothing to update, but we need to call Reject so the
618
+ // promise resolves with a rejection so our outer caller knows
619
+ reject(results);
620
+ }));
621
+ });
622
+ }
623
+ else {
624
+ // no transaction group, just execute this immediately...
625
+ this._bAllowRefresh = false; // stop refreshes of metadata while we're doing work
626
+ const result = await this.ExecuteSQL(sSQL);
627
+ this._bAllowRefresh = true; // allow refreshes now
628
+ if (result && result.length > 0) {
629
+ if (!options /*no options set*/ ||
630
+ options.SkipEntityAIActions !== true /*options set, but not set to skip entity AI actions*/)
631
+ this.HandleEntityAIActions(entity, false, user); // fire off any AFTER SAVE AI actions, but don't wait for them
632
+ return result[0];
633
+ }
634
+ else
635
+ throw new Error(`SQL Error: No result row returned from SQL: ` + sSQL);
636
+ }
637
+ }
638
+ else
639
+ return entity; // nothing to save, just return the entity
640
+ }
641
+ }
642
+ catch (e) {
643
+ this._bAllowRefresh = true; // allow refreshes again if we get a failure here
644
+ (0, core_1.LogError)(e);
645
+ throw e; // rethrow the error
646
+ }
647
+ }
648
+ getAllEntityColumnsSQL(entityInfo) {
649
+ let sRet = '', outputCount = 0;
650
+ for (let i = 0; i < entityInfo.Fields.length; i++) {
651
+ const f = entityInfo.Fields[i];
652
+ if (outputCount !== 0)
653
+ sRet += ',\n';
654
+ sRet += '[' + f.Name + '] ' + f.SQLFullType + ' ' + (f.AllowsNull ? 'NULL' : 'NOT NULL');
655
+ outputCount++;
656
+ }
657
+ return sRet;
658
+ }
659
+ generateSPParams(entity, isUpdate) {
660
+ let sRet = '', bFirst = true;
661
+ for (let i = 0; i < entity.EntityInfo.Fields.length; i++) {
662
+ const f = entity.EntityInfo.Fields[i];
663
+ if (!f.IsVirtual && f.AllowUpdateAPI) {
664
+ switch (f.Name.toLowerCase()) {
665
+ case 'id':
666
+ break;
667
+ case 'createdat':
668
+ case 'updatedat':
669
+ // do nothing
670
+ break;
671
+ default:
672
+ if (f.Type.trim().toLowerCase() !== 'uniqueidentifier' &&
673
+ f.AutoIncrement !== true) {
674
+ // DO NOT INCLUDE UNIQUEIDENTIFIER FIELDS or Auto Increment Fields
675
+ // FOR CREATE/UPDATE, THEY ARE GENERATED BY THE DB
676
+ let value = entity.Get(f.Name);
677
+ if (f.Type.trim().toLowerCase() === 'datetimeoffset') {
678
+ value = new Date(value).toISOString();
679
+ }
680
+ sRet += this.generateSingleSPParam(f, value, bFirst);
681
+ bFirst = false;
682
+ }
683
+ break;
684
+ }
685
+ }
686
+ }
687
+ if (isUpdate && bFirst === false) {
688
+ sRet += ', @ID = ' + entity.ID; // add ID to update SP at end, but only if other fields included
689
+ bFirst = false;
690
+ }
691
+ return sRet;
692
+ }
693
+ generateSingleSPParam(f, value, isFirst) {
694
+ let sRet = '';
695
+ let quotes = '';
696
+ let val = value;
697
+ switch ((0, core_1.TypeScriptTypeFromSQLType)(f.Type).toLowerCase()) {
698
+ case 'string':
699
+ quotes = "'";
700
+ break;
701
+ case 'date':
702
+ quotes = "'";
703
+ if (val !== null && val !== undefined) {
704
+ if (typeof val === 'number') {
705
+ // we have a timestamp - milliseconds since Unix Epoch
706
+ // convert to a date
707
+ val = new Date(val);
708
+ }
709
+ else if (typeof val === 'string') {
710
+ // we have a string, attempt to convert it to a date object
711
+ val = new Date(val);
712
+ }
713
+ val = val.toISOString(); // convert the date to ISO format for storage in the DB
714
+ }
715
+ break;
716
+ default:
717
+ break;
718
+ }
719
+ if (!isFirst)
720
+ sRet += ',\n ';
721
+ sRet += `@${f.Name}=${this.packageSPParam(val, quotes)}`;
722
+ return sRet;
723
+ }
724
+ packageSPParam(paramValue, quoteString) {
725
+ let pVal;
726
+ if (typeof paramValue === 'string') {
727
+ if (quoteString == "'")
728
+ pVal = paramValue.toString().replace(/'/g, "''");
729
+ else if (quoteString == '"')
730
+ pVal = paramValue.toString().replace(/"/g, '""');
731
+ else
732
+ pVal = paramValue;
733
+ }
734
+ else
735
+ pVal = paramValue;
736
+ return (paramValue === null || paramValue === undefined) ? null : quoteString + pVal + quoteString;
737
+ }
738
+ GetLogRecordChangeSQL(newData, oldData, entityName, recordID, entityInfo, user) {
739
+ const fullRecordJSON = JSON.stringify(this.escapeQuotesInProperties(newData ? newData : oldData, "'")); // stringify old data if we don't have new - means we are DELETING A RECORD
740
+ const changes = this.DiffObjects(oldData, newData, entityInfo, "'");
741
+ const changesKeys = changes ? Object.keys(changes) : [];
742
+ if (changesKeys.length > 0 || oldData === null /*new record*/ || newData === null /*deleted record*/) {
743
+ const changesJSON = changes !== null ? JSON.stringify(changes) : '';
744
+ const sSQL = `EXEC spCreateRecordChange @EntityName='${entityName}',
745
+ @RecordID=${recordID},
746
+ @UserID=${user.ID},
747
+ @ChangesJSON='${changesJSON}',
748
+ @ChangesDescription='${oldData && newData ? this.CreateUserDescription(changes) : !oldData ? 'Record Created' : 'Record Deleted'}',
749
+ @FullRecordJSON='${fullRecordJSON}',
750
+ @Status='Complete',
751
+ @Comments=null`;
752
+ return sSQL;
753
+ }
754
+ else
755
+ return null;
756
+ }
757
+ async LogRecordChange(newData, oldData, entityName, recordID, entityInfo, user) {
758
+ const sSQL = this.GetLogRecordChangeSQL(newData, oldData, entityName, recordID.toString(), entityInfo, user);
759
+ if (sSQL) {
760
+ const result = await this.ExecuteSQL(sSQL);
761
+ return result;
762
+ }
763
+ }
764
+ CreateUserDescription(changesObject, maxValueLength = 200) {
765
+ let sRet = '';
766
+ let keys = Object.keys(changesObject);
767
+ for (let i = 0; i < keys.length; i++) {
768
+ const change = changesObject[keys[i]];
769
+ if (sRet.length > 0) {
770
+ sRet += '\n';
771
+ }
772
+ if (change.oldValue && change.newValue) // both old and new values set, show change
773
+ sRet += `${change.field} changed from ${this.trimString(change.oldValue, maxValueLength, '...')} to ${this.trimString(change.newValue, maxValueLength, '...')}`;
774
+ else if (change.newValue) // old value was blank, new value isn't
775
+ sRet += `${change.field} set to ${this.trimString(change.newValue, maxValueLength, '...')}`;
776
+ else if (change.oldValue) // new value is blank, old value wasn't
777
+ sRet += `${change.field} cleared from ${this.trimString(change.oldValue, maxValueLength, '...')}`;
778
+ }
779
+ return sRet.replace(/'/g, "''");
780
+ }
781
+ trimString(value, maxLength, trailingChars) {
782
+ if (value && typeof value === 'string' && value.length > maxLength) {
783
+ value = value.substring(0, maxLength) + trailingChars;
784
+ }
785
+ return value;
786
+ }
787
+ escapeQuotesInProperties(obj, quoteToEscape) {
788
+ const sRet = {};
789
+ for (const key in obj) {
790
+ if (obj.hasOwnProperty(key)) {
791
+ const element = obj[key];
792
+ if (typeof element === 'string') {
793
+ const reg = new RegExp(quoteToEscape, 'g');
794
+ sRet[key] = element.replace(reg, quoteToEscape + quoteToEscape);
795
+ }
796
+ else
797
+ sRet[key] = element;
798
+ }
799
+ }
800
+ return sRet;
801
+ }
802
+ DiffObjects(oldData, newData, entityInfo, quoteToEscape) {
803
+ if (!oldData || !newData)
804
+ return null;
805
+ else {
806
+ const changes = {};
807
+ for (const key in newData) {
808
+ const f = entityInfo.Fields.find(f => f.Name.toLowerCase() === key.toLowerCase());
809
+ let bDiff = false;
810
+ if ((oldData[key] == undefined || oldData[key] == null) &&
811
+ (newData[key] == undefined || newData[key] == null))
812
+ bDiff = false; // this branch of logic ensures that undefined and null are treated the same
813
+ else {
814
+ switch (f.TSType) {
815
+ case core_1.EntityFieldTSType.String:
816
+ bDiff = oldData[key] !== newData[key];
817
+ break;
818
+ case core_1.EntityFieldTSType.Date:
819
+ bDiff = (new Date(oldData[key]).getTime() !== new Date(newData[key]).getTime());
820
+ break;
821
+ case core_1.EntityFieldTSType.Number:
822
+ case core_1.EntityFieldTSType.Boolean:
823
+ bDiff = oldData[key] !== newData[key];
824
+ break;
825
+ }
826
+ }
827
+ if (bDiff) {
828
+ // make sure we escape things properly
829
+ const r = new RegExp(quoteToEscape, 'g');
830
+ const o = (oldData[key] && typeof oldData[key] === 'string') ?
831
+ oldData[key].replace(r, quoteToEscape + quoteToEscape) : oldData[key];
832
+ const n = (newData[key] && typeof newData[key] === 'string') ?
833
+ newData[key].replace(r, quoteToEscape + quoteToEscape) : newData[key];
834
+ changes[key] = {
835
+ field: key,
836
+ oldValue: o,
837
+ newValue: n
838
+ };
839
+ }
840
+ }
841
+ return changes;
842
+ }
843
+ }
844
+ async Load(entity, RecordID, EntityRelationshipsToLoad = null, user) {
845
+ const sql = `SELECT * FROM ${entity.EntityInfo.BaseView} WHERE ID = ${RecordID}`;
846
+ const d = await this.ExecuteSQL(sql);
847
+ if (d && d.length > 0) {
848
+ // got the record, now process the relationships if there are any
849
+ const ret = d[0];
850
+ if (EntityRelationshipsToLoad && EntityRelationshipsToLoad.length > 0) {
851
+ for (let i = 0; i < EntityRelationshipsToLoad.length; i++) {
852
+ const rel = EntityRelationshipsToLoad[i];
853
+ const relInfo = entity.EntityInfo.RelatedEntities.find(r => r.RelatedEntity == rel);
854
+ if (relInfo) {
855
+ let relSql = '';
856
+ if (relInfo.Type.trim().toLowerCase() === 'one to many')
857
+ // one to many - simple query
858
+ relSql = `SELECT * FROM ${relInfo.RelatedEntityBaseView} WHERE ${relInfo.RelatedEntityJoinField} = ${ret.ID}`;
859
+ else
860
+ // many to many - need to use join table
861
+ relSql = ` SELECT
862
+ _theview.*
863
+ FROM
864
+ ${relInfo.RelatedEntityBaseView} _theview
865
+ INNER JOIN
866
+ ${relInfo.JoinView} _jv ON _theview.${relInfo.RelatedEntityJoinField} = _jv.${relInfo.JoinEntityInverseJoinField}
867
+ WHERE
868
+ _jv.${relInfo.JoinEntityJoinField} = ${ret.ID}`;
869
+ const relData = await this.ExecuteSQL(relSql);
870
+ if (relData && relData.length > 0) {
871
+ ret[rel] = relData;
872
+ }
873
+ }
874
+ }
875
+ }
876
+ return ret;
877
+ }
878
+ // if we get here, something didn't go right
879
+ return null;
880
+ }
881
+ GetDeleteSQL(entity, user) {
882
+ let sSQL = '';
883
+ const spName = entity.EntityInfo.spDelete ? entity.EntityInfo.spDelete : `spDelete${entity.EntityInfo.ClassName}`;
884
+ const sSimpleSQL = 'EXEC ' + spName + ' @ID=' + entity.ID;
885
+ const recordChangesEntityInfo = this.Entities.find(e => e.Name === 'Record Changes');
886
+ if (entity.EntityInfo.TrackRecordChanges) {
887
+ const oldData = entity.GetAll(true); // get all the OLD values
888
+ sSQL = `
889
+ IF OBJECT_ID('tempdb..#ResultTable') IS NOT NULL
890
+ DROP TABLE #ResultTable
891
+
892
+ DECLARE @ResultTable TABLE (
893
+ ID INT
894
+ )
895
+
896
+ INSERT INTO @ResultTable
897
+ ${sSimpleSQL}
898
+
899
+ DECLARE @ID INT
900
+ SELECT @ID = ID FROM @ResultTable
901
+ IF @ID IS NOT NULL AND @ID > 0
902
+ BEGIN
903
+ DECLARE @ResultChangesTable TABLE (
904
+ ${this.getAllEntityColumnsSQL(recordChangesEntityInfo)}
905
+ )
906
+
907
+ INSERT INTO @ResultChangesTable
908
+ ${this.GetLogRecordChangeSQL(null /*pass in null for new data for deleted records*/, oldData, entity.EntityInfo.Name, entity.ID.toString(), entity.EntityInfo, user)}
909
+ END
910
+
911
+ SELECT @ID AS ID`;
912
+ }
913
+ else {
914
+ // no record change tracking
915
+ // just delete the record
916
+ sSQL = sSimpleSQL;
917
+ }
918
+ return sSQL;
919
+ }
920
+ async Delete(entity, user) {
921
+ try {
922
+ if (!entity.ID || entity.ID <= 0)
923
+ // existing record and not allowed to update
924
+ throw new Error(`Delete() isn't callable for records that haven't yet been saved - ${entity.EntityInfo.Name}`);
925
+ if (!entity.EntityInfo.AllowDeleteAPI)
926
+ // not allowed to delete
927
+ throw new Error(`Delete() isn't callable for ${entity.EntityInfo.Name} as AllowDeleteAPI is false`);
928
+ // 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
929
+ // if we get here we can delete, so build the SQL and then handle appropriately either as part of TransGroup or directly...
930
+ const sSQL = this.GetDeleteSQL(entity, user);
931
+ if (entity.TransactionGroup) {
932
+ // we have a transaction group, need to play nice and be part of it
933
+ return new Promise((resolve, reject) => {
934
+ // we are part of a transaction group, so just add our query to the list
935
+ // and when the transaction is committed, we will send all the queries at once
936
+ entity.TransactionGroup.AddTransaction(new core_1.TransactionItem(sSQL, null, { dataSource: this._dataSource }, (results, success) => {
937
+ // we get here whenever the transaction group does gets around to committing
938
+ // our query.
939
+ if (success && results)
940
+ resolve(entity.ID === results[0].ID);
941
+ else
942
+ // the transaction failed, nothing to update, but we need to call Reject so the
943
+ // promise resolves with a rejection so our outer caller knows
944
+ reject(results);
945
+ }));
946
+ });
947
+ }
948
+ else {
949
+ return this._dataSource.transaction(async () => {
950
+ const d = await this.ExecuteSQL(sSQL);
951
+ if (d && d[0] && d[0].ID)
952
+ return entity.ID === d[0].ID; // returns the ID of the deleted record if SP is successful
953
+ else
954
+ return false;
955
+ });
956
+ }
957
+ }
958
+ catch (e) {
959
+ (0, core_1.LogError)(e);
960
+ return false;
961
+ }
962
+ }
963
+ /**************************************************************************/
964
+ // END ---- IEntityDataProvider
965
+ /**************************************************************************/
966
+ /**************************************************************************/
967
+ // START ---- IMetadataProvider
968
+ /**************************************************************************/
969
+ async GetDatasetByName(datasetName, itemFilters) {
970
+ const sSQL = `SELECT
971
+ d.ID DatasetID,
972
+ di.*,
973
+ e.BaseView EntityBaseView
974
+ FROM
975
+ vwDatasets d
976
+ INNER JOIN
977
+ vwDatasetItems di
978
+ ON
979
+ d.Name = di.DatasetName
980
+ INNER JOIN
981
+ vwEntities e
982
+ ON
983
+ di.EntityID = e.ID
984
+ WHERE
985
+ d.Name = @0`;
986
+ const items = await this.ExecuteSQL(sSQL, [datasetName]);
987
+ // now we have the dataset and the items, we need to get the update date from the items underlying entities
988
+ if (items && items.length > 0) {
989
+ // loop through each of the items and get the data from the underlying entity
990
+ const results = [];
991
+ let bSuccess = true;
992
+ for (let i = 0; i < items.length; i++) {
993
+ const item = items[i];
994
+ let filterSQL = '';
995
+ if (itemFilters && itemFilters.length > 0) {
996
+ const filter = itemFilters.find(f => f.ItemCode === item.Code);
997
+ if (filter)
998
+ filterSQL = (item.WhereClause ? ' AND ' : ' WHERE ') + '(' + filter.Filter + ')';
999
+ }
1000
+ const itemSQL = `SELECT * FROM ${item.EntityBaseView} ${item.WhereClause ? 'WHERE ' + item.WhereClause : ''}${filterSQL}`;
1001
+ const itemData = await this.ExecuteSQL(itemSQL);
1002
+ results.push({
1003
+ EntityID: item.EntityID,
1004
+ EntityName: item.Entity,
1005
+ Code: item.Code,
1006
+ Results: itemData
1007
+ });
1008
+ if (!itemData || itemData.length === 0)
1009
+ bSuccess = false;
1010
+ }
1011
+ const status = await this.GetDatasetStatusByName(datasetName);
1012
+ return {
1013
+ DatasetID: items[0].DatasetID,
1014
+ DatasetName: items[0].DatasetName,
1015
+ Success: bSuccess,
1016
+ Status: '',
1017
+ LatestUpdateDate: status.LatestUpdateDate,
1018
+ Results: results
1019
+ };
1020
+ }
1021
+ else {
1022
+ return {
1023
+ DatasetID: 0,
1024
+ DatasetName: '',
1025
+ Success: false,
1026
+ Status: 'No Dataset or Items found for DatasetName: ' + datasetName,
1027
+ LatestUpdateDate: null,
1028
+ Results: null,
1029
+ };
1030
+ }
1031
+ }
1032
+ async GetDatasetStatusByName(datasetName, itemFilters) {
1033
+ const sSQL = `
1034
+ SELECT
1035
+ d.ID DatasetID,
1036
+ di.*,
1037
+ e.BaseView EntityBaseView
1038
+ FROM
1039
+ vwDatasets d
1040
+ INNER JOIN
1041
+ vwDatasetItems di
1042
+ ON
1043
+ d.Name = di.DatasetName
1044
+ INNER JOIN
1045
+ vwEntities e
1046
+ ON
1047
+ di.EntityID = e.ID
1048
+ WHERE
1049
+ d.Name = @0`;
1050
+ const items = await this.ExecuteSQL(sSQL, [datasetName]);
1051
+ // now we have the dataset and the items, we need to get the update date from the items underlying entities
1052
+ if (items && items.length > 0) {
1053
+ // loop through each of the items and get the update date from the underlying entity
1054
+ const updateDates = [];
1055
+ let latestUpdateDate = new Date(1900, 1, 1);
1056
+ let bSuccess = true;
1057
+ for (let i = 0; i < items.length; i++) {
1058
+ const item = items[i];
1059
+ let filterSQL = '';
1060
+ if (itemFilters && itemFilters.length > 0) {
1061
+ const filter = itemFilters.find(f => f.ItemCode === item.Code);
1062
+ if (filter)
1063
+ filterSQL = ' WHERE ' + filter.Filter;
1064
+ }
1065
+ const itemSQL = `SELECT MAX(${item.DateFieldToCheck}) AS UpdateDate FROM ${item.EntityBaseView}${filterSQL}`;
1066
+ const itemUpdateDate = await this.ExecuteSQL(itemSQL);
1067
+ if (itemUpdateDate && itemUpdateDate.length > 0) {
1068
+ const updateDate = itemUpdateDate[0].UpdateDate;
1069
+ updateDates.push({
1070
+ EntityID: item.EntityID,
1071
+ EntityName: item.Entity,
1072
+ UpdateDate: updateDate
1073
+ });
1074
+ if (updateDate > latestUpdateDate)
1075
+ latestUpdateDate = updateDate;
1076
+ }
1077
+ }
1078
+ // at the end of the loop we have the latest update date for the dataset, package it up with the individual entity update dates
1079
+ return {
1080
+ DatasetID: items[0].DatasetID,
1081
+ DatasetName: items[0].DatasetName,
1082
+ Success: bSuccess,
1083
+ Status: '',
1084
+ LatestUpdateDate: latestUpdateDate,
1085
+ EntityUpdateDates: updateDates
1086
+ };
1087
+ }
1088
+ else {
1089
+ return {
1090
+ DatasetID: 0,
1091
+ DatasetName: '',
1092
+ Success: false,
1093
+ Status: 'No Dataset or Items found for DatasetName: ' + datasetName,
1094
+ EntityUpdateDates: null,
1095
+ LatestUpdateDate: null
1096
+ };
1097
+ }
1098
+ }
1099
+ async GetApplicationMetadata() {
1100
+ const apps = await this.ExecuteSQL('SELECT * FROM vwApplications', null);
1101
+ const appEntities = await this.ExecuteSQL('SELECT * FROM vwApplicationEntities ORDER BY ApplicationName');
1102
+ const ret = [];
1103
+ for (let i = 0; i < apps.length; i++) {
1104
+ ret.push(new core_1.ApplicationInfo(this, {
1105
+ ...apps[i],
1106
+ ApplicationEntities: appEntities.filter(ae => ae.ApplicationName.trim().toLowerCase() === apps[i].Name.trim().toLowerCase())
1107
+ }));
1108
+ }
1109
+ return ret;
1110
+ }
1111
+ async GetAuditLogTypeMetadata() {
1112
+ const alts = await this.ExecuteSQL('SELECT * FROM vwAuditLogTypes', null);
1113
+ const ret = [];
1114
+ for (let i = 0; i < alts.length; i++) {
1115
+ const alt = new core_1.AuditLogTypeInfo(alts[i]);
1116
+ ret.push(alt);
1117
+ }
1118
+ return ret;
1119
+ }
1120
+ async GetUserMetadata() {
1121
+ const users = await this.ExecuteSQL('SELECT * FROM vwUsers', null);
1122
+ const userRoles = await this.ExecuteSQL('SELECT * FROM vwUserRoles ORDER BY UserID');
1123
+ const ret = [];
1124
+ for (let i = 0; i < users.length; i++) {
1125
+ ret.push(new core_1.UserInfo(this, {
1126
+ ...users[i],
1127
+ UserRoles: userRoles.filter(ur => ur.UserID === users[i].ID)
1128
+ }));
1129
+ }
1130
+ return ret;
1131
+ }
1132
+ async GetAuthorizationMetadata() {
1133
+ const auths = await this.ExecuteSQL('SELECT * FROM vwAuthorizations', null);
1134
+ const authRoles = await this.ExecuteSQL('SELECT * FROM vwAuthorizationRoles ORDER BY AuthorizationName');
1135
+ const ret = [];
1136
+ for (let i = 0; i < auths.length; i++) {
1137
+ ret.push(new core_1.AuthorizationInfo(this, {
1138
+ ...auths[i],
1139
+ AuthorizationRoles: authRoles.filter(ar => ar.AuthorizationName.trim().toLowerCase() === auths[i].Name.trim().toLowerCase())
1140
+ }));
1141
+ }
1142
+ return ret;
1143
+ }
1144
+ async GetCurrentUser() {
1145
+ if (this.CurrentUser)
1146
+ return this.CurrentUser;
1147
+ else if (this._currentUserEmail && this._currentUserEmail.length > 0) {
1148
+ // attempt to lookup current user from email since this.CurrentUser is null for some reason (unexpected)
1149
+ if (UserCache_1.UserCache && UserCache_1.UserCache.Users)
1150
+ return UserCache_1.UserCache.Users.find(u => u.Email.trim().toLowerCase() === this._currentUserEmail.trim().toLowerCase());
1151
+ }
1152
+ // if we get here we can't get the current user
1153
+ return null;
1154
+ }
1155
+ async GetCurrentUserMetadata() {
1156
+ const user = await this.ExecuteSQL(`SELECT * FROM vwUsers WHERE Email='${this._currentUserEmail}'`);
1157
+ if (user && user.length === 1) {
1158
+ const userRoles = await this.ExecuteSQL(`SELECT * FROM vwUserRoles WHERE UserID=${user[0].ID}`);
1159
+ return new core_1.UserInfo(this, {
1160
+ ...user[0],
1161
+ UserRoles: userRoles ? userRoles : []
1162
+ });
1163
+ }
1164
+ else
1165
+ return null;
1166
+ }
1167
+ async GetRoleMetadata() {
1168
+ const roles = await this.ExecuteSQL('SELECT * FROM vwRoles', null);
1169
+ const ret = [];
1170
+ for (let i = 0; i < roles.length; i++) {
1171
+ const ri = new core_1.RoleInfo(roles[i]);
1172
+ ret.push(ri);
1173
+ }
1174
+ return ret;
1175
+ }
1176
+ async GetUserRoleMetadata() {
1177
+ const userRoles = await this.ExecuteSQL('SELECT * FROM vwUserRoles', null);
1178
+ const ret = [];
1179
+ for (let i = 0; i < userRoles.length; i++) {
1180
+ const uri = new core_1.UserRoleInfo(userRoles[i]);
1181
+ ret.push(uri);
1182
+ }
1183
+ return ret;
1184
+ }
1185
+ async GetRowLevelSecurityFilterMetadata() {
1186
+ const filters = await this.ExecuteSQL('SELECT * FROM vwRowLevelSecurityFilters', null);
1187
+ const ret = [];
1188
+ for (let i = 0; i < filters.length; i++) {
1189
+ const rlsfi = new core_1.RowLevelSecurityFilterInfo(filters[i]);
1190
+ ret.push(rlsfi);
1191
+ }
1192
+ return ret;
1193
+ }
1194
+ async ExecuteSQL(query, parameters = null) {
1195
+ try {
1196
+ const data = await this._dataSource.query(query, parameters);
1197
+ return data;
1198
+ }
1199
+ catch (e) {
1200
+ (0, core_1.LogError)(e);
1201
+ throw e; // force caller to handle
1202
+ }
1203
+ }
1204
+ get LocalStorageProvider() {
1205
+ if (!this._localStorageProvider)
1206
+ this._localStorageProvider = new NodeLocalStorageProvider();
1207
+ return this._localStorageProvider;
1208
+ }
1209
+ async GetEntityRecordNames(info) {
1210
+ const result = [];
1211
+ for (let i = 0; i < info.length; i++) {
1212
+ const r = await this.GetEntityRecordName(info[i].EntityName, info[i].RecordID);
1213
+ result.push({ EntityName: info[i].EntityName, RecordID: info[i].RecordID, RecordName: r, Success: r ? true : false, Status: r ? 'Success' : 'Error' });
1214
+ }
1215
+ return result;
1216
+ }
1217
+ async GetEntityRecordName(entityName, recordId) {
1218
+ try {
1219
+ const sql = this.GetEntityRecordNameSQL(entityName, recordId);
1220
+ if (sql) {
1221
+ const data = await this.ExecuteSQL(sql);
1222
+ if (data && data.length === 1) {
1223
+ const fields = Object.keys(data[0]);
1224
+ return data[0][fields[0]]; // return first field
1225
+ }
1226
+ else {
1227
+ (0, core_1.LogError)(`Entity ${entityName} record ${recordId} not found, returning null`);
1228
+ return null;
1229
+ }
1230
+ }
1231
+ }
1232
+ catch (e) {
1233
+ (0, core_1.LogError)(e);
1234
+ return null;
1235
+ }
1236
+ }
1237
+ GetEntityRecordNameSQL(entityName, recordId) {
1238
+ const e = this.Entities.find(e => e.Name === entityName);
1239
+ if (!e)
1240
+ throw new Error(`Entity ${entityName} not found`);
1241
+ else {
1242
+ let f = e.Fields.find(f => f.IsNameField);
1243
+ if (!f)
1244
+ f = e.Fields.find(f => f.Name === 'Name');
1245
+ if (!f) {
1246
+ (0, core_1.LogError)(`Entity ${entityName} does not have an IsNameField or a field with the column name of Name, returning null, use recordId`);
1247
+ return null;
1248
+ }
1249
+ else {
1250
+ // got our field, create a SQL Query
1251
+ return `SELECT ${f.Name} FROM ${e.BaseView} WHERE ID=${recordId}`;
1252
+ }
1253
+ }
1254
+ }
1255
+ async CreateTransactionGroup() {
1256
+ return new SQLServerTransactionGroup_1.SQLServerTransactionGroup();
1257
+ }
1258
+ /**************************************************************************/
1259
+ // END ---- IMetadataProvider
1260
+ /**************************************************************************/
1261
+ get Metadata() {
1262
+ return this;
1263
+ }
1264
+ }
1265
+ exports.SQLServerDataProvider = SQLServerDataProvider;
1266
+ // This implementation is purely in memory and doesn't bother to persist to a file. It is fine to load it once per server instance load
1267
+ class NodeLocalStorageProvider {
1268
+ constructor() {
1269
+ this._localStorage = {};
1270
+ }
1271
+ async getItem(key) {
1272
+ return new Promise((resolve) => {
1273
+ if (this._localStorage.hasOwnProperty(key))
1274
+ resolve(this._localStorage[key]);
1275
+ else
1276
+ resolve(null);
1277
+ });
1278
+ }
1279
+ async setItem(key, value) {
1280
+ return new Promise((resolve) => {
1281
+ this._localStorage[key] = value;
1282
+ resolve();
1283
+ });
1284
+ }
1285
+ async remove(key) {
1286
+ return new Promise((resolve) => {
1287
+ if (this._localStorage.hasOwnProperty(key))
1288
+ delete this._localStorage[key];
1289
+ resolve();
1290
+ });
1291
+ }
1292
+ }
1293
+ //# sourceMappingURL=SQLServerDataProvider.js.map