5.1 KiB
Data import — protocol, batching, and reference resolution
Detail for the data step. Source of truth: reference org-setup.mjs data step
(lines 1622-1764), buildApexInsert (1016-1061), and org-setup-utils.mjs
parseApexInsertResults (134-183) + planApexBatches (204-222).
Run this step only when a data plan exists (data-plan.json in the data/ dir). If it is
absent, no-op cleanly. Always ask the user before importing or cleaning data —
it deletes existing records.
Inputs
data-plan.json(in thedata/dir) — an array of entries, in import order:[{ "sobject": "Account", "files": ["accounts.json"], "saveRefs": true }, { "sobject": "Contact", "files": ["contacts.json"] }]- Each file is
{ "records": [ { "attributes": {...}, "Field": value, ... } ] }(SFDX data-tree shape).attributes.referenceIdis the record's ref handle. - Optional
prepare-import-unique-fields.js— if present in the project'sdata/dir, run it first to mangle unique fields so repeat imports don't collide. If the project lacks this script:- Copy
assets/prepare-import-unique-fields.js(bundled with this skill) into the project'sdata/directory. - Customize the
keyFieldmapping in the copied script based on the actual sobjects indata-plan.json. Map each sobject to its natural unique field (e.g.,Account: 'Name',Contact: 'Email', custom objectProperty__c: 'Address__c'). Only sobjects listed inkeyFieldwill getUniqueKey__cstamped. Read the comment block in the script for guidance. - Run it:
cd data/ node prepare-import-unique-fields.js
import.meta.urland requiresdata-plan.jsonto exist in the same directory. - Copy
Sequence
- Prepare (optional): run
prepare-import-unique-fields.jsif present. - Clean (reverse plan order): for each sobject, from last plan entry to
first, run
assets/data-delete.apex. Children are deleted before parents so FK constraints don't block the sweep. The sweep caps atLIMIT 10000per sobject (matches the reference) — if an org holds more seed rows than that, the surplus survives and a unique-field re-import may collide; note the cap to the user rather than assuming a clean slate. - Import (forward plan order): for each entry, for each file, resolve
references, batch the records, and run
assets/data-import.apexper batch.
Reference resolution (@referenceId)
Before building a batch, walk every field value. A string value beginning with
@ is a forward reference to a previously-inserted record's ref handle:
refMapmapsreferenceId -> real Salesforce Id, populated only from entries whose plan entry has"saveRefs": true(line 1756-1757).- For a field value
"@acctRef": ifrefMaphasacctRef, replace the value with the real Id. IfrefMapis non-empty but the ref is unknown, log a warning (unresolved ref @acctRef) and leave the value as-is — do NOT abort (lines 1687-1694).
Because refs resolve from earlier inserts, plan order is load-bearing: parents
(with saveRefs: true) must precede the children that reference them.
Measured batching
Do not guess batch sizes. Size each record's rendered Apex and pack batches
so overhead + sum(recordSizes) stays under the char limit, capped at the max
record count (lines 1698-1723, planApexBatches 204-222):
APEX_CHAR_LIMIT = 25000APEX_MAX_BATCH = 200- overhead = the length of
buildApexInsert(sobject, [], [])— the fixed DML boilerplate rendered with zero records — so batch sizing can't drift from the actual emitted Apex. - per-record size = the rendered
{ Sobj r = new Sobj(); ... recs.add(r); }block PLUS the record'srefIdliteral length (each record also grows the per-batchString[] refs = new String[]{...}line, so its ref is counted too, lines 1709-1717). - at-least-one guarantee: a single record larger than the limit is placed
alone in its own batch and left for Apex to accept or reject (never dropped,
never an empty batch —
planApexBatchesline 214).
Result protocol — SETUP_RESULT_JSON:
Each import batch emits exactly one machine-readable debug line:
SETUP_RESULT_JSON: followed by a JSON array of { ref, id } (success) or
{ ref, err } (failure). Parse it as parseApexInsertResults does (134-183):
- Find the
SETUP_RESULT_JSON:marker; take the rest of that line up to the first newline. - A missing or unparseable marker is a HARD FAILURE — never read it as "0 errors". A truncated log line must abort the import, not let a partial insert look successful (lines 139-160, 1743-1748).
- If any entry has
err, print up to 5 of them and abort the step (lines 1749-1754). - On success with
saveRefs: true, add each{ref -> id}torefMapfor later entries (lines 1756-1757).
Failure handling
sf apex runnon-zero AND output lacksCompiled successfully→ the Apex failed to run; surface stderr and abort (lines 1738-1741).- Any parsed record error → abort the whole data step (do not continue to the next sobject with a half-loaded parent).