Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .changeset/validate-only-data-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
"@objectstack/spec": minor
"@objectstack/objectql": minor
"@objectstack/metadata-protocol": minor
---

feat(spec,objectql,metadata-protocol): validate-only data operation — ask for the write's verdict instead of predicting it (#6037, #4633 ruling D)

`import`'s dry run predicted the write path's verdict with a hand-copied mirror
of the engine's rules (`rest/src/import-coerce.ts`). A copy cannot structurally
keep up with the family it mirrors — ADR-0104 value shapes, `format` checks,
object-level `validations`, the state machine — so ruling D replaces prediction
with the verdict itself.

**New:** `DataProtocol.validateData(request)` returns the write path's verdict
for candidate rows and persists nothing.

```ts
const verdict = await protocol.validateData({
object: 'lead',
mode: 'insert', // or 'update', which judges only supplied keys
data: [{ first_name: 'John', email: 'not-an-email' }],
});
// → { valid: false,
// results: [{ valid: false, errors: [{ field: 'email', code: 'invalid_email', … }], warnings: [] }],
// posture: { valueShapeStrict: true, mediaValueShapeStrict: false } }
```

**Declaration and execution land together, deliberately.** `engine.validate()`
(objectql) calls the same `validateRecord` / `evaluateValidationRules` that
`insert()` calls, and `metadata-protocol` implements `validateData` on top of
it. Agreement between preview and write is therefore guaranteed by
construction, and a test asserts it directly by running both against one engine
in both postures. This is the ruling's own clause, not a style choice:
`BatchOptions.validateOnly` was retired in #4052 as a flag that promised a dry
run while the batch surfaces persisted regardless, so a caller previewing a
mutation had it EXECUTED. The new operation avoids that spelling too — the
tombstone still stands and still rejects `validateOnly`.

**The verdict is the target deployment's, not an absolute.** The response
carries the ADR-0104 `posture` it was reached under. On a self-certified
deployment a bad value shape is an error; on a warn-first one the same row is
valid and the finding appears in `warnings` with the same `code` — one finding
that changed buckets, not two vocabularies. An unconditionally-strict preview
was considered and rejected (#4633 option B): it would fail rows on every
un-migrated deployment that the write would have accepted, which teaches
authors to distrust the one gate in front of a bulk import.

Two boundaries worth knowing, both deliberate and both documented at the
implementation:

- **No hooks run.** `beforeInsert` fires before validation on the real path, so
a hook deriving a *business* field could change a verdict this does not
simulate. Firing arbitrary user hooks in a preview — mail, outbound calls,
writes to other objects — is the #4052 defect in a new spelling, so the gap is
documented rather than closed. Audit/ownership stamps are `system`/`readonly`
and validation skips them regardless.
- **Warn-first admissions are not recorded as certification evidence.** The
`#4769` sink exists so a boot cannot certify a contract it has just written
against; a preview writes nothing, so recording there would let a *preview*
block a later migration.

Additive: `validateData` is optional on `DataProtocol`, and nothing existing
changes shape. `valueShapeStrictEffective` / `mediaStrictEffective` are now
exported from objectql's record validator so the response reports the posture
that actually decided the verdict rather than the raw deployment flag.

Unblocks #4633's consumption half (rest/import adopting the operation and
retiring the `import-coerce.ts` mirror).
45 changes: 43 additions & 2 deletions content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ description: Protocol protocol schemas
## TypeScript Usage

```typescript
import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, CreateViewRequestSchema, CreateViewResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DeleteViewRequestSchema, DeleteViewResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, GetViewRequestSchema, GetViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, ListViewsRequestSchema, ListViewsResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, UpdateViewRequestSchema, UpdateViewResponseSchema } from '@objectstack/spec/api';
import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse } from '@objectstack/spec/api';
import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, CreateViewRequestSchema, CreateViewResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DeleteViewRequestSchema, DeleteViewResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, GetViewRequestSchema, GetViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, ListViewsRequestSchema, ListViewsResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, UpdateViewRequestSchema, UpdateViewResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api';
import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api';

// Validate data
const result = AiAgentCapabilitiesSchema.parse(data);
Expand Down Expand Up @@ -1614,3 +1614,44 @@ Uninstall package response

---

## ValidateDataIssue

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **field** | `string` | ✅ | The field the finding is about (`_record` for an object-level rule). |
| **code** | `string` | ✅ | Machine-readable finding code, e.g. `required`, `invalid_type`, `rule_violation`. |
| **message** | `string` | ✅ | Human-readable message — a validation rule's author-written text where one exists. |


---

## ValidateDataRequest

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **object** | `string` | ✅ | The object name. |
| **data** | `Record<string, any> \| Record<string, any>[]` | ✅ | A candidate record, or an array of them. Nothing is persisted. |
| **mode** | `Enum<'insert' \| 'update'>` | optional | Which write the verdict should predict. `insert` (default) walks every declared field, so a missing required field is a finding; `update` judges only the supplied keys, matching a PATCH. |


---

## ValidateDataResponse

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **object** | `string` | ✅ | The object name. |
| **mode** | `Enum<'insert' \| 'update'>` | ✅ | The write mode the verdict was reached for. |
| **valid** | `boolean` | ✅ | True when EVERY row is valid — the whole-set answer. |
| **results** | `{ valid: boolean; errors: { field: string; code: string; message: string }[]; warnings: { field: string; code: string; message: string }[] }[]` | ✅ | Per-row verdicts, in submission order. |
| **posture** | `{ valueShapeStrict: boolean; mediaValueShapeStrict: boolean }` | ✅ | The ADR-0104 posture the verdict was reached under — reported because it is the difference between "this row is fine" and "this row is fine HERE". The same row can be an error on a self-certified deployment and an admitted warning on an un-migrated one, and a caller explaining a verdict needs to know which it got. An unconditionally-strict preview was considered and rejected (#4633 option B): it would fail rows on every un-migrated deployment that the write would have accepted. |


---

Loading
Loading