Skip to content

Latest commit

 

History

History
355 lines (245 loc) · 27.7 KB

batch.mdx

File metadata and controls

355 lines (245 loc) · 27.7 KB
title Batch schema — API Protocol property reference
navTitle Batch
description Batch Operations API — Provides efficient bulk data operations with transaction support.

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}

Batch Operations API

Provides efficient bulk data operations with transaction support. Implements P0/P1 requirements for ObjectStack kernel.

Features:

  • Batch create/update/delete operations
  • Atomic transaction support (all-or-none)
  • Partial success handling
  • Detailed error reporting per record

Industry alignment: Salesforce Bulk API, Microsoft Dynamics Bulk Operations

**Source:** `packages/spec/src/api/batch.zod.ts`

TypeScript Usage

import { BatchConfigSchema, BatchOperationResultSchema, BatchOperationType, BatchOptionsSchema, BatchRecordSchema, BatchUpdateRequestSchema, BatchUpdateResponseSchema, CrossObjectBatchDroppedFieldsSchema, CrossObjectBatchOperationSchema, CrossObjectBatchRequestSchema, CrossObjectBatchResponseSchema, DeleteManyRequestSchema, UpdateManyRecordSchema, UpdateManyRequestSchema } from '@objectstack/spec/api';
import type { BatchConfig, BatchOperationResult, BatchOperationType, BatchOptions, BatchRecord, BatchUpdateRequest, BatchUpdateResponse, CrossObjectBatchDroppedFields, CrossObjectBatchOperation, CrossObjectBatchRequest, CrossObjectBatchResponse, DeleteManyRequest, UpdateManyRecord, UpdateManyRequest } from '@objectstack/spec/api';

// Validate data
const result = BatchConfigSchema.parse(data);

BatchConfig

Properties

Property Type Required Description
enabled boolean optional (default: true) Enable batch operations
maxRecordsPerBatch integer optional (default: 200) Maximum records per batch
defaultOptions { atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean } optional Default batch options

Nested Shape: BatchConfig.defaultOptions

