Skip to content

Commit ec796d5

Browse files
authored
feat(spec)!: retire BatchOptions.validateOnly — a dry-run flag never implemented (#4052) (#4057)
`BatchOptions.validateOnly` promised a dry-run ("validate records without persisting changes") but no batch surface ever read it — updateManyData / deleteManyData / batchData all persist regardless, so a caller sending it to PREVIEW a mutation got it executed: a declared flag lying about a data-safety guarantee (PD #10). Retired rather than half-implemented — a real no-commit batch has its own design space and should be reintroduced deliberately. - Tombstone validateOnly with retiredKey() in BatchOptionsSchema (ADR-0104); type key becomes never. - HTTP-only, so recorded as a semantic migration on protocol-18 step (batch-options-validate-only-retired), not a stack conversion. - Fix a dangling doc reference: /createMany route's requestSchema 'CreateManyRequestSchema' (never exported) → real CreateManyDataRequestSchema. - Drop the retired key from hand-written batch examples + the client-sdk Batch Options table; regenerated references; major changeset. Closes #4052.
1 parent 5dc4d02 commit ec796d5

11 files changed

Lines changed: 102 additions & 21 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: retire `BatchOptions.validateOnly` — a dry-run flag that was never implemented (#4052)
6+
7+
`BatchOptions.validateOnly` promised a dry-run — "validate records without
8+
persisting changes" — but no batch surface ever read it. `updateManyData`,
9+
`deleteManyData` and `batchData` all persist regardless, so a caller sending
10+
`options.validateOnly: true` to PREVIEW a mutation got it executed. That is the
11+
dangerous direction of "declared ≠ enforced": a flag lying about a data-safety
12+
guarantee, not merely an inert no-op.
13+
14+
There is no dry-run today. Rather than back-fill an implementation to match a
15+
promise nothing kept — a real no-commit batch has its own design space (cascade
16+
and constraint semantics under rollback, a response contract that reports each
17+
row's would-succeed verdict) — the key is retired so it can be reintroduced
18+
deliberately when there is a real need.
19+
20+
**Breaking change.**
21+
22+
- `BatchOptions.validateOnly` is a retired key. It is tombstoned (`retiredKey`)
23+
in `BatchOptionsSchema`, so authoring it now fails with a fix-it prescription
24+
rather than being silently stripped (the ADR-0104 / #3733 quiet-failure class).
25+
The `BatchOptions` type's `validateOnly` becomes `never`.
26+
- The retirement is HTTP-only (the key never appeared in stored stack metadata),
27+
so it is recorded as a semantic migration on the protocol-18 chain step
28+
(`batch-options-validate-only-retired`) — a TODO for API callers, not a stack
29+
conversion.
30+
31+
**Migration.** Stop sending `options.validateOnly` on `/batch`, `/updateMany`
32+
and `/deleteMany`. It never previewed anything; removing it changes no behaviour.
33+
If you need to validate a batch without writing, follow #4052 so a real
34+
no-commit preview can be designed.
35+
36+
Also fixes a dangling documentation reference: the `/createMany` route
37+
registration named `requestSchema: 'CreateManyRequestSchema'`, a schema no
38+
module ever exported — pointed at the real `CreateManyDataRequestSchema`.

content/docs/api/client-sdk.mdx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,6 @@ const batchResult = await client.data.batch('account', {
225225
atomic: true,
226226
returnRecords: true,
227227
continueOnError: false,
228-
validateOnly: false,
229228
},
230229
});
231230

@@ -499,7 +498,6 @@ The `find` method accepts an options object with **canonical** (recommended) fie
499498
| `atomic` | `boolean` | `true` | Rollback entire batch on any failure |
500499
| `returnRecords` | `boolean` | `false` | Include full records in response |
501500
| `continueOnError` | `boolean` | `false` | Continue after errors (when atomic=false) |
502-
| `validateOnly` | `boolean` | `false` | Dry-run mode — validate without persisting |
503501

504502
---
505503

content/docs/api/data-api.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,7 @@ Execute a batch operation (create / update / upsert / delete) on multiple record
9292
"options": {
9393
"atomic": true,
9494
"returnRecords": true,
95-
"continueOnError": false,
96-
"validateOnly": false
95+
"continueOnError": false
9796
}
9897
}
9998
```

content/docs/api/wire-format.mdx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,8 +484,7 @@ Process many records of a **single** operation type in one request. The body car
484484
],
485485
"options": {
486486
"atomic": true,
487-
"continueOnError": false,
488-
"validateOnly": false
487+
"continueOnError": false
489488
}
490489
}
491490
```

content/docs/references/api/batch.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const result = BatchConfig.parse(data);
4747
| :--- | :--- | :--- | :--- |
4848
| **enabled** | `boolean` || Enable batch operations |
4949
| **maxRecordsPerBatch** | `integer` || Maximum records per batch |
50-
| **defaultOptions** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly: boolean }` | optional | Default batch options |
50+
| **defaultOptions** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Default batch options |
5151

5252

5353
---
@@ -89,7 +89,7 @@ const result = BatchConfig.parse(data);
8989
| **atomic** | `boolean` || If true, rollback entire batch on any failure (transaction mode) |
9090
| **returnRecords** | `boolean` || If true, return full record data in response |
9191
| **continueOnError** | `boolean` || If true (and atomic=false), continue processing remaining records after errors |
92-
| **validateOnly** | `boolean` | | If true, validate records without persisting changes (dry-run mode) |
92+
| **validateOnly** | `any` | optional | [REMOVED] `options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). 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. |
9393

9494

9595
---
@@ -115,7 +115,7 @@ const result = BatchConfig.parse(data);
115115
| :--- | :--- | :--- | :--- |
116116
| **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` || Type of batch operation |
117117
| **records** | `{ id?: string; data?: Record<string, any>; externalId?: string }[]` || Array of records to process (server caps the count — see batch.maxBatchSize) |
118-
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly: boolean }` | optional | Batch operation options |
118+
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Batch operation options |
119119

120120

121121
---
@@ -199,7 +199,7 @@ A cross-object batch strip event: dropped fields plus the operation index
199199
| Property | Type | Required | Description |
200200
| :--- | :--- | :--- | :--- |
201201
| **ids** | `string[]` || Array of record IDs to delete (server caps the count — see batch.maxBatchSize) |
202-
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly: boolean }` | optional | Delete options |
202+
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Delete options |
203203

204204

205205
---
@@ -223,7 +223,7 @@ A cross-object batch strip event: dropped fields plus the operation index
223223
| Property | Type | Required | Description |
224224
| :--- | :--- | :--- | :--- |
225225
| **records** | `{ id: string; data: Record<string, any> }[]` || Array of records to update (server caps the count — see batch.maxBatchSize) |
226-
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly: boolean }` | optional | Update options |
226+
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Update options |
227227

