Data Engine schema — Data Protocol reference
Defines the standard interface for data persistence engines in ObjectStack.
Data Engine Protocol
Defines the standard interface for data persistence engines in ObjectStack. This protocol abstracts the underlying storage mechanism (SQL, NoSQL, API, Memory), allowing the ObjectQL engine to execute standardized CRUD and Aggregation operations regardless of where the data resides.
The Data Engine acts as the "Driver" layer in the Hexagonal Architecture.
Source: packages/spec/src/data/data-engine.zod.ts
TypeScript Usage
import { BaseEngineOptionsSchema, DataEngineAggregateOptionsSchema, DataEngineAggregateRequestSchema, DataEngineCountOptionsSchema, DataEngineCountRequestSchema, DataEngineDeleteOptionsSchema, DataEngineDeleteRequestSchema, DataEngineExecuteRequestSchema, DataEngineFilterSchema, DataEngineFindOneRequestSchema, DataEngineFindRequestSchema, DataEngineInsertOptionsSchema, DataEngineInsertRequestSchema, DataEngineQueryOptionsSchema, DataEngineRequestSchema, DataEngineSortSchema, DataEngineUpdateOptionsSchema, DataEngineUpdateRequestSchema, DataEngineVectorFindRequestSchema, DroppedFieldsEventSchema, EngineAggregateOptionsSchema, EngineCountOptionsSchema, EngineDeleteOptionsSchema, EngineQueryOptionsSchema, EngineUpdateOptionsSchema, QueryTransportParamsSchema, QueryWithTransportSchema, ValidationAdvisoryEventSchema } from '@objectstack/spec/data';
import type { BaseEngineOptions, DataEngineAggregateOptions, DataEngineCountOptions, DataEngineDeleteOptions, DataEngineExecuteRequest, DataEngineFilter, DataEngineInsertOptions, DataEngineInsertRequest, DataEngineQueryOptions, DataEngineRequest, DataEngineSort, DataEngineUpdateOptions, DataEngineVectorFindRequest, DroppedFieldsEvent, EngineAggregateOptions, EngineCountOptions, EngineDeleteOptions, EngineQueryOptions, EngineUpdateOptions, QueryTransportParams, QueryWithTransport, ValidationAdvisoryEvent } from '@objectstack/spec/data';
// Validate data
const result = BaseEngineOptionsSchema.parse(data);BaseEngineOptions
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional |
Nested Shape: BaseEngineOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
DataEngineAggregateOptions
Options for DataEngine.aggregate operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
| groupBy | string[] | optional | |
| aggregations | { field: string; method: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; alias?: string }[] | optional |
Nested Shape: DataEngineAggregateOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
DataEngineAggregateRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'aggregate' | ✅ | |
| object | string | ✅ | |
| query | { context?: object; where?: Record<string, any> | any; groupBy?: (string | object)[]; aggregations?: object[]; … } | ✅ |
Nested Shape: DataEngineAggregateRequest.query
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| groupBy | (string | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string })[] | optional | GROUP BY targets (strings or {field, dateGranularity?} objects for date bucketing) |
| aggregations | { function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; field?: string; alias: string; filter?: any }[] | optional | |
| having | any | optional | HAVING — filter over the aggregated rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
| timezone | string | optional | |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
DataEngineCountOptions
Options for DataEngine.count operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
Nested Shape: DataEngineCountOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
DataEngineCountRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'count' | ✅ | |
| object | string | ✅ | |
| query | { context?: object; where?: Record<string, any> | any; search?: string | object; searchFields?: string[]; … } | optional |
Nested Shape: DataEngineCountRequest.query
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
DataEngineDeleteOptions
Options for DataEngine.delete operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
| multi | boolean | optional (default: false) |
Nested Shape: DataEngineDeleteOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
DataEngineDeleteRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'delete' | ✅ | |
| object | string | ✅ | |
| id | string | number | optional | ID for single delete, or use where in options |
| options | { context?: object; where?: Record<string, any> | any; multi?: boolean; filter?: Record<string, any> | any } | optional |
Nested Shape: DataEngineDeleteRequest.options
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| multi | boolean | optional (default: false) | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
DataEngineExecuteRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'execute' | ✅ | |
| command | any | ✅ | |
| options | Record<string, any> | optional |
DataEngineFilter
Data Engine query filter conditions
Union Options
This schema accepts one of the following structures:
Option 1
Type: Record<string, any>
Option 2
Reference: any
DataEngineFindOneRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'findOne' | ✅ | |
| object | string | ✅ | |
| query | { context?: object; where?: Record<string, any> | any; fields?: string[]; orderBy?: object[]; … } | optional |
Nested Shape: DataEngineFindOneRequest.query
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| fields | string[] | optional | |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | |
| limit | number | optional | |
| offset | number | optional | |
| top | number | optional | |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional | |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
| select | string[] | optional | |
| sort | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sort order definition |
| skip | integer | optional | |
| populate | string[] | optional |
DataEngineFindRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'find' | ✅ | |
| object | string | ✅ | |
| query | { context?: object; where?: Record<string, any> | any; fields?: string[]; orderBy?: object[]; … } | optional |
Nested Shape: DataEngineFindRequest.query
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| fields | string[] | optional | |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | |
| limit | number | optional | |
| offset | number | optional | |
| top | number | optional | |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional | |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
| select | string[] | optional | |
| sort | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sort order definition |
| skip | integer | optional | |
| populate | string[] | optional |
DataEngineInsertOptions
Options for DataEngine.insert operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| returning | boolean | optional (default: true) |
Nested Shape: DataEngineInsertOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
DataEngineInsertRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'insert' | ✅ | |
| object | string | ✅ | |
| data | Record<string, any> | Record<string, any>[] | ✅ | |
| options | { context?: object; returning?: boolean } | optional | Options for DataEngine.insert operations |
DataEngineQueryOptions
Query options for IDataEngine.find() operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
| select | string[] | optional | |
| sort | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sort order definition |
| limit | integer | optional | |
| skip | integer | optional | |
| top | integer | optional | |
| populate | string[] | optional |
Nested Shape: DataEngineQueryOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
DataEngineRequest
Virtual ObjectQL Request Protocol
Union Options
This schema accepts one of the following structures:
Option 1
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'find' | ✅ | |
| object | string | ✅ | |
| query | { context?: object; where?: Record<string, any> | any; fields?: string[]; orderBy?: object[]; … } | optional |
Nested Shape: DataEngineRequest[method='find'].query
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| fields | string[] | optional | |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | |
| limit | number | optional | |
| offset | number | optional | |
| top | number | optional | |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional | |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
| select | string[] | optional | |
| sort | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sort order definition |
| skip | integer | optional | |
| populate | string[] | optional |
Option 2
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'findOne' | ✅ | |
| object | string | ✅ | |
| query | { context?: object; where?: Record<string, any> | any; fields?: string[]; orderBy?: object[]; … } | optional |
Nested Shape: DataEngineRequest[method='findOne'].query
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| fields | string[] | optional | |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | |
| limit | number | optional | |
| offset | number | optional | |
| top | number | optional | |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional | |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
| select | string[] | optional | |
| sort | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sort order definition |
| skip | integer | optional | |
| populate | string[] | optional |
Option 3
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'insert' | ✅ | |
| object | string | ✅ | |
| data | Record<string, any> | Record<string, any>[] | ✅ | |
| options | { context?: object; returning?: boolean } | optional | Options for DataEngine.insert operations |
Option 4
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'update' | ✅ | |
| object | string | ✅ | |
| data | Record<string, any> | ✅ | |
| id | string | number | optional | ID for single update, or use where in options |
| options | { context?: object; where?: Record<string, any> | any; multi?: boolean; returning?: boolean; … } | optional |
Nested Shape: DataEngineRequest[method='update'].options
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| upsert | never | optional | [REMOVED] update.options.upsert was removed in @objectstack/spec 17 (ADR-0049) — it was declared and allowlisted but never implemented: no engine or driver path ever read it, so { upsert: true } was accepted and silently dropped and the update stayed a plain update. Delete the key. Express create-if-absent explicitly: a by-id update whose id names no row throws RECORD_NOT_FOUND (the by-id not-found gate) rather than inserting, so read the row first (findOne) and call insert or update on what you find. A first-class upsert, if ever built, must reconcile with that gate by design rather than through this silent flag. |
| multi | boolean | optional (default: false) | |
| returning | boolean | optional (default: false) | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
Option 5
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'delete' | ✅ | |
| object | string | ✅ | |
| id | string | number | optional | ID for single delete, or use where in options |
| options | { context?: object; where?: Record<string, any> | any; multi?: boolean; filter?: Record<string, any> | any } | optional |
Nested Shape: DataEngineRequest[method='delete'].options
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| multi | boolean | optional (default: false) | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
Option 6
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'count' | ✅ | |
| object | string | ✅ | |
| query | { context?: object; where?: Record<string, any> | any; search?: string | object; searchFields?: string[]; … } | optional |
Nested Shape: DataEngineRequest[method='count'].query
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
Option 7
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'aggregate' | ✅ | |
| object | string | ✅ | |
| query | { context?: object; where?: Record<string, any> | any; groupBy?: (string | object)[]; aggregations?: object[]; … } | ✅ |
Nested Shape: DataEngineRequest[method='aggregate'].query
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| groupBy | (string | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string })[] | optional | GROUP BY targets (strings or {field, dateGranularity?} objects for date bucketing) |
| aggregations | { function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; field?: string; alias: string; filter?: any }[] | optional | |
| having | any | optional | HAVING — filter over the aggregated rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
| timezone | string | optional | |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
Option 8
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'execute' | ✅ | |
| command | any | ✅ | |
| options | Record<string, any> | optional |
Option 9
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'vectorFind' | ✅ | |
| object | string | ✅ | |
| vector | number[] | ✅ | |
| where | Record<string, any> | any | optional | |
| fields | string[] | optional | |
| limit | integer | optional (default: 5) | |
| threshold | number | optional |
DataEngineSort
Sort order definition
Union Options
This schema accepts one of the following structures:
Option 1
Type: Record<string, Enum<'asc' | 'desc'>>
Option 2
Type: Record<string, 1 | -1>
Option 3
Type: { field: string; order?: Enum<'asc' | 'desc'> }[]
DataEngineUpdateOptions
Options for DataEngine.update operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
| upsert | never | optional | [REMOVED] update.options.upsert was removed in @objectstack/spec 17 (ADR-0049) — it was declared and allowlisted but never implemented: no engine or driver path ever read it, so { upsert: true } was accepted and silently dropped and the update stayed a plain update. Delete the key. Express create-if-absent explicitly: a by-id update whose id names no row throws RECORD_NOT_FOUND (the by-id not-found gate) rather than inserting, so read the row first (findOne) and call insert or update on what you find. A first-class upsert, if ever built, must reconcile with that gate by design rather than through this silent flag. |
| multi | boolean | optional (default: false) | |
| returning | boolean | optional (default: false) |
Nested Shape: DataEngineUpdateOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
DataEngineUpdateRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'update' | ✅ | |
| object | string | ✅ | |
| data | Record<string, any> | ✅ | |
| id | string | number | optional | ID for single update, or use where in options |
| options | { context?: object; where?: Record<string, any> | any; multi?: boolean; returning?: boolean; … } | optional |
Nested Shape: DataEngineUpdateRequest.options
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| upsert | never | optional | [REMOVED] update.options.upsert was removed in @objectstack/spec 17 (ADR-0049) — it was declared and allowlisted but never implemented: no engine or driver path ever read it, so { upsert: true } was accepted and silently dropped and the update stayed a plain update. Delete the key. Express create-if-absent explicitly: a by-id update whose id names no row throws RECORD_NOT_FOUND (the by-id not-found gate) rather than inserting, so read the row first (findOne) and call insert or update on what you find. A first-class upsert, if ever built, must reconcile with that gate by design rather than through this silent flag. |
| multi | boolean | optional (default: false) | |
| returning | boolean | optional (default: false) | |
| filter | Record<string, any> | any | optional | Data Engine query filter conditions |
DataEngineVectorFindRequest
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| method | 'vectorFind' | ✅ | |
| object | string | ✅ | |
| vector | number[] | ✅ | |
| where | Record<string, any> | any | optional | |
| fields | string[] | optional | |
| limit | integer | optional (default: 5) | |
| threshold | number | optional |
DroppedFieldsEvent
A write-path strip event: caller-supplied fields legally dropped from the payload
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object the write targeted (resolved object name) |
| fields | string[] | ✅ | Caller-supplied field names the engine removed from the write payload |
| reason | Enum<'readonly' | 'readonly_when' | 'primary_key' | 'computed'> | ✅ | 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, or a computed (formula) field no driver has a column for |
EngineAggregateOptions
QueryAST-aligned options for DataEngine.aggregate operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| groupBy | (string | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string })[] | optional | GROUP BY targets (strings or {field, dateGranularity?} objects for date bucketing) |
| aggregations | { function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; field?: string; alias: string; filter?: any }[] | optional | |
| having | any | optional | HAVING — filter over the aggregated rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
| timezone | string | optional | |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional |
Nested Shape: EngineAggregateOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
Nested Shape: EngineAggregateOptions.groupBy[number]
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field to group by |
| dateGranularity | Enum<'day' | 'week' | 'month' | 'quarter' | 'year'> | optional | Bucket date values into uniform periods (day/week/month/quarter/year) |
| alias | string | optional | Alias for the projected group value |
Nested Shape: EngineAggregateOptions.aggregations[number]
| Property | Type | Required | Description |
|---|---|---|---|
| function | Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'> | ✅ | Aggregation function |
| field | string | optional | Field to aggregate (optional for COUNT(*)) |
| alias | string | ✅ | Result column alias |
| distinct | never | optional | [REMOVED] query.aggregations[].distinct was removed in @objectstack/spec 17 (ADR-0049) — exactly ONE of the six faces that read an aggregation honoured it. The objectql in-memory fallback deduplicated the values before applying the function, while driver-sql, driver-turso, driver-mongodb, driver-memory and the service-analytics SQL builder all ignored it — so { function: 'sum', field: 'amount', distinct: true } answered a DEDUPLICATED sum when the engine fell back in memory and an ordinary sum on every SQL datasource: one query, two numbers, chosen by which backend happened to serve it. Both answers are plausible, so nothing surfaced the divergence. Delete the key. For a deduplicated COUNT the live spelling is the count_distinct aggregation function, which every SQL face compiles to COUNT(DISTINCT field) and the in-memory fallback computes identically. SUM(DISTINCT …) / AVG(DISTINCT …) get no replacement: no backend ever computed them here, and a per-row measure that needs deduplicating is a modelling problem to fix in the data, not a flag on the read. |
| filter | any | optional | Per-aggregation filter (SQL FILTER (WHERE …) semantics): narrows the source rows THIS aggregation reads, leaving sibling aggregations unfiltered. Enforced by engine.aggregate: lowered in memory for drivers without native conditional aggregation; a driver reached directly refuses rather than silently dropping it. |
Nested Shape: EngineAggregateOptions.search
| Property | Type | Required | Description |
|---|---|---|---|
| query | string | ✅ | Search query text |
| fields | string[] | optional | Fields to search in (if not specified, searches all text fields) |
| fuzzy | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only query + fields; no executor receives this flag. |
| operator | Enum<'and' | 'or'> | optional (default: "or") | [EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag. |
| boost | Record<string, number> | optional | [EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results. |
| minScore | number | optional | [EXPERIMENTAL — not enforced] Minimum relevance score threshold. No executor scores results. |
| language | string | optional | [EXPERIMENTAL — not enforced] Language for text analysis (e.g., "en", "zh", "es"). No executor selects an analyzer. |
| highlight | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Search result highlighting. No executor emits highlights. |
EngineCountOptions
QueryAST-aligned options for DataEngine.count operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional |
Nested Shape: EngineCountOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
Nested Shape: EngineCountOptions.search
| Property | Type | Required | Description |
|---|---|---|---|
| query | string | ✅ | Search query text |
| fields | string[] | optional | Fields to search in (if not specified, searches all text fields) |
| fuzzy | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only query + fields; no executor receives this flag. |
| operator | Enum<'and' | 'or'> | optional (default: "or") | [EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag. |
| boost | Record<string, number> | optional | [EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results. |
| minScore | number | optional | [EXPERIMENTAL — not enforced] Minimum relevance score threshold. No executor scores results. |
| language | string | optional | [EXPERIMENTAL — not enforced] Language for text analysis (e.g., "en", "zh", "es"). No executor selects an analyzer. |
| highlight | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Search result highlighting. No executor emits highlights. |
EngineDeleteOptions
QueryAST-aligned options for DataEngine.delete operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| multi | boolean | optional (default: false) |
Nested Shape: EngineDeleteOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
EngineQueryOptions
QueryAST-aligned query options for IDataEngine.find() operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| fields | string[] | optional | |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | |
| limit | number | optional | |
| offset | number | optional | |
| top | number | optional | |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | |
| searchFields | string[] | optional | |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
Nested Shape: EngineQueryOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
Nested Shape: EngineQueryOptions.search
| Property | Type | Required | Description |
|---|---|---|---|
| query | string | ✅ | Search query text |
| fields | string[] | optional | Fields to search in (if not specified, searches all text fields) |
| fuzzy | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only query + fields; no executor receives this flag. |
| operator | Enum<'and' | 'or'> | optional (default: "or") | [EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag. |
| boost | Record<string, number> | optional | [EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results. |
| minScore | number | optional | [EXPERIMENTAL — not enforced] Minimum relevance score threshold. No executor scores results. |
| language | string | optional | [EXPERIMENTAL — not enforced] Language for text analysis (e.g., "en", "zh", "es"). No executor selects an analyzer. |
| highlight | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Search result highlighting. No executor emits highlights. |
Nested Shape: EngineQueryOptions.expand[string]
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name (e.g. account) |
| fields | string[] | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (owner.name) is not a projection: no driver resolves one, and the ingress refuses it with 400 INVALID_FIELD. Related data is read with expand, whose nested QueryAST both filters (where) and selects (fields) the related record's columns. The projection must RETAIN the foreign-key column: fields: ['title'] with expand: 'project_id' resolves nothing, because the relation is carried by that key — add 'project_id' and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
| where | any | optional | Filtering criteria (WHERE) |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
| searchFields | string[] | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sorting instructions (ORDER BY) |
| limit | number | optional | Max records to return (LIMIT) |
| offset | number | optional | Records to skip (OFFSET) |
| top | number | optional | Alias for limit (OData compatibility) |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| joins | never | optional | [REMOVED] query.joins was removed in @objectstack/spec 17 (ADR-0049) — no engine or driver ever read it: a query carrying joins behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through expand — expand: { owner_id: { object: 'user', fields: ['name'] } } — which the engine resolves via batch $in queries, and whose nested query selects the related record's own columns. Keep the foreign key in your own projection (fields: ['title', 'owner_id']): the relation is carried by that column, so projecting it away leaves expansion nothing to resolve. A dotted fields path is NOT a replacement — no driver ever resolved one and the ingress refuses it (400 INVALID_FIELD). |
| aggregations | { function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; field?: string; alias: string; filter?: any }[] | optional | Aggregation functions |
| groupBy | (string | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string })[] | optional | GROUP BY targets (strings or {field, dateGranularity?} objects for date bucketing) |
| having | any | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
| windowFunctions | never | optional | [REMOVED] query.windowFunctions was removed in @objectstack/spec 17 (ADR-0049) — find() never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind SqlDriver.findWithWindowFunctions(object, query) (embedder-level; not on the IDataDriver contract or the REST surface); request-level analytics are aggregations + groupBy. |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (fields) and filter (where, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent limit/offset/orderBy are NOT applied on this path. |
EngineUpdateOptions
QueryAST-aligned options for DataEngine.update operations
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| context | { userId?: string; actor?: string; attributedUserId?: string; email?: string; … } | optional | |
| where | Record<string, any> | any | optional | |
| upsert | never | optional | [REMOVED] update.options.upsert was removed in @objectstack/spec 17 (ADR-0049) — it was declared and allowlisted but never implemented: no engine or driver path ever read it, so { upsert: true } was accepted and silently dropped and the update stayed a plain update. Delete the key. Express create-if-absent explicitly: a by-id update whose id names no row throws RECORD_NOT_FOUND (the by-id not-found gate) rather than inserting, so read the row first (findOne) and call insert or update on what you find. A first-class upsert, if ever built, must reconcile with that gate by design rather than through this silent flag. |
| multi | boolean | optional (default: false) | |
| returning | boolean | optional (default: false) |
Nested Shape: EngineUpdateOptions.context
| Property | Type | Required | Description |
|---|---|---|---|
| userId | string | optional | |
| actor | string | optional | |
| attributedUserId | string | optional | |
string | optional | ||
| tenantId | string | optional | |
| timezone | string | optional | |
| locale | string | optional | |
| currency | string | optional | |
| positions | string[] | optional (default: []) | |
| principalKind | Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> | optional | |
| audience | Enum<'internal' | 'external'> | optional | |
| posture | Enum<'PLATFORM_ADMIN' | 'TENANT_ADMIN' | 'MEMBER' | 'EXTERNAL'> | optional | ADR-0095 D2 posture rung — PLATFORM_ADMIN crosses the tenant wall where object posture permits; TENANT_ADMIN sees all rows in the org; MEMBER gets business RLS; EXTERNAL sees only explicitly shared rows. |
| authGate | { code: Enum<'PASSWORD_EXPIRED' | 'PASSWORD_RESET_REQUIRED' | 'MFA_REQUIRED'>; message: string } | optional | ADR-0069 authentication-policy gate: present only while the principal is blocked from protected resources until they remediate (expired password, enforced MFA), absent for every healthy session. code is the stable machine code the client branches on, from the closed vocabulary PASSWORD_EXPIRED / PASSWORD_RESET_REQUIRED (an administrator forced a password change) / MFA_REQUIRED, and message is what the blocked user reads; both are required because the transport seam renders them as the 403 body. Each code declares a scope: MFA_REQUIRED holds the account, so an API key whose owner holds it is refused with the same 403; PASSWORD_EXPIRED and PASSWORD_RESET_REQUIRED hold password sign-in alone, and API keys of that owner keep working. AUTHENTICATION, not authorization — it suspends access entirely rather than narrowing it, and nothing in the permission/RLS path reads it, while the allow-listed remediation endpoints stay reachable. Server-constructed only, never client-supplied; a guest/anonymous principal never carries one. |
| onBehalfOf | { userId: string; principalKind?: Enum<'human' | 'agent' | 'service' | 'guest' | 'system'> } | optional | |
| performedBy | { clientId: string } | optional | ADR-0090 D10 rule 4 dual attribution: the agent that PERFORMED this operation, when the performer is not the principal the write is authorized as. Set only at the /mcp OAuth door, on the same branch that decides principalKind: agent and onBehalfOf; absent everywhere else, and the absence is the record that the principal acted for itself. ATTRIBUTION ONLY — no security middleware reads it, it never becomes the authorization subject, and userId stays the human so owner-stamping and current_user.* RLS still resolve to them. Server-constructed only, never client-supplied. Surfaced to hooks as HookContext.provenance.performedByClientId and recorded by the audit writer as sys_audit_log.metadata.performed_by beside on_behalf_of; it does not move actor, which ADR-0118 D1/D5 keeps two-valued. |
| permissions | string[] | optional (default: []) | |
| systemPermissions | string[] | optional | |
| tabPermissions | Record<string, Enum<'visible' | 'hidden' | 'default_on' | 'default_off'>> | optional | |
| org_user_ids | string[] | optional | |
| accessible_org_ids | string[] | optional | |
| rlsMembership | Record<string, string[]> | optional | |
| isSystem | boolean | optional (default: false) | |
| flowRunId | string | optional | |
| skipTriggers | boolean | optional | |
| skipAutomations | boolean | optional | |
| seedReplay | boolean | optional | |
| skipStateMachine | boolean | optional | |
| preserveAudit | boolean | optional | Historical import: preserve the ORIGINAL audit timeline for this write instead of stamping it "now". Opt-in and server-constructed only, never client-supplied. On the UPDATE path it admits a whitelist — the audit/timestamp family (created_at / created_by / updated_at / updated_by) plus author-declared business readonly fields — while platform-managed system columns (tenancy, generated) stay stripped. On INSERT the exemption does NOT apply: the create-side static readonly strip runs inside engine.insert itself (after the beforeInsert hooks, before validation — the 2026-09-03 ruling; the DataProtocol ingress copy it replaced is deleted) and reads only context.isSystem, so a non-system create carrying preserveAudit still has those fields stripped and is warned (WARN) that the exemption is UPDATE-only — replaying archival readonly facts on create requires a system context. Permissions / RLS / field-level security are unaffected. |
| keptOptionValues | Record<string, string[]> | optional | Import with createMissingOptions: per field name, the option values the import kept because they matched none of the field's options. The write-path option check admits exactly these values on this write (and still refuses every other value outside the options); the field's options are not changed, and a later write that sends the field is judged against them again. Server-constructed only (set by the import runner), never client-supplied. |
| oauthScopes | string[] | optional | |
| accessToken | string | optional | |
| transaction | any | optional | |
| traceId | string | optional |
QueryTransportParams
Transport spellings of the QueryAST slots — folded to canonical keys at the boundary
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| $filter | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | null | optional | Transport spelling of where (OData $filter) |
| filters | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | null | optional | Transport spelling of where (plural of filter) |
| $top | number | string | null | optional | Transport spelling of limit (OData $top) — a number, or the digits a querystring carries it as |
| $skip | number | string | null | optional | Transport spelling of offset (OData $skip) — a number, or the digits a querystring carries it as |
| $orderby | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | null | optional | Transport spelling of orderBy (OData $orderby) |
| $select | string | string[] | null | optional | Transport spelling of fields (OData $select) |
| $expand | string | string[] | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | null | optional | Transport spelling of expand (OData $expand) |
| $search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | null | optional | Transport spelling of search (OData $search) |
| $searchFields | string | string[] | null | optional | Transport spelling of searchFields (OData $searchFields) |
| $count | boolean | Enum<'true' | 'false'> | null | optional | Transport spelling of the response total-count flag (OData $count) |
| count | boolean | Enum<'true' | 'false'> | null | optional | Response total-count flag — only an explicit false skips the COUNT query |
| filter | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | null | optional | Transport spelling of where |
| select | string | string[] | null | optional | Transport spelling of fields |
| sort | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | null | optional | Transport spelling of orderBy |
| skip | number | string | null | optional | Transport spelling of offset |
| populate | string | string[] | null | optional | Transport spelling of expand |
Nested Shape: QueryTransportParams.$expand[string]
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name (e.g. account) |
| fields | string[] | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (owner.name) is not a projection: no driver resolves one, and the ingress refuses it with 400 INVALID_FIELD. Related data is read with expand, whose nested QueryAST both filters (where) and selects (fields) the related record's columns. The projection must RETAIN the foreign-key column: fields: ['title'] with expand: 'project_id' resolves nothing, because the relation is carried by that key — add 'project_id' and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
| where | any | optional | Filtering criteria (WHERE) |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
| searchFields | string[] | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sorting instructions (ORDER BY) |
| limit | number | optional | Max records to return (LIMIT) |
| offset | number | optional | Records to skip (OFFSET) |
| top | number | optional | Alias for limit (OData compatibility) |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| joins | never | optional | [REMOVED] query.joins was removed in @objectstack/spec 17 (ADR-0049) — no engine or driver ever read it: a query carrying joins behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through expand — expand: { owner_id: { object: 'user', fields: ['name'] } } — which the engine resolves via batch $in queries, and whose nested query selects the related record's own columns. Keep the foreign key in your own projection (fields: ['title', 'owner_id']): the relation is carried by that column, so projecting it away leaves expansion nothing to resolve. A dotted fields path is NOT a replacement — no driver ever resolved one and the ingress refuses it (400 INVALID_FIELD). |
| aggregations | { function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; field?: string; alias: string; filter?: any }[] | optional | Aggregation functions |
| groupBy | (string | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string })[] | optional | GROUP BY targets (strings or {field, dateGranularity?} objects for date bucketing) |
| having | any | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
| windowFunctions | never | optional | [REMOVED] query.windowFunctions was removed in @objectstack/spec 17 (ADR-0049) — find() never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind SqlDriver.findWithWindowFunctions(object, query) (embedder-level; not on the IDataDriver contract or the REST surface); request-level analytics are aggregations + groupBy. |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (fields) and filter (where, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent limit/offset/orderBy are NOT applied on this path. |
Nested Shape: QueryTransportParams.$search
| Property | Type | Required | Description |
|---|---|---|---|
| query | string | ✅ | Search query text |
| fields | string[] | optional | Fields to search in (if not specified, searches all text fields) |
| fuzzy | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only query + fields; no executor receives this flag. |
| operator | Enum<'and' | 'or'> | optional (default: "or") | [EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag. |
| boost | Record<string, number> | optional | [EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results. |
| minScore | number | optional | [EXPERIMENTAL — not enforced] Minimum relevance score threshold. No executor scores results. |
| language | string | optional | [EXPERIMENTAL — not enforced] Language for text analysis (e.g., "en", "zh", "es"). No executor selects an analyzer. |
| highlight | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Search result highlighting. No executor emits highlights. |
QueryWithTransport
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name (e.g. account) |
| fields | string[] | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (owner.name) is not a projection: no driver resolves one, and the ingress refuses it with 400 INVALID_FIELD. Related data is read with expand, whose nested QueryAST both filters (where) and selects (fields) the related record's columns. The projection must RETAIN the foreign-key column: fields: ['title'] with expand: 'project_id' resolves nothing, because the relation is carried by that key — add 'project_id' and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
| where | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | optional | Filtering criteria (WHERE) — a filter condition, or the input-only FilterArray sugar (['status', '=', 'open']), which is lowered through parseFilterAST before the query is produced. |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
| searchFields | string[] | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sorting instructions (ORDER BY) |
| limit | number | optional | Max records to return (LIMIT) |
| offset | number | optional | Records to skip (OFFSET) |
| top | number | optional | Alias for limit (OData compatibility) |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| joins | never | optional | [REMOVED] query.joins was removed in @objectstack/spec 17 (ADR-0049) — no engine or driver ever read it: a query carrying joins behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through expand — expand: { owner_id: { object: 'user', fields: ['name'] } } — which the engine resolves via batch $in queries, and whose nested query selects the related record's own columns. Keep the foreign key in your own projection (fields: ['title', 'owner_id']): the relation is carried by that column, so projecting it away leaves expansion nothing to resolve. A dotted fields path is NOT a replacement — no driver ever resolved one and the ingress refuses it (400 INVALID_FIELD). |
| aggregations | { function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; field?: string; alias: string; filter?: any }[] | optional | Aggregation functions |
| groupBy | (string | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string })[] | optional | GROUP BY targets (strings or {field, dateGranularity?} objects for date bucketing) |
| having | any | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
| windowFunctions | never | optional | [REMOVED] query.windowFunctions was removed in @objectstack/spec 17 (ADR-0049) — find() never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind SqlDriver.findWithWindowFunctions(object, query) (embedder-level; not on the IDataDriver contract or the REST surface); request-level analytics are aggregations + groupBy. |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (fields) and filter (where, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent limit/offset/orderBy are NOT applied on this path. |
| $filter | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | null | optional | Transport spelling of where (OData $filter) |
| filters | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | null | optional | Transport spelling of where (plural of filter) |
| $top | number | string | null | optional | Transport spelling of limit (OData $top) — a number, or the digits a querystring carries it as |
| $skip | number | string | null | optional | Transport spelling of offset (OData $skip) — a number, or the digits a querystring carries it as |
| $orderby | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | null | optional | Transport spelling of orderBy (OData $orderby) |
| $select | string | string[] | null | optional | Transport spelling of fields (OData $select) |
| $expand | string | string[] | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | null | optional | Transport spelling of expand (OData $expand) |
| $search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | null | optional | Transport spelling of search (OData $search) |
| $searchFields | string | string[] | null | optional | Transport spelling of searchFields (OData $searchFields) |
| $count | boolean | Enum<'true' | 'false'> | null | optional | Transport spelling of the response total-count flag (OData $count) |
| count | boolean | Enum<'true' | 'false'> | null | optional | Response total-count flag — only an explicit false skips the COUNT query |
| filter | [string, string, any] | [string, string] | [string, object, ...object[]] | object[] | Record<string, any> | any | null | optional | Transport spelling of where |
| select | string | string[] | null | optional | Transport spelling of fields |
| sort | Record<string, Enum<'asc' | 'desc'>> | Record<string, 1 | -1> | { field: string; order?: Enum<'asc' | 'desc'> }[] | null | optional | Transport spelling of orderBy |
| skip | number | string | null | optional | Transport spelling of offset |
| populate | string | string[] | null | optional | Transport spelling of expand |
Nested Shape: QueryWithTransport.search
| Property | Type | Required | Description |
|---|---|---|---|
| query | string | ✅ | Search query text |
| fields | string[] | optional | Fields to search in (if not specified, searches all text fields) |
| fuzzy | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only query + fields; no executor receives this flag. |
| operator | Enum<'and' | 'or'> | optional (default: "or") | [EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag. |
| boost | Record<string, number> | optional | [EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results. |
| minScore | number | optional | [EXPERIMENTAL — not enforced] Minimum relevance score threshold. No executor scores results. |
| language | string | optional | [EXPERIMENTAL — not enforced] Language for text analysis (e.g., "en", "zh", "es"). No executor selects an analyzer. |
| highlight | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Search result highlighting. No executor emits highlights. |
Nested Shape: QueryWithTransport.aggregations[number]
| Property | Type | Required | Description |
|---|---|---|---|
| function | Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'> | ✅ | Aggregation function |
| field | string | optional | Field to aggregate (optional for COUNT(*)) |
| alias | string | ✅ | Result column alias |
| distinct | never | optional | [REMOVED] query.aggregations[].distinct was removed in @objectstack/spec 17 (ADR-0049) — exactly ONE of the six faces that read an aggregation honoured it. The objectql in-memory fallback deduplicated the values before applying the function, while driver-sql, driver-turso, driver-mongodb, driver-memory and the service-analytics SQL builder all ignored it — so { function: 'sum', field: 'amount', distinct: true } answered a DEDUPLICATED sum when the engine fell back in memory and an ordinary sum on every SQL datasource: one query, two numbers, chosen by which backend happened to serve it. Both answers are plausible, so nothing surfaced the divergence. Delete the key. For a deduplicated COUNT the live spelling is the count_distinct aggregation function, which every SQL face compiles to COUNT(DISTINCT field) and the in-memory fallback computes identically. SUM(DISTINCT …) / AVG(DISTINCT …) get no replacement: no backend ever computed them here, and a per-row measure that needs deduplicating is a modelling problem to fix in the data, not a flag on the read. |
| filter | any | optional | Per-aggregation filter (SQL FILTER (WHERE …) semantics): narrows the source rows THIS aggregation reads, leaving sibling aggregations unfiltered. Enforced by engine.aggregate: lowered in memory for drivers without native conditional aggregation; a driver reached directly refuses rather than silently dropping it. |
Nested Shape: QueryWithTransport.groupBy[number]
| Property | Type | Required | Description |
|---|---|---|---|
| field | string | ✅ | Field to group by |
| dateGranularity | Enum<'day' | 'week' | 'month' | 'quarter' | 'year'> | optional | Bucket date values into uniform periods (day/week/month/quarter/year) |
| alias | string | optional | Alias for the projected group value |
Nested Shape: QueryWithTransport.expand[string]
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name (e.g. account) |
| fields | string[] | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (owner.name) is not a projection: no driver resolves one, and the ingress refuses it with 400 INVALID_FIELD. Related data is read with expand, whose nested QueryAST both filters (where) and selects (fields) the related record's columns. The projection must RETAIN the foreign-key column: fields: ['title'] with expand: 'project_id' resolves nothing, because the relation is carried by that key — add 'project_id' and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
| where | any | optional | Filtering criteria (WHERE) |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
| searchFields | string[] | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sorting instructions (ORDER BY) |
| limit | number | optional | Max records to return (LIMIT) |
| offset | number | optional | Records to skip (OFFSET) |
| top | number | optional | Alias for limit (OData compatibility) |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| joins | never | optional | [REMOVED] query.joins was removed in @objectstack/spec 17 (ADR-0049) — no engine or driver ever read it: a query carrying joins behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through expand — expand: { owner_id: { object: 'user', fields: ['name'] } } — which the engine resolves via batch $in queries, and whose nested query selects the related record's own columns. Keep the foreign key in your own projection (fields: ['title', 'owner_id']): the relation is carried by that column, so projecting it away leaves expansion nothing to resolve. A dotted fields path is NOT a replacement — no driver ever resolved one and the ingress refuses it (400 INVALID_FIELD). |
| aggregations | { function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; field?: string; alias: string; filter?: any }[] | optional | Aggregation functions |
| groupBy | (string | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string })[] | optional | GROUP BY targets (strings or {field, dateGranularity?} objects for date bucketing) |
| having | any | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
| windowFunctions | never | optional | [REMOVED] query.windowFunctions was removed in @objectstack/spec 17 (ADR-0049) — find() never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind SqlDriver.findWithWindowFunctions(object, query) (embedder-level; not on the IDataDriver contract or the REST surface); request-level analytics are aggregations + groupBy. |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (fields) and filter (where, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent limit/offset/orderBy are NOT applied on this path. |
Nested Shape: QueryWithTransport.$expand[string]
| Property | Type | Required | Description |
|---|---|---|---|
| object | string | ✅ | Object name (e.g. account) |
| fields | string[] | optional | Fields to retrieve — names of the queried object's OWN columns. A dotted path (owner.name) is not a projection: no driver resolves one, and the ingress refuses it with 400 INVALID_FIELD. Related data is read with expand, whose nested QueryAST both filters (where) and selects (fields) the related record's columns. The projection must RETAIN the foreign-key column: fields: ['title'] with expand: 'project_id' resolves nothing, because the relation is carried by that key — add 'project_id' and it works. Where the value is wanted on the queried object itself, denormalise it onto that object (a stored field, written when the source changes), the same remedy the sort axis prescribes. |
| where | any | optional | Filtering criteria (WHERE) |
| search | string | { query: string; fields?: string[]; fuzzy?: boolean; operator?: Enum<'and' | 'or'>; … } | optional | Full-text search — the query text (canonical, ADR-0061 D1), or a structured FullTextSearch configuration |
| searchFields | string[] | optional | Narrow the search to these fields (server-intersected with the allowed searchable set — can only narrow, never widen; ADR-0061 D1) |
| orderBy | { field: string; order?: Enum<'asc' | 'desc'> }[] | optional | Sorting instructions (ORDER BY) |
| limit | number | optional | Max records to return (LIMIT) |
| offset | number | optional | Records to skip (OFFSET) |
| top | number | optional | Alias for limit (OData compatibility) |
| cursor | never | optional | [REMOVED] query.cursor was removed in @objectstack/spec 17 (ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; QueryBuilder.cursor() was removed with it. Express the keyset as an ordinary where predicate on your sort key — where: { created_at: { $gt: last.created_at } } with the matching orderBy — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
| joins | never | optional | [REMOVED] query.joins was removed in @objectstack/spec 17 (ADR-0049) — no engine or driver ever read it: a query carrying joins behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through expand — expand: { owner_id: { object: 'user', fields: ['name'] } } — which the engine resolves via batch $in queries, and whose nested query selects the related record's own columns. Keep the foreign key in your own projection (fields: ['title', 'owner_id']): the relation is carried by that column, so projecting it away leaves expansion nothing to resolve. A dotted fields path is NOT a replacement — no driver ever resolved one and the ingress refuses it (400 INVALID_FIELD). |
| aggregations | { function: Enum<'count' | 'sum' | 'avg' | 'min' | 'max' | 'count_distinct'>; field?: string; alias: string; filter?: any }[] | optional | Aggregation functions |
| groupBy | (string | { field: string; dateGranularity?: Enum<'day' | 'week' | 'month' | 'quarter' | 'year'>; alias?: string })[] | optional | GROUP BY targets (strings or {field, dateGranularity?} objects for date bucketing) |
| having | any | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
| windowFunctions | never | optional | [REMOVED] query.windowFunctions was removed in @objectstack/spec 17 (ADR-0049) — find() never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind SqlDriver.findWithWindowFunctions(object, query) (embedder-level; not on the IDataDriver contract or the REST surface); request-level analytics are aggregations + groupBy. |
| distinct | never | optional | [REMOVED] query.distinct was removed in @objectstack/spec 17 (ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded total/hasMore to a page-local estimate while still returning duplicate rows. Delete the key; QueryBuilder.distinct() was removed with it, and the count suppression is gone (total is truthful again). For unique values of one column use the SQL/memory drivers' distinct(object, field) door; for unique combinations, groupBy; for a deduplicated count, the count_distinct aggregation. |
| expand | Record<string, { object: string; fields?: string[]; where?: any; search?: string | object; … }> | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (fields) and filter (where, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent limit/offset/orderBy are NOT applied on this path. |
Nested Shape: QueryWithTransport.$search
| Property | Type | Required | Description |
|---|---|---|---|
| query | string | ✅ | Search query text |
| fields | string[] | optional | Fields to search in (if not specified, searches all text fields) |
| fuzzy | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Fuzzy matching (tolerate typos). The ADR-0061 expansion reads only query + fields; no executor receives this flag. |
| operator | Enum<'and' | 'or'> | optional (default: "or") | [EXPERIMENTAL — not enforced] Logical operator between terms. The ADR-0061 expansion applies its own term semantics; no executor receives this flag. |
| boost | Record<string, number> | optional | [EXPERIMENTAL — not enforced] Field-specific relevance boosting (field name -> boost factor). No executor scores results. |
| minScore | number | optional | [EXPERIMENTAL — not enforced] Minimum relevance score threshold. No executor scores results. |
| language | string | optional | [EXPERIMENTAL — not enforced] Language for text analysis (e.g., "en", "zh", "es"). No executor selects an analyzer. |
| highlight | boolean | optional (default: false) | [EXPERIMENTAL — not enforced] Search result highlighting. No executor emits highlights. |
ValidationAdvisoryEvent
An advisory (warning or info) validation-rule hit on a write that still succeeded
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| rule | string | ✅ | The advisory validation rule's declared name |
| severity | Enum<'warning' | 'info'> | ✅ | The rule's declared severity. Never error: an error rule refuses the write instead. |
| field | string | ✅ | The field the rule is about: its first declared field, or _record for an object-level rule |
| code | string | ✅ | Machine-readable finding code, the one a refused write reports for the same rule (rule_violation for a predicate rule) |
| message | string | ✅ | The rule's author-written message, in the caller's locale |