Property Type Required Description
atomic boolean optional (default: false) Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort.
returnRecords boolean optional (default: false) If true, return full record data in response
continueOnError boolean optional (default: false) If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total.
validateOnly never optional [REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds.

BatchOperationResult

Properties

Property Type Required Description
id string optional Record ID if operation succeeded
success boolean ✅ Whether this record was processed successfully
errors { code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }[] optional Array of errors if operation failed. Branch on errors[0].code — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error. A NON-atomic batch that stopped (the continueOnError: false default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back.
data Record<string, any> optional Full record data (if returnRecords=true)
index number optional Index of the record in the request array
droppedFields { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed' | 'stored_expression'> }[] optional Write-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the in-engine static readonly strip on create. Per-row because a batch can drop different fields on different rows (readonlyWhen is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot say which row dropped what, so this body field stays the canonical per-row channel; REST also sends the X-ObjectStack-Dropped-Fields response header on the answer, naming the union of the rows' drops (each field and reason once). Optional — omit-when-empty keeps the shape backward-compatible.

Nested Shape: BatchOperationResult.errors[number]

Property Type Required Description
code Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …> ✅ Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages)
declaredCode string optional The producer-declared code, verbatim, when it is not a member of the closed code vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112)
message string ✅ Readable error message
userMessage string optional Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces message.
refusal true optional Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category string optional Error category (e.g. validation, authorization)
httpStatus integer optional HTTP status of the response carrying this error
details any optional Additional error context (e.g. field validation errors)
requestId string optional Request ID for tracking

Nested Shape: BatchOperationResult.droppedFields[number]

A write-path strip event: caller-supplied fields legally dropped from the payload, or written with the engine's computed value in place of the caller's

Property Type Required Description
object string ✅ Object the write targeted (resolved object name)
fields string[] ✅ Caller-supplied field names whose value the engine did not store: removed from the write payload, or (stored_expression) written with the computed value in place of the caller's
reason Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed' | 'stored_expression'> ✅ Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, the primary-key strip of a payload id the engine ruled is not an identifier, a computed (formula) field no driver has a column for, or a stored field whose declared expression the engine computes on every write, so the caller's different value was replaced by the computed one

BatchOperationType

Allowed Values

  • create
  • update
  • upsert
  • delete

BatchOptions

Properties

Property Type Required Description
atomic boolean optional (default: false) Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort.
returnRecords boolean optional (default: false) If true, return full record data in response
continueOnError boolean optional (default: false) If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total.
validateOnly never optional [REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds.

BatchRecord

Properties

Property Type Required Description
id string optional Record ID (required for update/delete)
data Record<string, any> optional Record data (required for create/update/upsert)
externalId string optional External ID for upsert matching

BatchUpdateRequest

Properties

Property Type Required Description
operation Enum<'create' | 'update' | 'upsert' | 'delete'> ✅ Type of batch operation
records { id?: string; data?: Record<string, any>; externalId?: string }[] ✅ Array of records to process (server caps the count — see batch.maxBatchSize)
options { atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean } optional Batch operation options

Nested Shape: BatchUpdateRequest.records[number]

Property Type Required Description
id string optional Record ID (required for update/delete)
data Record<string, any> optional Record data (required for create/update/upsert)
externalId string optional External ID for upsert matching

Nested Shape: BatchUpdateRequest.options

Property Type Required Description
atomic boolean optional (default: false) Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort.
returnRecords boolean optional (default: false) If true, return full record data in response
continueOnError boolean optional (default: false) If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total.
validateOnly never optional [REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds.

BatchUpdateResponse

Properties

Property Type Required Description
success boolean ✅ Operation success status
error { code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … } optional Error details if success is false
meta { timestamp: string; duration?: integer; requestId?: string; traceId?: string } optional Response metadata
operation Enum<'create' | 'update' | 'upsert' | 'delete'> optional Operation type that was performed
total number ✅ Total number of records in the batch
succeeded number ✅ Number of records that succeeded
failed number ✅ Number of records that failed
results { id?: string; success: boolean; errors?: object[]; data?: Record<string, any>; … }[] ✅ Detailed results for each record

Nested Shape: BatchUpdateResponse.error

Property Type Required Description
code Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …> ✅ Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages)
declaredCode string optional The producer-declared code, verbatim, when it is not a member of the closed code vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112)
message string ✅ Readable error message
userMessage string optional Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces message.
refusal true optional Producer-declared: the 5xx this envelope carries is a deliberate refusal whose message is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose message is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — true is the only value.
category string optional Error category (e.g. validation, authorization)
httpStatus integer optional HTTP status of the response carrying this error
details any optional Additional error context (e.g. field validation errors)
requestId string optional Request ID for tracking

Nested Shape: BatchUpdateResponse.meta

Property Type Required Description
timestamp string ✅
duration integer optional Server-side processing duration in milliseconds
requestId string optional
traceId string optional

Nested Shape: BatchUpdateResponse.results[number]

Property Type Required Description
id string optional Record ID if operation succeeded
success boolean ✅ Whether this record was processed successfully
errors { code: Enum<'VALIDATION_ERROR' | 'INVALID_FIELD' | 'MISSING_REQUIRED_FIELD' | …>; declaredCode?: string; message: string; userMessage?: string; … }[] optional Array of errors if operation failed. Branch on errors[0].code — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error. A NON-atomic batch that stopped (the continueOnError: false default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back.
data Record<string, any> optional Full record data (if returnRecords=true)
index number optional Index of the record in the request array
droppedFields { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed' | 'stored_expression'> }[] optional Write-observability: caller-supplied fields LEGALLY stripped from THIS row before it was written — static readonly / TRUE readonlyWhen on update, or the in-engine static readonly strip on create. Per-row because a batch can drop different fields on different rows (readonlyWhen is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot say which row dropped what, so this body field stays the canonical per-row channel; REST also sends the X-ObjectStack-Dropped-Fields response header on the answer, naming the union of the rows' drops (each field and reason once). Optional — omit-when-empty keeps the shape backward-compatible.

CrossObjectBatchDroppedFields

A cross-object batch strip event: dropped fields plus the operation index

Properties

Property Type Required Description
object string ✅ Object the write targeted (resolved object name)
fields string[] ✅ Caller-supplied field names whose value the engine did not store: removed from the write payload, or (stored_expression) written with the computed value in place of the caller's
reason Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed' | 'stored_expression'> ✅ Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, the primary-key strip of a payload id the engine ruled is not an identifier, a computed (formula) field no driver has a column for, or a stored field whose declared expression the engine computes on every write, so the caller's different value was replaced by the computed one
index integer ✅ Index of the operation in the request operations array

CrossObjectBatchOperation

Properties

Property Type Required Description
object string ✅ Target object (table) name
action Enum<'create' | 'update' | 'delete'> optional (default: "create") Operation to perform (default: create)
id string optional Target record id — required for update and delete
data Record<string, any> optional Record payload for create/update; a value may be { $ref: <opIndex> } to reference an earlier op's created id

CrossObjectBatchRequest

Properties

Property Type Required Description
operations { object: string; action?: Enum<'create' | 'update' | 'delete'>; id?: string; data?: Record<string, any> }[] ✅ Ordered operations executed in one transaction
atomic boolean optional (default: true) Always true — the cross-object batch is all-or-nothing

Nested Shape: CrossObjectBatchRequest.operations[number]

Property Type Required Description
object string ✅ Target object (table) name
action Enum<'create' | 'update' | 'delete'> optional (default: "create") Operation to perform (default: create)
id string optional Target record id — required for update and delete
data Record<string, any> optional Record payload for create/update; a value may be { $ref: <opIndex> } to reference an earlier op's created id

CrossObjectBatchResponse

Properties

Property Type Required Description
results any[] ✅ Per-operation result, index-aligned with the request operations
droppedFields { object: string; fields: string[]; reason: Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed' | 'stored_expression'>; index: integer }[] optional Write-observability: caller-supplied fields the engine LEGALLY stripped from an operation before it was written — static readonly or a TRUE readonlyWhen predicate. This endpoint is the console record form's save path (master-detail writes parent + children in one transaction), so without it the ONE surface where a user edits a readonlyWhen field reported plain success while the value never landed. Each event carries the index of its operation. Present ONLY when ≥1 field was dropped; the batch still committed without them (results/success semantics unchanged). Optional — omit-when-empty keeps the shape backward-compatible.

Nested Shape: CrossObjectBatchResponse.droppedFields[number]

A cross-object batch strip event: dropped fields plus the operation index

Property Type Required Description
object string ✅ Object the write targeted (resolved object name)
fields string[] ✅ Caller-supplied field names whose value the engine did not store: removed from the write payload, or (stored_expression) written with the computed value in place of the caller's
reason Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed' | 'stored_expression'> ✅ Why the fields were dropped: static readonly, a TRUE readonlyWhen predicate, the primary-key strip of a payload id the engine ruled is not an identifier, a computed (formula) field no driver has a column for, or a stored field whose declared expression the engine computes on every write, so the caller's different value was replaced by the computed one
index integer ✅ Index of the operation in the request operations array

DeleteManyRequest

Properties

Property Type Required Description
ids string[] ✅ Array of record IDs to delete (server caps the count — see batch.maxBatchSize)
options { atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean } optional Delete options

Nested Shape: DeleteManyRequest.options

Property Type Required Description
atomic boolean optional (default: false) Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort.
returnRecords boolean optional (default: false) If true, return full record data in response
continueOnError boolean optional (default: false) If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total.
validateOnly never optional [REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds.

UpdateManyRecord

Properties

Property Type Required Description
id string ✅ Record ID
data Record<string, any> ✅ Fields to update

UpdateManyRequest

Properties

Property Type Required Description
records { id: string; data: Record<string, any> }[] ✅ Array of records to update (server caps the count — see batch.maxBatchSize)
options { atomic?: boolean; returnRecords?: boolean; continueOnError?: boolean } optional Update options

Nested Shape: UpdateManyRequest.records[number]

Property Type Required Description
id string ✅ Record ID
data Record<string, any> ✅ Fields to update

Nested Shape: UpdateManyRequest.options

Property Type Required Description
atomic boolean optional (default: false) Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries errors[0].code ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe capabilities.transactionalBatch on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort.
returnRecords boolean optional (default: false) If true, return full record data in response
continueOnError boolean optional (default: false) If true (and atomic=false), continue processing remaining records after errors. Default false: the first failure ENDS the run — records before it stay written (nothing is rolled back on this arm), and every record after it is reported errors[0].code NOT_ATTEMPTED rather than omitted, so results always covers all total records and succeeded + failed === total.
validateOnly never optional [REMOVED] options.validateOnly was removed from BatchOptions in @objectstack/spec 17.0.0. It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds.