@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.
- package/dist/SQLServerDataProvider.d.ts +89 -0
- package/dist/SQLServerDataProvider.js +1293 -0
- package/dist/SQLServerDataProvider.js.map +1 -0
- package/dist/SQLServerTransactionGroup.d.ts +4 -0
- package/dist/SQLServerTransactionGroup.js +32 -0
- package/dist/SQLServerTransactionGroup.js.map +1 -0
- package/dist/UserCache.d.ts +11 -0
- package/dist/UserCache.js +47 -0
- package/dist/UserCache.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +49 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
|
@@ -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
|