@memberjunction/record-cloning 0.0.1-seed.1 → 6.2.0-edge.3
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/LICENSE +183 -0
- package/README.md +98 -3
- package/dist/CloneAuthorization.d.ts +50 -0
- package/dist/CloneAuthorization.d.ts.map +1 -0
- package/dist/CloneAuthorization.js +75 -0
- package/dist/CloneAuthorization.js.map +1 -0
- package/dist/CloneExecutor.d.ts +50 -0
- package/dist/CloneExecutor.d.ts.map +1 -0
- package/dist/CloneExecutor.js +350 -0
- package/dist/CloneExecutor.js.map +1 -0
- package/dist/CloneKeys.d.ts +65 -0
- package/dist/CloneKeys.d.ts.map +1 -0
- package/dist/CloneKeys.js +118 -0
- package/dist/CloneKeys.js.map +1 -0
- package/dist/CloneMaterializer.d.ts +52 -0
- package/dist/CloneMaterializer.d.ts.map +1 -0
- package/dist/CloneMaterializer.js +267 -0
- package/dist/CloneMaterializer.js.map +1 -0
- package/dist/ClonePlanHash.d.ts +11 -0
- package/dist/ClonePlanHash.d.ts.map +1 -0
- package/dist/ClonePlanHash.js +16 -0
- package/dist/ClonePlanHash.js.map +1 -0
- package/dist/ClonePlanner.d.ts +33 -0
- package/dist/ClonePlanner.d.ts.map +1 -0
- package/dist/ClonePlanner.js +870 -0
- package/dist/ClonePlanner.js.map +1 -0
- package/dist/CloneRecordProcessor.d.ts +28 -0
- package/dist/CloneRecordProcessor.d.ts.map +1 -0
- package/dist/CloneRecordProcessor.js +101 -0
- package/dist/CloneRecordProcessor.js.map +1 -0
- package/dist/RecordCloneEngine.d.ts +35 -0
- package/dist/RecordCloneEngine.d.ts.map +1 -0
- package/dist/RecordCloneEngine.js +60 -0
- package/dist/RecordCloneEngine.js.map +1 -0
- package/dist/RecordCloningStartup.d.ts +14 -0
- package/dist/RecordCloningStartup.d.ts.map +1 -0
- package/dist/RecordCloningStartup.js +32 -0
- package/dist/RecordCloningStartup.js.map +1 -0
- package/dist/__tests__/helpers/cloneAuthorizations.d.ts +7 -0
- package/dist/__tests__/helpers/cloneAuthorizations.d.ts.map +1 -0
- package/dist/__tests__/helpers/cloneAuthorizations.js +17 -0
- package/dist/__tests__/helpers/cloneAuthorizations.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/operations.d.ts +64 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +390 -0
- package/dist/operations.js.map +1 -0
- package/package.json +29 -5
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file CloneExecutor.ts
|
|
3
|
+
* Executes deterministic clone plans inside an atomic database transaction.
|
|
4
|
+
* Materializes graphs, verifies plan hash stability, enforces root.Save() arbitration,
|
|
5
|
+
* and writes provenance: Record Changes, a Clone Log with one item per planned row, ClonedFrom
|
|
6
|
+
* Record Links, and a Record Cloned audit entry. Refused and failed runs are logged too.
|
|
7
|
+
* @see plans/record-cloning/README.md §6, §8, §10, §13.1
|
|
8
|
+
*/
|
|
9
|
+
import { EntitySaveOptions, LogError, Metadata, RunInEntityTransaction, WellKnownUserSource, } from '@memberjunction/core';
|
|
10
|
+
import { GenerateUUID, MaskSensitiveFieldChange, MaskSensitivePlan, } from '@memberjunction/record-cloning-base';
|
|
11
|
+
import { CloneMaterializer } from './CloneMaterializer.js';
|
|
12
|
+
import { ComputeClonePlanHash } from './ClonePlanHash.js';
|
|
13
|
+
import { ToRecordKeyString } from './CloneKeys.js';
|
|
14
|
+
import { CloneAuthorizer } from './CloneAuthorization.js';
|
|
15
|
+
/** Entities the executor writes provenance to. */
|
|
16
|
+
const PROVENANCE_ENTITIES = ['MJ: Record Clone Logs', 'MJ: Record Clone Log Items', 'MJ: Record Links'];
|
|
17
|
+
function toCompositeKey(key, defaultFieldName = 'ID') {
|
|
18
|
+
if (!key) {
|
|
19
|
+
return { KeyValuePairs: [{ FieldName: defaultFieldName, Value: '' }] };
|
|
20
|
+
}
|
|
21
|
+
if (typeof key === 'object' && 'KeyValuePairs' in key && Array.isArray(key.KeyValuePairs)) {
|
|
22
|
+
return {
|
|
23
|
+
KeyValuePairs: key.KeyValuePairs.map((kvp) => ({
|
|
24
|
+
FieldName: kvp.FieldName,
|
|
25
|
+
Value: kvp.Value,
|
|
26
|
+
})),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// A record-id string: "F|V" or "F|V||F|V" names its columns; a bare value uses the default column.
|
|
30
|
+
const text = String(key);
|
|
31
|
+
if (!text.includes('|'))
|
|
32
|
+
return { KeyValuePairs: [{ FieldName: defaultFieldName, Value: text }] };
|
|
33
|
+
return {
|
|
34
|
+
KeyValuePairs: text.split('||').map((pair) => {
|
|
35
|
+
const at = pair.indexOf('|');
|
|
36
|
+
return { FieldName: pair.slice(0, at), Value: pair.slice(at + 1) };
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export class CloneExecutor {
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this._provider = options?.Provider;
|
|
43
|
+
}
|
|
44
|
+
get Provider() {
|
|
45
|
+
return this._provider ?? Metadata.Provider;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Executes a ClonePlan atomically.
|
|
49
|
+
*/
|
|
50
|
+
async Execute(plan, contextUser) {
|
|
51
|
+
const md = this.Provider;
|
|
52
|
+
// 1. Refuse blocked plan
|
|
53
|
+
if (plan.Blocked) {
|
|
54
|
+
const logId = await this.WriteRefusalLog(plan, contextUser, 'Blocked: ' + plan.Warnings.filter((w) => w.Severity === 'Error').map((w) => w.Message).join('; '));
|
|
55
|
+
return {
|
|
56
|
+
Success: false,
|
|
57
|
+
ResultCode: 'BLOCKED',
|
|
58
|
+
CloneLogID: logId,
|
|
59
|
+
RootRecordKey: plan.RootTargetKey,
|
|
60
|
+
RecordsCloned: 0,
|
|
61
|
+
Warnings: plan.Warnings,
|
|
62
|
+
ErrorMessage: `Clone plan is blocked due to validation or permission errors: ${plan.Warnings.map((w) => w.Message).join('; ')}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// 2. Verify Plan Hash integrity (§13.1 RC7)
|
|
66
|
+
const currentHash = ComputeClonePlanHash({
|
|
67
|
+
Nodes: plan.Nodes,
|
|
68
|
+
Edges: plan.Edges,
|
|
69
|
+
Excluded: plan.Excluded,
|
|
70
|
+
});
|
|
71
|
+
if (currentHash !== plan.Hash && currentHash !== plan.PlanHash) {
|
|
72
|
+
const logId = await this.WriteRefusalLog(plan, contextUser, 'PLAN_CHANGED: the plan changed between review and execution.');
|
|
73
|
+
return {
|
|
74
|
+
Success: false,
|
|
75
|
+
ResultCode: 'PLAN_CHANGED',
|
|
76
|
+
CloneLogID: logId,
|
|
77
|
+
RootRecordKey: plan.RootTargetKey,
|
|
78
|
+
RecordsCloned: 0,
|
|
79
|
+
Warnings: [
|
|
80
|
+
...plan.Warnings,
|
|
81
|
+
{
|
|
82
|
+
Code: 'PLAN_CHANGED',
|
|
83
|
+
Severity: 'Error',
|
|
84
|
+
Message: 'Plan hash mismatch: the plan or underlying source entities changed between review and execution.',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
ErrorMessage: 'PLAN_CHANGED',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// A clone is all or nothing (plan §6): refuse rather than run it without a transaction.
|
|
91
|
+
// Transactions are an IEntityDataProvider capability; the metadata provider is also the data provider.
|
|
92
|
+
const transProvider = md;
|
|
93
|
+
if (transProvider.SupportsEntityTransactions !== true || typeof transProvider.BeginEntityTransaction !== 'function') {
|
|
94
|
+
const message = 'This provider cannot run the clone in a transaction, so it was not started (a partial clone cannot be undone).';
|
|
95
|
+
const logId = await this.WriteRefusalLog(plan, contextUser, message);
|
|
96
|
+
return { Success: false, ResultCode: 'EXECUTION_ERROR', CloneLogID: logId, RootRecordKey: plan.RootTargetKey, RecordsCloned: 0, Warnings: plan.Warnings, ErrorMessage: message };
|
|
97
|
+
}
|
|
98
|
+
const cloneLogId = GenerateUUID();
|
|
99
|
+
const startedAt = new Date();
|
|
100
|
+
const saveOptions = CloneExecutor.saveOptionsFor(plan);
|
|
101
|
+
const materializer = new CloneMaterializer(md);
|
|
102
|
+
try {
|
|
103
|
+
return await RunInEntityTransaction(transProvider, async () => {
|
|
104
|
+
// 3. Materialize in-memory graph
|
|
105
|
+
const { RootEntity: rootEntity, StagedEntities: stagedEntities, SidecarEntities: sidecarEntities, PrerequisiteEntities: prerequisiteEntities, DeferredNodeKeys: deferredNodeKeys, } = await materializer.Materialize(plan, contextUser);
|
|
106
|
+
// 4. Attach CloneContext to all staged entities (§10)
|
|
107
|
+
for (const [nodeKey, entity] of stagedEntities.entries()) {
|
|
108
|
+
const planNode = plan.Nodes.find((n) => (n.NodeKey ?? n.Key) === nodeKey);
|
|
109
|
+
entity.SetCloneContext({
|
|
110
|
+
CloneLogID: cloneLogId,
|
|
111
|
+
SourceEntityName: planNode?.EntityName || entity.EntityInfo.Name,
|
|
112
|
+
SourceRecordID: ToRecordKeyString(planNode?.SourceKey),
|
|
113
|
+
RootEntityName: plan.RootEntityName || entity.EntityInfo.Name,
|
|
114
|
+
RootSourceRecordID: ToRecordKeyString(plan.RootSourceKey),
|
|
115
|
+
RootTargetRecordID: ToRecordKeyString(plan.RootTargetKey),
|
|
116
|
+
Depth: planNode?.Depth ?? 0,
|
|
117
|
+
// A row deferred behind another new row saves as a sidecar, whatever the plan said.
|
|
118
|
+
Route: deferredNodeKeys?.has(nodeKey) ? 'Sidecar' : planNode?.Route ?? 'RootSave',
|
|
119
|
+
FieldChangeSummary: [],
|
|
120
|
+
Reason: plan.Reason,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// 4b. Save prerequisites (ForwardFK targets the root needs) deepest first: in a chain
|
|
124
|
+
// root -> A -> B, B must exist before A can point at it. Stable for equal depths.
|
|
125
|
+
const depthOf = new Map();
|
|
126
|
+
for (const [nodeKey, entity] of stagedEntities.entries()) {
|
|
127
|
+
depthOf.set(entity, plan.Nodes.find((n) => (n.NodeKey ?? n.Key) === nodeKey)?.Depth ?? 0);
|
|
128
|
+
}
|
|
129
|
+
const prereqEntities = [...(prerequisiteEntities ?? [])].sort((a, b) => (depthOf.get(b) ?? 0) - (depthOf.get(a) ?? 0));
|
|
130
|
+
for (const prereq of prereqEntities) {
|
|
131
|
+
const prereqSaved = await prereq.Save(saveOptions);
|
|
132
|
+
if (!prereqSaved) {
|
|
133
|
+
const errorMsg = prereq.LatestResult?.CompleteMessage ||
|
|
134
|
+
`Prerequisite clone save failed for entity '${prereq.EntityInfo.Name}'.`;
|
|
135
|
+
throw new Error(errorMsg);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 5. Atomic Save via root.Save()
|
|
139
|
+
const rootSaved = await rootEntity.Save(saveOptions); // forwarded to every child in the save plan
|
|
140
|
+
if (!rootSaved) {
|
|
141
|
+
const errorMsg = rootEntity.LatestResult?.CompleteMessage ||
|
|
142
|
+
`Composite clone save failed for root entity '${plan.RootEntityName}'.`;
|
|
143
|
+
throw new Error(errorMsg);
|
|
144
|
+
}
|
|
145
|
+
// 6. Save Sidecars in dependency order. They are staged too, so step 4 already gave
|
|
146
|
+
// each its clone context (with its real source record).
|
|
147
|
+
for (const sidecar of sidecarEntities) {
|
|
148
|
+
const sidecarSaved = await sidecar.Save(saveOptions);
|
|
149
|
+
if (!sidecarSaved) {
|
|
150
|
+
const errorMsg = sidecar.LatestResult?.CompleteMessage ||
|
|
151
|
+
`Sidecar clone save failed for entity '${sidecar.EntityInfo.Name}'.`;
|
|
152
|
+
throw new Error(errorMsg);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 7. Write provenance (§10) in the same transaction: a clone with no log or lineage
|
|
156
|
+
// is not a successful clone, so a provenance failure rolls the clone back.
|
|
157
|
+
await this.writeSuccessProvenance(plan, cloneLogId, startedAt, stagedEntities, contextUser, rootEntity);
|
|
158
|
+
// Every created row (root, children, prerequisites, sidecars) is in stagedEntities once.
|
|
159
|
+
const clonedCount = stagedEntities.size;
|
|
160
|
+
const rootTargetKey = rootEntity.PrimaryKey?.ToCompactURLSegment?.() ?? rootEntity.PrimaryKey?.ToConcatenatedString?.() ?? plan.RootTargetKey;
|
|
161
|
+
// Build created records mapping
|
|
162
|
+
const createdMappings = [];
|
|
163
|
+
for (const [nodeKey, entity] of stagedEntities.entries()) {
|
|
164
|
+
const planNode = plan.Nodes.find((n) => (n.NodeKey ?? n.Key) === nodeKey);
|
|
165
|
+
createdMappings.push({
|
|
166
|
+
EntityName: entity.EntityInfo.Name,
|
|
167
|
+
SourceKey: toCompositeKey(planNode?.SourceKey),
|
|
168
|
+
TargetKey: toCompositeKey(entity.PrimaryKey),
|
|
169
|
+
Depth: planNode?.Depth ?? 0,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
Success: true,
|
|
174
|
+
ResultCode: 'SUCCESS',
|
|
175
|
+
RootRecordKey: rootTargetKey,
|
|
176
|
+
Roots: [
|
|
177
|
+
{
|
|
178
|
+
EntityName: plan.RootEntityName,
|
|
179
|
+
SourceKey: toCompositeKey(plan.RootSourceKey),
|
|
180
|
+
TargetKey: toCompositeKey(rootEntity.PrimaryKey ?? rootTargetKey),
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
Created: createdMappings,
|
|
184
|
+
RecordsCloned: clonedCount,
|
|
185
|
+
CloneLogID: cloneLogId,
|
|
186
|
+
Warnings: plan.Warnings,
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
192
|
+
// The transaction rolled back, taking any log written inside it; record the failure outside it.
|
|
193
|
+
const logged = await this.writeLog(plan, contextUser, { ID: cloneLogId, Status: 'Error', StartedAt: startedAt, ErrorMessage: detail });
|
|
194
|
+
await this.writeAudit(plan, contextUser, false, cloneLogId, null, detail);
|
|
195
|
+
return {
|
|
196
|
+
CloneLogID: logged ? cloneLogId : null,
|
|
197
|
+
Success: false,
|
|
198
|
+
ResultCode: 'EXECUTION_ERROR',
|
|
199
|
+
RootRecordKey: plan.RootTargetKey,
|
|
200
|
+
RecordsCloned: 0,
|
|
201
|
+
Warnings: plan.Warnings,
|
|
202
|
+
ErrorMessage: detail,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Save options for the clone's own rows, from the plan's effective options: Entity Actions run
|
|
208
|
+
* by default and AI Actions don't, unless the entity's configuration (or a Fire Hooks holder's
|
|
209
|
+
* request) says otherwise.
|
|
210
|
+
*/
|
|
211
|
+
static saveOptionsFor(plan) {
|
|
212
|
+
const options = new EntitySaveOptions();
|
|
213
|
+
options.SkipEntityActions = plan.EffectiveOptions?.EntityActions !== 'fire';
|
|
214
|
+
options.SkipEntityAIActions = plan.EffectiveOptions?.AIActions !== 'fire';
|
|
215
|
+
return options;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Records a refused run (blocked, or the plan changed since review) as a Cancelled clone log,
|
|
219
|
+
* so every Execute leaves a trace. Written outside any transaction; never throws.
|
|
220
|
+
* @returns the log's ID, or null when it could not be written.
|
|
221
|
+
*/
|
|
222
|
+
async WriteRefusalLog(plan, contextUser, reason) {
|
|
223
|
+
const id = GenerateUUID();
|
|
224
|
+
const logged = await this.writeLog(plan, contextUser, { ID: id, Status: 'Cancelled', StartedAt: new Date(), ErrorMessage: reason });
|
|
225
|
+
await this.writeAudit(plan, contextUser, false, id, null, reason);
|
|
226
|
+
return logged ? id : null;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The identity provenance rows are saved as. Logs and lineage links are the platform's record
|
|
230
|
+
* of what happened, not the cloner's own data (the UI role may only read them), so they are
|
|
231
|
+
* written as the system user when this process has one. `InitiatedByUserID` still names the cloner.
|
|
232
|
+
*/
|
|
233
|
+
async provenanceUser(contextUser) {
|
|
234
|
+
return (await WellKnownUserSource.Instance.GetSystemUser(this.Provider)) ?? contextUser;
|
|
235
|
+
}
|
|
236
|
+
/** Clone log, one log item per plan node, ClonedFrom links, and the audit entry. Throws if the log or a link can't be saved. */
|
|
237
|
+
async writeSuccessProvenance(plan, cloneLogId, startedAt, stagedEntities, contextUser, rootEntity) {
|
|
238
|
+
const md = this.Provider;
|
|
239
|
+
const missing = PROVENANCE_ENTITIES.filter((name) => !md.EntityByName(name));
|
|
240
|
+
if (missing.length > 0) {
|
|
241
|
+
// Only a provider without MJ's own entities (a bare test provider) lands here.
|
|
242
|
+
LogError(`[RecordCloning] ${missing.join(', ')} not found; clone ${cloneLogId} has no provenance.`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const asUser = await this.provenanceUser(contextUser);
|
|
246
|
+
const rootTargetId = ToRecordKeyString(rootEntity.PrimaryKey ?? plan.RootTargetKey) || null;
|
|
247
|
+
const logSaved = await this.writeLog(plan, contextUser, {
|
|
248
|
+
ID: cloneLogId,
|
|
249
|
+
Status: 'Complete',
|
|
250
|
+
StartedAt: startedAt,
|
|
251
|
+
RootTargetRecordID: rootTargetId,
|
|
252
|
+
CreatedCount: stagedEntities.size,
|
|
253
|
+
ResultJSON: JSON.stringify({ Created: stagedEntities.size }),
|
|
254
|
+
});
|
|
255
|
+
if (!logSaved)
|
|
256
|
+
throw new Error(`The clone log could not be saved, so the clone was rolled back.`);
|
|
257
|
+
// A. Log items: what happened to each planned row, with its (masked) field changes.
|
|
258
|
+
let sequence = 0;
|
|
259
|
+
for (const node of plan.Nodes) {
|
|
260
|
+
const nodeKey = node.NodeKey ?? node.Key;
|
|
261
|
+
const created = stagedEntities.get(nodeKey);
|
|
262
|
+
const item = await md.GetEntityObject('MJ: Record Clone Log Items', asUser);
|
|
263
|
+
item.NewRecord();
|
|
264
|
+
item.RecordCloneLogID = cloneLogId;
|
|
265
|
+
item.EntityID = md.EntityByName(node.EntityName)?.ID ?? '';
|
|
266
|
+
item.SourceRecordID = ToRecordKeyString(node.SourceKey);
|
|
267
|
+
item.TargetRecordID = created ? ToRecordKeyString(created.PrimaryKey) : null;
|
|
268
|
+
item.Depth = node.Depth;
|
|
269
|
+
item.Route = node.Route;
|
|
270
|
+
item.Status = created ? 'Created' : node.Action === 'Reference' ? 'Referenced' : 'Skipped';
|
|
271
|
+
item.Sequence = sequence++;
|
|
272
|
+
item.Reason = node.Reason || null;
|
|
273
|
+
item.FieldChangesJSON = created ? JSON.stringify((node.FieldChanges ?? []).map(MaskSensitiveFieldChange)) : null;
|
|
274
|
+
if (!(await item.Save())) {
|
|
275
|
+
throw new Error(`Clone log item for ${nodeKey} not saved: ${item.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// B. Record Links (LinkType = 'ClonedFrom'): Source = the new clone, Target = its original.
|
|
279
|
+
for (const [nodeKey, entity] of stagedEntities.entries()) {
|
|
280
|
+
const planNode = plan.Nodes.find((n) => (n.NodeKey ?? n.Key) === nodeKey);
|
|
281
|
+
if (!planNode)
|
|
282
|
+
continue;
|
|
283
|
+
const link = await md.GetEntityObject('MJ: Record Links', asUser);
|
|
284
|
+
link.NewRecord();
|
|
285
|
+
link.LinkType = 'ClonedFrom';
|
|
286
|
+
link.SourceEntityID = entity.EntityInfo.ID;
|
|
287
|
+
link.SourceRecordID = ToRecordKeyString(entity.PrimaryKey ?? planNode.TargetKey);
|
|
288
|
+
link.TargetEntityID = entity.EntityInfo.ID;
|
|
289
|
+
link.TargetRecordID = ToRecordKeyString(planNode.SourceKey);
|
|
290
|
+
link.Metadata = JSON.stringify({ CloneLogID: cloneLogId, Depth: planNode.Depth ?? 0, RootTargetRecordID: rootTargetId });
|
|
291
|
+
if (!(await link.Save())) {
|
|
292
|
+
throw new Error(`Record link for ${nodeKey} not saved: ${link.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
await this.writeAudit(plan, contextUser, true, cloneLogId, rootTargetId, null);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Saves one clone log header. Returns whether it saved; failures are logged, never thrown, so the
|
|
299
|
+
* failure and refusal paths can call it after a rollback.
|
|
300
|
+
*/
|
|
301
|
+
async writeLog(plan, contextUser, fields) {
|
|
302
|
+
const md = this.Provider;
|
|
303
|
+
if (!md.EntityByName('MJ: Record Clone Logs'))
|
|
304
|
+
return false;
|
|
305
|
+
try {
|
|
306
|
+
const log = await md.GetEntityObject('MJ: Record Clone Logs', await this.provenanceUser(contextUser));
|
|
307
|
+
log.NewRecord();
|
|
308
|
+
log.ID = fields.ID;
|
|
309
|
+
log.Status = fields.Status;
|
|
310
|
+
log.StartedAt = fields.StartedAt;
|
|
311
|
+
log.EndedAt = new Date();
|
|
312
|
+
log.InitiatedByUserID = contextUser?.ID ?? '';
|
|
313
|
+
log.RootEntityID = md.EntityByName(plan.RootEntityName ?? '')?.ID ?? '';
|
|
314
|
+
log.RootSourceRecordID = ToRecordKeyString(plan.RootSourceKey) ?? '';
|
|
315
|
+
log.RootTargetRecordID = fields.RootTargetRecordID ?? null;
|
|
316
|
+
log.PlanHash = plan.PlanHash || plan.Hash || '';
|
|
317
|
+
log.PlanJSON = JSON.stringify(MaskSensitivePlan(plan));
|
|
318
|
+
log.OptionsJSON = JSON.stringify({ Effective: plan.EffectiveOptions, Overrides: plan.Overrides ?? [] });
|
|
319
|
+
log.ResultJSON = fields.ResultJSON ?? null;
|
|
320
|
+
log.ErrorMessage = fields.ErrorMessage ?? null;
|
|
321
|
+
log.Reason = plan.Reason ?? null;
|
|
322
|
+
log.CreatedCount = fields.CreatedCount ?? 0;
|
|
323
|
+
log.ReferencedCount = plan.Nodes.filter((n) => n.Action === 'Reference').length;
|
|
324
|
+
log.SkippedCount = plan.Excluded?.length ?? 0;
|
|
325
|
+
if (await log.Save())
|
|
326
|
+
return true;
|
|
327
|
+
LogError(`[RecordCloning] Clone log ${fields.ID} not saved: ${log.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
LogError(`[RecordCloning] Clone log ${fields.ID} not saved: ${err instanceof Error ? err.message : String(err)}`);
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
/** One `Record Cloned` audit entry per Execute, as the cloner. Best effort, like all audit logging. */
|
|
335
|
+
async writeAudit(plan, contextUser, success, cloneLogId, rootTargetId, error) {
|
|
336
|
+
const md = this.Provider;
|
|
337
|
+
if (typeof md.CreateAuditLogRecord !== 'function')
|
|
338
|
+
return;
|
|
339
|
+
const entity = md.EntityByName(plan.RootEntityName ?? '');
|
|
340
|
+
if (!entity)
|
|
341
|
+
return;
|
|
342
|
+
// Audited when the entity's clone authorization asks for it (every shipped one does).
|
|
343
|
+
const authorization = new CloneAuthorizer(md).EntityAuthorization(entity);
|
|
344
|
+
if (authorization?.UseAuditLog !== true)
|
|
345
|
+
return;
|
|
346
|
+
const entityId = entity.ID;
|
|
347
|
+
await md.CreateAuditLogRecord(contextUser, authorization.Name, 'Record Cloned', success ? 'Success' : 'Failed', JSON.stringify({ CloneLogID: cloneLogId, SourceRecordID: ToRecordKeyString(plan.RootSourceKey), TargetRecordID: rootTargetId, Error: error }), entityId, rootTargetId ?? ToRecordKeyString(plan.RootSourceKey), success ? `Cloned ${plan.RootEntityName} ${ToRecordKeyString(plan.RootSourceKey)}` : `Clone of ${plan.RootEntityName} ${ToRecordKeyString(plan.RootSourceKey)} failed`, null);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
//# sourceMappingURL=CloneExecutor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloneExecutor.js","sourceRoot":"","sources":["../src/CloneExecutor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file CloneKeys.ts
|
|
3
|
+
* Primary-key handling for record cloning, for single-column and composite keys alike.
|
|
4
|
+
*
|
|
5
|
+
* A clone needs a new primary key for every row it creates. How that key is chosen depends on
|
|
6
|
+
* the entity's key columns:
|
|
7
|
+
*
|
|
8
|
+
* - **mint**: a single UUID column (`uniqueidentifier`, or `uuid` on PostgreSQL) that is not itself
|
|
9
|
+
* a foreign key. The planner pre-mints a UUID so child rows can point at the copy before saving.
|
|
10
|
+
* - **server**: a single auto-increment column. The database assigns it on insert; child rows
|
|
11
|
+
* reach it through their collection, which stamps the parent key at save time.
|
|
12
|
+
* - **derived**: everything else, which is a composite key or a natural key. Each key column keeps
|
|
13
|
+
* the source row's value, except a column that is a foreign key to a row in the clone, which
|
|
14
|
+
* takes that row's new key. A junction row keyed by (UserID, RoleID) under a cloned user
|
|
15
|
+
* becomes (newUserID, RoleID). If no column changes, the copy would collide with its source,
|
|
16
|
+
* so the planner blocks that node. Two exceptions: a key column pointing at a cloned row whose
|
|
17
|
+
* key the database assigns counts as changed (it is left empty and filled at save), and a single
|
|
18
|
+
* UUID key that is also a foreign key (an IS-A subtype) whose parent is not in the clone gets a
|
|
19
|
+
* freshly minted UUID, because saving the subtype creates its parent row under that same ID.
|
|
20
|
+
*
|
|
21
|
+
* Record-id strings use the compact URL-segment form (`CompositeKey.ToCompactURLSegment`): the
|
|
22
|
+
* bare value for a single-column key, `Field1|Value1||Field2|Value2` for a composite one.
|
|
23
|
+
*
|
|
24
|
+
* @see plans/record-cloning/README.md §3.5
|
|
25
|
+
*/
|
|
26
|
+
import { CompositeKey, EntityInfo, KeyValuePair } from '@memberjunction/core';
|
|
27
|
+
import type { CompositeKeyLike } from '@memberjunction/record-cloning-base';
|
|
28
|
+
/**
|
|
29
|
+
* Builds a CompositeKey from pairs. Assigns `KeyValuePairs` directly because the constructor
|
|
30
|
+
* drops every pair when the first value is empty (a column the database fills in at save).
|
|
31
|
+
*/
|
|
32
|
+
export declare function KeyFromPairs(pairs: KeyValuePair[]): CompositeKey;
|
|
33
|
+
/** Whether a column holds UUIDs on any supported database (mirrors `EntityFieldInfo.IsUniqueIdentifier`). */
|
|
34
|
+
export declare function IsUuidColumn(field: {
|
|
35
|
+
Type?: string;
|
|
36
|
+
} | undefined): boolean;
|
|
37
|
+
/** How a clone gets its primary key. */
|
|
38
|
+
export type CloneKeyStrategy = 'mint' | 'server' | 'derived';
|
|
39
|
+
/** Chooses the key strategy for an entity from its primary key columns. */
|
|
40
|
+
export declare function KeyStrategyFor(entity: EntityInfo): CloneKeyStrategy;
|
|
41
|
+
/** Builds a CompositeKey with every primary key column from any key shape the engine carries. */
|
|
42
|
+
export declare function ToCompositeKey(entity: EntityInfo, key: CompositeKeyLike | CompositeKey | string | null | undefined): CompositeKey;
|
|
43
|
+
/** Record-id string for a key: the bare value for one column, the full segment for several. */
|
|
44
|
+
export declare function ToRecordKeyString(key: CompositeKeyLike | CompositeKey | string | null | undefined): string;
|
|
45
|
+
/**
|
|
46
|
+
* The name of an entity's only primary key column. Foreign keys in MJ always reference a
|
|
47
|
+
* single-column key, so code that joins a child to its parent or follows a forward FK can rely
|
|
48
|
+
* on this; it throws with the entity name if the entity somehow has a composite key.
|
|
49
|
+
*/
|
|
50
|
+
export declare function SingleKeyField(entity: EntityInfo, purpose: string): string;
|
|
51
|
+
/** Result of deriving a clone's key from its source key. */
|
|
52
|
+
export interface DerivedKey {
|
|
53
|
+
Key: CompositeKey;
|
|
54
|
+
/** Whether at least one column differs from the source; false means the copy would collide. */
|
|
55
|
+
Changed: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Derives a clone's key: each column keeps the source value unless it is a foreign key to a row
|
|
59
|
+
* in the clone, in which case it takes that row's new key from `keyMap` (entries of the form
|
|
60
|
+
* `<Entity Name>::<source value>`, falling back to the bare source value). A column pointing at
|
|
61
|
+
* a cloned row listed in `serverAssigned` (same entry form) is left null and counts as changed:
|
|
62
|
+
* the collection writes the parent's database-assigned key into it at save time.
|
|
63
|
+
*/
|
|
64
|
+
export declare function DeriveTargetKey(entity: EntityInfo, sourceKey: CompositeKey, keyMap: Record<string, string>, serverAssigned?: ReadonlySet<string>): DerivedKey;
|
|
65
|
+
//# sourceMappingURL=CloneKeys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloneKeys.d.ts","sourceRoot":"","sources":["../src/CloneKeys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAC;AAE5E;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,YAAY,CAIhE;AAED,6GAA6G;AAC7G,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GAAG,OAAO,CAG1E;AAED,wCAAwC;AACxC,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7D,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,CASnE;AAED,iGAAiG;AACjG,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,gBAAgB,GAAG,YAAY,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAKjI;AAED,+FAA+F;AAC/F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,GAAG,YAAY,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAW1G;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAM1E;AAED,4DAA4D;AAC5D,MAAM,WAAW,UAAU;IACvB,GAAG,EAAE,YAAY,CAAC;IAClB,+FAA+F;IAC/F,OAAO,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC3B,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,YAAY,EACvB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,cAAc,GAAE,WAAW,CAAC,MAAM,CAAa,GAChD,UAAU,CAmBZ"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file CloneKeys.ts
|
|
3
|
+
* Primary-key handling for record cloning, for single-column and composite keys alike.
|
|
4
|
+
*
|
|
5
|
+
* A clone needs a new primary key for every row it creates. How that key is chosen depends on
|
|
6
|
+
* the entity's key columns:
|
|
7
|
+
*
|
|
8
|
+
* - **mint**: a single UUID column (`uniqueidentifier`, or `uuid` on PostgreSQL) that is not itself
|
|
9
|
+
* a foreign key. The planner pre-mints a UUID so child rows can point at the copy before saving.
|
|
10
|
+
* - **server**: a single auto-increment column. The database assigns it on insert; child rows
|
|
11
|
+
* reach it through their collection, which stamps the parent key at save time.
|
|
12
|
+
* - **derived**: everything else, which is a composite key or a natural key. Each key column keeps
|
|
13
|
+
* the source row's value, except a column that is a foreign key to a row in the clone, which
|
|
14
|
+
* takes that row's new key. A junction row keyed by (UserID, RoleID) under a cloned user
|
|
15
|
+
* becomes (newUserID, RoleID). If no column changes, the copy would collide with its source,
|
|
16
|
+
* so the planner blocks that node. Two exceptions: a key column pointing at a cloned row whose
|
|
17
|
+
* key the database assigns counts as changed (it is left empty and filled at save), and a single
|
|
18
|
+
* UUID key that is also a foreign key (an IS-A subtype) whose parent is not in the clone gets a
|
|
19
|
+
* freshly minted UUID, because saving the subtype creates its parent row under that same ID.
|
|
20
|
+
*
|
|
21
|
+
* Record-id strings use the compact URL-segment form (`CompositeKey.ToCompactURLSegment`): the
|
|
22
|
+
* bare value for a single-column key, `Field1|Value1||Field2|Value2` for a composite one.
|
|
23
|
+
*
|
|
24
|
+
* @see plans/record-cloning/README.md §3.5
|
|
25
|
+
*/
|
|
26
|
+
import { CompositeKey, KeyValuePair } from '@memberjunction/core';
|
|
27
|
+
/**
|
|
28
|
+
* Builds a CompositeKey from pairs. Assigns `KeyValuePairs` directly because the constructor
|
|
29
|
+
* drops every pair when the first value is empty (a column the database fills in at save).
|
|
30
|
+
*/
|
|
31
|
+
export function KeyFromPairs(pairs) {
|
|
32
|
+
const key = new CompositeKey();
|
|
33
|
+
key.KeyValuePairs = pairs;
|
|
34
|
+
return key;
|
|
35
|
+
}
|
|
36
|
+
/** Whether a column holds UUIDs on any supported database (mirrors `EntityFieldInfo.IsUniqueIdentifier`). */
|
|
37
|
+
export function IsUuidColumn(field) {
|
|
38
|
+
const t = field?.Type?.trim().toLowerCase();
|
|
39
|
+
return t === 'uniqueidentifier' || t === 'uuid';
|
|
40
|
+
}
|
|
41
|
+
/** Chooses the key strategy for an entity from its primary key columns. */
|
|
42
|
+
export function KeyStrategyFor(entity) {
|
|
43
|
+
const keys = entity.PrimaryKeys ?? [];
|
|
44
|
+
if (keys.length === 1) {
|
|
45
|
+
// Read the column from Fields by name so partial key descriptors still carry type information.
|
|
46
|
+
const pk = entity.Fields?.find((f) => f.Name.toLowerCase() === keys[0].Name.toLowerCase()) ?? keys[0];
|
|
47
|
+
if (pk.AutoIncrement)
|
|
48
|
+
return 'server';
|
|
49
|
+
if (IsUuidColumn(pk) && !pk.RelatedEntityID)
|
|
50
|
+
return 'mint';
|
|
51
|
+
}
|
|
52
|
+
return 'derived';
|
|
53
|
+
}
|
|
54
|
+
/** Builds a CompositeKey with every primary key column from any key shape the engine carries. */
|
|
55
|
+
export function ToCompositeKey(entity, key) {
|
|
56
|
+
if (!key)
|
|
57
|
+
return new CompositeKey();
|
|
58
|
+
if (key instanceof CompositeKey)
|
|
59
|
+
return key;
|
|
60
|
+
if (typeof key === 'string')
|
|
61
|
+
return CompositeKey.FromURLSegment(entity, key);
|
|
62
|
+
return KeyFromPairs((key.KeyValuePairs ?? []).map((p) => new KeyValuePair(p.FieldName, p.Value)));
|
|
63
|
+
}
|
|
64
|
+
/** Record-id string for a key: the bare value for one column, the full segment for several. */
|
|
65
|
+
export function ToRecordKeyString(key) {
|
|
66
|
+
if (!key)
|
|
67
|
+
return '';
|
|
68
|
+
if (typeof key === 'string') {
|
|
69
|
+
if (!key.includes(CompositeKey.DefaultValueDelimiter))
|
|
70
|
+
return key;
|
|
71
|
+
// 'ID|abc' or 'A|1||B|2': normalize to the compact form ('abc', or the full segment when composite).
|
|
72
|
+
const parsed = new CompositeKey();
|
|
73
|
+
parsed.SimpleLoadFromURLSegment(key);
|
|
74
|
+
return parsed.KeyValuePairs.length > 0 ? parsed.ToCompactURLSegment() : key;
|
|
75
|
+
}
|
|
76
|
+
const ck = key instanceof CompositeKey ? key : KeyFromPairs((key.KeyValuePairs ?? []).map((p) => new KeyValuePair(p.FieldName, p.Value)));
|
|
77
|
+
return ck.KeyValuePairs.length > 0 ? ck.ToCompactURLSegment() : '';
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The name of an entity's only primary key column. Foreign keys in MJ always reference a
|
|
81
|
+
* single-column key, so code that joins a child to its parent or follows a forward FK can rely
|
|
82
|
+
* on this; it throws with the entity name if the entity somehow has a composite key.
|
|
83
|
+
*/
|
|
84
|
+
export function SingleKeyField(entity, purpose) {
|
|
85
|
+
const keys = entity.PrimaryKeys ?? [];
|
|
86
|
+
if (keys.length !== 1) {
|
|
87
|
+
throw new Error(`${purpose}: '${entity.Name}' has a ${keys.length}-column primary key; foreign keys can only reference a single-column key.`);
|
|
88
|
+
}
|
|
89
|
+
return keys[0].Name;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Derives a clone's key: each column keeps the source value unless it is a foreign key to a row
|
|
93
|
+
* in the clone, in which case it takes that row's new key from `keyMap` (entries of the form
|
|
94
|
+
* `<Entity Name>::<source value>`, falling back to the bare source value). A column pointing at
|
|
95
|
+
* a cloned row listed in `serverAssigned` (same entry form) is left null and counts as changed:
|
|
96
|
+
* the collection writes the parent's database-assigned key into it at save time.
|
|
97
|
+
*/
|
|
98
|
+
export function DeriveTargetKey(entity, sourceKey, keyMap, serverAssigned = new Set()) {
|
|
99
|
+
let changed = false;
|
|
100
|
+
const pairs = sourceKey.KeyValuePairs.map((pair) => {
|
|
101
|
+
const field = entity.Fields.find((f) => f.Name.toLowerCase() === pair.FieldName.toLowerCase());
|
|
102
|
+
const sourceValue = pair.Value == null ? '' : String(pair.Value);
|
|
103
|
+
if (field?.RelatedEntity && sourceValue && serverAssigned.has(`${field.RelatedEntity}::${sourceValue}`)) {
|
|
104
|
+
changed = true;
|
|
105
|
+
return new KeyValuePair(pair.FieldName, null);
|
|
106
|
+
}
|
|
107
|
+
if (field?.RelatedEntity && sourceValue) {
|
|
108
|
+
const mapped = keyMap[`${field.RelatedEntity}::${sourceValue}`] ?? keyMap[sourceValue];
|
|
109
|
+
if (mapped !== undefined && mapped !== sourceValue) {
|
|
110
|
+
changed = true;
|
|
111
|
+
return new KeyValuePair(pair.FieldName, mapped);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return new KeyValuePair(pair.FieldName, pair.Value);
|
|
115
|
+
});
|
|
116
|
+
return { Key: KeyFromPairs(pairs), Changed: changed };
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=CloneKeys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloneKeys.js","sourceRoot":"","sources":["../src/CloneKeys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file CloneMaterializer.ts
|
|
3
|
+
* Builds the in-memory entity graph for record cloning.
|
|
4
|
+
* Orchestrates collection routing (declared vs dynamic via DeclareRelatedRecordsDynamic),
|
|
5
|
+
* builds each row from NewRecord() plus the plan's field values, re-stamps join fields,
|
|
6
|
+
* and handles polymorphic IS-A child additions.
|
|
7
|
+
* @see plans/record-cloning/README.md §6, §13.1
|
|
8
|
+
*/
|
|
9
|
+
import { BaseEntity, IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
10
|
+
import { ClonePlan } from '@memberjunction/record-cloning-base';
|
|
11
|
+
export interface MaterializedGraph {
|
|
12
|
+
RootEntity: BaseEntity;
|
|
13
|
+
StagedEntities: Map<string, BaseEntity>;
|
|
14
|
+
SidecarEntities: BaseEntity[];
|
|
15
|
+
PrerequisiteEntities?: BaseEntity[];
|
|
16
|
+
/** Plan nodes saved as sidecars although planned in a collection (they point at another new row). */
|
|
17
|
+
DeferredNodeKeys?: Set<string>;
|
|
18
|
+
}
|
|
19
|
+
export declare class CloneMaterializer {
|
|
20
|
+
private _provider?;
|
|
21
|
+
constructor(provider?: IMetadataProvider);
|
|
22
|
+
protected get Provider(): IMetadataProvider;
|
|
23
|
+
/**
|
|
24
|
+
* Materializes a ClonePlan into memory.
|
|
25
|
+
*/
|
|
26
|
+
Materialize(plan: ClonePlan, contextUser: UserInfo): Promise<MaterializedGraph>;
|
|
27
|
+
/**
|
|
28
|
+
* Create rows with a remapped column, other than the join field to their parent, whose new
|
|
29
|
+
* value is another new row's key: they must save after that row, not inside the graph save.
|
|
30
|
+
*/
|
|
31
|
+
private static rowsToDefer;
|
|
32
|
+
/** New rows by their planned key, lower-cased, for matching a remapped column's value. */
|
|
33
|
+
private static newRowsByTargetKey;
|
|
34
|
+
/** Plan nodes a row points at through remapped columns other than `joinField`. */
|
|
35
|
+
private static remappedTargets;
|
|
36
|
+
/**
|
|
37
|
+
* Sidecars in an order where each saves after the sidecars it depends on: its parent, when
|
|
38
|
+
* the parent is a sidecar too, and the new rows its remapped columns point at. Rows saved in
|
|
39
|
+
* the graph save come first anyway. Stable: independent sidecars keep their planned order.
|
|
40
|
+
*/
|
|
41
|
+
private static orderSidecars;
|
|
42
|
+
/**
|
|
43
|
+
* Resolves an existing writable collection or declares a dynamic one on the parent entity.
|
|
44
|
+
*/
|
|
45
|
+
private resolveOrCreateCollection;
|
|
46
|
+
/**
|
|
47
|
+
* Writes the plan's field values in pipeline order, so later stages (reset, rename, remap,
|
|
48
|
+
* prompt) win over the copied value. Fields the plan excludes are never set.
|
|
49
|
+
*/
|
|
50
|
+
private applyFieldChanges;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=CloneMaterializer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CloneMaterializer.d.ts","sourceRoot":"","sources":["../src/CloneMaterializer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACH,UAAU,EACV,iBAAiB,EAIjB,QAAQ,EACX,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAmC,MAAM,qCAAqC,CAAC;AAcjG,MAAM,WAAW,iBAAiB;IAC9B,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxC,eAAe,EAAE,UAAU,EAAE,CAAC;IAC9B,oBAAoB,CAAC,EAAE,UAAU,EAAE,CAAC;IACpC,qGAAqG;IACrG,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAClC;AAED,qBAAa,iBAAiB;IAC1B,OAAO,CAAC,SAAS,CAAC,CAAoB;gBAEnB,QAAQ,CAAC,EAAE,iBAAiB;IAI/C,SAAS,KAAK,QAAQ,IAAI,iBAAiB,CAE1C;IAED;;OAEG;IACU,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA+G5F;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,WAAW;IAc1B,0FAA0F;IAC1F,OAAO,CAAC,MAAM,CAAC,kBAAkB;IAUjC,kFAAkF;IAClF,OAAO,CAAC,MAAM,CAAC,eAAe;IAW9B;;;;OAIG;IACH,OAAO,CAAC,MAAM,CAAC,aAAa;IAiC5B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAuCjC;;;OAGG;IACH,OAAO,CAAC,iBAAiB;CAW5B"}
|