@memberjunction/core 6.2.0-edge.4 → 6.2.0-edge.5
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/generic/BaseEntitySaveQueue.d.ts +7 -0
- package/dist/generic/BaseEntitySaveQueue.d.ts.map +1 -1
- package/dist/generic/BaseEntitySaveQueue.js +22 -4
- package/dist/generic/BaseEntitySaveQueue.js.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts +18 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts.map +1 -1
- package/dist/generic/baseEngine.d.ts +29 -5
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +117 -11
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +45 -1
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +308 -187
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/baseInfo.d.ts +31 -3
- package/dist/generic/baseInfo.d.ts.map +1 -1
- package/dist/generic/baseInfo.js +138 -40
- package/dist/generic/baseInfo.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +37 -8
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +162 -65
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityConfiguration.d.ts +8 -2
- package/dist/generic/entityConfiguration.d.ts.map +1 -1
- package/dist/generic/entityConfiguration.js +9 -0
- package/dist/generic/entityConfiguration.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +78 -3
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +348 -55
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/infoProviderResolution.d.ts +16 -0
- package/dist/generic/infoProviderResolution.d.ts.map +1 -0
- package/dist/generic/infoProviderResolution.js +17 -0
- package/dist/generic/infoProviderResolution.js.map +1 -0
- package/dist/generic/interfaces.d.ts +25 -1
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +238 -40
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +466 -71
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/mjapiUrl.d.ts +21 -0
- package/dist/generic/mjapiUrl.d.ts.map +1 -0
- package/dist/generic/mjapiUrl.js +25 -0
- package/dist/generic/mjapiUrl.js.map +1 -0
- package/dist/generic/providerBase.d.ts +118 -3
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +398 -100
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/queryInfo.d.ts +10 -9
- package/dist/generic/queryInfo.d.ts.map +1 -1
- package/dist/generic/queryInfo.js +60 -33
- package/dist/generic/queryInfo.js.map +1 -1
- package/dist/generic/scoring/ReciprocalRankFusion.d.ts.map +1 -1
- package/dist/generic/scoring/ReciprocalRankFusion.js +10 -3
- package/dist/generic/scoring/ReciprocalRankFusion.js.map +1 -1
- package/dist/generic/securityInfo.d.ts +6 -6
- package/dist/generic/securityInfo.d.ts.map +1 -1
- package/dist/generic/securityInfo.js +10 -10
- package/dist/generic/securityInfo.js.map +1 -1
- package/dist/generic/util.d.ts.map +1 -1
- package/dist/generic/util.js +3 -0
- package/dist/generic/util.js.map +1 -1
- package/dist/generic/wellKnownUserSource.d.ts +3 -2
- package/dist/generic/wellKnownUserSource.d.ts.map +1 -1
- package/dist/generic/wellKnownUserSource.js +18 -9
- package/dist/generic/wellKnownUserSource.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/readme.md +29 -4
|
@@ -92,6 +92,18 @@ let EntityField = class EntityField {
|
|
|
92
92
|
*/
|
|
93
93
|
MarkNotLoaded() {
|
|
94
94
|
this._notLoaded = true;
|
|
95
|
+
this._writtenAfterNotLoaded = false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Framework-internal, for the hydration paths: the source omitted this field, so it is
|
|
99
|
+
* not-loaded AND holds nothing. Unlike {@link MarkNotLoaded}, which only flags the field and keeps
|
|
100
|
+
* its in-memory value, this drops a default or a previous record's value that would otherwise
|
|
101
|
+
* read back through Get() as if it had been loaded.
|
|
102
|
+
*/
|
|
103
|
+
ClearAsNotLoaded() {
|
|
104
|
+
this._value = null;
|
|
105
|
+
this._oldValue = null;
|
|
106
|
+
this.MarkNotLoaded();
|
|
95
107
|
}
|
|
96
108
|
/**
|
|
97
109
|
* True when field-level security bars this user from supplying the field's value on INSERT,
|
|
@@ -198,6 +210,9 @@ let EntityField = class EntityField {
|
|
|
198
210
|
// this field.
|
|
199
211
|
const wasNotLoaded = this._notLoaded;
|
|
200
212
|
this._notLoaded = false;
|
|
213
|
+
if (wasNotLoaded) {
|
|
214
|
+
this._writtenAfterNotLoaded = true;
|
|
215
|
+
}
|
|
201
216
|
// 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
|
|
202
217
|
if (this._neverSet && !wasNotLoaded &&
|
|
203
218
|
(value !== null || this._oldValue !== null)) {
|
|
@@ -215,6 +230,12 @@ let EntityField = class EntityField {
|
|
|
215
230
|
ResetNeverSetFlag() {
|
|
216
231
|
this._neverSet = true;
|
|
217
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* True when the field has had no explicit write since construction or since {@link ResetNeverSetFlag}.
|
|
235
|
+
*/
|
|
236
|
+
get NeverSet() {
|
|
237
|
+
return this._neverSet;
|
|
238
|
+
}
|
|
218
239
|
/**
|
|
219
240
|
* 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.
|
|
220
241
|
* A {@link NotLoaded} field is never dirty either — its in-memory state is a construction
|
|
@@ -223,9 +244,11 @@ let EntityField = class EntityField {
|
|
|
223
244
|
get Dirty() {
|
|
224
245
|
if (this.ReadOnly || this._notLoaded)
|
|
225
246
|
return false;
|
|
247
|
+
else if (this._writtenAfterNotLoaded)
|
|
248
|
+
return true; // a write onto a value the record never loaded must reach the save, even null
|
|
226
249
|
else {
|
|
227
250
|
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
|
|
228
|
-
const curNull = this.Value === null || this.Value === undefined || Number.isNaN(this.
|
|
251
|
+
const curNull = this.Value === null || this.Value === undefined || Number.isNaN(this.Value);
|
|
229
252
|
if (oldNull && curNull)
|
|
230
253
|
return false; // BOTH are null or undefined, not dirty
|
|
231
254
|
else {
|
|
@@ -239,16 +262,16 @@ let EntityField = class EntityField {
|
|
|
239
262
|
// and sometimes the new values are date objects, convert to UTC timestamp for comparison
|
|
240
263
|
newCompare = this.Value.getTime();
|
|
241
264
|
}
|
|
265
|
+
const tsType = this._entityFieldInfo.TSType;
|
|
242
266
|
// Special handling for bit/Boolean types - treat truthy values as equivalent
|
|
243
|
-
if (
|
|
244
|
-
this._entityFieldInfo.Type.toLowerCase() === 'bit') {
|
|
267
|
+
if (tsType === EntityFieldTSType.Boolean) {
|
|
245
268
|
// Convert both values to boolean for comparison
|
|
246
269
|
const oldBool = this.convertToBoolean(oldCompare);
|
|
247
270
|
const newBool = this.convertToBoolean(newCompare);
|
|
248
271
|
return oldBool !== newBool;
|
|
249
272
|
}
|
|
250
273
|
// Special handling for numeric types - treat numeric strings that convert to same value as equivalent
|
|
251
|
-
if (
|
|
274
|
+
if (tsType === EntityFieldTSType.Number) {
|
|
252
275
|
const oldNum = this.convertToNumber(oldCompare);
|
|
253
276
|
const newNum = this.convertToNumber(newCompare);
|
|
254
277
|
// Handle NaN cases - if both are NaN, they're equivalent
|
|
@@ -262,7 +285,7 @@ let EntityField = class EntityField {
|
|
|
262
285
|
return oldNum !== newNum;
|
|
263
286
|
}
|
|
264
287
|
// for string types where the comparisons are not both strings
|
|
265
|
-
if (
|
|
288
|
+
if (tsType === EntityFieldTSType.String) {
|
|
266
289
|
if (typeof oldCompare === 'object') {
|
|
267
290
|
// need to convert the object to a string for comparison
|
|
268
291
|
oldCompare = JSON.stringify(oldCompare);
|
|
@@ -370,7 +393,7 @@ let EntityField = class EntityField {
|
|
|
370
393
|
}
|
|
371
394
|
else {
|
|
372
395
|
// 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
|
|
373
|
-
if (this._oldValue !== null && this._oldValue !== undefined) {
|
|
396
|
+
if ((this._oldValue !== null && this._oldValue !== undefined) || this._writtenAfterNotLoaded) {
|
|
374
397
|
result.Success = false;
|
|
375
398
|
result.Errors.push(new ValidationErrorInfo(ef.Name, `${ef.DisplayNameOrName} cannot be null`, null));
|
|
376
399
|
}
|
|
@@ -465,9 +488,16 @@ let EntityField = class EntityField {
|
|
|
465
488
|
result.Errors.push(new ValidationErrorInfo(ef.Name, `${ef.DisplayNameOrName} cannot be longer than ${ef.MaxByteLength} bytes. Current value is ${byteLength} bytes`, this.Value));
|
|
466
489
|
}
|
|
467
490
|
}
|
|
468
|
-
constructor(fieldInfo, Value) {
|
|
491
|
+
constructor(fieldInfo, Value, skipDefaultValue = false) {
|
|
469
492
|
this._neverSet = true;
|
|
470
493
|
this._notLoaded = false;
|
|
494
|
+
/**
|
|
495
|
+
* True once the field is explicitly set after being not-loaded: a write onto a value the record
|
|
496
|
+
* never loaded. The field is then dirty whatever it was set to — even null, which equals its
|
|
497
|
+
* cleared old value — so the write reaches the save, and Validate treats it as an existing
|
|
498
|
+
* record's value. Cleared when the old value is reset (load, save) or the field is not-loaded again.
|
|
499
|
+
*/
|
|
500
|
+
this._writtenAfterNotLoaded = false;
|
|
471
501
|
this._createSuppressed = false;
|
|
472
502
|
// NOTE: constructing an EntityField for a deprecated/disabled field is always allowed — the
|
|
473
503
|
// column is physically part of the entity and the instance must exist. Active-status is
|
|
@@ -477,94 +507,21 @@ let EntityField = class EntityField {
|
|
|
477
507
|
if (Value) {
|
|
478
508
|
this.Value = Value;
|
|
479
509
|
}
|
|
480
|
-
else if (fieldInfo.DefaultValue) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
this.Value = false;
|
|
487
|
-
}
|
|
488
|
-
else if (fieldInfo.TSType === EntityFieldTSType.Number) {
|
|
489
|
-
// special handling for numbers as we don't want a string passed into a value for a numeric field
|
|
490
|
-
if (!isNaN(Number(fieldInfo.DefaultValue))) {
|
|
491
|
-
this.Value = Number(fieldInfo.DefaultValue);
|
|
492
|
-
}
|
|
493
|
-
else if (fieldInfo.DefaultValue.trim().toLowerCase() === "null") {
|
|
494
|
-
this.Value = null;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
else if (fieldInfo.IsUniqueIdentifier) {
|
|
498
|
-
// special handling for GUIDs (SQL Server `uniqueidentifier` / PostgreSQL `uuid`),
|
|
499
|
-
// we don't want to populate anything here because the server always sets the value, leave blank
|
|
500
|
-
this.Value = null;
|
|
501
|
-
}
|
|
502
|
-
else if (fieldInfo.TSType === EntityFieldTSType.Date) {
|
|
503
|
-
if (fieldInfo.DefaultValue.trim().length > 0) {
|
|
504
|
-
// 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
|
|
505
|
-
try {
|
|
506
|
-
// attempt to convert the default value to a date
|
|
507
|
-
const temp = new Date(fieldInfo.DefaultValue);
|
|
508
|
-
// if we get here, that means it worked, but we could have an invalid date, so check for that
|
|
509
|
-
if (isNaN(temp.getTime())) {
|
|
510
|
-
// this is an invalid date, so throw an error
|
|
511
|
-
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
|
|
512
|
-
}
|
|
513
|
-
else
|
|
514
|
-
this.Value = temp;
|
|
515
|
-
}
|
|
516
|
-
catch (e) {
|
|
517
|
-
// 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
|
|
518
|
-
if (EntityFieldInfo.IsDefaultValueSQLCurrentDateFunction(fieldInfo.DefaultValue)) {
|
|
519
|
-
// 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
|
|
520
|
-
//setting the value, otherwise set the value to the current date
|
|
521
|
-
if (fieldInfo.IsSpecialDateField) {
|
|
522
|
-
this.Value = null;
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
this.Value = new Date();
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
else {
|
|
529
|
-
// we have a default value that is not a valid date and not a getdate() type default, so we need to throw an error
|
|
530
|
-
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`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
510
|
+
else if (!skipDefaultValue && fieldInfo.DefaultValue) {
|
|
511
|
+
const parsed = typeof fieldInfo.GetParsedDefaultValue === 'function'
|
|
512
|
+
? fieldInfo.GetParsedDefaultValue()
|
|
513
|
+
: EntityFieldInfo.ParseDefaultValue(fieldInfo);
|
|
514
|
+
if (parsed.isCurrentDateFunction) {
|
|
515
|
+
this.Value = new Date();
|
|
534
516
|
}
|
|
535
|
-
else {
|
|
536
|
-
|
|
537
|
-
//
|
|
538
|
-
// PG's pg_get_expr() (used by vwSQLColumnsAndEntityFields →
|
|
539
|
-
// EntityField.DefaultValue) renders a string default like:
|
|
540
|
-
// 'Single'::character varying
|
|
541
|
-
// 'pending'::text
|
|
542
|
-
// SQL Server stores the same default as just `'Single'` or
|
|
543
|
-
// `'pending'`. If we set the field value to the raw PG form,
|
|
544
|
-
// the value is `'Single'::character varying` (27 chars), and
|
|
545
|
-
// a MaxLength=20 constraint immediately fails validation —
|
|
546
|
-
// even though the actual content is `Single` (6 chars).
|
|
547
|
-
//
|
|
548
|
-
// Unwrap a leading single-quoted string followed by a `::type`
|
|
549
|
-
// suffix. Function-call defaults (`nextval('...')`,
|
|
550
|
-
// `now() AT TIME ZONE 'UTC'`) deliberately don't match this
|
|
551
|
-
// shape and are left untouched — the database evaluates them
|
|
552
|
-
// server-side at INSERT time.
|
|
553
|
-
const dv = fieldInfo.DefaultValue.trim();
|
|
554
|
-
const pgTypedLiteral = /^'((?:[^']|'')*)'::[A-Za-z][\w "()\[\],]*$/;
|
|
555
|
-
const m = dv.match(pgTypedLiteral);
|
|
556
|
-
if (m) {
|
|
557
|
-
// Replace the SQL-escaped doubled quotes back to a single quote.
|
|
558
|
-
this.Value = m[1].replace(/''/g, "'");
|
|
559
|
-
}
|
|
560
|
-
else {
|
|
561
|
-
this.Value = fieldInfo.DefaultValue;
|
|
562
|
-
}
|
|
517
|
+
else if (parsed.hasValue) {
|
|
518
|
+
this.Value = parsed.value instanceof Date ? new Date(parsed.value.getTime()) : parsed.value;
|
|
563
519
|
}
|
|
564
520
|
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
|
|
565
521
|
}
|
|
566
522
|
else {
|
|
567
|
-
this.
|
|
523
|
+
this._value = null; // we need to set the value to null instead of being undefined as the value is defined, it is NULL
|
|
524
|
+
this._oldValue = null;
|
|
568
525
|
this._neverSet = true; // set this back to true because we are setting the value to null;
|
|
569
526
|
}
|
|
570
527
|
}
|
|
@@ -573,6 +530,23 @@ let EntityField = class EntityField {
|
|
|
573
530
|
*/
|
|
574
531
|
ResetOldValue() {
|
|
575
532
|
this._oldValue = this.Value;
|
|
533
|
+
this._writtenAfterNotLoaded = false;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Framework-internal, for {@link BaseEntity.Revert}: puts the field back to its last loaded or
|
|
537
|
+
* saved state. A not-loaded field has nothing to revert and stays not-loaded; a field written
|
|
538
|
+
* after being not-loaded goes back to not-loaded, so the revert neither turns an unloaded field
|
|
539
|
+
* into "loaded" data a later update would write, nor leaves the write's dirty flag behind.
|
|
540
|
+
*/
|
|
541
|
+
RevertToOldValue() {
|
|
542
|
+
if (this._notLoaded) {
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (this._writtenAfterNotLoaded) {
|
|
546
|
+
this.ClearAsNotLoaded();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
this.Value = this.OldValue;
|
|
576
550
|
}
|
|
577
551
|
/**
|
|
578
552
|
* Restores the dirty-tracking baseline to a previously captured value.
|
|
@@ -585,7 +559,13 @@ let EntityField = class EntityField {
|
|
|
585
559
|
* back can put them back with {@link RestoreState}.
|
|
586
560
|
*/
|
|
587
561
|
GetState() {
|
|
588
|
-
return {
|
|
562
|
+
return {
|
|
563
|
+
Value: this._value,
|
|
564
|
+
OldValue: this._oldValue,
|
|
565
|
+
NotLoaded: this._notLoaded,
|
|
566
|
+
NeverSet: this._neverSet,
|
|
567
|
+
WrittenAfterNotLoaded: this._writtenAfterNotLoaded,
|
|
568
|
+
};
|
|
589
569
|
}
|
|
590
570
|
/**
|
|
591
571
|
* Framework-internal: puts back a state {@link GetState} captured. It writes the state as it was,
|
|
@@ -596,6 +576,7 @@ let EntityField = class EntityField {
|
|
|
596
576
|
this._oldValue = state.OldValue;
|
|
597
577
|
this._notLoaded = state.NotLoaded;
|
|
598
578
|
this._neverSet = state.NeverSet;
|
|
579
|
+
this._writtenAfterNotLoaded = state.WrittenAfterNotLoaded ?? false;
|
|
599
580
|
}
|
|
600
581
|
/**
|
|
601
582
|
* @deprecated No-op as of the active-status relocation. Active-status (deprecated/disabled)
|
|
@@ -620,7 +601,7 @@ let EntityField = class EntityField {
|
|
|
620
601
|
};
|
|
621
602
|
EntityField = EntityField_1 = __decorate([
|
|
622
603
|
OptionalKeyedSpecialization(),
|
|
623
|
-
__metadata("design:paramtypes", [EntityFieldInfo, Object])
|
|
604
|
+
__metadata("design:paramtypes", [EntityFieldInfo, Object, Boolean])
|
|
624
605
|
], EntityField);
|
|
625
606
|
export { EntityField };
|
|
626
607
|
/**
|
|
@@ -905,6 +886,12 @@ export class BaseEntity {
|
|
|
905
886
|
* commits atomically.
|
|
906
887
|
*/
|
|
907
888
|
this._transactionGroup = null;
|
|
889
|
+
/**
|
|
890
|
+
* RxJS `Subject` that fires `BaseEntityEvent`s (save, delete, field-change,
|
|
891
|
+
* remote-invalidate). Exposed publicly via `Event$` so engines and UI components can react
|
|
892
|
+
* to entity lifecycle events.
|
|
893
|
+
*/
|
|
894
|
+
this._eventSubject = null;
|
|
908
895
|
/**
|
|
909
896
|
* Bounded ring of `BaseEntityResult` objects from each Save/Delete attempt. The most
|
|
910
897
|
* recent entry is exposed via `LatestResult` for error inspection after a failure.
|
|
@@ -1112,7 +1099,7 @@ export class BaseEntity {
|
|
|
1112
1099
|
* however it is possible for the string in the map to be any unique key relative to the object so you could have vectors
|
|
1113
1100
|
* that embed multiple fields if desired.
|
|
1114
1101
|
*/
|
|
1115
|
-
this._vectors =
|
|
1102
|
+
this._vectors = null;
|
|
1116
1103
|
/**
|
|
1117
1104
|
* When true, GenerateEmbedding() returns immediately without computing vectors.
|
|
1118
1105
|
* Set this before calling Save() in batch/sync contexts where embedding computation
|
|
@@ -1120,7 +1107,6 @@ export class BaseEntity {
|
|
|
1120
1107
|
* per-process is expensive and vectors can be computed later by the API server).
|
|
1121
1108
|
*/
|
|
1122
1109
|
this._skipEmbeddings = false;
|
|
1123
|
-
this._eventSubject = new Subject();
|
|
1124
1110
|
this._entityInfo = Entity;
|
|
1125
1111
|
EntityInfo.AssertEntityActiveStatus(Entity, 'BaseEntity::constructor');
|
|
1126
1112
|
this._provider = Provider;
|
|
@@ -1662,7 +1648,24 @@ export class BaseEntity {
|
|
|
1662
1648
|
static errorText(e) {
|
|
1663
1649
|
return e instanceof Error ? e.message : String(e);
|
|
1664
1650
|
}
|
|
1665
|
-
|
|
1651
|
+
/**
|
|
1652
|
+
* The create path's SubtypeSelector lookup memo, partitioned by the provider that ran the lookup. A memo
|
|
1653
|
+
* shared across providers would hand provider B a row loaded through provider A (another connection,
|
|
1654
|
+
* another user's context) or a `null` miss recorded against A's database. Keyed weakly so a
|
|
1655
|
+
* request-scoped provider's memo is collected with it.
|
|
1656
|
+
*/
|
|
1657
|
+
static { this._subtypeLookupCache = new WeakMap(); }
|
|
1658
|
+
/** Partition key for lookups made by an entity that has no provider at all. */
|
|
1659
|
+
static { this._noProviderSubtypeKey = {}; }
|
|
1660
|
+
static subtypeLookupCacheFor(provider) {
|
|
1661
|
+
const partitionKey = provider ?? BaseEntity._noProviderSubtypeKey;
|
|
1662
|
+
let cache = BaseEntity._subtypeLookupCache.get(partitionKey);
|
|
1663
|
+
if (!cache) {
|
|
1664
|
+
cache = new Map();
|
|
1665
|
+
BaseEntity._subtypeLookupCache.set(partitionKey, cache);
|
|
1666
|
+
}
|
|
1667
|
+
return cache;
|
|
1668
|
+
}
|
|
1666
1669
|
/** Per entity (lowercased name): the resolver asked for load hints, decided once per registration. */
|
|
1667
1670
|
static { this._subtypeLoadHintResolvers = new Map(); }
|
|
1668
1671
|
/** Indexes over engine-cached arrays, keyed by the live array itself; see {@link findEngineCachedRow}. */
|
|
@@ -1674,7 +1677,7 @@ export class BaseEntity {
|
|
|
1674
1677
|
* reported again.
|
|
1675
1678
|
*/
|
|
1676
1679
|
static ClearSubtypeLookupCache() {
|
|
1677
|
-
BaseEntity._subtypeLookupCache
|
|
1680
|
+
BaseEntity._subtypeLookupCache = new WeakMap();
|
|
1678
1681
|
BaseEntity._subtypeLoadHintResolvers.clear();
|
|
1679
1682
|
BaseEntity._engineCachedRowIndexes = new WeakMap();
|
|
1680
1683
|
BaseEntity._loggedSubtypeLoadHintMessages.clear();
|
|
@@ -1897,25 +1900,26 @@ export class BaseEntity {
|
|
|
1897
1900
|
}
|
|
1898
1901
|
async getSubtypePathTargetEntity(entityName, pkValue) {
|
|
1899
1902
|
const cacheKey = `${entityName.trim().toLowerCase()}|${String(pkValue).trim().toLowerCase()}`;
|
|
1900
|
-
|
|
1901
|
-
|
|
1903
|
+
const provider = this.ProviderToUse;
|
|
1904
|
+
const lookupCache = BaseEntity.subtypeLookupCacheFor(provider);
|
|
1905
|
+
if (lookupCache.has(cacheKey)) {
|
|
1906
|
+
return lookupCache.get(cacheKey) ?? null;
|
|
1902
1907
|
}
|
|
1903
1908
|
// 1. Check BaseEngineRegistry for loaded cached entities
|
|
1904
1909
|
const cached = BaseEntity.findSubtypePathTargetInEngineCache(entityName, pkValue);
|
|
1905
1910
|
if (cached) {
|
|
1906
|
-
|
|
1911
|
+
lookupCache.set(cacheKey, cached);
|
|
1907
1912
|
return cached;
|
|
1908
1913
|
}
|
|
1909
1914
|
// 2. Fall back to loading via provider
|
|
1910
|
-
const provider = this.ProviderToUse;
|
|
1911
1915
|
if (!provider?.GetEntityObject) {
|
|
1912
|
-
|
|
1916
|
+
lookupCache.set(cacheKey, null);
|
|
1913
1917
|
return null;
|
|
1914
1918
|
}
|
|
1915
1919
|
try {
|
|
1916
1920
|
const targetObj = await provider.GetEntityObject(entityName, this._contextCurrentUser);
|
|
1917
1921
|
if (!targetObj) {
|
|
1918
|
-
|
|
1922
|
+
lookupCache.set(cacheKey, null);
|
|
1919
1923
|
return null;
|
|
1920
1924
|
}
|
|
1921
1925
|
let key;
|
|
@@ -1929,14 +1933,14 @@ export class BaseEntity {
|
|
|
1929
1933
|
}
|
|
1930
1934
|
const loaded = await targetObj.InnerLoad(key);
|
|
1931
1935
|
if (loaded) {
|
|
1932
|
-
|
|
1936
|
+
lookupCache.set(cacheKey, targetObj);
|
|
1933
1937
|
return targetObj;
|
|
1934
1938
|
}
|
|
1935
1939
|
}
|
|
1936
1940
|
catch {
|
|
1937
1941
|
// On load failure, return null
|
|
1938
1942
|
}
|
|
1939
|
-
|
|
1943
|
+
lookupCache.set(cacheKey, null);
|
|
1940
1944
|
return null;
|
|
1941
1945
|
}
|
|
1942
1946
|
/**
|
|
@@ -3039,6 +3043,9 @@ export class BaseEntity {
|
|
|
3039
3043
|
* @returns
|
|
3040
3044
|
*/
|
|
3041
3045
|
RegisterEventHandler(callback) {
|
|
3046
|
+
if (!this._eventSubject) {
|
|
3047
|
+
this._eventSubject = new Subject();
|
|
3048
|
+
}
|
|
3042
3049
|
return this._eventSubject.asObservable().subscribe(callback);
|
|
3043
3050
|
}
|
|
3044
3051
|
/**
|
|
@@ -3060,6 +3067,9 @@ export class BaseEntity {
|
|
|
3060
3067
|
this.RaiseEvent('transaction_ready', null);
|
|
3061
3068
|
}
|
|
3062
3069
|
static { this._baseEventCode = 'BaseEntityEvent'; }
|
|
3070
|
+
static { this._globalEventTypes = new Set([
|
|
3071
|
+
'save', 'delete', 'load_complete', 'save_started', 'delete_started', 'load_started'
|
|
3072
|
+
]); }
|
|
3063
3073
|
/**
|
|
3064
3074
|
* 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.
|
|
3065
3075
|
* Any MJGlobal event that is raised by a BaseEntity class will use a BaseEntityEvent type as the args parameter
|
|
@@ -3081,11 +3091,10 @@ export class BaseEntity {
|
|
|
3081
3091
|
const provider = this.ProviderToUse;
|
|
3082
3092
|
// this is the local event handler that is specific to THIS instance of the entity object
|
|
3083
3093
|
LogDebug(`BaseEntity.RaiseEvent() - ${type === 'save' ? 'save:' + saveSubType : type} event raised for ${this.EntityInfo.Name}, about to call this._eventSubject.next()`);
|
|
3084
|
-
this._eventSubject
|
|
3094
|
+
this._eventSubject?.next({ type: type, payload: payload, saveSubType: saveSubType, baseEntity: this, provider });
|
|
3085
3095
|
// this next call is to MJGlobal to let everyone who cares knows that we had an event on an entity object
|
|
3086
3096
|
// we broadcast save/delete/load events and their _started counterparts
|
|
3087
|
-
|
|
3088
|
-
if (globalEventTypes.includes(type)) {
|
|
3097
|
+
if (BaseEntity._globalEventTypes.has(type)) {
|
|
3089
3098
|
const event = new BaseEntityEvent();
|
|
3090
3099
|
event.baseEntity = this;
|
|
3091
3100
|
event.payload = payload;
|
|
@@ -3272,6 +3281,10 @@ export class BaseEntity {
|
|
|
3272
3281
|
}
|
|
3273
3282
|
// Lookup needs an EntityField instance to return — hydrate.
|
|
3274
3283
|
this.hydrateFieldsIfNeeded();
|
|
3284
|
+
if (this._entityInfo && typeof this._entityInfo.FieldIndexByName === 'function' && this._fields.length === this._entityInfo.Fields.length && this._fieldCache === null) {
|
|
3285
|
+
const idx = this._entityInfo.FieldIndexByName(fieldName);
|
|
3286
|
+
return idx >= 0 ? (this._fields[idx] ?? null) : null;
|
|
3287
|
+
}
|
|
3275
3288
|
const lcase = fieldName.trim().toLowerCase(); // do this once as we will use it multiple times
|
|
3276
3289
|
if (this._fieldCache === null) {
|
|
3277
3290
|
this._fieldCache = new Map();
|
|
@@ -3315,6 +3328,9 @@ export class BaseEntity {
|
|
|
3315
3328
|
* ```
|
|
3316
3329
|
*/
|
|
3317
3330
|
FieldIsDirty(fieldName, ...more) {
|
|
3331
|
+
if (!this._fieldsHydrated && this._raw !== null && !this._parentEntity) {
|
|
3332
|
+
return false;
|
|
3333
|
+
}
|
|
3318
3334
|
const names = more.length === 0 ? [fieldName] : [fieldName, ...more];
|
|
3319
3335
|
for (const name of names) {
|
|
3320
3336
|
if (this.GetFieldByName(name)?.Dirty === true) {
|
|
@@ -3334,6 +3350,10 @@ export class BaseEntity {
|
|
|
3334
3350
|
}
|
|
3335
3351
|
// Lookup needs an EntityField instance to return — hydrate.
|
|
3336
3352
|
this.hydrateFieldsIfNeeded();
|
|
3353
|
+
if (this._entityInfo && typeof this._entityInfo.FieldIndexByCodeName === 'function' && this._fields.length === this._entityInfo.Fields.length && this._codeNameCache === null) {
|
|
3354
|
+
const idx = this._entityInfo.FieldIndexByCodeName(codeName);
|
|
3355
|
+
return idx >= 0 ? (this._fields[idx] ?? null) : null;
|
|
3356
|
+
}
|
|
3337
3357
|
const lcase = codeName.trim().toLowerCase();
|
|
3338
3358
|
if (this._codeNameCache === null) {
|
|
3339
3359
|
this._codeNameCache = new Map();
|
|
@@ -3488,41 +3508,47 @@ export class BaseEntity {
|
|
|
3488
3508
|
setLocal(FieldName, Value) {
|
|
3489
3509
|
const field = this.GetFieldByName(FieldName);
|
|
3490
3510
|
if (field != null) {
|
|
3491
|
-
|
|
3492
|
-
|
|
3511
|
+
this.setFieldValue(field, Value);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
setFieldValue(field, Value) {
|
|
3515
|
+
const tsType = field.EntityFieldInfo.TSType;
|
|
3516
|
+
if (tsType === EntityFieldTSType.Date && (typeof Value === 'string' || typeof Value === 'number')) {
|
|
3517
|
+
field.Value = new Date(Value);
|
|
3518
|
+
}
|
|
3519
|
+
else if (tsType === EntityFieldTSType.Number && typeof Value === 'string' && Value !== null && Value !== undefined) {
|
|
3520
|
+
const numericValue = Number(Value);
|
|
3521
|
+
if (!isNaN(numericValue)) {
|
|
3522
|
+
field.Value = numericValue;
|
|
3493
3523
|
}
|
|
3494
|
-
else
|
|
3495
|
-
|
|
3496
|
-
if (!isNaN(numericValue)) {
|
|
3497
|
-
field.Value = numericValue;
|
|
3498
|
-
}
|
|
3499
|
-
else {
|
|
3500
|
-
field.Value = Value;
|
|
3501
|
-
}
|
|
3524
|
+
else {
|
|
3525
|
+
field.Value = Value;
|
|
3502
3526
|
}
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
}
|
|
3511
|
-
else {
|
|
3512
|
-
field.Value = Value;
|
|
3513
|
-
}
|
|
3527
|
+
}
|
|
3528
|
+
else if (tsType === EntityFieldTSType.Boolean && Value !== null && Value !== undefined) {
|
|
3529
|
+
if (typeof Value === 'string') {
|
|
3530
|
+
const trimmed = Value.trim();
|
|
3531
|
+
const lower = trimmed.toLowerCase();
|
|
3532
|
+
if (trimmed === '1' || lower === 'true') {
|
|
3533
|
+
field.Value = true;
|
|
3514
3534
|
}
|
|
3515
|
-
else if (
|
|
3516
|
-
field.Value =
|
|
3535
|
+
else if (trimmed === '0' || lower === 'false') {
|
|
3536
|
+
field.Value = false;
|
|
3517
3537
|
}
|
|
3518
3538
|
else {
|
|
3519
3539
|
field.Value = Value;
|
|
3520
3540
|
}
|
|
3521
3541
|
}
|
|
3542
|
+
else if (typeof Value === 'number') {
|
|
3543
|
+
field.Value = Value !== 0;
|
|
3544
|
+
}
|
|
3522
3545
|
else {
|
|
3523
3546
|
field.Value = Value;
|
|
3524
3547
|
}
|
|
3525
3548
|
}
|
|
3549
|
+
else {
|
|
3550
|
+
field.Value = Value;
|
|
3551
|
+
}
|
|
3526
3552
|
}
|
|
3527
3553
|
/**
|
|
3528
3554
|
* 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.
|
|
@@ -3533,6 +3559,15 @@ export class BaseEntity {
|
|
|
3533
3559
|
* @param FieldName
|
|
3534
3560
|
* @returns
|
|
3535
3561
|
*/
|
|
3562
|
+
/**
|
|
3563
|
+
* A raw-mode row's value for a field: under its Name, or else its CodeName. Hydration accepts
|
|
3564
|
+
* both keys (as SetMany does), so every raw-mode read must too, or a CodeName-keyed row reads
|
|
3565
|
+
* back empty. `undefined` means the row omitted the field.
|
|
3566
|
+
*/
|
|
3567
|
+
rawValueFor(raw, field) {
|
|
3568
|
+
const byName = raw[field.Name];
|
|
3569
|
+
return byName !== undefined || field.CodeName === field.Name ? byName : raw[field.CodeName];
|
|
3570
|
+
}
|
|
3536
3571
|
/** Records a value derived from `_raw` so later reads skip the conversion. See {@link _rawConverted}. */
|
|
3537
3572
|
memoizeRawConversion(fieldName, value) {
|
|
3538
3573
|
(this._rawConverted ??= new Map()).set(fieldName, value);
|
|
@@ -3559,28 +3594,31 @@ export class BaseEntity {
|
|
|
3559
3594
|
// hydration. Cache data uses exact SQL column names so no case-insensitive scan needed.
|
|
3560
3595
|
if (!this._fieldsHydrated && this._raw) {
|
|
3561
3596
|
let value = this._raw[FieldName];
|
|
3562
|
-
if (value === undefined)
|
|
3563
|
-
return null;
|
|
3564
3597
|
// Conversions mirror the hydrated path, and memoize into `_rawConverted` rather than
|
|
3565
3598
|
// back into `_raw` — the row may be shared, frozen cache state. Fields needing no
|
|
3566
3599
|
// conversion (the vast majority) never touch the memo at all, so the fast path stays
|
|
3567
3600
|
// a single property read.
|
|
3568
3601
|
const fi = this._entityInfo?.FieldByName(FieldName);
|
|
3602
|
+
if (value === undefined && fi)
|
|
3603
|
+
value = this.rawValueFor(this._raw, fi);
|
|
3604
|
+
if (value === undefined)
|
|
3605
|
+
return null;
|
|
3606
|
+
const memoKey = fi ? fi.Name : FieldName;
|
|
3569
3607
|
if (fi?.TSType === EntityFieldTSType.Date && (typeof value === 'string' || typeof value === 'number')) {
|
|
3570
|
-
const memo = this._rawConverted?.get(
|
|
3608
|
+
const memo = this._rawConverted?.get(memoKey);
|
|
3571
3609
|
if (memo !== undefined)
|
|
3572
3610
|
return memo;
|
|
3573
3611
|
const d = new Date(value);
|
|
3574
|
-
this.memoizeRawConversion(
|
|
3612
|
+
this.memoizeRawConversion(memoKey, d);
|
|
3575
3613
|
return d;
|
|
3576
3614
|
}
|
|
3577
3615
|
// Mirror the EntityField.Value setter: rtrim padding for fixed-width string columns.
|
|
3578
3616
|
if (typeof value === 'string' && fi?.FixedWidthColumn) {
|
|
3579
|
-
const memo = this._rawConverted?.get(
|
|
3617
|
+
const memo = this._rawConverted?.get(memoKey);
|
|
3580
3618
|
if (memo !== undefined)
|
|
3581
3619
|
return memo;
|
|
3582
3620
|
value = trimTrailingSpaces(value);
|
|
3583
|
-
this.memoizeRawConversion(
|
|
3621
|
+
this.memoizeRawConversion(memoKey, value);
|
|
3584
3622
|
}
|
|
3585
3623
|
return value;
|
|
3586
3624
|
}
|
|
@@ -3624,8 +3662,8 @@ export class BaseEntity {
|
|
|
3624
3662
|
if (checkStatus) {
|
|
3625
3663
|
EntityFieldInfo.AssertEntityFieldActiveStatus(field.EntityFieldInfo, 'BaseEntity.SetMany');
|
|
3626
3664
|
}
|
|
3627
|
-
// Use
|
|
3628
|
-
this.
|
|
3665
|
+
// Use setFieldValue directly so we don't repeat GetFieldByName(key)
|
|
3666
|
+
this.setFieldValue(field, object[key]);
|
|
3629
3667
|
if (replaceOldValues) {
|
|
3630
3668
|
field.ResetOldValue();
|
|
3631
3669
|
}
|
|
@@ -3638,8 +3676,8 @@ export class BaseEntity {
|
|
|
3638
3676
|
if (checkStatus) {
|
|
3639
3677
|
EntityFieldInfo.AssertEntityFieldActiveStatus(field.EntityFieldInfo, 'BaseEntity.SetMany');
|
|
3640
3678
|
}
|
|
3641
|
-
// Use
|
|
3642
|
-
this.
|
|
3679
|
+
// Use setFieldValue directly so we don't repeat GetFieldByName(field.Name)
|
|
3680
|
+
this.setFieldValue(field, object[key]);
|
|
3643
3681
|
if (replaceOldValues) {
|
|
3644
3682
|
field.ResetOldValue();
|
|
3645
3683
|
}
|
|
@@ -3707,21 +3745,51 @@ export class BaseEntity {
|
|
|
3707
3745
|
*/
|
|
3708
3746
|
GetAll(oldValues = false, onlyDirtyFields = false) {
|
|
3709
3747
|
let obj = {};
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3748
|
+
if (!this._fieldsHydrated && this._raw !== null) {
|
|
3749
|
+
if (!onlyDirtyFields) {
|
|
3750
|
+
for (const ef of this.EntityInfo.Fields) {
|
|
3751
|
+
const name = ef.Name;
|
|
3752
|
+
const rawVal = this.rawValueFor(this._raw, ef);
|
|
3753
|
+
// Key absence in _raw means NotLoaded (D-3 decision): omit from serialized output
|
|
3754
|
+
if (rawVal === undefined)
|
|
3755
|
+
continue;
|
|
3756
|
+
let val;
|
|
3757
|
+
const memo = this._rawConverted?.get(name);
|
|
3758
|
+
if (memo !== undefined) {
|
|
3759
|
+
val = memo;
|
|
3760
|
+
}
|
|
3761
|
+
else if (ef.TSType === EntityFieldTSType.Date && (typeof rawVal === 'string' || typeof rawVal === 'number')) {
|
|
3762
|
+
val = new Date(rawVal);
|
|
3763
|
+
this.memoizeRawConversion(name, val);
|
|
3764
|
+
}
|
|
3765
|
+
else if (typeof rawVal === 'string' && ef.FixedWidthColumn) {
|
|
3766
|
+
val = trimTrailingSpaces(rawVal);
|
|
3767
|
+
this.memoizeRawConversion(name, val);
|
|
3768
|
+
}
|
|
3769
|
+
else {
|
|
3770
|
+
val = rawVal;
|
|
3771
|
+
}
|
|
3772
|
+
obj[name] = val;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
else {
|
|
3777
|
+
for (let field of this.Fields) {
|
|
3778
|
+
// NotLoaded fields are OMITTED (D-3 decision): their in-memory state is a
|
|
3779
|
+
// construction artifact, and serializing it would launder a default/null into
|
|
3780
|
+
// something downstream code treats as data — the exact masquerade the flag exists
|
|
3781
|
+
// to prevent. Key-absence also propagates the flag naturally: hydrating another
|
|
3782
|
+
// entity from this output re-marks the same fields not-loaded.
|
|
3783
|
+
if (field.NotLoaded)
|
|
3784
|
+
continue;
|
|
3785
|
+
if (!onlyDirtyFields || (onlyDirtyFields && field.Dirty)) {
|
|
3786
|
+
// Reads field.Value directly — serialization is framework-internal, so it does not
|
|
3787
|
+
// (and must not) assert active status. No suppression toggle needed: the assertion no
|
|
3788
|
+
// longer lives on EntityField.Value (it moved to BaseEntity.Get/Set/SetMany).
|
|
3789
|
+
obj[field.Name] = oldValues ? field.OldValue : field.Value;
|
|
3790
|
+
if (field.EntityFieldInfo.TSType == EntityFieldTSType.Date && obj[field.Name] && !(obj[field.Name] instanceof Date)) {
|
|
3791
|
+
obj[field.Name] = new Date(obj[field.Name]); // a timestamp, convert to JS Date Object
|
|
3792
|
+
}
|
|
3725
3793
|
}
|
|
3726
3794
|
}
|
|
3727
3795
|
}
|
|
@@ -3921,26 +3989,33 @@ export class BaseEntity {
|
|
|
3921
3989
|
this._fieldsHydrated = true;
|
|
3922
3990
|
if (!this.EntityInfo)
|
|
3923
3991
|
return;
|
|
3924
|
-
// 1. Allocate one EntityField per column.
|
|
3925
|
-
//
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3992
|
+
// 1. Allocate one EntityField per column. Check once whether any custom EntityField
|
|
3993
|
+
// subclasses are registered in ClassFactory; in standard deployments none are, so we
|
|
3994
|
+
// can instantiate EntityField directly without N string concatenations + factory lookups.
|
|
3995
|
+
const hasCustomEntityFieldRegistrations = MJGlobal.Instance.ClassFactory.GetAllRegistrations(EntityField).length > 0;
|
|
3996
|
+
const entityFields = this.EntityInfo.Fields;
|
|
3997
|
+
const skipDefaultValue = this._raw !== null;
|
|
3998
|
+
if (hasCustomEntityFieldRegistrations) {
|
|
3999
|
+
const prefix = this.EntityInfo.Name + '.';
|
|
4000
|
+
for (let i = 0; i < entityFields.length; i++) {
|
|
4001
|
+
const rawField = entityFields[i];
|
|
4002
|
+
const newField = MJGlobal.Instance.ClassFactory.CreateInstance(EntityField, prefix + rawField.Name, rawField, undefined, skipDefaultValue);
|
|
4003
|
+
this._fields.push(newField);
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
else {
|
|
4007
|
+
for (let i = 0; i < entityFields.length; i++) {
|
|
4008
|
+
this._fields.push(new EntityField(entityFields[i], undefined, skipDefaultValue));
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
// 2. If we have raw data from LoadFromData, populate Fields and mark omitted fields as
|
|
4012
|
+
// NotLoaded in a single pass.
|
|
3935
4013
|
if (this._raw) {
|
|
3936
|
-
|
|
3937
|
-
|
|
4014
|
+
const raw = this._raw;
|
|
4015
|
+
for (let i = 0; i < this._fields.length; i++) {
|
|
4016
|
+
const field = this._fields[i];
|
|
4017
|
+
const value = this.rawValueFor(raw, field.EntityFieldInfo);
|
|
3938
4018
|
if (value !== undefined) {
|
|
3939
|
-
// Populating fields from the loaded row writes EntityField.Value directly, which does
|
|
3940
|
-
// not assert active status — so a deprecated column still present in the table emits
|
|
3941
|
-
// no warning on load (hydration is not user use of the field). No suppression toggle
|
|
3942
|
-
// is needed now that the assertion lives at BaseEntity.Get/Set/SetMany.
|
|
3943
|
-
// Date conversion mirrors what SetLocal / Get does for raw string/number dates.
|
|
3944
4019
|
if (field.EntityFieldInfo.TSType === EntityFieldTSType.Date && (typeof value === 'string' || typeof value === 'number')) {
|
|
3945
4020
|
field.Value = new Date(value);
|
|
3946
4021
|
}
|
|
@@ -3948,10 +4023,10 @@ export class BaseEntity {
|
|
|
3948
4023
|
field.Value = value;
|
|
3949
4024
|
}
|
|
3950
4025
|
}
|
|
4026
|
+
else {
|
|
4027
|
+
field.MarkNotLoaded();
|
|
4028
|
+
}
|
|
3951
4029
|
}
|
|
3952
|
-
// _raw IS a hydration source (LoadFromData fast path) — fields whose key it omitted
|
|
3953
|
-
// are not-loaded, not defaulted. See EntityField.NotLoaded.
|
|
3954
|
-
this.markFieldsOmittedBySourceAsNotLoaded(this._raw);
|
|
3955
4030
|
// Raw data has been promoted into Fields — release the reference so we don't carry
|
|
3956
4031
|
// duplicate state. Fields hold their own copies, so a frozen source no longer
|
|
3957
4032
|
// constrains anything from here on.
|
|
@@ -3971,7 +4046,9 @@ export class BaseEntity {
|
|
|
3971
4046
|
return;
|
|
3972
4047
|
for (const field of this.Fields) {
|
|
3973
4048
|
if (source[field.Name] === undefined && source[field.CodeName] === undefined) {
|
|
3974
|
-
|
|
4049
|
+
// Cleared, not just flagged: otherwise a default from construction, or the previous
|
|
4050
|
+
// record's value on a reload, reads back through Get() as if it had been loaded.
|
|
4051
|
+
field.ClearAsNotLoaded();
|
|
3975
4052
|
}
|
|
3976
4053
|
}
|
|
3977
4054
|
}
|
|
@@ -4479,7 +4556,7 @@ export class BaseEntity {
|
|
|
4479
4556
|
const data = await this.ProviderToUse.Save(this, this.ActiveUser, _options);
|
|
4480
4557
|
if (!this.TransactionGroup) {
|
|
4481
4558
|
// no transaction group, so we have our results here
|
|
4482
|
-
const result = this.finalizeSave(data, saveSubType);
|
|
4559
|
+
const result = this.finalizeSave(data, saveSubType, _options);
|
|
4483
4560
|
// Settle the scope this entity opened, if any (only the initiator holds
|
|
4484
4561
|
// one). The provider can fail by RETURNING falsy data rather than
|
|
4485
4562
|
// throwing — a validate-type entity action rejecting the save does
|
|
@@ -4500,7 +4577,7 @@ export class BaseEntity {
|
|
|
4500
4577
|
this.TransactionGroup.TransactionNotifications$.subscribe(({ success, results, error }) => {
|
|
4501
4578
|
const transItem = success && results ? results.find(r => r.Transaction.BaseEntity === this) : undefined;
|
|
4502
4579
|
if (transItem) {
|
|
4503
|
-
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
|
|
4580
|
+
this.finalizeSave(transItem.Result, saveSubType, _options); // we get the resulting data from the transaction result, not data above as that will be blank when in a TG
|
|
4504
4581
|
}
|
|
4505
4582
|
else {
|
|
4506
4583
|
// The transaction group failed / rolled back, OR (should never happen) reported success
|
|
@@ -4602,10 +4679,37 @@ export class BaseEntity {
|
|
|
4602
4679
|
// If no remaining children, safe to delete parent
|
|
4603
4680
|
return remainingChildren.length === 0;
|
|
4604
4681
|
}
|
|
4605
|
-
finalizeSave(data, saveSubType) {
|
|
4682
|
+
finalizeSave(data, saveSubType, options) {
|
|
4606
4683
|
if (data) {
|
|
4684
|
+
const effectiveReturnMode = options?.SaveReturnMode ?? this.EntityInfo.SaveReturnMode;
|
|
4685
|
+
const hasGetAll = (val) => val instanceof BaseEntity || typeof val.GetAll === 'function';
|
|
4686
|
+
const rawFieldData = hasGetAll(data) ? data.GetAll() : data;
|
|
4687
|
+
// In 'datestamps_only' mode, the database only returned PK + __mj_CreatedAt / __mj_UpdatedAt.
|
|
4688
|
+
// Merge those returned values onto the current in-memory values BEFORE init() wipes Fields,
|
|
4689
|
+
// so caller-set fields remain loaded and intact without a post-save BaseView SELECT.
|
|
4690
|
+
// When DatabaseProviderBase.Save already merged its pre-save snapshot (`__mj_preMerged`),
|
|
4691
|
+
// reuse `rawFieldData` directly to avoid a redundant second `GetAll()` call.
|
|
4692
|
+
let fieldData;
|
|
4693
|
+
if (effectiveReturnMode === 'datestamps_only' && typeof rawFieldData === 'object' && !Array.isArray(rawFieldData)) {
|
|
4694
|
+
if (rawFieldData.__mj_preMerged) {
|
|
4695
|
+
fieldData = rawFieldData;
|
|
4696
|
+
}
|
|
4697
|
+
else {
|
|
4698
|
+
const currentValues = this.GetAll();
|
|
4699
|
+
if (saveSubType === 'create' || !this.IsSaved) {
|
|
4700
|
+
for (const f of this.EntityInfo.Fields) {
|
|
4701
|
+
if (f.IsVirtual && !f.IsPrimaryKey && !(f.Name in rawFieldData) && (currentValues[f.Name] === null || currentValues[f.Name] === undefined)) {
|
|
4702
|
+
delete currentValues[f.Name];
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
fieldData = { ...currentValues, ...rawFieldData };
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
else {
|
|
4710
|
+
fieldData = rawFieldData;
|
|
4711
|
+
}
|
|
4607
4712
|
this.init(); // wipe out the current data to flush out the DIRTY flags, load the ID as part of this too
|
|
4608
|
-
const fieldData = (data instanceof BaseEntity || (data && typeof data.GetAll === 'function')) ? data.GetAll() : data;
|
|
4609
4713
|
// IS-A GetAll() merges the parent, including parent virtuals this entity
|
|
4610
4714
|
// does not own (e.g. OrderHeader on Event Order Line). Keep only columns
|
|
4611
4715
|
// this entity defines, and ignore anything leftover.
|
|
@@ -4903,7 +5007,7 @@ export class BaseEntity {
|
|
|
4903
5007
|
this.discardJSONFieldObjects(); // unconditional: an un-flushed edit through a stale reference must go too
|
|
4904
5008
|
if (this.Dirty) {
|
|
4905
5009
|
for (let field of this.Fields) {
|
|
4906
|
-
field.
|
|
5010
|
+
field.RevertToOldValue();
|
|
4907
5011
|
}
|
|
4908
5012
|
// IS-A composition: revert parent entity chain as well
|
|
4909
5013
|
if (this._parentEntity) {
|
|
@@ -5108,12 +5212,26 @@ export class BaseEntity {
|
|
|
5108
5212
|
// This is the dominant warm-load case (cache restore in TransformSimpleObjectToEntityObject
|
|
5109
5213
|
// populating engine arrays of read-only data) and was previously the per-row bottleneck.
|
|
5110
5214
|
const isPlainObject = data && typeof data === 'object' && !Array.isArray(data) && !(data instanceof Date);
|
|
5215
|
+
const isFirstOrUnhydratedLoad = this._raw === null &&
|
|
5216
|
+
!this._recordLoaded &&
|
|
5217
|
+
!this._everSaved &&
|
|
5218
|
+
((!this._fieldsHydrated && this._fields.length === 0) || this._fields.every(f => f.NeverSet));
|
|
5219
|
+
const hasCompanionPayload = this.HasCompanions &&
|
|
5220
|
+
isPlainObject &&
|
|
5221
|
+
Array.isArray(data[COMPANION_PAYLOAD_KEY]);
|
|
5111
5222
|
const canTakeFastPath = isPlainObject &&
|
|
5112
|
-
|
|
5113
|
-
this._fields.length === 0 &&
|
|
5223
|
+
isFirstOrUnhydratedLoad &&
|
|
5114
5224
|
!this._parentEntity &&
|
|
5115
|
-
!this.EntityInfo?.IsParentType
|
|
5225
|
+
!this.EntityInfo?.IsParentType &&
|
|
5226
|
+
!hasCompanionPayload;
|
|
5116
5227
|
if (canTakeFastPath) {
|
|
5228
|
+
if (this._fieldsHydrated || this._fields.length > 0) {
|
|
5229
|
+
this._fields = [];
|
|
5230
|
+
this._fieldsHydrated = false;
|
|
5231
|
+
this._fieldCache = null;
|
|
5232
|
+
this._codeNameCache = null;
|
|
5233
|
+
}
|
|
5234
|
+
this._compositeKey = null;
|
|
5117
5235
|
this._raw = data;
|
|
5118
5236
|
// Drop any conversions memoized from a previously-loaded row — they describe the old
|
|
5119
5237
|
// `_raw`, not this one. No isFrozen probe is needed: `Get()` never writes to `_raw`,
|
|
@@ -5124,7 +5242,7 @@ export class BaseEntity {
|
|
|
5124
5242
|
const pks = this.EntityInfo?.PrimaryKeys ?? [];
|
|
5125
5243
|
let allPksPresent = pks.length > 0;
|
|
5126
5244
|
for (const pkInfo of pks) {
|
|
5127
|
-
const v = this._raw
|
|
5245
|
+
const v = this.rawValueFor(this._raw, pkInfo);
|
|
5128
5246
|
if (v === null || v === undefined) {
|
|
5129
5247
|
allPksPresent = false;
|
|
5130
5248
|
break;
|
|
@@ -6091,13 +6209,16 @@ export class BaseEntity {
|
|
|
6091
6209
|
* use this to track vectors associated with
|
|
6092
6210
|
*/
|
|
6093
6211
|
get Vectors() {
|
|
6212
|
+
if (!this._vectors) {
|
|
6213
|
+
this._vectors = new Map();
|
|
6214
|
+
}
|
|
6094
6215
|
return this._vectors;
|
|
6095
6216
|
}
|
|
6096
6217
|
/**
|
|
6097
6218
|
* Resets the vector embeddings for this entity to an empty state.
|
|
6098
6219
|
*/
|
|
6099
6220
|
ResetVectors() {
|
|
6100
|
-
this._vectors
|
|
6221
|
+
this._vectors?.clear();
|
|
6101
6222
|
}
|
|
6102
6223
|
/**
|
|
6103
6224
|
* Resolves the recursive foreign key field for this entity. If `parentFieldName` is provided,
|