228228

229229
---

content/docs/references/api/protocol.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ const result = AiAgentCapabilities.parse(data);
448448
| Property | Type | Required | Description |
449449
| :--- | :--- | :--- | :--- |
450450
| **ids** | `string[]` || Array of record IDs to delete (server caps the count — see batch.maxBatchSize) |
451-
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly: boolean }` | optional | Delete options |
451+
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Delete options |
452452
| **object** | `string` || Object name |
453453

454454

@@ -1404,7 +1404,7 @@ const result = AiAgentCapabilities.parse(data);
14041404
| Property | Type | Required | Description |
14051405
| :--- | :--- | :--- | :--- |
14061406
| **records** | `{ id: string; data: Record<string, any> }[]` || Array of records to update (server caps the count — see batch.maxBatchSize) |
1407-
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly: boolean }` | optional | Update options |
1407+
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Update options |
14081408
| **object** | `string` || Object name |
14091409

14101410

packages/spec/authorable-surface.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,7 @@
682682
"api/BatchOptions:atomic",
683683
"api/BatchOptions:continueOnError",
684684
"api/BatchOptions:returnRecords",
685-
"api/BatchOptions:validateOnly",
685+
"api/BatchOptions:validateOnly [RETIRED]",
686686
"api/BatchRecord:data",
687687
"api/BatchRecord:externalId",
688688
"api/BatchRecord:id",

