@memberjunction/record-cloning
Server-side engine for cloning an entity record together with the graph of records around it:
owned child rows and the references it points at. (A Deep IS-A subtype or embedded row blocks the
plan with EDGE_KIND_UNSUPPORTED in this release.) It copies what the entity’s
Configuration.Clone bag allows, re-points foreign keys at the new rows, and saves everything in
one transaction with full provenance.
Pure logic lives in @memberjunction/record-cloning-base: policy resolution,
the field mapper, naming, JSON remap, validation and the contract types. Graph traversal comes from
@memberjunction/record-graph. The design is in
plans/record-cloning/README.md.
Flow: Describe → Plan → Execute
Section titled “Flow: Describe → Plan → Execute”| Step | What happens | Writes |
|---|---|---|
| Describe | Whether the user may clone this entity and what the UI may offer: presets, UserEditable, relationships with their default policy, and the Fire Hooks / Override Scope flags. | nothing |
| Plan | ClonePlanner walks the graph (DependencyGraphWalker), resolves each edge to Deep, Reference or Skip, runs the field pipeline on every row, applies MaxDepth/MaxRecords caps and authorization checks, and hashes the result. | nothing |
| Execute | Plans again. Refuses with BLOCKED/FORBIDDEN when the plan is blocked, or PLAN_CHANGED when the hash differs from the ExpectedPlanHash the user reviewed. Otherwise CloneMaterializer builds the in-memory graph and CloneExecutor saves it. | the clone and its provenance |
ClonePlannerproduces a deterministicClonePlan(nodes, edges, field changes, warnings,Blocked).CloneMaterializerturns the plan into unsaved entity objects: the root, children in the parent’s writable collection (or one declared withBaseEntity.DeclareRelatedRecordsDynamic), prerequisites for forward-FK targets, and sidecars.CloneExecutorsets a clone context on each staged row (SetCloneContext), then saves prerequisites,root.Save()and sidecars insideRunInEntityTransaction, followed by the provenance.
Entry points
Section titled “Entry points”Remote operations (src/operations.ts): RecordClone.Describe, RecordClone.Plan,
RecordClone.Execute and RecordClone.GetLineage. Each server class extends the operation class
CodeGen emits into @memberjunction/core-entities. The input/output contracts are the types in
metadata/remote-operations/types/record-clone-*.ts. Plan output masks encrypted values.
GetLineage reads ClonedFrom record links in both directions.
Programmatic API, server-side:
import { LogError } from '@memberjunction/core';import { RecordCloneEngine } from '@memberjunction/record-cloning';
const engine = new RecordCloneEngine(provider);const plan = await engine.Plan({ EntityName: 'MJ: AI Agents', SourceRecordKey: key }, contextUser);if (!plan.Blocked) { const result = await engine.Clone( { EntityName: 'MJ: AI Agents', SourceRecordKey: key, Options: { Preset: 'deep-prompts' } }, contextUser ); if (!result.Success) LogError(result.ErrorMessage);}Clone returns the plan without writing anything when Options.DryRun is true. Describe and
GetLineage are also available.
Actions (packages/Actions/CoreActions/src/custom/record-cloning/):
Clone Record:EntityName,RecordID, optionalOptionsJSON andDryRun. OutputsNewRecordID,CloneLogID,CreatedCountandWarnings.Clone Records: takesRecordIDsinstead and requiresClone Records: Batch. Each root is its own clone and transaction. TheResultsoutput reports each root, and the result code isPARTIALwhen only some roots succeed.
Record processes: the Clone work type is CloneRecordProcessor. RecordCloningStartup
(@RegisterForStartup) registers it with RecordProcessorRegistry at server boot. The run’s
Configuration JSON supplies Options, EdgeOverrides, FieldOverrides and DryRun. It requires
Clone Records: Batch.
Authorization (CloneAuthorization.ts)
Section titled “Authorization (CloneAuthorization.ts)”CloneAuthorizer checks every created row against its entity’s clone authorization. That is
Configuration.Clone.RequiredAuthorization if set. Otherwise it is Clone Records: <Entity Name>
if that authorization exists, and otherwise Clone Records in Platform Schema (for __mj) or
Clone Records in Custom Schemas. Checks walk ancestors, so a holder of Clone Records passes all
of them. Clone Records: Fire Hooks lets a request change whether Entity Actions and AI Actions fire.
Clone Records: Override Scope lets a request widen a clone with a Deep override. An authorization
missing from metadata grants nothing. The planner also requires CanCreate on every created entity and
the root’s RequiredUserType, if set.
Provenance
Section titled “Provenance”A successful clone writes, in the same transaction:
- a Record Change per new row (for entities that track record changes), with
Source = 'Clone'andChangeContextJSON - one
MJ: Record Clone Logsheader (masked plan, options, counts,InitiatedByUserID= the cloner) - one
MJ: Record Clone Log Itemsrow per planned node (Created/Referenced/Skipped) - one
MJ: Record Linksrow per new record, withLinkType = 'ClonedFrom'(Source = clone, Target = original) - a
Record Clonedaudit log entry, when the entity’s clone authorization hasUseAuditLog(every shipped one does)
If the log or a link fails to save, the whole clone rolls back. The log, log items and links are
saved as the system user when one is available, because they are the platform’s record, not the
cloner’s data. The audit entry is recorded as the cloner. Refused and failed runs still get a log
(Cancelled / Error) and, under the same UseAuditLog rule, an audit entry, written outside any transaction.
Transactions
Section titled “Transactions”A clone is all or nothing. If the provider lacks SupportsEntityTransactions and
BeginEntityTransaction, Execute refuses before writing anything and returns EXECUTION_ERROR.
It still records a refusal log. Run clones on the server, through the remote operations, the
actions or a record process.