diff --git a/.changeset/import-dryrun-hook-boundary-docs.md b/.changeset/import-dryrun-hook-boundary-docs.md new file mode 100644 index 0000000000..34ae2a8399 --- /dev/null +++ b/.changeset/import-dryrun-hook-boundary-docs.md @@ -0,0 +1,28 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): `ImportRequest.dryRun` states the boundary an import preview has — no automations run (#6537) + +An import dry run routes through the engine's own write-path validation +(`DataProtocol.validateData`, #6037 / #4633 ruling D) and applies no +`runAutomations` gate, because gating it would leave the common dry run with no +validation at all — the false all-clear that work set out to close. The engine's +validate-only path deliberately runs **no hooks**: a preview that fired +user-authored side effects (mail, outbound calls, writes to other objects) would +be the retired `BatchOptions.validateOnly` defect (#4052) in a new spelling. + +The consequence an author can meet is narrow and, until now, written down only in +the engine's and the import runner's source comments: on an object whose +`beforeInsert` hook derives a **required business field**, a dry run with +`runAutomations: true` can report `required` for a row the real import would have +created. Audit and ownership stamps (`created_by`, `owner_id`, +`organization_id`, …) are `system`/`readonly` and skipped by validation anyway, so +they cannot produce this. + +`ImportRequest.dryRun`'s description now says so, which puts it on the reference +page an author reads before sending the request — the same schema backs +`CreateImportJobRequest`, so the synchronous route and the async import job both +carry it. **Zero behaviour change**: one description string, and the +`content/docs/references/api/export.mdx` cells regenerated from it by +`gen:schema && gen:docs` (no hand edits). diff --git a/content/docs/references/api/export.mdx b/content/docs/references/api/export.mdx index 6575db0e5b..8b9fba1bd4 100644 --- a/content/docs/references/api/export.mdx +++ b/content/docs/references/api/export.mdx @@ -76,7 +76,7 @@ const result = CreateExportJobRequestSchema.parse(data); | **xlsxBase64** | `string` | optional | Base64-encoded .xlsx workbook bytes (when format = xlsx); parsed server-side | | **sheet** | `string \| integer` | optional | Worksheet name or 1-based index to read (xlsx; defaults to the first sheet) | | **mapping** | `Record \| { sourceField: string; targetField: string; targetLabel?: string; transform: Enum<'none' \| 'uppercase' \| 'lowercase' \| 'trim' \| 'date_format' \| 'lookup'>; … }[]` | optional | Source column → target field mapping | -| **dryRun** | `boolean` | ✅ | Validate + coerce every row without persisting | +| **dryRun** | `boolean` | ✅ | Validate + coerce every row without persisting. The verdict is the engine's own write-path validation, with one boundary an author should know: a preview runs NO automations. Hooks never fire in a dry run (#6037) — a preview that executed user-authored side effects (mail, outbound calls, writes to other objects) would be the retired `validateOnly` defect in a new spelling. So a dry run with `runAutomations: true` can report `required` for a field a `beforeInsert` hook would populate during the real import; for hook-derived fields the real write is authoritative. | | **writeMode** | `Enum<'insert' \| 'update' \| 'upsert'>` | ✅ | insert / update / upsert semantics | | **matchFields** | `string[]` | optional | Fields that identify an existing record (required for update/upsert) | | **runAutomations** | `boolean` | ✅ | Fire triggers/hooks for each imported row (off by default for bulk) | @@ -365,7 +365,7 @@ Type: `{ sourceField: string; targetField: string; targetLabel?: string; transfo | **xlsxBase64** | `string` | optional | Base64-encoded .xlsx workbook bytes (when format = xlsx); parsed server-side | | **sheet** | `string \| integer` | optional | Worksheet name or 1-based index to read (xlsx; defaults to the first sheet) | | **mapping** | `Record \| { sourceField: string; targetField: string; targetLabel?: string; transform: Enum<'none' \| 'uppercase' \| 'lowercase' \| 'trim' \| 'date_format' \| 'lookup'>; … }[]` | optional | Source column → target field mapping | -| **dryRun** | `boolean` | ✅ | Validate + coerce every row without persisting | +| **dryRun** | `boolean` | ✅ | Validate + coerce every row without persisting. The verdict is the engine's own write-path validation, with one boundary an author should know: a preview runs NO automations. Hooks never fire in a dry run (#6037) — a preview that executed user-authored side effects (mail, outbound calls, writes to other objects) would be the retired `validateOnly` defect in a new spelling. So a dry run with `runAutomations: true` can report `required` for a field a `beforeInsert` hook would populate during the real import; for hook-derived fields the real write is authoritative. | | **writeMode** | `Enum<'insert' \| 'update' \| 'upsert'>` | ✅ | insert / update / upsert semantics | | **matchFields** | `string[]` | optional | Fields that identify an existing record (required for update/upsert) | | **runAutomations** | `boolean` | ✅ | Fire triggers/hooks for each imported row (off by default for bulk) | diff --git a/packages/spec/src/api/export.zod.ts b/packages/spec/src/api/export.zod.ts index 0ca0022383..2c8c073464 100644 --- a/packages/spec/src/api/export.zod.ts +++ b/packages/spec/src/api/export.zod.ts @@ -328,7 +328,14 @@ export const ImportRequestSchema = lazySchema(() => z.object({ mapping: ImportMappingSchema.optional() .describe('Source column → target field mapping'), dryRun: z.boolean().default(false) - .describe('Validate + coerce every row without persisting'), + .describe( + 'Validate + coerce every row without persisting. The verdict is the engine\'s own write-path ' + + 'validation, with one boundary an author should know: a preview runs NO automations. Hooks never ' + + 'fire in a dry run (#6037) — a preview that executed user-authored side effects (mail, outbound ' + + 'calls, writes to other objects) would be the retired `validateOnly` defect in a new spelling. So a ' + + 'dry run with `runAutomations: true` can report `required` for a field a `beforeInsert` hook would ' + + 'populate during the real import; for hook-derived fields the real write is authoritative.', + ), writeMode: ImportWriteMode.default('insert') .describe('insert / update / upsert semantics'), matchFields: z.array(z.string()).optional()