@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.
Files changed (117) hide show
  1. package/dist/Core.d.ts +29 -0
  2. package/dist/Core.d.ts.map +1 -0
  3. package/dist/Core.js +58 -0
  4. package/dist/Core.js.map +1 -0
  5. package/dist/generic/QueryCache.d.ts +85 -0
  6. package/dist/generic/QueryCache.d.ts.map +1 -0
  7. package/dist/generic/QueryCache.js +198 -0
  8. package/dist/generic/QueryCache.js.map +1 -0
  9. package/dist/generic/QueryCacheConfig.d.ts +72 -0
  10. package/dist/generic/QueryCacheConfig.d.ts.map +1 -0
  11. package/dist/generic/QueryCacheConfig.js +3 -0
  12. package/dist/generic/QueryCacheConfig.js.map +1 -0
  13. package/dist/generic/applicationInfo.d.ts +138 -0
  14. package/dist/generic/applicationInfo.d.ts.map +1 -0
  15. package/dist/generic/applicationInfo.js +177 -0
  16. package/dist/generic/applicationInfo.js.map +1 -0
  17. package/dist/generic/authEvaluator.d.ts +25 -0
  18. package/dist/generic/authEvaluator.d.ts.map +1 -0
  19. package/dist/generic/authEvaluator.js +49 -0
  20. package/dist/generic/authEvaluator.js.map +1 -0
  21. package/dist/generic/authTypes.d.ts +193 -0
  22. package/dist/generic/authTypes.d.ts.map +1 -0
  23. package/dist/generic/authTypes.js +19 -0
  24. package/dist/generic/authTypes.js.map +1 -0
  25. package/dist/generic/baseEngine.d.ts +260 -0
  26. package/dist/generic/baseEngine.d.ts.map +1 -0
  27. package/dist/generic/baseEngine.js +510 -0
  28. package/dist/generic/baseEngine.js.map +1 -0
  29. package/dist/generic/baseEntity.d.ts +691 -0
  30. package/dist/generic/baseEntity.d.ts.map +1 -0
  31. package/dist/generic/baseEntity.js +1688 -0
  32. package/dist/generic/baseEntity.js.map +1 -0
  33. package/dist/generic/baseInfo.d.ts +24 -0
  34. package/dist/generic/baseInfo.d.ts.map +1 -0
  35. package/dist/generic/baseInfo.js +53 -0
  36. package/dist/generic/baseInfo.js.map +1 -0
  37. package/dist/generic/compositeKey.d.ts +206 -0
  38. package/dist/generic/compositeKey.d.ts.map +1 -0
  39. package/dist/generic/compositeKey.js +412 -0
  40. package/dist/generic/compositeKey.js.map +1 -0
  41. package/dist/generic/databaseProviderBase.d.ts +46 -0
  42. package/dist/generic/databaseProviderBase.d.ts.map +1 -0
  43. package/dist/generic/databaseProviderBase.js +14 -0
  44. package/dist/generic/databaseProviderBase.js.map +1 -0
  45. package/dist/generic/entityInfo.d.ts +983 -0
  46. package/dist/generic/entityInfo.d.ts.map +1 -0
  47. package/dist/generic/entityInfo.js +1401 -0
  48. package/dist/generic/entityInfo.js.map +1 -0
  49. package/dist/generic/explorerNavigationItem.d.ts +20 -0
  50. package/dist/generic/explorerNavigationItem.d.ts.map +1 -0
  51. package/dist/generic/explorerNavigationItem.js +29 -0
  52. package/dist/generic/explorerNavigationItem.js.map +1 -0
  53. package/dist/generic/interfaces.d.ts +610 -0
  54. package/dist/generic/interfaces.d.ts.map +1 -0
  55. package/dist/generic/interfaces.js +211 -0
  56. package/dist/generic/interfaces.js.map +1 -0
  57. package/dist/generic/libraryInfo.d.ts +40 -0
  58. package/dist/generic/libraryInfo.d.ts.map +1 -0
  59. package/dist/generic/libraryInfo.js +56 -0
  60. package/dist/generic/libraryInfo.js.map +1 -0
  61. package/dist/generic/logging.d.ts +179 -0
  62. package/dist/generic/logging.d.ts.map +1 -0
  63. package/dist/generic/logging.js +382 -0
  64. package/dist/generic/logging.js.map +1 -0
  65. package/dist/generic/metadata.d.ts +305 -0
  66. package/dist/generic/metadata.d.ts.map +1 -0
  67. package/dist/generic/metadata.js +454 -0
  68. package/dist/generic/metadata.js.map +1 -0
  69. package/dist/generic/metadataUtil.d.ts +8 -0
  70. package/dist/generic/metadataUtil.d.ts.map +1 -0
  71. package/dist/generic/metadataUtil.js +36 -0
  72. package/dist/generic/metadataUtil.js.map +1 -0
  73. package/dist/generic/providerBase.d.ts +546 -0
  74. package/dist/generic/providerBase.d.ts.map +1 -0
  75. package/dist/generic/providerBase.js +999 -0
  76. package/dist/generic/providerBase.js.map +1 -0
  77. package/dist/generic/queryInfo.d.ts +460 -0
  78. package/dist/generic/queryInfo.d.ts.map +1 -0
  79. package/dist/generic/queryInfo.js +633 -0
  80. package/dist/generic/queryInfo.js.map +1 -0
  81. package/dist/generic/querySQLFilters.d.ts +54 -0
  82. package/dist/generic/querySQLFilters.d.ts.map +1 -0
  83. package/dist/generic/querySQLFilters.js +84 -0
  84. package/dist/generic/querySQLFilters.js.map +1 -0
  85. package/dist/generic/runQuery.d.ts +96 -0
  86. package/dist/generic/runQuery.d.ts.map +1 -0
  87. package/dist/generic/runQuery.js +66 -0
  88. package/dist/generic/runQuery.js.map +1 -0
  89. package/dist/generic/runQuerySQLFilterImplementations.d.ts +51 -0
  90. package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -0
  91. package/dist/generic/runQuerySQLFilterImplementations.js +238 -0
  92. package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -0
  93. package/dist/generic/runReport.d.ts +25 -0
  94. package/dist/generic/runReport.d.ts.map +1 -0
  95. package/dist/generic/runReport.js +42 -0
  96. package/dist/generic/runReport.js.map +1 -0
  97. package/dist/generic/securityInfo.d.ts +355 -0
  98. package/dist/generic/securityInfo.d.ts.map +1 -0
  99. package/dist/generic/securityInfo.js +425 -0
  100. package/dist/generic/securityInfo.js.map +1 -0
  101. package/dist/generic/transactionGroup.d.ts +184 -0
  102. package/dist/generic/transactionGroup.d.ts.map +1 -0
  103. package/dist/generic/transactionGroup.js +357 -0
  104. package/dist/generic/transactionGroup.js.map +1 -0
  105. package/dist/generic/util.d.ts +81 -0
  106. package/dist/generic/util.d.ts.map +1 -0
  107. package/dist/generic/util.js +301 -0
  108. package/dist/generic/util.js.map +1 -0
  109. package/dist/views/runView.d.ts +150 -0
  110. package/dist/views/runView.d.ts.map +1 -0
  111. package/dist/views/runView.js +100 -0
  112. package/dist/views/runView.js.map +1 -0
  113. package/dist/views/viewInfo.d.ts +121 -0
  114. package/dist/views/viewInfo.d.ts.map +1 -0
  115. package/dist/views/viewInfo.js +182 -0
  116. package/dist/views/viewInfo.js.map +1 -0
  117. 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