@memberjunction/core 5.51.0 → 6.1.0-edge.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +7 -0
  2. package/dist/generic/baseEngine.d.ts.map +1 -1
  3. package/dist/generic/baseEngine.js +13 -2
  4. package/dist/generic/baseEngine.js.map +1 -1
  5. package/dist/generic/baseEngineRegistry.d.ts +14 -0
  6. package/dist/generic/baseEngineRegistry.d.ts.map +1 -1
  7. package/dist/generic/baseEngineRegistry.js +32 -0
  8. package/dist/generic/baseEngineRegistry.js.map +1 -1
  9. package/dist/generic/baseEntity.d.ts +329 -26
  10. package/dist/generic/baseEntity.d.ts.map +1 -1
  11. package/dist/generic/baseEntity.js +788 -79
  12. package/dist/generic/baseEntity.js.map +1 -1
  13. package/dist/generic/databaseProviderBase.d.ts +54 -17
  14. package/dist/generic/databaseProviderBase.d.ts.map +1 -1
  15. package/dist/generic/databaseProviderBase.js +133 -52
  16. package/dist/generic/databaseProviderBase.js.map +1 -1
  17. package/dist/generic/entityCompanion.d.ts +218 -0
  18. package/dist/generic/entityCompanion.d.ts.map +1 -0
  19. package/dist/generic/entityCompanion.js +170 -0
  20. package/dist/generic/entityCompanion.js.map +1 -0
  21. package/dist/generic/entityInfo.d.ts +146 -0
  22. package/dist/generic/entityInfo.d.ts.map +1 -1
  23. package/dist/generic/entityInfo.js +188 -0
  24. package/dist/generic/entityInfo.js.map +1 -1
  25. package/dist/generic/entitySavePlan.d.ts +199 -0
  26. package/dist/generic/entitySavePlan.d.ts.map +1 -0
  27. package/dist/generic/entitySavePlan.js +213 -0
  28. package/dist/generic/entitySavePlan.js.map +1 -0
  29. package/dist/generic/entityTransactionScope.d.ts +125 -0
  30. package/dist/generic/entityTransactionScope.d.ts.map +1 -0
  31. package/dist/generic/entityTransactionScope.js +115 -0
  32. package/dist/generic/entityTransactionScope.js.map +1 -0
  33. package/dist/generic/interfaces.d.ts +93 -35
  34. package/dist/generic/interfaces.d.ts.map +1 -1
  35. package/dist/generic/interfaces.js +27 -0
  36. package/dist/generic/interfaces.js.map +1 -1
  37. package/dist/generic/providerBase.d.ts +13 -0
  38. package/dist/generic/providerBase.d.ts.map +1 -1
  39. package/dist/generic/providerBase.js +64 -5
  40. package/dist/generic/providerBase.js.map +1 -1
  41. package/dist/generic/relatedRecordBatchLoader.d.ts +39 -0
  42. package/dist/generic/relatedRecordBatchLoader.d.ts.map +1 -0
  43. package/dist/generic/relatedRecordBatchLoader.js +154 -0
  44. package/dist/generic/relatedRecordBatchLoader.js.map +1 -0
  45. package/dist/generic/relatedRecordCollection.d.ts +578 -0
  46. package/dist/generic/relatedRecordCollection.d.ts.map +1 -0
  47. package/dist/generic/relatedRecordCollection.js +1004 -0
  48. package/dist/generic/relatedRecordCollection.js.map +1 -0
  49. package/dist/generic/saveEntityGraphOperation.d.ts +148 -0
  50. package/dist/generic/saveEntityGraphOperation.d.ts.map +1 -0
  51. package/dist/generic/saveEntityGraphOperation.js +157 -0
  52. package/dist/generic/saveEntityGraphOperation.js.map +1 -0
  53. package/dist/generic/securityInfo.d.ts +99 -1
  54. package/dist/generic/securityInfo.d.ts.map +1 -1
  55. package/dist/generic/securityInfo.js +88 -6
  56. package/dist/generic/securityInfo.js.map +1 -1
  57. package/dist/generic/telemetryManager.d.ts +21 -1
  58. package/dist/generic/telemetryManager.d.ts.map +1 -1
  59. package/dist/generic/telemetryManager.js +21 -6
  60. package/dist/generic/telemetryManager.js.map +1 -1
  61. package/dist/index.d.ts +6 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +6 -3
  64. package/dist/index.js.map +1 -1
  65. package/dist/views/runView.d.ts +31 -0
  66. package/dist/views/runView.d.ts.map +1 -1
  67. package/dist/views/runView.js.map +1 -1
  68. package/package.json +13 -13
  69. package/readme.md +159 -1
  70. package/dist/generic/runReport.d.ts +0 -25
  71. package/dist/generic/runReport.d.ts.map +0 -1
  72. package/dist/generic/runReport.js +0 -38
  73. package/dist/generic/runReport.js.map +0 -1
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @fileoverview `EntitySavePlan` — the ordered unit of work produced by an entity and its
3
+ * companions, and the local executor that runs it inside a single provider transaction.
4
+ *
5
+ * ## Why a plan rather than direct recursion
6
+ *
7
+ * Making the work explicit before executing any of it buys three things that matter:
8
+ *
9
+ * 1. **Validation can see the whole graph.** Cross-child invariants ("debits must equal credits")
10
+ * have to be checked against the complete set — including pending removals — *before* the first
11
+ * row is written. A plan is the natural place to assert that.
12
+ * 2. **One transaction decision.** The plan is built, then executed. The executor opens exactly one
13
+ * scope for the whole graph instead of each level guessing whether it should start one.
14
+ * 3. **The remote path becomes trivial.** A plan is data. The client serializes the graph, the
15
+ * server rebuilds it and runs the *same* executor. There is one cascade implementation, placed
16
+ * in one of two locations — never two implementations to keep in sync.
17
+ *
18
+ * ## Platform guarantees are preserved by construction
19
+ *
20
+ * Every node is persisted by calling that record's own `BaseEntity.Save()` / `.Delete()`. Nothing
21
+ * is written with direct SQL. Record Changes, entity actions, field validation, subclass `Save`
22
+ * overrides, `PreSave` data hooks, `save_started` / `save` / `delete` events and cache invalidation
23
+ * therefore all fire per node exactly as they do for a standalone save — with no graph-specific
24
+ * plumbing and no risk of the graph path quietly skipping a guarantee the single-record path has.
25
+ *
26
+ * @module @memberjunction/core
27
+ */
28
+ import type { BaseEntity } from './baseEntity.js';
29
+ import type { EntitySaveOptions, EntityDeleteOptions } from './interfaces.js';
30
+ /**
31
+ * What a plan node does to its record.
32
+ */
33
+ export type EntitySavePlanOperation = 'Save' | 'Delete';
34
+ /**
35
+ * A single unit of work within an {@link EntitySavePlan}.
36
+ */
37
+ export type EntitySavePlanNode = {
38
+ /** The record to operate on. */
39
+ Entity: BaseEntity;
40
+ /** Whether this node saves or deletes its record. */
41
+ Operation: EntitySavePlanOperation;
42
+ /**
43
+ * Human-readable origin of this node, used in error messages so a failure names the collection
44
+ * it came from rather than just an entity name (e.g. `Lines[3]`).
45
+ */
46
+ Label: string;
47
+ /**
48
+ * Applied to the record immediately before the node executes.
49
+ *
50
+ * This is how a child collection stamps the parent's freshly-assigned primary key onto each
51
+ * child's foreign key: the parent node runs first, so by the time this callback fires the key
52
+ * exists. Deferring it to execution time — rather than setting it when the plan is built — is
53
+ * what makes creating a parent and its children in one call work.
54
+ */
55
+ Prepare?: () => void;
56
+ /**
57
+ * When true, this node executes the record's **own** save/delete only, without letting that
58
+ * record build and run a graph of its own.
59
+ *
60
+ * Set on the root node — the record whose `Save()` produced this plan. Without it the root
61
+ * would re-enter graph planning from inside its own graph execution, recursing forever, and
62
+ * would deadlock on its own in-flight save debounce.
63
+ *
64
+ * Deliberately **not** set on child nodes: a child that declares companions of its own must
65
+ * build and run its own sub-graph, which is what makes nesting (payment → line → allocation)
66
+ * work.
67
+ */
68
+ SelfOnly?: boolean;
69
+ };
70
+ /**
71
+ * The outcome of executing one plan node.
72
+ */
73
+ export type EntitySavePlanNodeResult = {
74
+ /** The node that ran. */
75
+ Node: EntitySavePlanNode;
76
+ /** Whether the record's own Save()/Delete() reported success. */
77
+ Success: boolean;
78
+ /** The failure detail, when `Success` is false. */
79
+ ErrorMessage?: string;
80
+ };
81
+ /**
82
+ * The outcome of executing a whole plan.
83
+ */
84
+ export type EntitySavePlanResult = {
85
+ /** True only when every node succeeded and the transaction committed. */
86
+ Success: boolean;
87
+ /** Per-node outcomes, in execution order. */
88
+ NodeResults: EntitySavePlanNodeResult[];
89
+ /** The first failure's message, hoisted for convenience. */
90
+ ErrorMessage?: string;
91
+ };
92
+ /**
93
+ * An ordered set of record operations that must succeed or fail together.
94
+ *
95
+ * Built by `BaseEntity.BuildSavePlan()` / `BuildDeletePlan()`, which seed the root node and then let
96
+ * each companion contribute via `EntityCompanion.ContributeSaveWork()` /
97
+ * `ContributeDeleteWork()`.
98
+ *
99
+ * @remarks
100
+ * Ordering is explicit and positional — nodes execute in the order they were added. Companions that
101
+ * need their work to run before the parent (deletions of children, for instance) add it before the
102
+ * parent node exists, which is why `ContributeDeleteWork` is called first on the delete path and
103
+ * `ContributeSaveWork` last on the save path.
104
+ */
105
+ export declare class EntitySavePlan {
106
+ private nodes;
107
+ /**
108
+ * The record the plan is rooted at — the entity whose `Save()` / `Delete()` was called.
109
+ */
110
+ readonly Root: BaseEntity;
111
+ /**
112
+ * @param root - The record this plan is rooted at.
113
+ */
114
+ constructor(root: BaseEntity);
115
+ /**
116
+ * The nodes in execution order.
117
+ */
118
+ get Nodes(): readonly EntitySavePlanNode[];
119
+ /**
120
+ * How many operations this plan will perform.
121
+ *
122
+ * A count of 1 means the plan is just the root record, and `BaseEntity` takes its ordinary
123
+ * single-record path — no plan execution, no transaction scope, byte-for-byte the behavior
124
+ * that existed before companions. This is what keeps the overwhelmingly common case free of
125
+ * any new cost or risk.
126
+ */
127
+ get NodeCount(): number;
128
+ /**
129
+ * Appends a node to the plan.
130
+ *
131
+ * @param node - The unit of work to append.
132
+ * @returns This plan, for chaining.
133
+ */
134
+ Add(node: EntitySavePlanNode): EntitySavePlan;
135
+ /**
136
+ * Convenience wrapper over {@link Add} for a save node.
137
+ *
138
+ * @param entity - The record to save.
139
+ * @param label - Origin label used in error messages.
140
+ * @param prepare - Optional callback applied immediately before the node executes.
141
+ * @returns This plan, for chaining.
142
+ */
143
+ AddSave(entity: BaseEntity, label: string, prepare?: () => void, selfOnly?: boolean): EntitySavePlan;
144
+ /**
145
+ * Convenience wrapper over {@link Add} for a delete node.
146
+ *
147
+ * @param entity - The record to delete.
148
+ * @param label - Origin label used in error messages.
149
+ * @returns This plan, for chaining.
150
+ */
151
+ AddDelete(entity: BaseEntity, label: string): EntitySavePlan;
152
+ }
153
+ /**
154
+ * Per-node option sets used when executing a plan.
155
+ *
156
+ * The root node needs its own variants carrying the `IsGraphNodeSave` / `IsGraphNodeDelete` flag,
157
+ * which prevents it from re-entering graph planning and bypasses its in-flight save debounce.
158
+ * `BaseEntity` constructs all four, because it already holds the option classes as values —
159
+ * building them here would force a runtime import of `interfaces.ts` and close an import cycle for
160
+ * no benefit.
161
+ */
162
+ export type EntitySavePlanExecuteOptions = {
163
+ /** Options for non-root save nodes. */
164
+ SaveOptions?: EntitySaveOptions;
165
+ /** Options for the root save node — must carry `IsGraphNodeSave: true`. */
166
+ RootSaveOptions?: EntitySaveOptions;
167
+ /** Options for non-root delete nodes. */
168
+ DeleteOptions?: EntityDeleteOptions;
169
+ /** Options for the root delete node — must carry `IsGraphNodeDelete: true`. */
170
+ RootDeleteOptions?: EntityDeleteOptions;
171
+ /**
172
+ * Keys of the records already being persisted higher up in this unit of work — the cycle guard.
173
+ *
174
+ * A child node runs the child's own `Save()`, which builds and executes the child's own plan.
175
+ * That is what makes nesting work (a payment's line's allocations all land in one transaction),
176
+ * but on a **self-referential** collection it is also what makes a cycle fatal: declare
177
+ * `SubAgents` on `MJ: AI Agents` via `ParentID`, then wire `a.SubAgents.Add(b)` and
178
+ * `b.SubAgents.Add(a)`, and the recursion only ends when the call stack does.
179
+ *
180
+ * The set is threaded through the options rather than held in a module-scoped variable
181
+ * deliberately. A process-global would be shared by every concurrent save in the process, so
182
+ * two unrelated requests saving the *same* record at the same time would report a cycle that
183
+ * does not exist. Carried on the options, its lifetime is exactly one unit of work.
184
+ */
185
+ Visited?: Set<string>;
186
+ };
187
+ /**
188
+ * Executes a plan's nodes in order, stopping at the first failure.
189
+ *
190
+ * This function does **not** manage the transaction — the caller owns that, because the caller is
191
+ * the only one that knows whether the graph is the outermost unit of work or nested inside a larger
192
+ * one. See `BaseEntity.executeGraphLocal`.
193
+ *
194
+ * @param plan - The plan to execute.
195
+ * @param options - Per-node option sets.
196
+ * @returns The per-node outcomes and an overall success flag.
197
+ */
198
+ export declare function ExecuteEntitySavePlan(plan: EntitySavePlan, options?: EntitySavePlanExecuteOptions): Promise<EntitySavePlanResult>;
199
+ //# sourceMappingURL=entitySavePlan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entitySavePlan.d.ts","sourceRoot":"","sources":["../../src/generic/entitySavePlan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAG3E;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExD;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC7B,gCAAgC;IAChC,MAAM,EAAE,UAAU,CAAC;IACnB,qDAAqD;IACrD,SAAS,EAAE,uBAAuB,CAAC;IACnC;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACnC,yBAAyB;IACzB,IAAI,EAAE,kBAAkB,CAAC;IACzB,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC;IACjB,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG;IAC/B,yEAAyE;IACzE,OAAO,EAAE,OAAO,CAAC;IACjB,6CAA6C;IAC7C,WAAW,EAAE,wBAAwB,EAAE,CAAC;IACxC,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAc;IACvB,OAAO,CAAC,KAAK,CAA4B;IAEzC;;OAEG;IACH,SAAgB,IAAI,EAAE,UAAU,CAAC;IAEjC;;OAEG;gBACS,IAAI,EAAE,UAAU;IAI5B;;OAEG;IACH,IAAW,KAAK,IAAI,SAAS,kBAAkB,EAAE,CAEhD;IAED;;;;;;;OAOG;IACH,IAAW,SAAS,IAAI,MAAM,CAE7B;IAED;;;;;OAKG;IACI,GAAG,CAAC,IAAI,EAAE,kBAAkB,GAAG,cAAc;IAKpD;;;;;;;OAOG;IACI,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,IAAI,EAAE,QAAQ,UAAQ,GAAG,cAAc;IAIzG;;;;;;OAMG;IACI,SAAS,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc;CAGtE;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACvC,uCAAuC;IACvC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,2EAA2E;IAC3E,eAAe,CAAC,EAAE,iBAAiB,CAAC;IACpC,yCAAyC;IACzC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACzB,CAAC;AAkBF;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CACvC,IAAI,EAAE,cAAc,EACpB,OAAO,GAAE,4BAAiC,GAC3C,OAAO,CAAC,oBAAoB,CAAC,CAsD/B"}
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @fileoverview `EntitySavePlan` — the ordered unit of work produced by an entity and its
3
+ * companions, and the local executor that runs it inside a single provider transaction.
4
+ *
5
+ * ## Why a plan rather than direct recursion
6
+ *
7
+ * Making the work explicit before executing any of it buys three things that matter:
8
+ *
9
+ * 1. **Validation can see the whole graph.** Cross-child invariants ("debits must equal credits")
10
+ * have to be checked against the complete set — including pending removals — *before* the first
11
+ * row is written. A plan is the natural place to assert that.
12
+ * 2. **One transaction decision.** The plan is built, then executed. The executor opens exactly one
13
+ * scope for the whole graph instead of each level guessing whether it should start one.
14
+ * 3. **The remote path becomes trivial.** A plan is data. The client serializes the graph, the
15
+ * server rebuilds it and runs the *same* executor. There is one cascade implementation, placed
16
+ * in one of two locations — never two implementations to keep in sync.
17
+ *
18
+ * ## Platform guarantees are preserved by construction
19
+ *
20
+ * Every node is persisted by calling that record's own `BaseEntity.Save()` / `.Delete()`. Nothing
21
+ * is written with direct SQL. Record Changes, entity actions, field validation, subclass `Save`
22
+ * overrides, `PreSave` data hooks, `save_started` / `save` / `delete` events and cache invalidation
23
+ * therefore all fire per node exactly as they do for a standalone save — with no graph-specific
24
+ * plumbing and no risk of the graph path quietly skipping a guarantee the single-record path has.
25
+ *
26
+ * @module @memberjunction/core
27
+ */
28
+ import { LogError } from './logging.js';
29
+ /**
30
+ * An ordered set of record operations that must succeed or fail together.
31
+ *
32
+ * Built by `BaseEntity.BuildSavePlan()` / `BuildDeletePlan()`, which seed the root node and then let
33
+ * each companion contribute via `EntityCompanion.ContributeSaveWork()` /
34
+ * `ContributeDeleteWork()`.
35
+ *
36
+ * @remarks
37
+ * Ordering is explicit and positional — nodes execute in the order they were added. Companions that
38
+ * need their work to run before the parent (deletions of children, for instance) add it before the
39
+ * parent node exists, which is why `ContributeDeleteWork` is called first on the delete path and
40
+ * `ContributeSaveWork` last on the save path.
41
+ */
42
+ export class EntitySavePlan {
43
+ /**
44
+ * @param root - The record this plan is rooted at.
45
+ */
46
+ constructor(root) {
47
+ this.nodes = [];
48
+ this.Root = root;
49
+ }
50
+ /**
51
+ * The nodes in execution order.
52
+ */
53
+ get Nodes() {
54
+ return this.nodes;
55
+ }
56
+ /**
57
+ * How many operations this plan will perform.
58
+ *
59
+ * A count of 1 means the plan is just the root record, and `BaseEntity` takes its ordinary
60
+ * single-record path — no plan execution, no transaction scope, byte-for-byte the behavior
61
+ * that existed before companions. This is what keeps the overwhelmingly common case free of
62
+ * any new cost or risk.
63
+ */
64
+ get NodeCount() {
65
+ return this.nodes.length;
66
+ }
67
+ /**
68
+ * Appends a node to the plan.
69
+ *
70
+ * @param node - The unit of work to append.
71
+ * @returns This plan, for chaining.
72
+ */
73
+ Add(node) {
74
+ this.nodes.push(node);
75
+ return this;
76
+ }
77
+ /**
78
+ * Convenience wrapper over {@link Add} for a save node.
79
+ *
80
+ * @param entity - The record to save.
81
+ * @param label - Origin label used in error messages.
82
+ * @param prepare - Optional callback applied immediately before the node executes.
83
+ * @returns This plan, for chaining.
84
+ */
85
+ AddSave(entity, label, prepare, selfOnly = false) {
86
+ return this.Add({ Entity: entity, Operation: 'Save', Label: label, Prepare: prepare, SelfOnly: selfOnly });
87
+ }
88
+ /**
89
+ * Convenience wrapper over {@link Add} for a delete node.
90
+ *
91
+ * @param entity - The record to delete.
92
+ * @param label - Origin label used in error messages.
93
+ * @returns This plan, for chaining.
94
+ */
95
+ AddDelete(entity, label) {
96
+ return this.Add({ Entity: entity, Operation: 'Delete', Label: label });
97
+ }
98
+ }
99
+ /**
100
+ * Stable identity for cycle detection: the entity plus its primary key.
101
+ *
102
+ * Object identity is not enough — the same row can be represented by two different `BaseEntity`
103
+ * instances within one graph, which is precisely the shape a cycle takes after a round trip.
104
+ *
105
+ * @param entity - The record to key.
106
+ * @returns The key, or `null` for a record with no primary-key value yet (a brand-new record cannot
107
+ * be its own ancestor, so it needs no guard).
108
+ */
109
+ function GraphNodeKey(entity) {
110
+ const entityName = entity.EntityInfo?.Name;
111
+ const key = entity.PrimaryKey?.ToString();
112
+ return entityName && key ? `${entityName}|${key}` : null;
113
+ }
114
+ /**
115
+ * Executes a plan's nodes in order, stopping at the first failure.
116
+ *
117
+ * This function does **not** manage the transaction — the caller owns that, because the caller is
118
+ * the only one that knows whether the graph is the outermost unit of work or nested inside a larger
119
+ * one. See `BaseEntity.executeGraphLocal`.
120
+ *
121
+ * @param plan - The plan to execute.
122
+ * @param options - Per-node option sets.
123
+ * @returns The per-node outcomes and an overall success flag.
124
+ */
125
+ export async function ExecuteEntitySavePlan(plan, options = {}) {
126
+ const nodeResults = [];
127
+ const visited = options.Visited ?? new Set();
128
+ // The root is an ancestor of everything this plan will run, so it goes in before the loop.
129
+ // Its own node is `SelfOnly` and therefore exempt from the check below — it cannot recurse.
130
+ const rootKey = GraphNodeKey(plan.Root);
131
+ const rootWasAlreadyVisited = rootKey !== null && visited.has(rootKey);
132
+ if (rootKey && !rootWasAlreadyVisited) {
133
+ visited.add(rootKey);
134
+ }
135
+ try {
136
+ for (const node of plan.Nodes) {
137
+ // A child node re-entering a record already in progress above it is a cycle. Detect it
138
+ // here rather than letting the recursion run until the stack overflows, which surfaces
139
+ // as an unattributable crash rather than a fixable message.
140
+ if (!node.SelfOnly) {
141
+ const key = GraphNodeKey(node.Entity);
142
+ if (key && visited.has(key)) {
143
+ const message = `Cycle detected in the entity graph at ${node.Label} (${key}): this record is already ` +
144
+ `being saved higher up in the same unit of work. A self-referential related-record ` +
145
+ `collection cannot contain one of its own ancestors.`;
146
+ LogError(message);
147
+ return { Success: false, NodeResults: nodeResults, ErrorMessage: message };
148
+ }
149
+ }
150
+ // Late-bind anything that depends on values produced by earlier nodes — most importantly
151
+ // a child's foreign key, which cannot exist until the parent row has been inserted.
152
+ if (node.Prepare) {
153
+ node.Prepare();
154
+ }
155
+ const outcome = await executePlanNode(node, { ...options, Visited: visited });
156
+ nodeResults.push(outcome);
157
+ if (!outcome.Success) {
158
+ // Stop immediately. The caller rolls the transaction back, so continuing would only
159
+ // pile up work that is about to be undone — and would let a later, more confusing
160
+ // failure mask the real one.
161
+ return { Success: false, NodeResults: nodeResults, ErrorMessage: outcome.ErrorMessage };
162
+ }
163
+ }
164
+ return { Success: true, NodeResults: nodeResults };
165
+ }
166
+ finally {
167
+ // Leave the set exactly as it was found, so sibling branches of the same graph are not
168
+ // poisoned by an ancestor this branch happened to add.
169
+ if (rootKey && !rootWasAlreadyVisited) {
170
+ visited.delete(rootKey);
171
+ }
172
+ }
173
+ }
174
+ /**
175
+ * Runs a single plan node and normalizes its outcome.
176
+ *
177
+ * `BaseEntity.Save()` / `.Delete()` signal logical failure by returning `false` rather than
178
+ * throwing, so both shapes have to be handled: a `false` return and a genuine exception.
179
+ *
180
+ * @param node - The node to run.
181
+ * @param saveOptions - Options for a save node.
182
+ * @param deleteOptions - Options for a delete node.
183
+ * @returns The node outcome.
184
+ */
185
+ async function executePlanNode(node, options) {
186
+ try {
187
+ // The root node runs "self only": it must not re-enter graph planning from inside the graph
188
+ // it is already executing, and it must bypass its own in-flight save debounce. Child nodes
189
+ // get the ordinary options, so a child with companions of its own runs its own sub-graph.
190
+ const ok = node.Operation === 'Save'
191
+ ? await node.Entity.Save(node.SelfOnly ? options.RootSaveOptions : options.SaveOptions)
192
+ : await node.Entity.Delete(node.SelfOnly ? options.RootDeleteOptions : options.DeleteOptions);
193
+ if (ok) {
194
+ return { Node: node, Success: true };
195
+ }
196
+ const detail = node.Entity.LatestResult?.CompleteMessage ?? 'unknown error';
197
+ return {
198
+ Node: node,
199
+ Success: false,
200
+ ErrorMessage: `${node.Operation} failed for ${node.Label} (${node.Entity.EntityInfo?.Name}): ${detail}`,
201
+ };
202
+ }
203
+ catch (e) {
204
+ const detail = e instanceof Error ? e.message : String(e);
205
+ LogError(`EntitySavePlan node threw for ${node.Label}: ${detail}`);
206
+ return {
207
+ Node: node,
208
+ Success: false,
209
+ ErrorMessage: `${node.Operation} threw for ${node.Label} (${node.Entity.EntityInfo?.Name}): ${detail}`,
210
+ };
211
+ }
212
+ }
213
+ //# sourceMappingURL=entitySavePlan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entitySavePlan.js","sourceRoot":"","sources":["../../src/generic/entitySavePlan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @fileoverview Provider-arbitrated transaction scopes for multi-record entity operations.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Before this abstraction MemberJunction had **two mutually-blind transaction mechanisms**:
7
+ *
8
+ * 1. `DatabaseProviderBase.BeginTransaction()` / `CommitTransaction()` / `RollbackTransaction()` —
9
+ * a properly re-entrant ambient manager (depth counter, `SAVE TRANSACTION` savepoints at depth
10
+ * ≥ 2, serialization against an in-flight outermost begin). Application composite-save code
11
+ * (order headers + lines, journal entries + lines, payments + allocations) calls this directly.
12
+ * 2. `BeginISATransaction()` / `CommitISATransaction()` / `RollbackISATransaction()` — an
13
+ * IS-A-specific trio that opened a brand-new physical transaction on the pool with **no depth
14
+ * awareness at all**.
15
+ *
16
+ * Because neither knew about the other, an entity that hit both paths — an IS-A entity saved
17
+ * inside an application transaction, or a composite whose child is an IS-A leaf — wrote into *two
18
+ * independent physical transactions on the same connection pool*. Rolling one back left the other
19
+ * committed. A torn write, with no error raised.
20
+ *
21
+ * ## The fix: one arbiter, zero coupling between participants
22
+ *
23
+ * Every participant now asks for the same thing and never asks who else is in a transaction:
24
+ *
25
+ * ```typescript
26
+ * const scope = await provider.BeginEntityTransaction();
27
+ * try {
28
+ * // ...work...
29
+ * await scope.Commit();
30
+ * } catch (e) {
31
+ * await scope.Rollback();
32
+ * throw e;
33
+ * }
34
+ * ```
35
+ *
36
+ * The **provider** decides whether that is a physical `BEGIN` or a join to a transaction already in
37
+ * flight. This deliberately *reduces* coupling: IS-A orchestration, composite graph saves and
38
+ * hand-written application code each remain ignorant of one another, and correctness no longer
39
+ * depends on that ignorance being harmless.
40
+ *
41
+ * ## Tier behaviour
42
+ *
43
+ * - **Server** (`DatabaseProviderBase` and subclasses): `SupportsEntityTransactions === true`;
44
+ * scopes map onto the depth-counted ambient transaction.
45
+ * - **Client** (`GraphQLDataProvider`): `SupportsEntityTransactions === false`; there is no local
46
+ * transaction to join. Callers that need atomicity across several records route the whole unit of
47
+ * work to the server instead — see `MJ.SaveEntityGraph` and
48
+ * `guides/TRANSACTIONS_AND_BATCHING_GUIDE.md`.
49
+ *
50
+ * ## Concurrency
51
+ *
52
+ * The underlying ambient transaction is a field on the **provider instance**, not a global. MJServer
53
+ * builds per-request providers (`createPerRequestProviders` in `packages/MJServer/src/context.ts`),
54
+ * so an ambient transaction is effectively request-scoped and concurrent requests cannot interleave
55
+ * within one. Long-lived single-provider processes (CLI tools, workers) should not run concurrent
56
+ * transactional work on one provider instance — that constraint predates this abstraction and is
57
+ * unchanged by it.
58
+ *
59
+ * @module @memberjunction/core
60
+ */
61
+ /**
62
+ * A handle to a unit of work that will either commit or roll back as a whole.
63
+ *
64
+ * Obtained from {@link IMetadataProvider.BeginEntityTransaction}. The scope is **settle-once**:
65
+ * the first call to {@link EntityTransactionScope.Commit} or
66
+ * {@link EntityTransactionScope.Rollback} settles it and every later call is a no-op. That makes
67
+ * the standard `try { work; Commit() } catch { Rollback() }` shape safe even when the work itself
68
+ * already rolled back on the way out.
69
+ *
70
+ * @remarks
71
+ * A scope does **not** necessarily correspond to a physical database transaction. When one is
72
+ * already in flight on the provider, the scope joins it — committing the inner scope releases a
73
+ * savepoint rather than committing the outer transaction. Inspect {@link IsNested} to tell the two
74
+ * apart; correctness never requires you to.
75
+ */
76
+ export interface EntityTransactionScope {
77
+ /**
78
+ * True when this scope joined a transaction that was already in flight on the provider, rather
79
+ * than starting a new physical one.
80
+ *
81
+ * Informational only — for logging and diagnostics. Commit/rollback behave correctly either
82
+ * way, and callers must not branch on this to decide whether to settle the scope.
83
+ */
84
+ readonly IsNested: boolean;
85
+ /**
86
+ * Settles the scope successfully. On the outermost scope this commits the physical transaction;
87
+ * on a nested scope it releases the savepoint. No-op if the scope is already settled.
88
+ */
89
+ Commit(): Promise<void>;
90
+ /**
91
+ * Settles the scope by undoing its work. On the outermost scope this rolls the physical
92
+ * transaction back; on a nested scope it rolls back to the savepoint. No-op if the scope is
93
+ * already settled.
94
+ */
95
+ Rollback(): Promise<void>;
96
+ }
97
+ /**
98
+ * Runs `work` inside a provider-arbitrated transaction scope, committing on success and rolling
99
+ * back on any thrown error.
100
+ *
101
+ * This is the preferred entry point — it makes the settle-once contract impossible to get wrong and
102
+ * keeps the `try`/`catch` boilerplate in one place.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const total = await RunInEntityTransaction(this.ProviderToUse, async () => {
107
+ * await header.Save();
108
+ * for (const line of lines) await line.Save();
109
+ * return lines.length;
110
+ * });
111
+ * ```
112
+ *
113
+ * @param provider - The provider to obtain the scope from. When it does not support entity
114
+ * transactions the work still runs, just without transactional guarantees —
115
+ * callers needing atomicity must check `SupportsEntityTransactions` first and
116
+ * route the unit of work to the server instead.
117
+ * @param work - The work to perform inside the scope.
118
+ * @returns Whatever `work` returns.
119
+ * @throws Re-throws whatever `work` throws, after rolling the scope back.
120
+ */
121
+ export declare function RunInEntityTransaction<T>(provider: {
122
+ SupportsEntityTransactions?: boolean;
123
+ BeginEntityTransaction?(): Promise<EntityTransactionScope>;
124
+ } | null | undefined, work: () => Promise<T>): Promise<T>;
125
+ //# sourceMappingURL=entityTransactionScope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entityTransactionScope.d.ts","sourceRoot":"","sources":["../../src/generic/entityTransactionScope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AAIH;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,sBAAsB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAE3B;;;OAGG;IACH,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAExB;;;;OAIG;IACH,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC1C,QAAQ,EAAE;IACN,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,sBAAsB,CAAC,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC9D,GAAG,IAAI,GAAG,SAAS,EACpB,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,CAAC,CAAC,CA6BZ"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @fileoverview Provider-arbitrated transaction scopes for multi-record entity operations.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Before this abstraction MemberJunction had **two mutually-blind transaction mechanisms**:
7
+ *
8
+ * 1. `DatabaseProviderBase.BeginTransaction()` / `CommitTransaction()` / `RollbackTransaction()` —
9
+ * a properly re-entrant ambient manager (depth counter, `SAVE TRANSACTION` savepoints at depth
10
+ * ≥ 2, serialization against an in-flight outermost begin). Application composite-save code
11
+ * (order headers + lines, journal entries + lines, payments + allocations) calls this directly.
12
+ * 2. `BeginISATransaction()` / `CommitISATransaction()` / `RollbackISATransaction()` — an
13
+ * IS-A-specific trio that opened a brand-new physical transaction on the pool with **no depth
14
+ * awareness at all**.
15
+ *
16
+ * Because neither knew about the other, an entity that hit both paths — an IS-A entity saved
17
+ * inside an application transaction, or a composite whose child is an IS-A leaf — wrote into *two
18
+ * independent physical transactions on the same connection pool*. Rolling one back left the other
19
+ * committed. A torn write, with no error raised.
20
+ *
21
+ * ## The fix: one arbiter, zero coupling between participants
22
+ *
23
+ * Every participant now asks for the same thing and never asks who else is in a transaction:
24
+ *
25
+ * ```typescript
26
+ * const scope = await provider.BeginEntityTransaction();
27
+ * try {
28
+ * // ...work...
29
+ * await scope.Commit();
30
+ * } catch (e) {
31
+ * await scope.Rollback();
32
+ * throw e;
33
+ * }
34
+ * ```
35
+ *
36
+ * The **provider** decides whether that is a physical `BEGIN` or a join to a transaction already in
37
+ * flight. This deliberately *reduces* coupling: IS-A orchestration, composite graph saves and
38
+ * hand-written application code each remain ignorant of one another, and correctness no longer
39
+ * depends on that ignorance being harmless.
40
+ *
41
+ * ## Tier behaviour
42
+ *
43
+ * - **Server** (`DatabaseProviderBase` and subclasses): `SupportsEntityTransactions === true`;
44
+ * scopes map onto the depth-counted ambient transaction.
45
+ * - **Client** (`GraphQLDataProvider`): `SupportsEntityTransactions === false`; there is no local
46
+ * transaction to join. Callers that need atomicity across several records route the whole unit of
47
+ * work to the server instead — see `MJ.SaveEntityGraph` and
48
+ * `guides/TRANSACTIONS_AND_BATCHING_GUIDE.md`.
49
+ *
50
+ * ## Concurrency
51
+ *
52
+ * The underlying ambient transaction is a field on the **provider instance**, not a global. MJServer
53
+ * builds per-request providers (`createPerRequestProviders` in `packages/MJServer/src/context.ts`),
54
+ * so an ambient transaction is effectively request-scoped and concurrent requests cannot interleave
55
+ * within one. Long-lived single-provider processes (CLI tools, workers) should not run concurrent
56
+ * transactional work on one provider instance — that constraint predates this abstraction and is
57
+ * unchanged by it.
58
+ *
59
+ * @module @memberjunction/core
60
+ */
61
+ import { LogError } from './logging.js';
62
+ /**
63
+ * Runs `work` inside a provider-arbitrated transaction scope, committing on success and rolling
64
+ * back on any thrown error.
65
+ *
66
+ * This is the preferred entry point — it makes the settle-once contract impossible to get wrong and
67
+ * keeps the `try`/`catch` boilerplate in one place.
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const total = await RunInEntityTransaction(this.ProviderToUse, async () => {
72
+ * await header.Save();
73
+ * for (const line of lines) await line.Save();
74
+ * return lines.length;
75
+ * });
76
+ * ```
77
+ *
78
+ * @param provider - The provider to obtain the scope from. When it does not support entity
79
+ * transactions the work still runs, just without transactional guarantees —
80
+ * callers needing atomicity must check `SupportsEntityTransactions` first and
81
+ * route the unit of work to the server instead.
82
+ * @param work - The work to perform inside the scope.
83
+ * @returns Whatever `work` returns.
84
+ * @throws Re-throws whatever `work` throws, after rolling the scope back.
85
+ */
86
+ export async function RunInEntityTransaction(provider, work) {
87
+ const canTransact = provider?.SupportsEntityTransactions === true && !!provider.BeginEntityTransaction;
88
+ if (!canTransact) {
89
+ // No local transaction available (client provider, or a provider that does not implement
90
+ // the capability). Run the work as-is — the caller is responsible for having decided that
91
+ // non-atomic execution is acceptable here, or for routing elsewhere.
92
+ return work();
93
+ }
94
+ const scope = await provider.BeginEntityTransaction();
95
+ try {
96
+ const result = await work();
97
+ await scope.Commit();
98
+ return result;
99
+ }
100
+ catch (e) {
101
+ // Rollback failures are logged and swallowed so the CALLER'S error survives: this is the
102
+ // failure path, and a doomed transaction (savepoint rollback refused, connection gone)
103
+ // throwing here would replace the error that explains what actually went wrong with a
104
+ // secondary one that explains less. Mirrors BaseEntity's own scope helper.
105
+ try {
106
+ await scope.Rollback();
107
+ }
108
+ catch (rollbackError) {
109
+ LogError(`RunInEntityTransaction: rollback failed after the work threw — reporting the original error. ` +
110
+ `Rollback failure: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
111
+ }
112
+ throw e;
113
+ }
114
+ }
115
+ //# sourceMappingURL=entityTransactionScope.js.map