@memberjunction/global 2.111.1 → 2.112.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Core.d.ts +29 -0
- package/dist/Core.d.ts.map +1 -0
- package/dist/Core.js +58 -0
- package/dist/Core.js.map +1 -0
- package/dist/generic/QueryCache.d.ts +85 -0
- package/dist/generic/QueryCache.d.ts.map +1 -0
- package/dist/generic/QueryCache.js +198 -0
- package/dist/generic/QueryCache.js.map +1 -0
- package/dist/generic/QueryCacheConfig.d.ts +72 -0
- package/dist/generic/QueryCacheConfig.d.ts.map +1 -0
- package/dist/generic/QueryCacheConfig.js +3 -0
- package/dist/generic/QueryCacheConfig.js.map +1 -0
- package/dist/generic/applicationInfo.d.ts +138 -0
- package/dist/generic/applicationInfo.d.ts.map +1 -0
- package/dist/generic/applicationInfo.js +177 -0
- package/dist/generic/applicationInfo.js.map +1 -0
- package/dist/generic/authEvaluator.d.ts +25 -0
- package/dist/generic/authEvaluator.d.ts.map +1 -0
- package/dist/generic/authEvaluator.js +49 -0
- package/dist/generic/authEvaluator.js.map +1 -0
- package/dist/generic/authTypes.d.ts +193 -0
- package/dist/generic/authTypes.d.ts.map +1 -0
- package/dist/generic/authTypes.js +19 -0
- package/dist/generic/authTypes.js.map +1 -0
- package/dist/generic/baseEngine.d.ts +260 -0
- package/dist/generic/baseEngine.d.ts.map +1 -0
- package/dist/generic/baseEngine.js +510 -0
- package/dist/generic/baseEngine.js.map +1 -0
- package/dist/generic/baseEntity.d.ts +691 -0
- package/dist/generic/baseEntity.d.ts.map +1 -0
- package/dist/generic/baseEntity.js +1688 -0
- package/dist/generic/baseEntity.js.map +1 -0
- package/dist/generic/baseInfo.d.ts +24 -0
- package/dist/generic/baseInfo.d.ts.map +1 -0
- package/dist/generic/baseInfo.js +53 -0
- package/dist/generic/baseInfo.js.map +1 -0
- package/dist/generic/compositeKey.d.ts +206 -0
- package/dist/generic/compositeKey.d.ts.map +1 -0
- package/dist/generic/compositeKey.js +412 -0
- package/dist/generic/compositeKey.js.map +1 -0
- package/dist/generic/databaseProviderBase.d.ts +46 -0
- package/dist/generic/databaseProviderBase.d.ts.map +1 -0
- package/dist/generic/databaseProviderBase.js +14 -0
- package/dist/generic/databaseProviderBase.js.map +1 -0
- package/dist/generic/entityInfo.d.ts +983 -0
- package/dist/generic/entityInfo.d.ts.map +1 -0
- package/dist/generic/entityInfo.js +1401 -0
- package/dist/generic/entityInfo.js.map +1 -0
- package/dist/generic/explorerNavigationItem.d.ts +20 -0
- package/dist/generic/explorerNavigationItem.d.ts.map +1 -0
- package/dist/generic/explorerNavigationItem.js +29 -0
- package/dist/generic/explorerNavigationItem.js.map +1 -0
- package/dist/generic/interfaces.d.ts +610 -0
- package/dist/generic/interfaces.d.ts.map +1 -0
- package/dist/generic/interfaces.js +211 -0
- package/dist/generic/interfaces.js.map +1 -0
- package/dist/generic/libraryInfo.d.ts +40 -0
- package/dist/generic/libraryInfo.d.ts.map +1 -0
- package/dist/generic/libraryInfo.js +56 -0
- package/dist/generic/libraryInfo.js.map +1 -0
- package/dist/generic/logging.d.ts +179 -0
- package/dist/generic/logging.d.ts.map +1 -0
- package/dist/generic/logging.js +382 -0
- package/dist/generic/logging.js.map +1 -0
- package/dist/generic/metadata.d.ts +305 -0
- package/dist/generic/metadata.d.ts.map +1 -0
- package/dist/generic/metadata.js +454 -0
- package/dist/generic/metadata.js.map +1 -0
- package/dist/generic/metadataUtil.d.ts +8 -0
- package/dist/generic/metadataUtil.d.ts.map +1 -0
- package/dist/generic/metadataUtil.js +36 -0
- package/dist/generic/metadataUtil.js.map +1 -0
- package/dist/generic/providerBase.d.ts +546 -0
- package/dist/generic/providerBase.d.ts.map +1 -0
- package/dist/generic/providerBase.js +999 -0
- package/dist/generic/providerBase.js.map +1 -0
- package/dist/generic/queryInfo.d.ts +460 -0
- package/dist/generic/queryInfo.d.ts.map +1 -0
- package/dist/generic/queryInfo.js +633 -0
- package/dist/generic/queryInfo.js.map +1 -0
- package/dist/generic/querySQLFilters.d.ts +54 -0
- package/dist/generic/querySQLFilters.d.ts.map +1 -0
- package/dist/generic/querySQLFilters.js +84 -0
- package/dist/generic/querySQLFilters.js.map +1 -0
- package/dist/generic/runQuery.d.ts +96 -0
- package/dist/generic/runQuery.d.ts.map +1 -0
- package/dist/generic/runQuery.js +66 -0
- package/dist/generic/runQuery.js.map +1 -0
- package/dist/generic/runQuerySQLFilterImplementations.d.ts +51 -0
- package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -0
- package/dist/generic/runQuerySQLFilterImplementations.js +238 -0
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -0
- package/dist/generic/runReport.d.ts +25 -0
- package/dist/generic/runReport.d.ts.map +1 -0
- package/dist/generic/runReport.js +42 -0
- package/dist/generic/runReport.js.map +1 -0
- package/dist/generic/securityInfo.d.ts +355 -0
- package/dist/generic/securityInfo.d.ts.map +1 -0
- package/dist/generic/securityInfo.js +425 -0
- package/dist/generic/securityInfo.js.map +1 -0
- package/dist/generic/transactionGroup.d.ts +184 -0
- package/dist/generic/transactionGroup.d.ts.map +1 -0
- package/dist/generic/transactionGroup.js +357 -0
- package/dist/generic/transactionGroup.js.map +1 -0
- package/dist/generic/util.d.ts +81 -0
- package/dist/generic/util.d.ts.map +1 -0
- package/dist/generic/util.js +301 -0
- package/dist/generic/util.js.map +1 -0
- package/dist/views/runView.d.ts +150 -0
- package/dist/views/runView.d.ts.map +1 -0
- package/dist/views/runView.js +100 -0
- package/dist/views/runView.js.map +1 -0
- package/dist/views/viewInfo.d.ts +121 -0
- package/dist/views/viewInfo.d.ts.map +1 -0
- package/dist/views/viewInfo.js +182 -0
- package/dist/views/viewInfo.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1688 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BaseEntity = exports.BaseEntityEvent = exports.BaseEntityResult = exports.BaseEntityAIActionParams = exports.DataObjectParams = exports.DataObjectRelatedEntityParam = exports.EntityField = void 0;
|
|
4
|
+
const entityInfo_1 = require("./entityInfo");
|
|
5
|
+
const interfaces_1 = require("./interfaces");
|
|
6
|
+
const metadata_1 = require("./metadata");
|
|
7
|
+
const runView_1 = require("../views/runView");
|
|
8
|
+
const logging_1 = require("./logging");
|
|
9
|
+
const compositeKey_1 = require("./compositeKey");
|
|
10
|
+
const rxjs_1 = require("rxjs");
|
|
11
|
+
const Global_1 = require("../Global");
|
|
12
|
+
const interface_1 = require("../interface");
|
|
13
|
+
const util_1 = require("../util");
|
|
14
|
+
const ValidationTypes_1 = require("../ValidationTypes");
|
|
15
|
+
/**
|
|
16
|
+
* Represents a field in an instance of the BaseEntity class. This class is used to store the value of the field, dirty state, as well as other run-time information about the field. The class encapsulates the underlying field metadata and exposes some of the more commonly
|
|
17
|
+
* used properties from the entity field metadata.
|
|
18
|
+
*/
|
|
19
|
+
class EntityField {
|
|
20
|
+
get Name() {
|
|
21
|
+
return this._entityFieldInfo.Name;
|
|
22
|
+
}
|
|
23
|
+
get FieldType() {
|
|
24
|
+
return this._entityFieldInfo.TSType;
|
|
25
|
+
}
|
|
26
|
+
get SQLType() {
|
|
27
|
+
return this._entityFieldInfo.Type;
|
|
28
|
+
}
|
|
29
|
+
get IsPrimaryKey() {
|
|
30
|
+
return this._entityFieldInfo.IsPrimaryKey;
|
|
31
|
+
}
|
|
32
|
+
get NeedsQuotes() {
|
|
33
|
+
return this._entityFieldInfo.NeedsQuotes;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Removes spaces from the field name and returns the result.
|
|
37
|
+
*/
|
|
38
|
+
get CodeName() {
|
|
39
|
+
return this._entityFieldInfo.CodeName;
|
|
40
|
+
}
|
|
41
|
+
get IsUnique() {
|
|
42
|
+
return this._entityFieldInfo.IsUnique;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns the current value of the field.
|
|
46
|
+
*/
|
|
47
|
+
get Value() {
|
|
48
|
+
// Asserting status here for deprecated or disabled fields, not in constructor because
|
|
49
|
+
// we legacy fields will exist
|
|
50
|
+
if (this._assertActiveStatusRequired) {
|
|
51
|
+
entityInfo_1.EntityFieldInfo.AssertEntityFieldActiveStatus(this._entityFieldInfo, 'EntityField.Value setter');
|
|
52
|
+
}
|
|
53
|
+
return this._Value;
|
|
54
|
+
}
|
|
55
|
+
get ReadOnly() {
|
|
56
|
+
return this._entityFieldInfo.ReadOnly;
|
|
57
|
+
}
|
|
58
|
+
get EntityFieldInfo() {
|
|
59
|
+
return this._entityFieldInfo;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns true if the field is a uniqueidentifier in the database.
|
|
63
|
+
*/
|
|
64
|
+
get IsUniqueIdentifier() {
|
|
65
|
+
return this._entityFieldInfo.IsUniqueIdentifier;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns true if the field has a default value set
|
|
69
|
+
*/
|
|
70
|
+
get HasDefaultValue() {
|
|
71
|
+
return this._entityFieldInfo.HasDefaultValue;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Sets the value of the field. If the field is read only, nothing happens. If the field is not read only, the value is set and the internal representation of the dirty flag is flipped if the value is different from the old value.
|
|
75
|
+
*/
|
|
76
|
+
set Value(value) {
|
|
77
|
+
if (this._assertActiveStatusRequired && value !== this._Value) {
|
|
78
|
+
// asserting status here becuase the flag is on AND the values
|
|
79
|
+
// are different - this avoid assertions during sysops like SetMany that often aren't changing
|
|
80
|
+
// the value of the field
|
|
81
|
+
entityInfo_1.EntityFieldInfo.AssertEntityFieldActiveStatus(this._entityFieldInfo, 'EntityField.Value setter');
|
|
82
|
+
}
|
|
83
|
+
if (!this.ReadOnly ||
|
|
84
|
+
this._NeverSet /* Allow one time set of any field because BaseEntity Object passes in ReadOnly fields when we load,
|
|
85
|
+
after that load for a given INSTANCE of an EntityField object we never set a ReadOnly Field*/) {
|
|
86
|
+
this._Value = value;
|
|
87
|
+
// in the below, we set the OldValue, but only if (a) we have never set the value before, or (b) the value or the old value is not null - which means that we are in a record setup scenario
|
|
88
|
+
if (this._NeverSet &&
|
|
89
|
+
(value !== null || this._OldValue !== null)) {
|
|
90
|
+
// initial value set
|
|
91
|
+
this._OldValue = value;
|
|
92
|
+
}
|
|
93
|
+
this._NeverSet = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resets the NeverSet flag - this is generally an internal method but is available when working with read only fields (mainly primary key fields) to allow them
|
|
98
|
+
* to be set/changed once after the object is created. This is useful for scenarios where you want to set a read only field
|
|
99
|
+
* after the object is created, but only once. This is typically used in the BaseEntity class when loading an entity from an array of values or the DB and reusing an existing object.
|
|
100
|
+
*/
|
|
101
|
+
ResetNeverSetFlag() {
|
|
102
|
+
this._NeverSet = true;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Returns true if the field is dirty, false otherwise. A field is considered dirty if the value is different from the old value. If the field is read only, it is never dirty.
|
|
106
|
+
*/
|
|
107
|
+
get Dirty() {
|
|
108
|
+
if (this.ReadOnly)
|
|
109
|
+
return false;
|
|
110
|
+
else {
|
|
111
|
+
const oldNull = this._OldValue === null || this.OldValue === undefined || Number.isNaN(this.OldValue); // check for NaN because sometimes we have old values that are NaN and we need to account for that
|
|
112
|
+
const curNull = this.Value === null || this.Value === undefined || Number.isNaN(this.OldValue);
|
|
113
|
+
if (oldNull && curNull)
|
|
114
|
+
return false; // BOTH are null or undefined, not dirty
|
|
115
|
+
else {
|
|
116
|
+
let oldCompare = this._OldValue;
|
|
117
|
+
let newCompare = this.Value;
|
|
118
|
+
if (this._OldValue instanceof Date) {
|
|
119
|
+
// sometimes we have old values that are date objects, convert to UTC timestamp for comparison
|
|
120
|
+
oldCompare = this._OldValue.getTime();
|
|
121
|
+
}
|
|
122
|
+
if (this.Value instanceof Date) {
|
|
123
|
+
// and sometimes the new values are date objects, convert to UTC timestamp for comparison
|
|
124
|
+
newCompare = this.Value.getTime();
|
|
125
|
+
}
|
|
126
|
+
// Special handling for bit/Boolean types - treat truthy values as equivalent
|
|
127
|
+
if (this._entityFieldInfo.TSType === entityInfo_1.EntityFieldTSType.Boolean ||
|
|
128
|
+
this._entityFieldInfo.Type.toLowerCase() === 'bit') {
|
|
129
|
+
// Convert both values to boolean for comparison
|
|
130
|
+
const oldBool = this.convertToBoolean(oldCompare);
|
|
131
|
+
const newBool = this.convertToBoolean(newCompare);
|
|
132
|
+
return oldBool !== newBool;
|
|
133
|
+
}
|
|
134
|
+
// Special handling for numeric types - treat numeric strings that convert to same value as equivalent
|
|
135
|
+
if (this._entityFieldInfo.TSType === entityInfo_1.EntityFieldTSType.Number || this.isNumericType(this._entityFieldInfo.Type)) {
|
|
136
|
+
const oldNum = this.convertToNumber(oldCompare);
|
|
137
|
+
const newNum = this.convertToNumber(newCompare);
|
|
138
|
+
// Handle NaN cases - if both are NaN, they're equivalent
|
|
139
|
+
if (isNaN(oldNum) && isNaN(newNum)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
// If only one is NaN, they're different
|
|
143
|
+
if (isNaN(oldNum) || isNaN(newNum)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return oldNum !== newNum;
|
|
147
|
+
}
|
|
148
|
+
// for string types where the comparisons are not both strings
|
|
149
|
+
if (this._entityFieldInfo.TSType === entityInfo_1.EntityFieldTSType.String) {
|
|
150
|
+
if (typeof oldCompare === 'object') {
|
|
151
|
+
// need to convert the object to a string for comparison
|
|
152
|
+
oldCompare = JSON.stringify(oldCompare);
|
|
153
|
+
}
|
|
154
|
+
if (typeof newCompare === 'object') {
|
|
155
|
+
// need to convert the object to a string for comparison
|
|
156
|
+
newCompare = JSON.stringify(newCompare);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return oldCompare !== newCompare;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Helper method to convert a value to boolean for comparison purposes.
|
|
165
|
+
* Treats truthy values as true regardless of data type.
|
|
166
|
+
*/
|
|
167
|
+
convertToBoolean(value) {
|
|
168
|
+
if (value === null || value === undefined) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (typeof value === 'boolean') {
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
if (typeof value === 'number') {
|
|
175
|
+
return value !== 0;
|
|
176
|
+
}
|
|
177
|
+
if (typeof value === 'string') {
|
|
178
|
+
const normalized = value.trim().toLowerCase();
|
|
179
|
+
return normalized === 'true' || normalized === '1';
|
|
180
|
+
}
|
|
181
|
+
// For any other type, use JavaScript's truthiness
|
|
182
|
+
return !!value;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Helper method to check if a SQL type is numeric.
|
|
186
|
+
*/
|
|
187
|
+
isNumericType(sqlType) {
|
|
188
|
+
if (!sqlType)
|
|
189
|
+
return false;
|
|
190
|
+
const normalizedType = sqlType.toLowerCase().trim();
|
|
191
|
+
return ['int', 'smallint', 'bigint', 'tinyint', 'money', 'decimal', 'numeric', 'float', 'real'].includes(normalizedType) ||
|
|
192
|
+
normalizedType.startsWith('decimal(') ||
|
|
193
|
+
normalizedType.startsWith('numeric(');
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Helper method to convert a value to number for comparison purposes.
|
|
197
|
+
*/
|
|
198
|
+
convertToNumber(value) {
|
|
199
|
+
if (value === null || value === undefined) {
|
|
200
|
+
return NaN;
|
|
201
|
+
}
|
|
202
|
+
if (typeof value === 'number') {
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
if (typeof value === 'string') {
|
|
206
|
+
const trimmed = value.trim();
|
|
207
|
+
if (trimmed === '') {
|
|
208
|
+
return NaN;
|
|
209
|
+
}
|
|
210
|
+
return Number(trimmed);
|
|
211
|
+
}
|
|
212
|
+
if (typeof value === 'boolean') {
|
|
213
|
+
return value ? 1 : 0;
|
|
214
|
+
}
|
|
215
|
+
// For any other type, attempt conversion
|
|
216
|
+
return Number(value);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Convenience method to format the value of the field. This method calls the static method on EntityFieldInfo to do the actual formatting.
|
|
220
|
+
* @param decimals
|
|
221
|
+
* @param currency
|
|
222
|
+
* @returns
|
|
223
|
+
*/
|
|
224
|
+
FormatValue(decimals = 2, currency = 'USD') {
|
|
225
|
+
return this.EntityFieldInfo.FormatValue(this.Value, decimals, currency);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Validates the current value of the field. If the field is read only, or if the field is marked to skip validation, nothing happens.
|
|
229
|
+
* If the field is not read only, and the field is not marked to skip validation, the value is checked against the validation rules defined in the metadata for the field.
|
|
230
|
+
* @returns
|
|
231
|
+
*/
|
|
232
|
+
Validate() {
|
|
233
|
+
const ef = this._entityFieldInfo;
|
|
234
|
+
const result = new ValidationTypes_1.ValidationResult();
|
|
235
|
+
result.Success = true; // assume success
|
|
236
|
+
if (!ef.ReadOnly && !ef.SkipValidation) {
|
|
237
|
+
// only do validation on updatable fields and skip the special case fields defined inside the SkipValidation property (like ID/CreatedAt/UpdatedAt)
|
|
238
|
+
if (!ef.AllowsNull && (this.Value === null || this.Value === undefined)) {
|
|
239
|
+
// make sure this isn't a field that has a default value and we are inside a new record
|
|
240
|
+
if (ef.DefaultValue === null || ef.DefaultValue === undefined || ef.DefaultValue.trim().length === 0) {
|
|
241
|
+
// we have no default value, so this is an error
|
|
242
|
+
result.Success = false;
|
|
243
|
+
result.Errors.push(new ValidationTypes_1.ValidationErrorInfo(ef.Name, `${ef.DisplayNameOrName} cannot be null`, null));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// we do have a default value, but our current value is null. If we are in an EXISTING record, this is an error, check the OldValue to determine this
|
|
247
|
+
if (this._OldValue !== null && this._OldValue !== undefined) {
|
|
248
|
+
result.Success = false;
|
|
249
|
+
result.Errors.push(new ValidationTypes_1.ValidationErrorInfo(ef.Name, `${ef.DisplayNameOrName} cannot be null`, null));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (ef.TSType == entityInfo_1.EntityFieldTSType.String && ef.MaxLength > 0 && this.Value && this.Value.length > ef.MaxLength) {
|
|
254
|
+
result.Success = false;
|
|
255
|
+
result.Errors.push(new ValidationTypes_1.ValidationErrorInfo(ef.Name, `${ef.DisplayNameOrName} cannot be longer than ${ef.MaxLength} characters. Current value is ${this.Value.length} characters`, this.Value));
|
|
256
|
+
}
|
|
257
|
+
if (ef.TSType == entityInfo_1.EntityFieldTSType.Date && (this.Value !== null && this.Value !== undefined && !(this.Value instanceof Date))) {
|
|
258
|
+
// invalid non-null date, but that is okay if we are a new record and we have a default value
|
|
259
|
+
result.Success = false;
|
|
260
|
+
result.Errors.push(new ValidationTypes_1.ValidationErrorInfo(ef.Name, `${this.Value} is not a valid date for ${ef.DisplayNameOrName}`, this.Value));
|
|
261
|
+
}
|
|
262
|
+
// add validation to ensure a number value is within range based on the
|
|
263
|
+
// underlying SQL type
|
|
264
|
+
if (ef.TSType === 'number') {
|
|
265
|
+
const typeLookup = EntityField.SQLTypeValueRanges[ef.Type.toLowerCase()];
|
|
266
|
+
if (typeLookup) {
|
|
267
|
+
if (this.Value < typeLookup.min || this.Value > typeLookup.max) {
|
|
268
|
+
result.Success = false;
|
|
269
|
+
result.Errors.push(new ValidationTypes_1.ValidationErrorInfo(ef.Name, `${ef.DisplayNameOrName} is ${ef.SQLFullType} in the database and must be a valid number between ${-typeLookup.min} and ${typeLookup.max}. Current value is ${this.Value}`, this.Value));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
constructor(fieldInfo, Value) {
|
|
277
|
+
// NOTE: Do not assert EntityFieldInfo status here, because we are
|
|
278
|
+
// creating a new EntityField object and it is possible that the field
|
|
279
|
+
// is disabled or is deprecated, but we still need to create the object
|
|
280
|
+
// since it is physically part of the entity. We DO assert for the status
|
|
281
|
+
// if the Value is later accessed or set.
|
|
282
|
+
/**
|
|
283
|
+
* Indicates whether the active status of the field should be asserted when accessing or setting the value.
|
|
284
|
+
* Starts off as false and turns to true after contructor is done doing all its setup work. Internally, this can be
|
|
285
|
+
* temporarily turned off to allow for legacy fields to be created without asserting the active status.
|
|
286
|
+
*/
|
|
287
|
+
this._assertActiveStatusRequired = false;
|
|
288
|
+
this._NeverSet = true;
|
|
289
|
+
this._entityFieldInfo = fieldInfo;
|
|
290
|
+
if (Value) {
|
|
291
|
+
this.Value = Value;
|
|
292
|
+
}
|
|
293
|
+
else if (fieldInfo.DefaultValue) {
|
|
294
|
+
if (fieldInfo.TSType === entityInfo_1.EntityFieldTSType.Boolean) {
|
|
295
|
+
// special handling for booleans as we don't want a string passed into a boolean field, we want a true boolean
|
|
296
|
+
if (typeof fieldInfo.DefaultValue === "string" && fieldInfo.DefaultValue.trim() === "1" || fieldInfo.DefaultValue.trim().toLowerCase() === "true")
|
|
297
|
+
this.Value = true;
|
|
298
|
+
else
|
|
299
|
+
this.Value = false;
|
|
300
|
+
}
|
|
301
|
+
else if (fieldInfo.TSType === entityInfo_1.EntityFieldTSType.Number) {
|
|
302
|
+
// special handling for numbers as we don't want a string passed into a value for a numeric field
|
|
303
|
+
if (!isNaN(Number(fieldInfo.DefaultValue))) {
|
|
304
|
+
this.Value = Number(fieldInfo.DefaultValue);
|
|
305
|
+
}
|
|
306
|
+
else if (fieldInfo.DefaultValue.trim().toLowerCase() === "null") {
|
|
307
|
+
this.Value = null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else if (fieldInfo.Type.trim().toLowerCase() === "uniqueidentifier") {
|
|
311
|
+
// special handling for GUIDs, we don't want to populate anything here because the server always sets the value, leave blank
|
|
312
|
+
this.Value = null;
|
|
313
|
+
}
|
|
314
|
+
else if (fieldInfo.TSType === entityInfo_1.EntityFieldTSType.Date) {
|
|
315
|
+
if (fieldInfo.DefaultValue.trim().length > 0) {
|
|
316
|
+
// special handling for dates as we don't want to use getdate() type defaults as is, we want to convert them to a JS Date object
|
|
317
|
+
try {
|
|
318
|
+
// attempt to convert the default value to a date
|
|
319
|
+
const temp = new Date(fieldInfo.DefaultValue);
|
|
320
|
+
// if we get here, that means it worked, but we could have an invalid date, so check for that
|
|
321
|
+
if (isNaN(temp.getTime())) {
|
|
322
|
+
// this is an invalid date, so throw an error
|
|
323
|
+
throw new Error(); // blank error because we just end up going to the catch block anyway and in there we have logic to handle this properly
|
|
324
|
+
}
|
|
325
|
+
else
|
|
326
|
+
this.Value = temp;
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
// if we get here, that means the default value is not a valid date, so we need to check to see if it's a SQL current date function
|
|
330
|
+
if (entityInfo_1.EntityFieldInfo.IsDefaultValueSQLCurrentDateFunction(fieldInfo.DefaultValue)) {
|
|
331
|
+
// we have a SQL current date function default, leave the field alone if its a special date field as the server (i.e. database) will handle
|
|
332
|
+
//setting the value, otherwise set the value to the current date
|
|
333
|
+
if (fieldInfo.IsSpecialDateField) {
|
|
334
|
+
this.Value = null;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
this.Value = new Date();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// we have a default value that is not a valid date and not a getdate() type default, so we need to throw an error
|
|
342
|
+
throw new Error(`Invalid default value for ${fieldInfo.Entity}.${fieldInfo.Name} of ${fieldInfo.DefaultValue}. Default values for date fields must be a valid date or a getdate() type default`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
// for strings we're good to just set the value
|
|
349
|
+
this.Value = fieldInfo.DefaultValue;
|
|
350
|
+
}
|
|
351
|
+
this._NeverSet = true; // set this back to true because we are setting the default value and we want to be able to set this ONCE from BaseEntity when we load
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
this.Value = null; // we need to set the value to null instead of being undefined as the value is defined, it is NULL
|
|
355
|
+
this._NeverSet = true; // set this back to true because we are setting the value to null;
|
|
356
|
+
}
|
|
357
|
+
this._assertActiveStatusRequired = true; // turn on assertion for active status now that we're done with constructor.
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* This method will set the internal Old Value which is used to track dirty state, to the current value of the field. This effectively resets the dirty state of the field to false. Use this method sparingly.
|
|
361
|
+
*/
|
|
362
|
+
ResetOldValue() {
|
|
363
|
+
this._assertActiveStatusRequired = false; // temporarily turn off assertion for active status so we can set the old value without asserting
|
|
364
|
+
this._OldValue = this.Value;
|
|
365
|
+
this._assertActiveStatusRequired = true; // turn it back on after we're done
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* This property temporarily will set the active status assertions for this particular instance of EntityField.
|
|
369
|
+
* It is temporary because other behaviors in the class instance could reset this value for example calling
|
|
370
|
+
* ResetOldValue() or another caller setting this property to another value.
|
|
371
|
+
*/
|
|
372
|
+
get ActiveStatusAssertions() {
|
|
373
|
+
return this._assertActiveStatusRequired;
|
|
374
|
+
}
|
|
375
|
+
set ActiveStatusAssertions(value) {
|
|
376
|
+
this._assertActiveStatusRequired = value;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Returns the old value of the field. This is the value that was set when the field was last loaded from the database.
|
|
380
|
+
*/
|
|
381
|
+
get OldValue() {
|
|
382
|
+
return this._OldValue;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
exports.EntityField = EntityField;
|
|
386
|
+
/**
|
|
387
|
+
* Static object containing the value ranges for various SQL number types.
|
|
388
|
+
* This is used to validate the value of the field when it is set or validated.
|
|
389
|
+
*/
|
|
390
|
+
EntityField.SQLTypeValueRanges = {
|
|
391
|
+
"int": { min: -2147483648, max: 2147483647 },
|
|
392
|
+
"bigint": { min: -9223372036854775808, max: 9223372036854775807 },
|
|
393
|
+
"smallint": { min: -32768, max: 32767 },
|
|
394
|
+
"tinyint": { min: 0, max: 255 },
|
|
395
|
+
"decimal": { min: -7922816251426433759354395033, max: 79228162514264337593543950335 },
|
|
396
|
+
"numeric": { min: -7922816251426433759354395033, max: 79228162514264337593543950335 },
|
|
397
|
+
"float": { min: -1.7976931348623157e+308, max: 1.7976931348623157e+308 },
|
|
398
|
+
"real": { min: -3.402823466e+38, max: 3.402823466e+38 },
|
|
399
|
+
"money": { min: -922337203685477.5808, max: 922337203685477.5807 },
|
|
400
|
+
};
|
|
401
|
+
class DataObjectRelatedEntityParam {
|
|
402
|
+
}
|
|
403
|
+
exports.DataObjectRelatedEntityParam = DataObjectRelatedEntityParam;
|
|
404
|
+
class DataObjectParams {
|
|
405
|
+
constructor(oldValues = false, omitNullValues = false, omitEmptyStrings = false, excludeFields = [], includeRelatedEntityData = false, relatedEntityList = []) {
|
|
406
|
+
this.oldValues = oldValues;
|
|
407
|
+
this.omitNullValues = omitNullValues;
|
|
408
|
+
this.omitEmptyStrings = omitEmptyStrings;
|
|
409
|
+
this.excludeFields = excludeFields;
|
|
410
|
+
this.includeRelatedEntityData = includeRelatedEntityData;
|
|
411
|
+
this.relatedEntityList = relatedEntityList;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
exports.DataObjectParams = DataObjectParams;
|
|
415
|
+
class BaseEntityAIActionParams {
|
|
416
|
+
}
|
|
417
|
+
exports.BaseEntityAIActionParams = BaseEntityAIActionParams;
|
|
418
|
+
/**
|
|
419
|
+
* Used for storing the result of a Save or Delete or other transactional operation within a BaseEntity
|
|
420
|
+
*/
|
|
421
|
+
class BaseEntityResult {
|
|
422
|
+
constructor(success, message, type) {
|
|
423
|
+
/**
|
|
424
|
+
* A copy of the values of the entity object BEFORE the operation was performed
|
|
425
|
+
*/
|
|
426
|
+
this.OriginalValues = [];
|
|
427
|
+
/**
|
|
428
|
+
* A copy of the values of the entity object AFTER the operation was performed
|
|
429
|
+
*/
|
|
430
|
+
this.NewValues = [];
|
|
431
|
+
this.Success = success === undefined ? false : success;
|
|
432
|
+
this.Type = type === undefined ? 'create' : type;
|
|
433
|
+
this.Message = message === undefined ? '' : message;
|
|
434
|
+
this.Error = null;
|
|
435
|
+
this.Errors = [];
|
|
436
|
+
this.StartedAt = new Date();
|
|
437
|
+
this.EndedAt = new Date();
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Returns a complete message that includes the Message property (if present), the Error property (if present), and any Errors array items (if present).
|
|
441
|
+
*/
|
|
442
|
+
get CompleteMessage() {
|
|
443
|
+
let msg = undefined;
|
|
444
|
+
// first check the message property
|
|
445
|
+
if (this.Message && this.Message.trim().length > 0) {
|
|
446
|
+
msg = this.Message;
|
|
447
|
+
}
|
|
448
|
+
// now check the simple Error property
|
|
449
|
+
if (this.Error) {
|
|
450
|
+
msg = (msg ? msg + '\n' : '');
|
|
451
|
+
if (typeof this.Error === 'string') {
|
|
452
|
+
msg += this.Error;
|
|
453
|
+
}
|
|
454
|
+
else if (this.Error.message) {
|
|
455
|
+
msg += this.Error.message;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
msg += JSON.stringify(this.Error);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// now check the Errors array
|
|
462
|
+
if (this.Errors && this.Errors.length > 0) {
|
|
463
|
+
// append
|
|
464
|
+
msg = (msg ? msg + '\n' : '') + this.Errors.map(err => err.message || JSON.stringify(err)).join('\n');
|
|
465
|
+
}
|
|
466
|
+
return msg;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
exports.BaseEntityResult = BaseEntityResult;
|
|
470
|
+
/**
|
|
471
|
+
* Event type that is used to raise events and provided structured callbacks for any caller that is interested in registering for events.
|
|
472
|
+
* This type is also used for whenever a BaseEntity instance raises an event with MJGlobal.
|
|
473
|
+
*/
|
|
474
|
+
class BaseEntityEvent {
|
|
475
|
+
}
|
|
476
|
+
exports.BaseEntityEvent = BaseEntityEvent;
|
|
477
|
+
/**
|
|
478
|
+
* Base class used for all entity objects. This class is abstract and is sub-classes for each particular entity using the CodeGen tool. This class provides the basic functionality for loading, saving, and validating entity objects.
|
|
479
|
+
*/
|
|
480
|
+
class BaseEntity {
|
|
481
|
+
constructor(Entity, Provider = null) {
|
|
482
|
+
this._Fields = [];
|
|
483
|
+
this._recordLoaded = false;
|
|
484
|
+
this._contextCurrentUser = null;
|
|
485
|
+
this._transactionGroup = null;
|
|
486
|
+
this._resultHistory = [];
|
|
487
|
+
this._provider = null;
|
|
488
|
+
this._everSaved = false;
|
|
489
|
+
this._compositeKey = null;
|
|
490
|
+
// Holds the current pending save observable (if any)
|
|
491
|
+
this._pendingSave$ = null;
|
|
492
|
+
/**
|
|
493
|
+
* private storage for vectors that might be used for this entity, typically in association with individual fields
|
|
494
|
+
* however it is possible for the string in the map to be any unique key relative to the object so you could have vectors
|
|
495
|
+
* that embed multiple fields if desired.
|
|
496
|
+
*/
|
|
497
|
+
this._vectors = new Map();
|
|
498
|
+
this._eventSubject = new rxjs_1.Subject();
|
|
499
|
+
this._EntityInfo = Entity;
|
|
500
|
+
entityInfo_1.EntityInfo.AssertEntityActiveStatus(Entity, 'BaseEntity::constructor');
|
|
501
|
+
this._provider = Provider;
|
|
502
|
+
this.init();
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Returns this provider to be used for a given instance of a BaseEntity derived subclass. If the provider is not set, the BaseEntity.Provider is returned.
|
|
506
|
+
*/
|
|
507
|
+
get ProviderToUse() {
|
|
508
|
+
return this._provider || BaseEntity.Provider;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Returns the RunViewProvider to be used for a given instance of a BaseEntity derived subclass.
|
|
512
|
+
*/
|
|
513
|
+
get RunViewProviderToUse() {
|
|
514
|
+
return this.ProviderToUse;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Returns the RunQueryProvider to be used for a given instance of a BaseEntity derived subclass.
|
|
518
|
+
*/
|
|
519
|
+
get RunQueryProviderToUse() {
|
|
520
|
+
return this.ProviderToUse;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Returns the RunReportProvider to be used for a given instance of a BaseEntity derived subclass.
|
|
524
|
+
*/
|
|
525
|
+
get RunReportProviderToUse() {
|
|
526
|
+
return this.ProviderToUse;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* This method can be used to register a callback for events that will be raised by the instance of the BaseEntity object. The callback will be called with a
|
|
530
|
+
* BaseEntityEvent object that contains the type of event and any payload that is associated with the event. Subclasses of the BaseEntity can define their
|
|
531
|
+
* own event types and payloads as needed.
|
|
532
|
+
* @param callback
|
|
533
|
+
* @returns
|
|
534
|
+
*/
|
|
535
|
+
RegisterEventHandler(callback) {
|
|
536
|
+
return this._eventSubject.asObservable().subscribe(callback);
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* If the entity object has a TransactionGroup associated with it, the TransactionGroup will be notified that we are doing some transaction pre-processing so that the TransactionGroup can
|
|
540
|
+
* properly wait for those pre-processing steps to complete before submitting the transaction. This method should generally NOT be called by anyone other than a provider that is handling
|
|
541
|
+
* the tier-specific processing for the entity object.
|
|
542
|
+
*/
|
|
543
|
+
RegisterTransactionPreprocessing() {
|
|
544
|
+
if (this.TransactionGroup) {
|
|
545
|
+
this.TransactionGroup.RegisterPreprocessing(this);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Raises the transaction_ready event. This is used to indicate that the entity object is ready to be submitted for transaction processing. This is used by the TransactionGroup class to know when all async preprocessing is
|
|
550
|
+
* done and it can submit the transaction. This is an internal method and shouldn't be used by sub-classes or external callers in most cases. It is primarily used by Provider classes who are handling the tier-specific processing
|
|
551
|
+
* for the entity object.
|
|
552
|
+
*/
|
|
553
|
+
RaiseReadyForTransaction() {
|
|
554
|
+
this.RaiseEvent('transaction_ready', null);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* When a BaseEntity class raises an event with MJGlobal, the eventCode property is set to this value. This is used to identify events that are raised by BaseEntity objects.
|
|
558
|
+
* Any MJGlobal event that is raised by a BaseEntity class will use a BaseEntityEvent type as the args parameter
|
|
559
|
+
*/
|
|
560
|
+
static get BaseEventCode() {
|
|
561
|
+
return BaseEntity._baseEventCode;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Used for raising events within the BaseEntity and can be used by sub-classes to raise events that are specific to the entity.
|
|
565
|
+
*/
|
|
566
|
+
RaiseEvent(type, payload, saveSubType = undefined) {
|
|
567
|
+
// this is the local event handler that is specific to THIS instance of the entity object
|
|
568
|
+
(0, logging_1.LogDebug)(`BaseEntity.RaiseEvent() - ${type === 'save' ? 'save:' + saveSubType : type} event raised for ${this.EntityInfo.Name}, about to call this._eventSubject.next()`);
|
|
569
|
+
this._eventSubject.next({ type: type, payload: payload, saveSubType: saveSubType, baseEntity: this });
|
|
570
|
+
// this next call is to MJGlobal to let everyone who cares knows that we had an event on an entity object
|
|
571
|
+
// at the moment we only broadcast save/delete not the others
|
|
572
|
+
if (type === 'save' || type === 'delete') {
|
|
573
|
+
const event = new BaseEntityEvent();
|
|
574
|
+
event.baseEntity = this;
|
|
575
|
+
event.payload = payload;
|
|
576
|
+
event.type = type;
|
|
577
|
+
event.saveSubType = saveSubType;
|
|
578
|
+
(0, logging_1.LogDebug)(`BaseEntity.RaiseEvent() - ${type === 'save' ? 'save:' + saveSubType : type} event raised for ${this.EntityInfo.Name}, about to call MJGlobal.RaiseEvent()`);
|
|
579
|
+
Global_1.MJGlobal.Instance.RaiseEvent({
|
|
580
|
+
component: this,
|
|
581
|
+
event: interface_1.MJEventType.ComponentEvent,
|
|
582
|
+
eventCode: BaseEntity.BaseEventCode,
|
|
583
|
+
args: event
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* This method MUST be called right after the class is instantiated to provide an async/await pair for any asynchronous operations a given entity needs to do when it is first
|
|
589
|
+
* created/configured. When you call Metadata/Provider GetEntityObject() this is done automatically for you. In nearly all cases you should go through GetEntityObject() anyway
|
|
590
|
+
* and not ever directly instantiate a BaseEntity derived class.
|
|
591
|
+
*/
|
|
592
|
+
async Config(contextUser) {
|
|
593
|
+
this.ContextCurrentUser = contextUser;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Returns true if the record has been saved to the database, false otherwise. This is a useful property to check to determine if the record is a "New Record" or an existing one.
|
|
597
|
+
*/
|
|
598
|
+
get IsSaved() {
|
|
599
|
+
return this._everSaved;
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Transaction Groups are used to group multiple transactions into a single ATOMic transaction in a database. They are also useful even in situations with ATOMicity is less important but you want
|
|
603
|
+
* to submit a group of changes to the API server in a single network call.
|
|
604
|
+
*/
|
|
605
|
+
get TransactionGroup() {
|
|
606
|
+
return this._transactionGroup;
|
|
607
|
+
}
|
|
608
|
+
set TransactionGroup(group) {
|
|
609
|
+
this._transactionGroup = group;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* The result history shows the history of the attempted transactions (Save and Delete) for this particular entity object. This is useful for tracking the results of operations on the entity object.
|
|
613
|
+
*/
|
|
614
|
+
get ResultHistory() {
|
|
615
|
+
return this._resultHistory;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Returns the most recent result from the result history. If there are no results in the history, this method will return null.
|
|
619
|
+
*/
|
|
620
|
+
get LatestResult() {
|
|
621
|
+
if (this._resultHistory.length > 0)
|
|
622
|
+
return this._resultHistory[this._resultHistory.length - 1];
|
|
623
|
+
else
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Access to the underlying metadata for the entity object.
|
|
628
|
+
*/
|
|
629
|
+
get EntityInfo() {
|
|
630
|
+
return this._EntityInfo;
|
|
631
|
+
}
|
|
632
|
+
get Fields() {
|
|
633
|
+
return this._Fields;
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Convenience method to access a field by name. This method is case-insensitive and will return null if the field is not found. You can do the same thing with more fine tune controlled by accessing the Fields property directly.
|
|
637
|
+
* @param fieldName
|
|
638
|
+
* @returns
|
|
639
|
+
*/
|
|
640
|
+
GetFieldByName(fieldName) {
|
|
641
|
+
if (!fieldName) {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
const lcase = fieldName.trim().toLowerCase(); // do this once as we will use it multiple times
|
|
645
|
+
return this.Fields.find(f => f.Name.trim().toLowerCase() === lcase);
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Returns true if the object is Dirty, meaning something has changed since it was last saved to the database, and false otherwise. For new records, this will always return true.
|
|
649
|
+
*/
|
|
650
|
+
get Dirty() {
|
|
651
|
+
return !this.IsSaved || this.Fields.some(f => f.Dirty);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Returns an array of all primary key fields for the entity. If the entity has a composite primary key, this method will return an array of all primary key fields.
|
|
655
|
+
* If the entity has a single primary key, this method will return an array with a single field in it.
|
|
656
|
+
*/
|
|
657
|
+
get PrimaryKeys() {
|
|
658
|
+
return this.EntityInfo.PrimaryKeys.map(pk => this.GetFieldByName(pk.Name));
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Returns the primary key for the record. The CompositeKey class is a multi-valued key that can have any number of key/value pairs within it. Always traverse the full
|
|
662
|
+
* set of key/value pairs to get the full primary key for the record.
|
|
663
|
+
*/
|
|
664
|
+
get PrimaryKey() {
|
|
665
|
+
if (this._compositeKey === null) {
|
|
666
|
+
this._compositeKey = new compositeKey_1.CompositeKey();
|
|
667
|
+
this._compositeKey.LoadFromEntityFields(this.PrimaryKeys);
|
|
668
|
+
}
|
|
669
|
+
return this._compositeKey;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Helper method to return just the first Primary Key
|
|
673
|
+
*/
|
|
674
|
+
get FirstPrimaryKey() {
|
|
675
|
+
return this.PrimaryKeys[0];
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Returns true if the record has been loaded from the database, false otherwise. This is useful to check to see if the record is in a "New Record" state or not.
|
|
679
|
+
*/
|
|
680
|
+
get RecordLoaded() {
|
|
681
|
+
return this._recordLoaded;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Sets the value of a given field. If the field doesn't exist, nothing happens.
|
|
685
|
+
* The field's type is used to convert the value to the appropriate type.
|
|
686
|
+
* @param FieldName
|
|
687
|
+
* @param Value
|
|
688
|
+
*/
|
|
689
|
+
Set(FieldName, Value) {
|
|
690
|
+
const field = this.GetFieldByName(FieldName);
|
|
691
|
+
if (field != null) {
|
|
692
|
+
if (field.EntityFieldInfo.TSType === entityInfo_1.EntityFieldTSType.Date && (typeof Value === 'string' || typeof Value === 'number')) {
|
|
693
|
+
field.Value = new Date(Value);
|
|
694
|
+
}
|
|
695
|
+
else if (field.EntityFieldInfo.TSType === entityInfo_1.EntityFieldTSType.Number && typeof Value === 'string' && Value !== null && Value !== undefined) {
|
|
696
|
+
const numericValue = Number(Value);
|
|
697
|
+
if (!isNaN(numericValue)) {
|
|
698
|
+
field.Value = numericValue;
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
field.Value = Value;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
else if (field.EntityFieldInfo.TSType === entityInfo_1.EntityFieldTSType.Boolean && Value !== null && Value !== undefined) {
|
|
705
|
+
if (typeof Value === 'string') {
|
|
706
|
+
if (Value.trim() === '1' || Value.trim().toLowerCase() === 'true') {
|
|
707
|
+
field.Value = true;
|
|
708
|
+
}
|
|
709
|
+
else if (Value.trim() === '0' || Value.trim().toLowerCase() === 'false') {
|
|
710
|
+
field.Value = false;
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
field.Value = Value;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
else if (typeof Value === 'number') {
|
|
717
|
+
field.Value = Value !== 0;
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
field.Value = Value;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
field.Value = Value;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Returns the value of the field with the given name. If the field is a date, and the value is a string, it will be converted to a date object.
|
|
730
|
+
* @param FieldName
|
|
731
|
+
* @returns
|
|
732
|
+
*/
|
|
733
|
+
Get(FieldName) {
|
|
734
|
+
const field = this.GetFieldByName(FieldName);
|
|
735
|
+
if (field != null) {
|
|
736
|
+
// if the field is a date and the value is a string, convert it to a date
|
|
737
|
+
if (field.EntityFieldInfo.TSType === entityInfo_1.EntityFieldTSType.Date && (typeof field.Value === 'string' || typeof field.Value === 'number')) {
|
|
738
|
+
field.Value = new Date(field.Value);
|
|
739
|
+
}
|
|
740
|
+
return field.Value;
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* NOTE: Do not call this method directly. Use the {@link From} method instead
|
|
746
|
+
*
|
|
747
|
+
* Sets any number of values on the entity object from the object passed in. The properties of the object being passed in must either match the field name (in most cases) or the CodeName (which is only different from field name if field name has spaces in it)
|
|
748
|
+
* @param object
|
|
749
|
+
* @param ignoreNonExistentFields - if set to true, fields that don't exist on the entity object will be ignored, if false, an error will be thrown if a field doesn't exist
|
|
750
|
+
* @param replaceOldValues - if set to true, the old values of the fields will be reset to the values provided in the object parameter, if false, they will be left alone
|
|
751
|
+
* @param ignoreActiveStatusAssertions - if set to true, the active status assertions for the fields will be ignored, if false, an error will be thrown if a field is not active. Defaults to false.
|
|
752
|
+
*/
|
|
753
|
+
SetMany(object, ignoreNonExistentFields = false, replaceOldValues = false, ignoreActiveStatusAssertions = false) {
|
|
754
|
+
if (!object)
|
|
755
|
+
throw new Error('calling BaseEntity.SetMany(), object cannot be null or undefined');
|
|
756
|
+
for (let key in object) {
|
|
757
|
+
const field = this.GetFieldByName(key);
|
|
758
|
+
if (field) {
|
|
759
|
+
// check to see if key matches a field name, if so, set it
|
|
760
|
+
const priorActiveStatusAssertions = field.ActiveStatusAssertions; // save the current active status assertions
|
|
761
|
+
if (ignoreActiveStatusAssertions) {
|
|
762
|
+
field.ActiveStatusAssertions = false; // disable active status assertions for this field
|
|
763
|
+
}
|
|
764
|
+
this.Set(key, object[key]);
|
|
765
|
+
if (replaceOldValues) {
|
|
766
|
+
field.ResetOldValue();
|
|
767
|
+
}
|
|
768
|
+
if (ignoreActiveStatusAssertions) {
|
|
769
|
+
field.ActiveStatusAssertions = priorActiveStatusAssertions; // restore the active status assertions
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
// if we don't find a match for the field name, check to see if we have a match for the code name
|
|
774
|
+
// because some objects passed in will use the code name
|
|
775
|
+
const field = this.Fields.find(f => f.CodeName.trim().toLowerCase() == key.trim().toLowerCase());
|
|
776
|
+
if (field) {
|
|
777
|
+
const priorActiveStatusAssertions = field.ActiveStatusAssertions; // save the current active status assertions
|
|
778
|
+
if (ignoreActiveStatusAssertions) {
|
|
779
|
+
field.ActiveStatusAssertions = false; // disable active status assertions for this field
|
|
780
|
+
}
|
|
781
|
+
this.Set(field.Name, object[key]);
|
|
782
|
+
if (replaceOldValues) {
|
|
783
|
+
field.ResetOldValue();
|
|
784
|
+
}
|
|
785
|
+
if (ignoreActiveStatusAssertions) {
|
|
786
|
+
field.ActiveStatusAssertions = priorActiveStatusAssertions; // restore the active status assertions
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
// if we get here, we have a field that doesn't match either the field name or the code name, so throw an error
|
|
791
|
+
if (!ignoreNonExistentFields)
|
|
792
|
+
throw new Error(`Field ${key} does not exist on ${this.EntityInfo.Name}`);
|
|
793
|
+
else
|
|
794
|
+
console.warn(`Field ${key} does not exist on ${this.EntityInfo.Name}, ignoring because ignoreNonExistentFields was set to true`);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* NOTE: Do not call this method directly. Use the {@link To} method instead
|
|
801
|
+
*
|
|
802
|
+
* Utility method to create an object and return it with properties in the newly created and returned object for each field in the entity object. This is useful for scenarios where you need to be able to persist the data
|
|
803
|
+
* in a format to send to a network call, save to a file or database, etc. This method will return an object with properties that match the field names of the entity object.
|
|
804
|
+
* @param oldValues When set to true, the old values of the fields will be returned instead of the current values.
|
|
805
|
+
* @param onlyDirtyFields When set to true, only the fields that are dirty will be returned.
|
|
806
|
+
* @returns
|
|
807
|
+
*/
|
|
808
|
+
GetAll(oldValues = false, onlyDirtyFields = false) {
|
|
809
|
+
let obj = {};
|
|
810
|
+
for (let field of this.Fields) {
|
|
811
|
+
if (!onlyDirtyFields || (onlyDirtyFields && field.Dirty)) {
|
|
812
|
+
const tempStatus = field.ActiveStatusAssertions; // save the current active status assertions
|
|
813
|
+
field.ActiveStatusAssertions = false; // disable active status assertions for this field
|
|
814
|
+
obj[field.Name] = oldValues ? field.OldValue : field.Value;
|
|
815
|
+
if (field.EntityFieldInfo.TSType == entityInfo_1.EntityFieldTSType.Date && obj[field.Name] && !(obj[field.Name] instanceof Date)) {
|
|
816
|
+
obj[field.Name] = new Date(obj[field.Name]); // a timestamp, convert to JS Date Object
|
|
817
|
+
}
|
|
818
|
+
field.ActiveStatusAssertions = tempStatus; // restore the prior status for assertions
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return obj;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Returns a partial object that contains only the fields that have changed since the last time the record was saved. This is useful for scenarios where you want to send only the changes to the server or to a client.
|
|
825
|
+
* It is also helpful for quickly finding the fields that are "dirty".
|
|
826
|
+
* @returns
|
|
827
|
+
*/
|
|
828
|
+
GetChangesSinceLastSave() {
|
|
829
|
+
return this.GetAll(false, true);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* This utility method calls GetDataObject() internally and formats the result as a JSON string. If you want to get the data as an object instead of a string, call GetDataObject() directly.
|
|
833
|
+
* @param params
|
|
834
|
+
* @param minifyJSON
|
|
835
|
+
* @returns
|
|
836
|
+
*/
|
|
837
|
+
async GetDataObjectJSON(params = null, minifyJSON = true) {
|
|
838
|
+
const obj = await this.GetDataObject(params);
|
|
839
|
+
if (minifyJSON)
|
|
840
|
+
return JSON.stringify(obj);
|
|
841
|
+
else
|
|
842
|
+
return JSON.stringify(obj, null, 2);
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* This utility method generates a completely new object that has properties that map to the fields and values in the entity at the time it is called. It is a copy, NOT a link, so any changes
|
|
846
|
+
* made to the object after calling this method will NOT be reflected in the object that is returned. This is useful for things like sending data to a client, or for use in a view model.
|
|
847
|
+
* @param params
|
|
848
|
+
* @returns
|
|
849
|
+
*/
|
|
850
|
+
async GetDataObject(params = null) {
|
|
851
|
+
if (!params)
|
|
852
|
+
params = new DataObjectParams();
|
|
853
|
+
// first, get the object from GetAll
|
|
854
|
+
const obj = this.GetAll(params.oldValues);
|
|
855
|
+
// next, check each value and if it is null or empty string, remove it if those options were set
|
|
856
|
+
for (let key in obj) {
|
|
857
|
+
const v = obj[key];
|
|
858
|
+
if (params.omitNullValues && v === null)
|
|
859
|
+
delete obj[key]; // null value, and caller didn't want null values
|
|
860
|
+
else if (params.omitEmptyStrings && (v !== null && typeof v === 'string' && v.trim().length === 0))
|
|
861
|
+
delete obj[key]; // blank string, and caller didn't want blank strings
|
|
862
|
+
else if (params.excludeFields && params.excludeFields.indexOf(key) >= 0)
|
|
863
|
+
delete obj[key]; // caller asked to not get this field
|
|
864
|
+
}
|
|
865
|
+
if (params.includeRelatedEntityData) {
|
|
866
|
+
// now, get the related entity data
|
|
867
|
+
for (let i = 0; i < this._EntityInfo.RelatedEntities.length; i++) {
|
|
868
|
+
const re = this._EntityInfo.RelatedEntities[i];
|
|
869
|
+
const pre = params.relatedEntityList ? params.relatedEntityList.find(r => r.relatedEntityName === re.RelatedEntity) : null;
|
|
870
|
+
if (pre) {
|
|
871
|
+
// pre now has the param that matches the related entity (re) that we are looking at
|
|
872
|
+
// we are now here because either the caller didn't provide a list of entities to include
|
|
873
|
+
// (which means to include all of 'em), or they did and this entity is in the list
|
|
874
|
+
const reData = await this.GetRelatedEntityDataExt(re, pre.filter, pre.maxRecords);
|
|
875
|
+
if (reData) {
|
|
876
|
+
obj[re.RelatedEntity] = reData; // got some data (or an empty array) back, add it to the object
|
|
877
|
+
if (pre.maxRecords > 0) {
|
|
878
|
+
// add a note to the object to let the caller know that only the first X records are returned so
|
|
879
|
+
// that a caller can know that there could be more records available if they want them
|
|
880
|
+
let msg;
|
|
881
|
+
if (pre.maxRecords < reData.TotalRowCount)
|
|
882
|
+
msg = `Only the first ${pre.maxRecords} records are included in this response. There are ${reData.TotalRowCount} total records available.`;
|
|
883
|
+
else
|
|
884
|
+
msg = `All ${reData.TotalRowCount} records are included in this response.`;
|
|
885
|
+
obj[re.RelatedEntity].Note = msg; // add the message to the object as "Note"
|
|
886
|
+
obj[re.RelatedEntity].MaxRecordsFilter = pre.maxRecords; // add the max records to the object as "MaxRecords"
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return obj;
|
|
893
|
+
}
|
|
894
|
+
async GetRelatedEntityData(re, filter = null, maxRecords = null) {
|
|
895
|
+
const ret = await this.GetRelatedEntityDataExt(re, filter, maxRecords);
|
|
896
|
+
return ret?.Data;
|
|
897
|
+
}
|
|
898
|
+
async GetRelatedEntityDataExt(re, filter = null, maxRecords = null) {
|
|
899
|
+
// we need to query the database to get related entity info
|
|
900
|
+
const params = entityInfo_1.EntityInfo.BuildRelationshipViewParams(this, re, filter, maxRecords);
|
|
901
|
+
const rv = new runView_1.RunView();
|
|
902
|
+
const result = await rv.RunView(params, this._contextCurrentUser);
|
|
903
|
+
if (result && result.Success) {
|
|
904
|
+
return {
|
|
905
|
+
Data: result.Results,
|
|
906
|
+
TotalRowCount: result.TotalRowCount
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
else
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
init() {
|
|
913
|
+
this._compositeKey = null;
|
|
914
|
+
this._resultHistory = [];
|
|
915
|
+
this._recordLoaded = false;
|
|
916
|
+
this._Fields = [];
|
|
917
|
+
if (this.EntityInfo) {
|
|
918
|
+
for (const rawField of this.EntityInfo.Fields) {
|
|
919
|
+
const key = this.EntityInfo.Name + '.' + rawField.Name;
|
|
920
|
+
// support for sub-classes of the EntityField class
|
|
921
|
+
const newField = Global_1.MJGlobal.Instance.ClassFactory.CreateInstance(EntityField, key, rawField);
|
|
922
|
+
this.Fields.push(newField);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* This method will copy the values from the other entity object into the current one. This is useful for things like cloning a record.
|
|
928
|
+
* This method will ONLY copy values for fields that exist in the current entity object. If the other object has fields that don't exist in the current object, they will be ignored.
|
|
929
|
+
* @param other - the other entity object to copy values from
|
|
930
|
+
* @param includePrimaryKeys - if true, the primary keys will be copied as well, if false, they will be ignored, defaults to false and generally you want to leave it that way
|
|
931
|
+
* @param replaceOldValues - if true, the old values of the fields will be reset to the values provided in the other parameter, if false, they will be left alone, defaults to false and generally you want to leave it that way
|
|
932
|
+
*/
|
|
933
|
+
CopyFrom(other, includePrimaryKeys = false, replaceOldValues = false) {
|
|
934
|
+
try {
|
|
935
|
+
// iterate through all of OUR fields and set them to the value of the other object, if they exist in the other object
|
|
936
|
+
for (let field of this.Fields) {
|
|
937
|
+
if (!field.IsPrimaryKey || includePrimaryKeys) {
|
|
938
|
+
const otherField = other.GetFieldByName(field.Name);
|
|
939
|
+
if (otherField) {
|
|
940
|
+
this.Set(field.Name, otherField.Value);
|
|
941
|
+
if (replaceOldValues) {
|
|
942
|
+
field.ResetOldValue();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
catch (e) {
|
|
950
|
+
(0, logging_1.LogError)(`Error in BaseEntity.CopyFrom: ${e}`);
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* The ContextCurrentUser is a property used to manually set the "current" user for scenarios, primarily on the server side, where the user changes per request. For situations where there is no global CurrentUser in the Metadata.Provider,
|
|
956
|
+
* you MUST set this property to the user you want to use for the current operation. If you used Metadata.GetEntityObject() to get the entity object, this property will be set automatically for you as that method has a parameter that can
|
|
957
|
+
* be provided for the ContextCurrentUser.
|
|
958
|
+
*/
|
|
959
|
+
set ContextCurrentUser(user) {
|
|
960
|
+
this._contextCurrentUser = user;
|
|
961
|
+
}
|
|
962
|
+
get ContextCurrentUser() {
|
|
963
|
+
return this._contextCurrentUser;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* This method will create a new state for the object that is equivalent to a new record including default values.
|
|
967
|
+
* @param newValues - optional parameter to set the values of the fields to something other than the default values. The expected parameter is an object that has properties that map to field names in this entity.
|
|
968
|
+
* This is the same as creating a NewRecord and then using SetMany(), but it is a convenience/helper approach.
|
|
969
|
+
* @returns
|
|
970
|
+
*/
|
|
971
|
+
NewRecord(newValues) {
|
|
972
|
+
this.init();
|
|
973
|
+
this._everSaved = false; // Reset save state for new record
|
|
974
|
+
// Generate UUID for non-auto-increment uniqueidentifier primary keys
|
|
975
|
+
if (this.EntityInfo.PrimaryKeys.length === 1) {
|
|
976
|
+
const pk = this.EntityInfo.PrimaryKeys[0];
|
|
977
|
+
if (!pk.AutoIncrement &&
|
|
978
|
+
pk.Type.toLowerCase().trim() === 'uniqueidentifier' &&
|
|
979
|
+
!this.Get(pk.Name)) {
|
|
980
|
+
// Generate and set UUID for this primary key
|
|
981
|
+
const uuid = (0, util_1.uuidv4)();
|
|
982
|
+
this.Set(pk.Name, uuid);
|
|
983
|
+
const field = this.GetFieldByName(pk.Name);
|
|
984
|
+
if (field) {
|
|
985
|
+
// Reset the never set flag so that we can set this value later if needed, this is so that people who do deferred load after new record are still ok
|
|
986
|
+
field.ResetNeverSetFlag();
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (newValues) {
|
|
991
|
+
newValues.KeyValuePairs.filter(kv => kv.Value !== null && kv.Value !== undefined).forEach(kv => {
|
|
992
|
+
this.Set(kv.FieldName, kv.Value);
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
this.RaiseEvent('new_record', null);
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Saves the current state of the object to the database. Uses the active provider to handle the actual saving of the record.
|
|
1000
|
+
* If the record is new, it will be created, if it already exists, it will be updated.
|
|
1001
|
+
*
|
|
1002
|
+
* Debounces multiple calls so that if Save() is called again while a save is in progress,
|
|
1003
|
+
* the second call will simply receive the same result as the first.
|
|
1004
|
+
*
|
|
1005
|
+
* @param options
|
|
1006
|
+
* @returns Promise<boolean>
|
|
1007
|
+
*/
|
|
1008
|
+
async Save(options) {
|
|
1009
|
+
// If a save is already in progress, return its promise.
|
|
1010
|
+
if (this._pendingSave$) {
|
|
1011
|
+
return (0, rxjs_1.firstValueFrom)(this._pendingSave$);
|
|
1012
|
+
}
|
|
1013
|
+
// Create a new observable that debounces duplicative calls, and executes the save.
|
|
1014
|
+
this._pendingSave$ = (0, rxjs_1.of)(options).pipe(
|
|
1015
|
+
// Execute the actual save logic.
|
|
1016
|
+
(0, rxjs_1.switchMap)(opts => (0, rxjs_1.from)(this._InnerSave(opts))),
|
|
1017
|
+
// When the save completes (whether successfully or not), clear the pending save observable.
|
|
1018
|
+
(0, rxjs_1.finalize)(() => { this._pendingSave$ = null; }),
|
|
1019
|
+
// Ensure that all subscribers get the same result.
|
|
1020
|
+
(0, rxjs_1.shareReplay)(1));
|
|
1021
|
+
return (0, rxjs_1.firstValueFrom)(this._pendingSave$);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Private, internal method to handle saving the current state of the object to the database. This method is called by the public facing Save() method
|
|
1025
|
+
* and is debounced to prevent multiple calls from being executed simultaneously.
|
|
1026
|
+
* @param options
|
|
1027
|
+
* @returns
|
|
1028
|
+
*/
|
|
1029
|
+
async _InnerSave(options) {
|
|
1030
|
+
const currentResultCount = this.ResultHistory.length;
|
|
1031
|
+
const newResult = new BaseEntityResult();
|
|
1032
|
+
newResult.StartedAt = new Date();
|
|
1033
|
+
try {
|
|
1034
|
+
const _options = options ? options : new interfaces_1.EntitySaveOptions();
|
|
1035
|
+
const type = this.IsSaved ? entityInfo_1.EntityPermissionType.Update : entityInfo_1.EntityPermissionType.Create;
|
|
1036
|
+
const saveSubType = this.IsSaved ? 'update' : 'create';
|
|
1037
|
+
this.CheckPermissions(type, true); // this will throw an error and exit out if we don't have permission
|
|
1038
|
+
if (_options.IgnoreDirtyState || this.Dirty || _options.ReplayOnly) {
|
|
1039
|
+
if (!this.ProviderToUse) {
|
|
1040
|
+
throw new Error('No provider set');
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
let valResult = new ValidationTypes_1.ValidationResult();
|
|
1044
|
+
if (_options.ReplayOnly) {
|
|
1045
|
+
valResult.Success = true; // bypassing validation since we are in replay only....
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
// First run synchronous validation
|
|
1049
|
+
valResult = this.Validate();
|
|
1050
|
+
// Determine if we should run async validation:
|
|
1051
|
+
// 1. Explicitly set in options, OR
|
|
1052
|
+
// 2. Use the subclass's default if not specified in options
|
|
1053
|
+
const skipAsyncValidation = _options.SkipAsyncValidation !== undefined ?
|
|
1054
|
+
_options.SkipAsyncValidation : this.DefaultSkipAsyncValidation;
|
|
1055
|
+
// If not skipping async validation, run it - even if sync validation failed
|
|
1056
|
+
// This ensures all validation errors (sync and async) are collected
|
|
1057
|
+
if (!skipAsyncValidation) {
|
|
1058
|
+
const asyncResult = await this.ValidateAsync();
|
|
1059
|
+
// Combine the results of both validations
|
|
1060
|
+
// If either validation fails, the overall result fails
|
|
1061
|
+
valResult.Success = valResult.Success && asyncResult.Success;
|
|
1062
|
+
// Add any async validation errors to the result
|
|
1063
|
+
asyncResult.Errors.forEach(error => {
|
|
1064
|
+
valResult.Errors.push(error);
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (valResult.Success) {
|
|
1069
|
+
const data = await this.ProviderToUse.Save(this, this.ActiveUser, _options);
|
|
1070
|
+
if (!this.TransactionGroup) {
|
|
1071
|
+
// no transaction group, so we have our results here
|
|
1072
|
+
return this.finalizeSave(data, saveSubType);
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
// we are part of a transaction group, so we return true and subscribe to the transaction groups' events and do the finalization work then
|
|
1076
|
+
this.TransactionGroup.TransactionNotifications$.subscribe(({ success, results, error }) => {
|
|
1077
|
+
if (success) {
|
|
1078
|
+
const transItem = results.find(r => r.Transaction.BaseEntity === this);
|
|
1079
|
+
if (transItem) {
|
|
1080
|
+
this.finalizeSave(transItem.Result, saveSubType); // we get the resulting data from the transaction result, not data above as that will be blank when in a TG
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
// should never get here, but if we do, we need to throw an error
|
|
1084
|
+
throw new Error('Transaction group did not return a result for the entity object');
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
throw error; // push this to the catch block below and that will add to the result history
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
throw valResult; // pass this along to the caller
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
else
|
|
1100
|
+
return true; // nothing to save since we're not dirty
|
|
1101
|
+
}
|
|
1102
|
+
catch (e) {
|
|
1103
|
+
if (currentResultCount === this.ResultHistory.length) {
|
|
1104
|
+
// this means that NO new results were added to the history anywhere
|
|
1105
|
+
// so we need to add a new result to the history here
|
|
1106
|
+
newResult.Success = false;
|
|
1107
|
+
newResult.Type = this.IsSaved ? 'update' : 'create';
|
|
1108
|
+
newResult.Message = e.message || null;
|
|
1109
|
+
newResult.Errors = e.Errors || [];
|
|
1110
|
+
newResult.OriginalValues = this.Fields.map(f => { return { FieldName: f.CodeName, Value: f.OldValue }; });
|
|
1111
|
+
newResult.EndedAt = new Date();
|
|
1112
|
+
this.ResultHistory.push(newResult);
|
|
1113
|
+
}
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
finalizeSave(data, saveSubType) {
|
|
1118
|
+
if (data) {
|
|
1119
|
+
this.init(); // wipe out the current data to flush out the DIRTY flags, load the ID as part of this too
|
|
1120
|
+
this.SetMany(data, false, true, true); // set the new values from the data returned from the save, this will also reset the old values
|
|
1121
|
+
this._everSaved = true; // Mark as saved after successful save
|
|
1122
|
+
const result = this.LatestResult;
|
|
1123
|
+
if (result)
|
|
1124
|
+
result.NewValues = this.Fields.map(f => { return { FieldName: f.CodeName, Value: f.Value }; }); // set the latest values here
|
|
1125
|
+
this.RaiseEvent('save', null, saveSubType);
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
else {
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Internal helper method for the class and sub-classes - used to easily get the Active User which is either the ContextCurrentUser, if defined, or the Metadata.Provider.CurrentUser if not.
|
|
1134
|
+
*/
|
|
1135
|
+
get ActiveUser() {
|
|
1136
|
+
return this.ContextCurrentUser || metadata_1.Metadata.Provider.CurrentUser; // use the context user ahead of the Provider.Current User - this is for SERVER side ops where the user changes per request
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Utility method that returns true if the given permission being checked is enabled for the current user, and false if not.
|
|
1140
|
+
* @param type
|
|
1141
|
+
* @param throwError
|
|
1142
|
+
* @returns
|
|
1143
|
+
*/
|
|
1144
|
+
CheckPermissions(type, throwError) {
|
|
1145
|
+
const u = this.ActiveUser;
|
|
1146
|
+
if (!u)
|
|
1147
|
+
throw new Error('No user set - either the context user for the entity object must be set, or the Metadata.Provider.CurrentUser must be set');
|
|
1148
|
+
// first check if the AllowCreateAPI/AllowUpdateAPI/AllowDeleteAPI settings are flipped on for the entity in question
|
|
1149
|
+
switch (type) {
|
|
1150
|
+
case entityInfo_1.EntityPermissionType.Create:
|
|
1151
|
+
if (!this.EntityInfo.AllowCreateAPI) {
|
|
1152
|
+
if (throwError)
|
|
1153
|
+
throw new Error(`Create API is disabled for ${this.EntityInfo.Name}`);
|
|
1154
|
+
else
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
break;
|
|
1158
|
+
case entityInfo_1.EntityPermissionType.Update:
|
|
1159
|
+
if (!this.EntityInfo.AllowUpdateAPI) {
|
|
1160
|
+
if (throwError)
|
|
1161
|
+
throw new Error(`Update API is disabled for ${this.EntityInfo.Name}`);
|
|
1162
|
+
else
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
break;
|
|
1166
|
+
case entityInfo_1.EntityPermissionType.Delete:
|
|
1167
|
+
if (!this.EntityInfo.AllowDeleteAPI) {
|
|
1168
|
+
if (throwError)
|
|
1169
|
+
throw new Error(`Delete API is disabled for ${this.EntityInfo.Name}`);
|
|
1170
|
+
else
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
break;
|
|
1174
|
+
case entityInfo_1.EntityPermissionType.Read:
|
|
1175
|
+
if (!this.EntityInfo.IncludeInAPI) {
|
|
1176
|
+
if (throwError)
|
|
1177
|
+
throw new Error(`API is disabled for ${this.EntityInfo.Name}`);
|
|
1178
|
+
else
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
const permissions = this.EntityInfo.GetUserPermisions(u);
|
|
1184
|
+
let bAllowed = false;
|
|
1185
|
+
switch (type) {
|
|
1186
|
+
case entityInfo_1.EntityPermissionType.Create:
|
|
1187
|
+
bAllowed = permissions.CanCreate;
|
|
1188
|
+
break;
|
|
1189
|
+
case entityInfo_1.EntityPermissionType.Read:
|
|
1190
|
+
bAllowed = permissions.CanRead;
|
|
1191
|
+
break;
|
|
1192
|
+
case entityInfo_1.EntityPermissionType.Update:
|
|
1193
|
+
bAllowed = permissions.CanUpdate;
|
|
1194
|
+
break;
|
|
1195
|
+
case entityInfo_1.EntityPermissionType.Delete:
|
|
1196
|
+
bAllowed = permissions.CanDelete;
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
if (!bAllowed && throwError) {
|
|
1200
|
+
this.ThrowPermissionError(u, type, null);
|
|
1201
|
+
}
|
|
1202
|
+
else
|
|
1203
|
+
return bAllowed;
|
|
1204
|
+
}
|
|
1205
|
+
ThrowPermissionError(u, type, additionalInfoMessage) {
|
|
1206
|
+
throw new Error(`User: ${u.Name} (ID: ${u.ID}, Email: ${u.Email})
|
|
1207
|
+
Does NOT have permission to ${entityInfo_1.EntityPermissionType[type]} ${this.EntityInfo.Name} records.
|
|
1208
|
+
If you believe this is an error, please contact your system administrator.${additionalInfoMessage ? '\nAdditional Information: ' + additionalInfoMessage : ''}}`);
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* This method will revert the internal state of the object back to what it was when it was last saved, or if never saved, from when it was intially loaded from the database. This is useful if you want to offer a user an "undo" type of feature in a UI.
|
|
1212
|
+
* @returns
|
|
1213
|
+
*/
|
|
1214
|
+
Revert() {
|
|
1215
|
+
if (this.Dirty) {
|
|
1216
|
+
for (let field of this.Fields) {
|
|
1217
|
+
field.Value = field.OldValue;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* * This method loads a single record from the database. Make sure you first get the correct BaseEntity sub-class for your entity by calling Metadata.GetEntityObject() first. From there, you can
|
|
1224
|
+
* call this method to load your records.
|
|
1225
|
+
* * NOTE: You should not be calling this method directly from outside of a sub-class in most cases. You will use the auto-generated sub-classes that have overriden versions of this method that blow out the primary keys into individual parameters. This is much easier to program against.
|
|
1226
|
+
* @param CompositeKey Wrapper that holds an array of objects that contain the field name and value for the primary key of the record you want to load. For example, if you have a table called "Customers" with a primary key of "ID", you would pass in an array with a single object like this: {FieldName: "ID", Value: 1234}.
|
|
1227
|
+
* *If you had a composite primary key, you would pass in an array with multiple objects, one for each field in the primary key. You may ONLY pass in the primary key fields, no other fields are allowed.
|
|
1228
|
+
* @param EntityRelationshipsToLoad Optional, you can specify the names of the relationships to load up. This is an expensive operation as it loads up an array of the related entity objects for the main record, so use it sparingly.
|
|
1229
|
+
* @returns true if success, false otherwise
|
|
1230
|
+
*/
|
|
1231
|
+
async InnerLoad(CompositeKey, EntityRelationshipsToLoad = null) {
|
|
1232
|
+
if (!this.ProviderToUse) {
|
|
1233
|
+
throw new Error('No provider set');
|
|
1234
|
+
}
|
|
1235
|
+
else {
|
|
1236
|
+
const valResult = CompositeKey.Validate();
|
|
1237
|
+
if (!valResult || !valResult.IsValid)
|
|
1238
|
+
throw new Error(`Invalid CompositeKey passed to BaseEntity.Load(${this.EntityInfo.Name}): ${valResult.ErrorMessage}`);
|
|
1239
|
+
this.CheckPermissions(entityInfo_1.EntityPermissionType.Read, true); // this will throw an error and exit out if we don't have permission
|
|
1240
|
+
if (!this.IsSaved) {
|
|
1241
|
+
this.init(); // wipe out current data if we're loading on top of existing record
|
|
1242
|
+
}
|
|
1243
|
+
const data = await this.ProviderToUse.Load(this, CompositeKey, EntityRelationshipsToLoad, this.ActiveUser);
|
|
1244
|
+
if (!data) {
|
|
1245
|
+
(0, logging_1.LogError)(`Error in BaseEntity.Load(${this.EntityInfo.Name}, Key: ${CompositeKey.ToString()}`);
|
|
1246
|
+
return false; // no data loaded, return false
|
|
1247
|
+
}
|
|
1248
|
+
this.SetMany(data, false, true, true); // don't ignore non-existent fields, but DO replace old values
|
|
1249
|
+
if (EntityRelationshipsToLoad) {
|
|
1250
|
+
for (let relationship of EntityRelationshipsToLoad) {
|
|
1251
|
+
if (data[relationship]) {
|
|
1252
|
+
// we have some data, put into an array for ease of access
|
|
1253
|
+
this[relationship] = data[relationship];
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
this._recordLoaded = true;
|
|
1258
|
+
this._everSaved = true; // Mark as saved since we loaded from database
|
|
1259
|
+
this._compositeKey = CompositeKey; // set the composite key to the one we just loaded
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Loads entity data from a plain object, typically from database query results.
|
|
1265
|
+
*
|
|
1266
|
+
* This method is meant to be used only in situations where you are sure that the data you are loading
|
|
1267
|
+
* is current in the database. MAKE SURE YOU ARE PASSING IN ALL FIELDS. The Dirty flags and other internal
|
|
1268
|
+
* state will assume what is loading from the data parameter you pass in is equivalent to what is in the database.
|
|
1269
|
+
*
|
|
1270
|
+
* @remarks
|
|
1271
|
+
* Generally speaking, you should use Load() instead of this method. The main use cases where this makes sense are:
|
|
1272
|
+
* 1. On the server if you are pulling data you know is fresh from the result of another DB operation
|
|
1273
|
+
* 2. If on any tier you run a fresh RunView result that gives you data from the database
|
|
1274
|
+
* 3. When the RunView Object RunView() method is called with ResultType='entity_object'
|
|
1275
|
+
*
|
|
1276
|
+
* **Important for Subclasses**: As of v2.53.0, this method is now async to support subclasses that need to
|
|
1277
|
+
* perform additional asynchronous loading operations (e.g., loading related data, fetching additional metadata).
|
|
1278
|
+
*
|
|
1279
|
+
* Subclasses that need to perform additional loading should override BOTH this method AND Load() to ensure
|
|
1280
|
+
* consistent behavior regardless of how the entity is populated. This is because these two methods have
|
|
1281
|
+
* different execution paths:
|
|
1282
|
+
* - Load() fetches data from the network/database and then calls provider-specific loading
|
|
1283
|
+
* - LoadFromData() is called when data is already available (e.g., from RunView results)
|
|
1284
|
+
*
|
|
1285
|
+
* @example
|
|
1286
|
+
* ```typescript
|
|
1287
|
+
* // Subclass implementation
|
|
1288
|
+
* public override async LoadFromData(data: any, replaceOldValues: boolean = false): Promise<boolean> {
|
|
1289
|
+
* const result = await super.LoadFromData(data, replaceOldValues);
|
|
1290
|
+
* if (result) {
|
|
1291
|
+
* // Perform additional async loading here
|
|
1292
|
+
* await this.LoadRelatedData();
|
|
1293
|
+
* await this.LoadMetadata();
|
|
1294
|
+
* }
|
|
1295
|
+
* return result;
|
|
1296
|
+
* }
|
|
1297
|
+
*
|
|
1298
|
+
* // Don't forget to also override Load() for consistency, unless you INTEND to have different behavior
|
|
1299
|
+
* // for Load() vs LoadFromData()
|
|
1300
|
+
* public override async Load(ID: string, EntityRelationshipsToLoad: string[] = null): Promise<boolean> {
|
|
1301
|
+
* const result = await super.Load(ID, EntityRelationshipsToLoad);
|
|
1302
|
+
* if (result) {
|
|
1303
|
+
* // Same additional loading as in LoadFromData
|
|
1304
|
+
* await this.LoadRelatedData();
|
|
1305
|
+
* await this.LoadMetadata();
|
|
1306
|
+
* }
|
|
1307
|
+
* return result;
|
|
1308
|
+
* }
|
|
1309
|
+
* ```
|
|
1310
|
+
*
|
|
1311
|
+
* @param data - A simple object that has properties that match the field names of the entity object
|
|
1312
|
+
* @param replaceOldValues - If true, the old values of the fields will be set to the values provided
|
|
1313
|
+
* in the data parameter; if false, they will be left alone
|
|
1314
|
+
* @returns Promise<boolean> - Returns true if the load was successful
|
|
1315
|
+
*/
|
|
1316
|
+
async LoadFromData(data, _replaceOldValues = false) {
|
|
1317
|
+
this.SetMany(data, true, _replaceOldValues, true); // ignore non-existent fields, but DO replace old values based on the provided param
|
|
1318
|
+
// now, check to see if we have the primary key set, if so, we should consider ourselves
|
|
1319
|
+
// loaded from the database and set the _recordLoaded flag to true along with the _everSaved flag
|
|
1320
|
+
if (this.PrimaryKeys && this.PrimaryKeys.length > 0) {
|
|
1321
|
+
// chck each pkey's value to make sur it is set
|
|
1322
|
+
this._recordLoaded = true; // all primary keys are set, so we are loaded
|
|
1323
|
+
this._everSaved = true; // Mark as saved since we loaded from data
|
|
1324
|
+
for (let pkey of this.PrimaryKeys) {
|
|
1325
|
+
if (pkey.Value === null || pkey.Value === undefined) {
|
|
1326
|
+
this._recordLoaded = false;
|
|
1327
|
+
this._everSaved = false; // if any primary key is not set, we cannot consider ourselves loaded
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
else {
|
|
1332
|
+
// this is an error state as every entity must have > 0 primary keys defined
|
|
1333
|
+
(0, logging_1.LogError)(`BaseEntity.LoadFromData() called on ${this.EntityInfo.Name} with no primary keys defined. This is an error state and should not happen.`);
|
|
1334
|
+
this._recordLoaded = false;
|
|
1335
|
+
this._everSaved = false; // Mark as NOT saved since we loaded from data without primary keys
|
|
1336
|
+
}
|
|
1337
|
+
return true;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* This method is used automatically within Save() and is used to determine if the state of the object is valid relative to the validation rules that are defined in metadata. In addition, sub-classes can
|
|
1341
|
+
* override or wrap this base class method to add other logic for validation.
|
|
1342
|
+
*
|
|
1343
|
+
* @returns ValidationResult The validation result
|
|
1344
|
+
*/
|
|
1345
|
+
Validate() {
|
|
1346
|
+
const result = new ValidationTypes_1.ValidationResult();
|
|
1347
|
+
result.Success = true; // start off with assumption of success, if any field fails, we'll set this to false
|
|
1348
|
+
for (let field of this.Fields) {
|
|
1349
|
+
const err = field.Validate();
|
|
1350
|
+
err.Errors.forEach(element => {
|
|
1351
|
+
result.Errors.push(element);
|
|
1352
|
+
});
|
|
1353
|
+
result.Success = result.Success && err.Success; // if any field fails, we fail, but keep going to get all of the validation messages
|
|
1354
|
+
}
|
|
1355
|
+
return result;
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Default value for whether async validation should be skipped.
|
|
1359
|
+
* Subclasses can override this property to enable async validation by default.
|
|
1360
|
+
* When the options object is passed to Save(), and it includes a value for the
|
|
1361
|
+
* SkipAsyncValidation property, that value will take precedence over this default.
|
|
1362
|
+
*
|
|
1363
|
+
* @see {@link Save}
|
|
1364
|
+
*
|
|
1365
|
+
* @protected
|
|
1366
|
+
*/
|
|
1367
|
+
get DefaultSkipAsyncValidation() {
|
|
1368
|
+
return true; // By default, skip async validation unless explicitly enabled
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Asynchronous validation method that can be overridden by subclasses to add custom async validation logic.
|
|
1372
|
+
* This method is automatically called by Save() AFTER the synchronous Validate() passes.
|
|
1373
|
+
*
|
|
1374
|
+
* IMPORTANT:
|
|
1375
|
+
* 1. This should NEVER be called INSTEAD of the synchronous Validate() method
|
|
1376
|
+
* 2. This is meant to be overridden by subclasses that need to perform async validations
|
|
1377
|
+
* 3. The base implementation just returns success - no actual validation is performed
|
|
1378
|
+
*
|
|
1379
|
+
* Subclasses should override this to add complex validations that require database queries
|
|
1380
|
+
* or other async operations that cannot be performed in the synchronous Validate() method.
|
|
1381
|
+
*
|
|
1382
|
+
* @returns Promise<ValidationResult> A promise that resolves to the validation result
|
|
1383
|
+
*/
|
|
1384
|
+
async ValidateAsync() {
|
|
1385
|
+
// Default implementation just returns success
|
|
1386
|
+
// Subclasses should override this to perform actual async validation
|
|
1387
|
+
const result = new ValidationTypes_1.ValidationResult();
|
|
1388
|
+
result.Success = true;
|
|
1389
|
+
return result;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* This method deletes a record from the database. You must call Load() first in order to load the context of the record you are deleting.
|
|
1393
|
+
* @returns
|
|
1394
|
+
*/
|
|
1395
|
+
async Delete(options) {
|
|
1396
|
+
const currentResultCount = this.ResultHistory.length;
|
|
1397
|
+
const newResult = new BaseEntityResult();
|
|
1398
|
+
newResult.StartedAt = new Date();
|
|
1399
|
+
try {
|
|
1400
|
+
if (!this.ProviderToUse) {
|
|
1401
|
+
throw new Error('No provider set');
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
this.CheckPermissions(entityInfo_1.EntityPermissionType.Delete, true); // this will throw an error and exit out if we don't have permission
|
|
1405
|
+
// stash the old values for the event
|
|
1406
|
+
const oldVals = await this.GetDataObject({
|
|
1407
|
+
oldValues: false,
|
|
1408
|
+
omitNullValues: false,
|
|
1409
|
+
omitEmptyStrings: false,
|
|
1410
|
+
excludeFields: null,
|
|
1411
|
+
includeRelatedEntityData: false,
|
|
1412
|
+
relatedEntityList: null
|
|
1413
|
+
});
|
|
1414
|
+
if (await this.ProviderToUse.Delete(this, options, this.ActiveUser)) {
|
|
1415
|
+
if (!this.TransactionGroup) {
|
|
1416
|
+
// NOT part of a transaction - raise event immediately
|
|
1417
|
+
// record deleted correctly
|
|
1418
|
+
this.RaiseEvent('delete', { OldValues: oldVals });
|
|
1419
|
+
// wipe out the current data to flush out the DIRTY flags by calling NewRecord()
|
|
1420
|
+
this.NewRecord(); // will trigger a new record event here too
|
|
1421
|
+
}
|
|
1422
|
+
else {
|
|
1423
|
+
// part of a transaction, wait for the transaction to submit successfully and then
|
|
1424
|
+
// raise the event
|
|
1425
|
+
this.TransactionGroup.TransactionNotifications$.subscribe(({ success, results, error }) => {
|
|
1426
|
+
if (success) {
|
|
1427
|
+
this.RaiseEvent('delete', { OldValues: oldVals });
|
|
1428
|
+
// wipe out the current data to flush out the DIRTY flags by calling NewRecord()
|
|
1429
|
+
this.NewRecord(); // will trigger a new record event here too
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
// transaction failed, so we need to add a new result to the history here
|
|
1433
|
+
newResult.Success = false;
|
|
1434
|
+
newResult.Type = 'delete';
|
|
1435
|
+
newResult.Message = error && error.message ? error.message : error;
|
|
1436
|
+
newResult.Errors = error.Errors || [];
|
|
1437
|
+
newResult.OriginalValues = this.Fields.map(f => { return { FieldName: f.CodeName, Value: f.OldValue }; });
|
|
1438
|
+
newResult.EndedAt = new Date();
|
|
1439
|
+
this.ResultHistory.push(newResult);
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
return true;
|
|
1444
|
+
}
|
|
1445
|
+
else // record didn't save, return false, but also don't wipe out the entity like we do if the Delete() worked
|
|
1446
|
+
return false;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
catch (e) {
|
|
1450
|
+
if (currentResultCount === this.ResultHistory.length) {
|
|
1451
|
+
// this means that NO new results were added to the history anywhere
|
|
1452
|
+
// so we need to add a new result to the history here
|
|
1453
|
+
newResult.Success = false;
|
|
1454
|
+
newResult.Type = 'delete';
|
|
1455
|
+
newResult.Message = e.message || null;
|
|
1456
|
+
newResult.Errors = e.Errors || [];
|
|
1457
|
+
newResult.OriginalValues = this.Fields.map(f => { return { FieldName: f.CodeName, Value: f.OldValue }; });
|
|
1458
|
+
newResult.EndedAt = new Date();
|
|
1459
|
+
this.ResultHistory.push(newResult);
|
|
1460
|
+
}
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Called before an Action is executed by the AI Engine
|
|
1466
|
+
* This is intended to be overriden by subclass as needed, these methods called at the right time by the execution context
|
|
1467
|
+
*/
|
|
1468
|
+
async BeforeEntityAIAction(params) {
|
|
1469
|
+
return true; // default implementation does nothing
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Called after an Action is executed by the AI Engine
|
|
1473
|
+
*/
|
|
1474
|
+
async AfterEntityAIAction(params) {
|
|
1475
|
+
return true; // default implementation does nothing
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Static property to get/set the IEntityDataProvider that is used by all BaseEntity objects. This is a global setting that is used by all BaseEntity objects. It can be overriden for a given BaseEntity object instance by passing in a provider to the
|
|
1479
|
+
* constructor of the BaseEntity object. Typically, a provider will pass itself into BaseEntity objects it creates to create a tight coupling between the provider and the BaseEntity objects it creates. This allows multiple concurrent
|
|
1480
|
+
* connections to exist in the same process space without interfering with each other.
|
|
1481
|
+
*/
|
|
1482
|
+
static get Provider() {
|
|
1483
|
+
const g = Global_1.MJGlobal.Instance.GetGlobalObjectStore();
|
|
1484
|
+
if (g)
|
|
1485
|
+
return g[BaseEntity._globalProviderKey];
|
|
1486
|
+
else
|
|
1487
|
+
throw new Error('No global object store, so we cant get the static provider');
|
|
1488
|
+
}
|
|
1489
|
+
static set Provider(value) {
|
|
1490
|
+
const g = Global_1.MJGlobal.Instance.GetGlobalObjectStore();
|
|
1491
|
+
if (g)
|
|
1492
|
+
g[BaseEntity._globalProviderKey] = value;
|
|
1493
|
+
else
|
|
1494
|
+
throw new Error('No global object store, so we cant set the static provider');
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Returns a list of changes made to this record, over time. Only works if TrackRecordChanges bit set to 1 on the entity you're working with.
|
|
1498
|
+
*/
|
|
1499
|
+
get RecordChanges() {
|
|
1500
|
+
if (this.IsSaved) {
|
|
1501
|
+
return BaseEntity.GetRecordChanges(this.EntityInfo.Name, this.PrimaryKey, this.ProviderToUse);
|
|
1502
|
+
}
|
|
1503
|
+
else {
|
|
1504
|
+
throw new Error('Cannot get record changes for a record that has not been saved yet');
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Static Utility method to get RecordChanges for a given entityName/KeyValuePair combination
|
|
1509
|
+
* @param entityName
|
|
1510
|
+
* @returns
|
|
1511
|
+
*/
|
|
1512
|
+
static async GetRecordChanges(entityName, primaryKey, provider = null) {
|
|
1513
|
+
const providerToUse = provider || BaseEntity.Provider;
|
|
1514
|
+
if (!providerToUse) {
|
|
1515
|
+
throw new Error('No provider set or passed in');
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
const results = await providerToUse.GetRecordChanges(entityName, primaryKey);
|
|
1519
|
+
if (results) {
|
|
1520
|
+
const changes = [];
|
|
1521
|
+
for (let result of results)
|
|
1522
|
+
changes.push(new entityInfo_1.RecordChange(result));
|
|
1523
|
+
return changes;
|
|
1524
|
+
}
|
|
1525
|
+
else
|
|
1526
|
+
return [];
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Strongly-typed wrapper for the {@link SetMany} method.
|
|
1531
|
+
* @oaram data - the data to set on the entity object
|
|
1532
|
+
* @param schema - the zod schema to validate the data against
|
|
1533
|
+
*/
|
|
1534
|
+
From(data, schema) {
|
|
1535
|
+
this.init();
|
|
1536
|
+
if (schema) {
|
|
1537
|
+
const parseResult = schema.safeParse(data);
|
|
1538
|
+
if (parseResult.success) {
|
|
1539
|
+
this.SetMany(parseResult.data, false, false, true);
|
|
1540
|
+
return true;
|
|
1541
|
+
}
|
|
1542
|
+
else {
|
|
1543
|
+
(0, logging_1.LogError)(parseResult.error.flatten());
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
else {
|
|
1548
|
+
this.SetMany(data, false, false, true);
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Strongly-typed wrapper for the {@link GetAll} method
|
|
1554
|
+
* @param schema - the zod schema to validate the data against
|
|
1555
|
+
*/
|
|
1556
|
+
To(schema) {
|
|
1557
|
+
if (schema) {
|
|
1558
|
+
const data = this.GetAll();
|
|
1559
|
+
const parseResult = schema.safeParse(data);
|
|
1560
|
+
if (parseResult.success) {
|
|
1561
|
+
parseResult.data;
|
|
1562
|
+
}
|
|
1563
|
+
else {
|
|
1564
|
+
(0, logging_1.LogError)(parseResult.error.flatten());
|
|
1565
|
+
return null;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
return this.GetAll();
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Generates vector embeddings for multiple text fields by their field names.
|
|
1572
|
+
* Processes fields in parallel for better performance.
|
|
1573
|
+
* @param fields - Array of field configurations specifying source text field, target vector field, and model ID field names
|
|
1574
|
+
* @returns Promise that resolves to true if all embeddings were generated successfully, false if any failed
|
|
1575
|
+
*/
|
|
1576
|
+
async GenerateEmbeddingsByFieldName(fields) {
|
|
1577
|
+
const promises = [];
|
|
1578
|
+
for (const { fieldName, vectorFieldName, modelFieldName } of fields) {
|
|
1579
|
+
promises.push(this.GenerateEmbeddingByFieldName(fieldName, vectorFieldName, modelFieldName));
|
|
1580
|
+
}
|
|
1581
|
+
const results = await Promise.all(promises);
|
|
1582
|
+
return results.every(result => result === true);
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Generates a vector embedding for a single text field identified by field name.
|
|
1586
|
+
* Retrieves the field objects and delegates to GenerateEmbedding method.
|
|
1587
|
+
* @param fieldName - Name of the text field to generate embedding from
|
|
1588
|
+
* @param vectorFieldName - Name of the field to store the vector embedding
|
|
1589
|
+
* @param modelFieldName - Name of the field to store the model ID used for embedding
|
|
1590
|
+
* @returns Promise that resolves to true if embedding was generated successfully, false otherwise
|
|
1591
|
+
*/
|
|
1592
|
+
async GenerateEmbeddingByFieldName(fieldName, vectorFieldName, modelFieldName) {
|
|
1593
|
+
const field = this.GetFieldByName(fieldName);
|
|
1594
|
+
const vectorField = this.GetFieldByName(vectorFieldName);
|
|
1595
|
+
const modelField = this.GetFieldByName(modelFieldName);
|
|
1596
|
+
if (!field)
|
|
1597
|
+
throw new Error(`Field not found: ${fieldName}`);
|
|
1598
|
+
if (!vectorField)
|
|
1599
|
+
throw new Error(`Vector field not found: ${vectorFieldName}`);
|
|
1600
|
+
if (modelFieldName?.trim().length > 0 && !modelField)
|
|
1601
|
+
throw new Error(`Model field not found: ${modelFieldName}`);
|
|
1602
|
+
return await this.GenerateEmbedding(field, vectorField, modelField);
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Generates vector embeddings for multiple text fields using EntityField objects.
|
|
1606
|
+
* Processes fields in parallel for better performance.
|
|
1607
|
+
* @param fields - Array of field configurations with EntityField objects for source, vector, and model fields
|
|
1608
|
+
* @returns Promise that resolves to true if all embeddings were generated successfully, false if any failed
|
|
1609
|
+
*/
|
|
1610
|
+
async GenerateEmbeddings(fields) {
|
|
1611
|
+
const promises = [];
|
|
1612
|
+
for (const { field, vectorField, modelField } of fields) {
|
|
1613
|
+
promises.push(this.GenerateEmbedding(field, vectorField, modelField));
|
|
1614
|
+
}
|
|
1615
|
+
const results = await Promise.all(promises);
|
|
1616
|
+
return results.every(result => result === true);
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1619
|
+
* Generates a vector embedding for a single text field using AI engine.
|
|
1620
|
+
* Only generates embeddings for new records or when the source field has changed.
|
|
1621
|
+
* Stores both the vector embedding and the model ID used to generate it.
|
|
1622
|
+
* @param field - The EntityField containing the text to embed
|
|
1623
|
+
* @param vectorField - The EntityField to store the generated vector embedding (as JSON string)
|
|
1624
|
+
* @param modelField - The EntityField to store the ID of the AI model used
|
|
1625
|
+
* @returns Promise that resolves to true if embedding was generated successfully, false otherwise
|
|
1626
|
+
*/
|
|
1627
|
+
async GenerateEmbedding(field, vectorField, modelField) {
|
|
1628
|
+
try {
|
|
1629
|
+
if (!this.IsSaved || field.Dirty) {
|
|
1630
|
+
if (field.Value?.trim().length > 0) {
|
|
1631
|
+
// recalc vector
|
|
1632
|
+
const e = await this.EmbedTextLocal(field.Value);
|
|
1633
|
+
if (e && e.vector) {
|
|
1634
|
+
vectorField.Value = JSON.stringify(e.vector);
|
|
1635
|
+
if (modelField)
|
|
1636
|
+
modelField.Value = e.modelID;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
vectorField.Value = null;
|
|
1641
|
+
if (modelField)
|
|
1642
|
+
modelField.Value = null;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
catch (e) {
|
|
1648
|
+
console.error("Error generating embedding:", e);
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* In the BaseEntity class this method is not implemented. This method shoudl be implemented only in
|
|
1654
|
+
* **server-side** sub-classes only by calling AIEngine or other methods to generate embeddings for a given
|
|
1655
|
+
* piece of text provided. Subclasses that override this method to implement embedding support should also
|
|
1656
|
+
* override @see SupportsEmbedTextLocal and return true
|
|
1657
|
+
* @param textToEmbed
|
|
1658
|
+
*/
|
|
1659
|
+
async EmbedTextLocal(textToEmbed) {
|
|
1660
|
+
throw new Error("EmbedTextLocal not implemented in BaseEntity, sub-classes must implement this functionality to use it");
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Specifies if the current object supports the @see EmbedTextLocal method or not - useful to know before calling it for conditional
|
|
1664
|
+
* code that has fallbacks as needed. BaseEntity does not implement this method but server-side sub-classes often do, but it is not mandatory for
|
|
1665
|
+
* any sub-class.
|
|
1666
|
+
* @returns
|
|
1667
|
+
*/
|
|
1668
|
+
SupportsEmbedTextLocal() {
|
|
1669
|
+
return false;
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Utility storage for vector embeddings that represent the active record. Each string in the Map can be any unique key relative to the object so you can
|
|
1673
|
+
* use this to track vectors associated with
|
|
1674
|
+
*/
|
|
1675
|
+
get Vectors() {
|
|
1676
|
+
return this._vectors;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Resets the vector embeddings for this entity to an empty state.
|
|
1680
|
+
*/
|
|
1681
|
+
ResetVectors() {
|
|
1682
|
+
this._vectors.clear();
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
exports.BaseEntity = BaseEntity;
|
|
1686
|
+
BaseEntity._baseEventCode = 'BaseEntityEvent';
|
|
1687
|
+
BaseEntity._globalProviderKey = 'MJ_BaseEntityProvider';
|
|
1688
|
+
//# sourceMappingURL=baseEntity.js.map
|