packages/spec/src/api/batch.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,28 @@ describe('BatchOptionsSchema', () => {
5757
expect(options.atomic).toBe(true);
5858
expect(options.returnRecords).toBe(false);
5959
expect(options.continueOnError).toBe(false);
60-
expect(options.validateOnly).toBe(false);
6160
});
6261

6362
it('should accept custom options', () => {
6463
const options = BatchOptionsSchema.parse({
6564
atomic: false,
6665
returnRecords: true,
6766
continueOnError: true,
68-
validateOnly: true,
6967
});
7068

7169
expect(options.atomic).toBe(false);
7270
expect(options.returnRecords).toBe(true);
7371
expect(options.continueOnError).toBe(true);
74-
expect(options.validateOnly).toBe(true);
72+
});
73+
74+
it('rejects the retired `validateOnly` key with its prescription (#3963 follow-up)', () => {
75+
// Never implemented — a "dry-run" that silently persisted. Tombstoned so
76+
// writing it is audible rather than silently stripped (ADR-0104 / PD #10).
77+
const result = BatchOptionsSchema.safeParse({ validateOnly: true });
78+
expect(result.success).toBe(false);
79+
if (!result.success) {
80+
expect(result.error.issues[0]?.message).toMatch(/validateOnly.*removed|never implemented/i);
81+
}
7582
});
7683
});
7784

packages/spec/src/api/batch.zod.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { z } from 'zod';
44
import { ApiErrorSchema, BaseResponseSchema, RecordDataSchema } from './contract.zod';
55
import { DroppedFieldsEventSchema } from '../data/data-engine.zod';
6+
import { retiredKey } from '../shared/retired-key';
67

78
/**
89
* Batch Operations API
@@ -61,7 +62,22 @@ export const BatchOptionsSchema = lazySchema(() => z.object({
6162
atomic: z.boolean().optional().default(true).describe('If true, rollback entire batch on any failure (transaction mode)'),
6263
returnRecords: z.boolean().optional().default(false).describe('If true, return full record data in response'),
6364
continueOnError: z.boolean().optional().default(false).describe('If true (and atomic=false), continue processing remaining records after errors'),
64-
validateOnly: z.boolean().optional().default(false).describe('If true, validate records without persisting changes (dry-run mode)'),
65+
// `validateOnly` promised a dry-run — "validate records without persisting" —
66+
// but no batch surface ever read it (`updateManyData` / `deleteManyData` /
67+
// `batchData` all persist regardless). A caller sending `validateOnly: true`
68+
// to preview a mutation got it EXECUTED: a declared flag actively lying about
69+
// a data-safety guarantee, the worst shape of "declared ≠ enforced" (PD #10).
70+
// Retired rather than half-implemented: a real dry-run has its own design
71+
// space (cascade / constraint semantics under no-commit, response contract)
72+
// and should be reintroduced deliberately, not back-filled to match a promise
73+
// nothing kept. Tombstoned so writing it is audible, not silently stripped.
74+
validateOnly: retiredKey(
75+
'`options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). '
76+
+ 'It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have '
77+
+ 'silently executed. There is no dry-run today — drop the key. If you need to preview a batch '
78+
+ 'without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) '
79+
+ 'and reintroduced as a flag that actually holds.',
80+
),
6581
}));
6682

6783
export type BatchOptions = z.infer<typeof BatchOptionsSchema>;

packages/spec/src/api/plugin-rest-api.zod.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,10 @@ export const DEFAULT_BATCH_ROUTES: RestApiRouteRegistration = {
915915
summary: 'Batch create',
916916
description: 'Create multiple records in a single operation',
917917
tags: ['Batch'],
918-
requestSchema: 'CreateManyRequestSchema',
918+
// Was 'CreateManyRequestSchema' — a name no schema ever exported (the real
919+
// request contract is CreateManyDataRequestSchema in protocol.zod.ts). A
920+
// dangling documentation reference; point it at the schema that exists.
921+
requestSchema: 'CreateManyDataRequestSchema',
919922
responseSchema: 'BatchUpdateResponseSchema',
920923
permissions: ['data.create', 'data.batch'],
921924
timeout: 60000,

0 commit comments

Comments
 (0)