import type { OrgAuth } from "../auth/sf-auth.ts"; import type { DescribeClient } from "../describe/client.ts"; import type { Field } from "../describe/types.ts"; import type { DependencyGraph } from "../graph/build.ts"; import type { LoadPlan } from "../graph/order.ts"; import { type ScopePath } from "./extract.ts"; import { IdMap } from "./id-map.ts"; import { type MaskSelection } from "./mask/types.ts"; import { type TargetIdentity } from "./project-id-map.ts"; import type { ExecuteSummary, UpsertDecisionSummary } from "./session.ts"; /** * The full run. For each step of the restricted load plan: * * single: * 1. SELECT createable fields + Id from source, scoped to the root. * 2. Substitute FK values using the session's id-map. * - FK target is in id-map → use the mapped target ID. * - FK target is a standard-root (User, RecordType) → leave as-is. * - FK target is unmapped and field is nillable → null it. * - FK target is unmapped and field is NOT nillable → skip the * record (log the error) and continue; caller reviews the log. * 3. POST composite/sobjects in batches of 200. Capture new IDs, * update id-map. * * cycle: * Phase 1 — insert every object in the SCC with breakEdge.fieldName * nulled (it MUST be nillable; computeLoadOrder prefers nillable). * Phase 2 — PATCH each record in the SCC whose source record had the * breakEdge field set, using the id-map to resolve the reference. * * All I/O touching record data stays out of the caller's response. The * summary returned records only counts + log path. */ export type ExecuteOptions = { sourceAuth: OrgAuth; targetAuth: OrgAuth; sourceDescribe: DescribeClient; targetDescribe: DescribeClient; graph: DependencyGraph; rootObject: string; whereClause: string; finalObjectList: string[]; loadPlan: LoadPlan; sessionDir: string; fetchFn?: typeof fetch; /** * If true, snapshot + deactivate + reactivate target-org validation * rules scoped to `finalObjectList` around the insert phase. Only the * rules that were `Active = true` at snapshot time are touched — * rules the user had pre-disabled are left alone. */ disableValidationRules?: boolean; /** Session identifier — threaded into the VR snapshot file. */ sessionId?: string; /** Target-org alias (for the VR snapshot file, UX only). */ targetOrgAlias?: string; /** * Per-object upsert-key decisions produced by `runDryRun`. When an * object's decision is `{kind: "picked", field}`, `seedSingle` routes * records through composite UPSERT on that external-id — safe for * re-runs against a target that already has matching rows. Anything * else (ambiguous, missing, or map entirely absent) uses INSERT, * identical to pre-upsert behavior. The single source of truth is * what the user saw in the dry-run report. */ upsertDecisions?: Record; /** * User-selected "Child + 1" lookups. Forwarded to computeScopePaths and * composeScopeSoqls so lookup-target objects get a resolvable scope. * See src/seed/extract.ts ScopePath kind="child-lookup". */ childLookups?: Record; /** * Source/target sf aliases. When both are set (and `isolateIdMap` is not * true), the run reads the persistent project-level id-map at * `~/.sandbox-seed/id-maps/__.json` so prior runs' * source→target mappings are reused (cross-run FK stitching) and * already-seeded source rows are skipped on INSERT. New mappings from * this run are merged back at the end. */ sourceAlias?: string; /** See `sourceAlias`. Falls back to `targetOrgAlias` when only one is provided. */ targetAliasForIdMap?: string; /** Target org identity for project-id-map invalidation (orgId + LastRefreshDate). */ targetIdentity?: TargetIdentity; /** * Opt out of the persistent project-level id-map for this run. Useful * when a user wants a clean slate against a target. The session-local * id-map.json is still written either way. */ isolateIdMap?: boolean; /** * Pre-materialized root IDs from `start`-time sampling. When present, * `runExecute` uses these as the root scope verbatim and skips the * root SOQL altogether — `whereClause` then alone is too coarse to * reproduce the sample, so callers MUST thread these in to honor * `sampleSize`. Also disables the >2000-IDs direct-child subquery * shortcut in `composeScopeSoqls`, since that shortcut re-evaluates * the WHERE clause server-side and would over-fetch beyond the * sample. */ sampledRootIds?: string[]; /** * Optional field masking. When set, scalar (non-reference) values for the * fields named in `selection` are replaced with deterministic, keyed, * format-preserving fakes before insert (see src/seed/mask). Absent ⇒ values * are copied verbatim — byte-identical to pre-masking behavior. The salt * makes masking reproducible across runs (UPSERT idempotence) and is never * logged or returned. Reference fields are NEVER masked — the id-map owns * FK remapping. */ masking?: { salt: string; selection: MaskSelection; }; }; export declare function runExecute(opts: ExecuteOptions): Promise; /** * Parse the existing-target ID out of a Salesforce DUPLICATE_VALUE error. * * Salesforce returns DUPLICATE_VALUE on a uniqueness-constrained INSERT * with a message of the form: * * "duplicate value found: duplicates value on record * with id: <15-or-18-char-id>" * * We extract that id so callers can write a source→target mapping into * the id-map and treat the row as already-seeded. Conservative: only * matches the documented message shape — if Salesforce changes the * wording, we fall back to counting it as an error (no silent data * loss). Returns null when no DUPLICATE_VALUE entry is present OR when * the ID couldn't be parsed. * * Exported for unit testing. */ export declare function extractDuplicateValueTargetId(errs: unknown): string | null; /** * Populate the id-map with source→target mappings for standard-root objects * that we CAN resolve deterministically from describe metadata: * * RecordType — matched by (SobjectType + DeveloperName). Each object's * `describe.recordTypeInfos` gives us `{developerName, recordTypeId}` * for every RT on both source and target. We match by developerName * and write the pair into the id-map under key `RecordType:`. * * Other standard-roots (User, BusinessHours, Group, Queue) could in * principle be mapped by DeveloperName too, but that requires a SOQL * query against both orgs. Punting to a follow-up; for now the * rewriteRecordForTarget logic omits unresolvable standard-root FKs so * Salesforce's default picker kicks in. * * Exported for unit testing. */ export declare function prepopulateStandardRootMappings(args: { objects: string[]; sourceDescribe: DescribeClient; targetDescribe: DescribeClient; idMap: IdMap; appendLog?: (msg: string) => Promise; }): Promise<{ mappedCount: number; unmappedByObject: Record; }>; /** * Intersect source createable fields with the target org's actual field * list, dropping any source-only fields. Returns the surviving set plus * the names of anything dropped (for logging). * * If the target describe fails (object missing in target entirely, auth * error, etc.) we fall back to keeping all candidates — the composite * insert will surface the real error with better context than we can. * * Exported for unit testing. */ export declare function intersectWithTargetFields(args: { object: string; candidates: Field[]; targetDescribe: DescribeClient; }): Promise<{ kept: Field[]; dropped: string[]; lengthClamped: string[]; }>; /** * Compose one or more `SELECT FROM ` queries with a WHERE * clause appropriate to how this object relates to the root. Returns * multiple queries when the root-ID list is large enough that a single * `IN (...)` clause would push the GET `/query` URL over Salesforce's * ~16 KB URI limit. Callers iterate and union the resulting records * (dedup by Id — chunks from `chunkIds` are disjoint, but the dedup is * defensive against future caller changes). * * Returns an empty array when the scope is unresolvable (unknown path, * transitive chain not yet materialized, or required path fields absent). */ declare function composeScopeSoqls(args: { scope: ScopePath; object: string; fields: string[]; rootObject: string; whereClause: string; rootIds: string[]; /** * When true (sampleSize was applied at start), force chunked IN-list * SOQL for the direct-child case even when `rootIds.length > 2000`. * The subquery shortcut otherwise used at that size re-runs the * user's WHERE clause server-side, which over-fetches past the sample. */ sampleApplied?: boolean; }): string[]; export { composeScopeSoqls as _composeScopeSoqls };