@memberjunction/core 6.2.0-edge.6 → 6.2.0-edge.8
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/EntityFieldRules.d.ts.map +1 -1
- package/dist/generic/EntityFieldRules.js +1 -0
- package/dist/generic/EntityFieldRules.js.map +1 -1
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts +32 -0
- package/dist/generic/JSONType-interfaces/IEntityConfiguration.d.ts.map +1 -1
- package/dist/generic/baseEngine.d.ts +165 -12
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +622 -98
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEngineRegistry.d.ts +10 -2
- package/dist/generic/baseEngineRegistry.d.ts.map +1 -1
- package/dist/generic/baseEngineRegistry.js +33 -5
- package/dist/generic/baseEngineRegistry.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +121 -6
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +1144 -64
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/baseRemotableOperation.d.ts +95 -6
- package/dist/generic/baseRemotableOperation.d.ts.map +1 -1
- package/dist/generic/baseRemotableOperation.js +112 -8
- package/dist/generic/baseRemotableOperation.js.map +1 -1
- package/dist/generic/databaseProviderBase.d.ts +37 -0
- package/dist/generic/databaseProviderBase.d.ts.map +1 -1
- package/dist/generic/databaseProviderBase.js +262 -46
- package/dist/generic/databaseProviderBase.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +25 -1
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +70 -12
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/externalDataSourceReadRouter.d.ts +10 -0
- package/dist/generic/externalDataSourceReadRouter.d.ts.map +1 -1
- package/dist/generic/externalDataSourceReadRouter.js +12 -0
- package/dist/generic/externalDataSourceReadRouter.js.map +1 -1
- package/dist/generic/fieldSecuritySQLGate.d.ts +11 -2
- package/dist/generic/fieldSecuritySQLGate.d.ts.map +1 -1
- package/dist/generic/fieldSecuritySQLGate.js +6 -2
- package/dist/generic/fieldSecuritySQLGate.js.map +1 -1
- package/dist/generic/interfaces.d.ts +50 -1
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/listMembershipFilter.d.ts +5 -0
- package/dist/generic/listMembershipFilter.d.ts.map +1 -0
- package/dist/generic/listMembershipFilter.js +23 -0
- package/dist/generic/listMembershipFilter.js.map +1 -0
- package/dist/generic/localCacheManager.d.ts +69 -5
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +253 -18
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/logging.d.ts +105 -38
- package/dist/generic/logging.d.ts.map +1 -1
- package/dist/generic/logging.js +196 -5
- package/dist/generic/logging.js.map +1 -1
- package/dist/generic/providerBase.d.ts +198 -3
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +721 -223
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/relatedRecordCollection.d.ts +5 -0
- package/dist/generic/relatedRecordCollection.d.ts.map +1 -1
- package/dist/generic/relatedRecordCollection.js +11 -2
- package/dist/generic/relatedRecordCollection.js.map +1 -1
- package/dist/generic/remoteOperationDispatch.d.ts.map +1 -1
- package/dist/generic/remoteOperationDispatch.js +14 -2
- package/dist/generic/remoteOperationDispatch.js.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.d.ts +33 -6
- package/dist/generic/runQuerySQLFilterImplementations.d.ts.map +1 -1
- package/dist/generic/runQuerySQLFilterImplementations.js +81 -27
- package/dist/generic/runQuerySQLFilterImplementations.js.map +1 -1
- package/dist/generic/sharedLoad.d.ts +42 -0
- package/dist/generic/sharedLoad.d.ts.map +1 -0
- package/dist/generic/sharedLoad.js +74 -0
- package/dist/generic/sharedLoad.js.map +1 -0
- package/dist/generic/telemetryManager.d.ts +31 -0
- package/dist/generic/telemetryManager.d.ts.map +1 -1
- package/dist/generic/telemetryManager.js +85 -9
- package/dist/generic/telemetryManager.js.map +1 -1
- package/dist/generic/transactionGroup.d.ts +19 -0
- package/dist/generic/transactionGroup.d.ts.map +1 -1
- package/dist/generic/transactionGroup.js +55 -7
- package/dist/generic/transactionGroup.js.map +1 -1
- package/dist/generic/transportError.d.ts +41 -0
- package/dist/generic/transportError.d.ts.map +1 -0
- package/dist/generic/transportError.js +167 -0
- package/dist/generic/transportError.js.map +1 -0
- package/dist/generic/util.d.ts +13 -1
- package/dist/generic/util.d.ts.map +1 -1
- package/dist/generic/util.js +53 -4
- package/dist/generic/util.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/views/runView.d.ts +23 -10
- package/dist/views/runView.d.ts.map +1 -1
- package/dist/views/runView.js +5 -1
- package/dist/views/runView.js.map +1 -1
- package/package.json +3 -3
|
@@ -9,7 +9,8 @@ import { CompositeKey } from "./compositeKey.js";
|
|
|
9
9
|
import { LogError } from "./logging.js";
|
|
10
10
|
import { LocalCacheManager } from "./localCacheManager.js";
|
|
11
11
|
import { AssertClientInputWritableForUser } from "./entityValueFieldSecurity.js";
|
|
12
|
-
import {
|
|
12
|
+
import { CanonicalPrimaryKeyValue } from "./util.js";
|
|
13
|
+
import { FormatBinaryChangeValue, ScanSQLForScreening, SQLExpressionValidator, uuidv4, EscapeSQLString } from "@memberjunction/global";
|
|
13
14
|
import { GetDialect } from "@memberjunction/sql-dialect";
|
|
14
15
|
// Re-export PlatformSQL types from their canonical location for backward compatibility
|
|
15
16
|
export { IsPlatformSQL } from "./platformSQL.js";
|
|
@@ -321,6 +322,28 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
321
322
|
get MJCoreSchemaName() {
|
|
322
323
|
return this.ConfigData?.MJCoreSchemaName ?? '__mj';
|
|
323
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Builds a batched UNION ALL query probing all `childEntities`' `BaseView`s for a set
|
|
327
|
+
* of parent primary-key values (`WHERE pk IN (...)`), returning `(EntityName, RecordPK, Priority)`.
|
|
328
|
+
* Dialect providers override this to use platform-native string-literal and CAST syntax.
|
|
329
|
+
*/
|
|
330
|
+
BuildChildDiscoveryBatchSQL(childEntities, recordPKValues) {
|
|
331
|
+
if (recordPKValues.length === 0)
|
|
332
|
+
return '';
|
|
333
|
+
const escapedKeys = recordPKValues
|
|
334
|
+
.map(v => `'${EscapeSQLString(String(v))}'`)
|
|
335
|
+
.join(', ');
|
|
336
|
+
const unionParts = childEntities
|
|
337
|
+
.filter(child => child.PrimaryKeys.length > 0)
|
|
338
|
+
.map((child, idx) => {
|
|
339
|
+
const schema = child.SchemaName || this.MJCoreSchemaName || '__mj';
|
|
340
|
+
const sourceRef = this.QuoteSchemaAndView(schema, child.BaseView);
|
|
341
|
+
const pkRef = this.QuoteIdentifier(child.FirstPrimaryKey.Name); // first-pk-ok: IS-A children share the parent's single key
|
|
342
|
+
const escapedName = `'${EscapeSQLString(child.Name)}'`;
|
|
343
|
+
return `SELECT ${escapedName} AS ${this.QuoteIdentifier('EntityName')}, CAST(${pkRef} AS VARCHAR(255)) AS ${this.QuoteIdentifier('RecordPK')}, ${idx + 1} AS ${this.QuoteIdentifier('Priority')} FROM ${sourceRef} WHERE ${pkRef} IN (${escapedKeys})`;
|
|
344
|
+
});
|
|
345
|
+
return unionParts.length > 0 ? unionParts.join(' UNION ALL ') : '';
|
|
346
|
+
}
|
|
324
347
|
/**
|
|
325
348
|
* Combined regex covering UUID functions from ALL platforms.
|
|
326
349
|
* Used by the static convenience method for code that doesn't have a provider instance.
|
|
@@ -585,6 +608,91 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
585
608
|
}
|
|
586
609
|
return [];
|
|
587
610
|
}
|
|
611
|
+
static { this.DEFAULT_ISA_DISCOVERY_BATCH_CHUNK_SIZE = 500; }
|
|
612
|
+
/**
|
|
613
|
+
* Maximum number of parent primary keys included in a single `BuildChildDiscoveryBatchSQL`
|
|
614
|
+
* `WHERE PK IN (...)` statement inside {@link FindISAChildEntitiesBatch}.
|
|
615
|
+
*/
|
|
616
|
+
get ISADiscoveryBatchChunkSize() {
|
|
617
|
+
return DatabaseProviderBase.DEFAULT_ISA_DISCOVERY_BATCH_CHUNK_SIZE;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Batch-discovers IS-A child entity types for multiple parent primary keys in a single
|
|
621
|
+
* UNION ALL SQL query (chunked at {@link ISADiscoveryBatchChunkSize} keys per statement to
|
|
622
|
+
* stay well within SQL limits).
|
|
623
|
+
*/
|
|
624
|
+
async FindISAChildEntitiesBatch(entityInfo, recordPKValues, contextUser) {
|
|
625
|
+
const resultMap = new Map();
|
|
626
|
+
const childEntities = entityInfo.ChildEntities;
|
|
627
|
+
if (childEntities.length === 0 || recordPKValues.length === 0) {
|
|
628
|
+
return resultMap;
|
|
629
|
+
}
|
|
630
|
+
// IS-A children share the parent's single key, so the parent's key type decides how values
|
|
631
|
+
// are canonicalized (UUIDs case-insensitively, anything else by exact text).
|
|
632
|
+
const pkIsUuid = Boolean(entityInfo.FirstPrimaryKey?.IsUniqueIdentifier); // first-pk-ok: IS-A children share the parent's single key
|
|
633
|
+
const uniquePKs = this.deduplicateCanonicalPKs(recordPKValues, pkIsUuid);
|
|
634
|
+
if (uniquePKs.length === 0) {
|
|
635
|
+
return resultMap;
|
|
636
|
+
}
|
|
637
|
+
const priorityByChildName = new Map();
|
|
638
|
+
for (let i = 0; i < childEntities.length; i++) {
|
|
639
|
+
priorityByChildName.set(childEntities[i].Name.toLowerCase(), i + 1);
|
|
640
|
+
}
|
|
641
|
+
const chunkSize = this.ISADiscoveryBatchChunkSize;
|
|
642
|
+
const collected = new Map();
|
|
643
|
+
for (let start = 0; start < uniquePKs.length; start += chunkSize) {
|
|
644
|
+
const chunk = uniquePKs.slice(start, start + chunkSize);
|
|
645
|
+
await this.collectISAChildBatchChunk(childEntities, chunk, pkIsUuid, priorityByChildName, collected, contextUser);
|
|
646
|
+
}
|
|
647
|
+
for (const [pkKey, entries] of collected) {
|
|
648
|
+
entries.sort((a, b) => a.priority - b.priority);
|
|
649
|
+
const names = [];
|
|
650
|
+
for (const entry of entries) {
|
|
651
|
+
if (!names.includes(entry.name)) {
|
|
652
|
+
names.push(entry.name);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
resultMap.set(pkKey, names);
|
|
656
|
+
}
|
|
657
|
+
return resultMap;
|
|
658
|
+
}
|
|
659
|
+
deduplicateCanonicalPKs(recordPKValues, pkIsUuid) {
|
|
660
|
+
const uniquePKs = [];
|
|
661
|
+
const seen = new Set();
|
|
662
|
+
for (const rawPk of recordPKValues) {
|
|
663
|
+
if (!rawPk)
|
|
664
|
+
continue;
|
|
665
|
+
const norm = CanonicalPrimaryKeyValue(String(rawPk), pkIsUuid);
|
|
666
|
+
if (!seen.has(norm)) {
|
|
667
|
+
seen.add(norm);
|
|
668
|
+
uniquePKs.push(String(rawPk).trim());
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return uniquePKs;
|
|
672
|
+
}
|
|
673
|
+
async collectISAChildBatchChunk(childEntities, chunk, pkIsUuid, priorityByChildName, collected, contextUser) {
|
|
674
|
+
const batchSQL = this.BuildChildDiscoveryBatchSQL(childEntities, chunk);
|
|
675
|
+
if (!batchSQL)
|
|
676
|
+
return;
|
|
677
|
+
const rows = await this.ExecuteSQL(batchSQL, undefined, undefined, contextUser);
|
|
678
|
+
if (!rows || rows.length === 0)
|
|
679
|
+
return;
|
|
680
|
+
for (const row of rows) {
|
|
681
|
+
const entityName = row.EntityName ?? row.ChildEntityName;
|
|
682
|
+
if (!entityName || row.RecordPK == null)
|
|
683
|
+
continue;
|
|
684
|
+
const pkKey = CanonicalPrimaryKeyValue(String(row.RecordPK), pkIsUuid);
|
|
685
|
+
const priority = typeof row.Priority === 'number'
|
|
686
|
+
? row.Priority
|
|
687
|
+
: (priorityByChildName.get(entityName.toLowerCase()) ?? Number.MAX_SAFE_INTEGER);
|
|
688
|
+
let bucket = collected.get(pkKey);
|
|
689
|
+
if (!bucket) {
|
|
690
|
+
bucket = [];
|
|
691
|
+
collected.set(pkKey, bucket);
|
|
692
|
+
}
|
|
693
|
+
bucket.push({ name: entityName, priority });
|
|
694
|
+
}
|
|
695
|
+
}
|
|
588
696
|
/**
|
|
589
697
|
* Checks whether a given entity matches the target name, or is an ancestor
|
|
590
698
|
* of the target (i.e., the target is somewhere in its descendant sub-tree).
|
|
@@ -959,6 +1067,27 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
959
1067
|
async OnBeforeSaveExecute(_entity, _user, _options, _context) {
|
|
960
1068
|
/* no-op by default */
|
|
961
1069
|
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Returns true when `Save()` must enlist {@link OnDurableAfterSaveExecute} transactionally
|
|
1072
|
+
* (wrapping direct saves in an {@link EntityTransactionScope}, and awaiting durable action
|
|
1073
|
+
* submission inside `TransactionGroup` save callbacks before group completion).
|
|
1074
|
+
*
|
|
1075
|
+
* Default: `false` (zero extra transaction overhead on ordinary entity saves).
|
|
1076
|
+
*/
|
|
1077
|
+
async ShouldEnlistSaveInTransaction(_entity, _user, _options, _isNewRecord) {
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Awaited immediately after the save SQL succeeds (inside direct `Save()`'s
|
|
1082
|
+
* {@link EntityTransactionScope} before commit, or inside a `TransactionGroup` item's completion
|
|
1083
|
+
* callback after hydrating DB-generated PKs and before {@link OnAfterSaveExecute}) when
|
|
1084
|
+
* {@link ShouldEnlistSaveInTransaction} returned `true`.
|
|
1085
|
+
* Used by `GenericDatabaseProvider` to enlist durable after-save entity actions atomically in the
|
|
1086
|
+
* same SQL transaction as the entity write.
|
|
1087
|
+
*/
|
|
1088
|
+
async OnDurableAfterSaveExecute(_entity, _user, _options, _context) {
|
|
1089
|
+
/* no-op by default */
|
|
1090
|
+
}
|
|
962
1091
|
/**
|
|
963
1092
|
* Called after a successful save (both direct and transaction-callback paths).
|
|
964
1093
|
* Intentionally synchronous (fire-and-forget) — SQL Server overrides to dispatch
|
|
@@ -1485,8 +1614,11 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
1485
1614
|
this.OnResumeRefresh();
|
|
1486
1615
|
}
|
|
1487
1616
|
};
|
|
1617
|
+
let transactionPreprocessingRegistered = false;
|
|
1618
|
+
let transactionReadyRaised = false;
|
|
1488
1619
|
try {
|
|
1489
1620
|
entity.RegisterTransactionPreprocessing();
|
|
1621
|
+
transactionPreprocessingRegistered = Boolean(entity.TransactionGroup);
|
|
1490
1622
|
// External-data-source entities are read-only — MJ is never the system of record for
|
|
1491
1623
|
// remote data. Refuse writes at the provider layer so the guarantee holds regardless of
|
|
1492
1624
|
// the generated base class (ReadOnlyExternalBaseEntity covers the normal path — returning
|
|
@@ -1617,6 +1749,7 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
1617
1749
|
if (entity.TransactionGroup && !bReplay) {
|
|
1618
1750
|
// ---- Transaction Group path ----
|
|
1619
1751
|
entity.RaiseReadyForTransaction();
|
|
1752
|
+
transactionReadyRaised = true;
|
|
1620
1753
|
suspendRefresh();
|
|
1621
1754
|
const extraData = this.GetTransactionExtraData(entity);
|
|
1622
1755
|
Object.defineProperty(extraData, 'simpleSQLFallback', {
|
|
@@ -1625,72 +1758,137 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
1625
1758
|
configurable: true,
|
|
1626
1759
|
});
|
|
1627
1760
|
extraData.entityName = entity.EntityInfo.Name;
|
|
1628
|
-
entity.TransactionGroup.AddTransaction(new TransactionItem(entity, entityResult.Type === 'create' ? 'Create' : 'Update', sqlDetails.literalSQL ?? sqlDetails.fullSQL, sqlDetails.literalSQL ? null : (sqlDetails.parameters ?? null), extraData, (transactionResult, success) => {
|
|
1761
|
+
entity.TransactionGroup.AddTransaction(new TransactionItem(entity, entityResult.Type === 'create' ? 'Create' : 'Update', sqlDetails.literalSQL ?? sqlDetails.fullSQL, sqlDetails.literalSQL ? null : (sqlDetails.parameters ?? null), extraData, async (transactionResult, success) => {
|
|
1629
1762
|
resumeRefresh();
|
|
1630
1763
|
entityResult.EndedAt = new Date();
|
|
1631
1764
|
if (success && transactionResult) {
|
|
1765
|
+
if (bNewRecord) {
|
|
1766
|
+
let hydratedPk = false;
|
|
1767
|
+
for (const pkField of entity.EntityInfo.PrimaryKeys) {
|
|
1768
|
+
const fieldObj = entity.Fields.find((f) => f.Name === pkField.Name);
|
|
1769
|
+
if (fieldObj &&
|
|
1770
|
+
(fieldObj.Value === null || fieldObj.Value === undefined) &&
|
|
1771
|
+
transactionResult[pkField.Name] != null) {
|
|
1772
|
+
fieldObj.Value = transactionResult[pkField.Name];
|
|
1773
|
+
hydratedPk = true;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
if (hydratedPk) {
|
|
1777
|
+
entity.PrimaryKey.LoadFromEntityFields(entity.PrimaryKeys);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (await this.ShouldEnlistSaveInTransaction(entity, user, options, bNewRecord)) {
|
|
1781
|
+
await this.OnDurableAfterSaveExecute(entity, user, options, saveContext);
|
|
1782
|
+
}
|
|
1783
|
+
if (preSaveValues) {
|
|
1784
|
+
for (const [key, val] of Object.entries(preSaveValues)) {
|
|
1785
|
+
if (!(key in transactionResult)) {
|
|
1786
|
+
transactionResult[key] = val;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
Object.defineProperty(transactionResult, '__mj_preMerged', {
|
|
1790
|
+
value: true,
|
|
1791
|
+
enumerable: false,
|
|
1792
|
+
configurable: true,
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
const patches = await this.OnSaveCompleted(entity, sqlDetails, user, options, saveContext);
|
|
1796
|
+
if (patches) {
|
|
1797
|
+
Object.assign(transactionResult, patches);
|
|
1798
|
+
}
|
|
1632
1799
|
this.OnAfterSaveExecute(entity, user, options, saveContext);
|
|
1633
1800
|
entityResult.Success = true;
|
|
1634
|
-
|
|
1635
|
-
? { ...preSaveValues, ...transactionResult }
|
|
1636
|
-
: transactionResult;
|
|
1637
|
-
entityResult.NewValues = this.MapTransactionResultToNewValues(mergedResult);
|
|
1801
|
+
entityResult.NewValues = this.MapTransactionResultToNewValues(transactionResult);
|
|
1638
1802
|
}
|
|
1639
1803
|
else {
|
|
1640
1804
|
entityResult.Success = false;
|
|
1641
|
-
entityResult.Message
|
|
1805
|
+
if (!entityResult.Message) {
|
|
1806
|
+
entityResult.Message =
|
|
1807
|
+
transactionResult instanceof Error ? transactionResult.message : 'Transaction Failed';
|
|
1808
|
+
}
|
|
1642
1809
|
}
|
|
1643
1810
|
}));
|
|
1644
1811
|
return true;
|
|
1645
1812
|
}
|
|
1646
1813
|
else {
|
|
1647
1814
|
// ---- Direct execution path ----
|
|
1815
|
+
const enlistInTx = !bReplay && await this.ShouldEnlistSaveInTransaction(entity, user, options, bNewRecord);
|
|
1816
|
+
const saveTxScope = enlistInTx ? await this.BeginEntityTransaction() : null;
|
|
1817
|
+
let saveTxCommitted = false;
|
|
1648
1818
|
suspendRefresh();
|
|
1649
1819
|
let result;
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
else {
|
|
1654
|
-
const execOptions = this.BuildSaveExecuteOptions(entity, sqlDetails);
|
|
1655
|
-
const rawResult = await this.ExecuteSQL(sqlDetails.fullSQL, sqlDetails.parameters ?? undefined, execOptions, user);
|
|
1656
|
-
result = await this.PostProcessRows(rawResult, entity.EntityInfo, user);
|
|
1657
|
-
}
|
|
1658
|
-
resumeRefresh();
|
|
1659
|
-
entityResult.EndedAt = new Date();
|
|
1660
|
-
if (result && result.length > 0) {
|
|
1661
|
-
this.OnAfterSaveExecute(entity, user, options, saveContext);
|
|
1662
|
-
entityResult.Success = true;
|
|
1663
|
-
// In 'datestamps_only' mode, the SP/query returned only the PK and
|
|
1664
|
-
// __mj_CreatedAt/__mj_UpdatedAt columns. Merge those returned values
|
|
1665
|
-
// onto the pre-save snapshot so caller-set fields remain intact without
|
|
1666
|
-
// falsely marking concurrent in-flight mutations as clean.
|
|
1667
|
-
let mergedRow;
|
|
1668
|
-
if (preSaveValues) {
|
|
1669
|
-
mergedRow = { ...preSaveValues, ...result[0] };
|
|
1670
|
-
Object.defineProperty(mergedRow, '__mj_preMerged', {
|
|
1671
|
-
value: true,
|
|
1672
|
-
enumerable: false,
|
|
1673
|
-
configurable: true,
|
|
1674
|
-
});
|
|
1820
|
+
try {
|
|
1821
|
+
if (bReplay) {
|
|
1822
|
+
result = [entity.GetAll()];
|
|
1675
1823
|
}
|
|
1676
1824
|
else {
|
|
1677
|
-
|
|
1825
|
+
const execOptions = this.BuildSaveExecuteOptions(entity, sqlDetails);
|
|
1826
|
+
const rawResult = await this.ExecuteSQL(sqlDetails.fullSQL, sqlDetails.parameters ?? undefined, execOptions, user);
|
|
1827
|
+
result = await this.PostProcessRows(rawResult, entity.EntityInfo, user);
|
|
1678
1828
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1829
|
+
resumeRefresh();
|
|
1830
|
+
entityResult.EndedAt = new Date();
|
|
1831
|
+
if (result && result.length > 0) {
|
|
1832
|
+
if (bNewRecord) {
|
|
1833
|
+
let hydratedPk = false;
|
|
1834
|
+
for (const pkField of entity.EntityInfo.PrimaryKeys) {
|
|
1835
|
+
const fieldObj = entity.Fields.find(f => f.Name === pkField.Name);
|
|
1836
|
+
if (fieldObj && (fieldObj.Value === null || fieldObj.Value === undefined) && result[0][pkField.Name] != null) {
|
|
1837
|
+
fieldObj.Value = result[0][pkField.Name];
|
|
1838
|
+
hydratedPk = true;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (hydratedPk) {
|
|
1842
|
+
entity.PrimaryKey.LoadFromEntityFields(entity.PrimaryKeys);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if (enlistInTx) {
|
|
1846
|
+
await this.OnDurableAfterSaveExecute(entity, user, options, saveContext);
|
|
1847
|
+
}
|
|
1848
|
+
if (saveTxScope) {
|
|
1849
|
+
await saveTxScope.Commit();
|
|
1850
|
+
saveTxCommitted = true;
|
|
1851
|
+
}
|
|
1685
1852
|
}
|
|
1686
|
-
|
|
1853
|
+
else {
|
|
1854
|
+
if (bNewRecord)
|
|
1855
|
+
throw new Error(`SQL Error: Error creating new record, no rows returned from SQL: ${sqlDetails.literalSQL ?? sqlDetails.fullSQL}`);
|
|
1856
|
+
else
|
|
1857
|
+
throw new Error(`SQL Error: Error updating record, no MATCHING rows found within the database: ${sqlDetails.literalSQL ?? sqlDetails.fullSQL}`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
catch (innerErr) {
|
|
1861
|
+
if (saveTxScope && !saveTxCommitted) {
|
|
1862
|
+
await saveTxScope.Rollback();
|
|
1863
|
+
}
|
|
1864
|
+
throw innerErr;
|
|
1865
|
+
}
|
|
1866
|
+
this.OnAfterSaveExecute(entity, user, options, saveContext);
|
|
1867
|
+
entityResult.Success = true;
|
|
1868
|
+
// In 'datestamps_only' mode, the SP/query returned only the PK and
|
|
1869
|
+
// __mj_CreatedAt/__mj_UpdatedAt columns. Merge those returned values
|
|
1870
|
+
// onto the pre-save snapshot so caller-set fields remain intact without
|
|
1871
|
+
// falsely marking concurrent in-flight mutations as clean.
|
|
1872
|
+
let mergedRow;
|
|
1873
|
+
if (preSaveValues) {
|
|
1874
|
+
mergedRow = { ...preSaveValues, ...result[0] };
|
|
1875
|
+
Object.defineProperty(mergedRow, '__mj_preMerged', {
|
|
1876
|
+
value: true,
|
|
1877
|
+
enumerable: false,
|
|
1878
|
+
configurable: true,
|
|
1879
|
+
});
|
|
1687
1880
|
}
|
|
1688
1881
|
else {
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1882
|
+
mergedRow = result[0];
|
|
1883
|
+
}
|
|
1884
|
+
// OnSaveCompleted may return patch fields (e.g., updated lat/lng
|
|
1885
|
+
// from geocoding) that need to be merged onto the SP result before
|
|
1886
|
+
// the entity loads the data via finalizeSave().
|
|
1887
|
+
const patches = await this.OnSaveCompleted(entity, sqlDetails, user, options, saveContext);
|
|
1888
|
+
if (patches) {
|
|
1889
|
+
Object.assign(mergedRow, patches);
|
|
1693
1890
|
}
|
|
1891
|
+
return mergedRow;
|
|
1694
1892
|
}
|
|
1695
1893
|
}
|
|
1696
1894
|
else {
|
|
@@ -1704,6 +1902,11 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
1704
1902
|
LogError(e);
|
|
1705
1903
|
throw e;
|
|
1706
1904
|
}
|
|
1905
|
+
finally {
|
|
1906
|
+
if (transactionPreprocessingRegistered && !transactionReadyRaised) {
|
|
1907
|
+
entity.RaiseReadyForTransaction();
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1707
1910
|
}
|
|
1708
1911
|
/**
|
|
1709
1912
|
* Deletes an entity record — the full orchestration flow shared by all DB providers.
|
|
@@ -1717,8 +1920,11 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
1717
1920
|
*/
|
|
1718
1921
|
async Delete(entity, options, user) {
|
|
1719
1922
|
const entityResult = new BaseEntityResult();
|
|
1923
|
+
let transactionPreprocessingRegistered = false;
|
|
1924
|
+
let transactionReadyRaised = false;
|
|
1720
1925
|
try {
|
|
1721
1926
|
entity.RegisterTransactionPreprocessing();
|
|
1927
|
+
transactionPreprocessingRegistered = Boolean(entity.TransactionGroup);
|
|
1722
1928
|
// External-data-source entities are read-only (see Save) — refuse deletes at the provider
|
|
1723
1929
|
// layer regardless of the generated base class. The normal path (ReadOnlyExternalBaseEntity)
|
|
1724
1930
|
// returns false before reaching here; this backstop intentionally throws only for the
|
|
@@ -1758,6 +1964,7 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
1758
1964
|
if (entity.TransactionGroup && !bReplay) {
|
|
1759
1965
|
// ---- Transaction Group path ----
|
|
1760
1966
|
entity.RaiseReadyForTransaction();
|
|
1967
|
+
transactionReadyRaised = true;
|
|
1761
1968
|
const extraData = this.GetTransactionExtraData(entity);
|
|
1762
1969
|
if (entity.EntityInfo.TrackRecordChanges) {
|
|
1763
1970
|
Object.defineProperty(extraData, 'simpleSQLFallback', {
|
|
@@ -1774,7 +1981,8 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
1774
1981
|
// (PostgreSQL accepts its legacy `_result_id` shape there).
|
|
1775
1982
|
if (!this.ValidateDeleteResult(entity, [transactionResult], entityResult)) {
|
|
1776
1983
|
entityResult.Success = false;
|
|
1777
|
-
|
|
1984
|
+
throw new Error(entityResult.Message ||
|
|
1985
|
+
`Failed to delete ${entity.EntityInfo.Name}: record not found or primary key mismatch`);
|
|
1778
1986
|
}
|
|
1779
1987
|
this.OnAfterDeleteExecute(entity, user, options);
|
|
1780
1988
|
entityResult.NewValues = this.MapTransactionResultToNewValues(transactionResult);
|
|
@@ -1782,7 +1990,10 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
1782
1990
|
}
|
|
1783
1991
|
else {
|
|
1784
1992
|
entityResult.Success = false;
|
|
1785
|
-
entityResult.Message
|
|
1993
|
+
if (!entityResult.Message) {
|
|
1994
|
+
entityResult.Message =
|
|
1995
|
+
transactionResult instanceof Error ? transactionResult.message : 'Transaction failed to commit';
|
|
1996
|
+
}
|
|
1786
1997
|
}
|
|
1787
1998
|
}));
|
|
1788
1999
|
return true;
|
|
@@ -1821,6 +2032,11 @@ export class DatabaseProviderBase extends ProviderBase {
|
|
|
1821
2032
|
entityResult.EndedAt = new Date();
|
|
1822
2033
|
return false;
|
|
1823
2034
|
}
|
|
2035
|
+
finally {
|
|
2036
|
+
if (transactionPreprocessingRegistered && !transactionReadyRaised) {
|
|
2037
|
+
entity.RaiseReadyForTransaction();
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
1824
2040
|
}
|
|
1825
2041
|
/**************************************************************************/
|
|
1826
2042
|
// END ---- Save/Delete Orchestration
|