From 8720de1842d5c30a0231a86388cc4ac70ebaad11 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 15:22:04 +0000 Subject: [PATCH 1/8] feat(spec): thank-you panel record slot and public-form submit answer shape (wip) Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX --- packages/spec/src/api/protocol.zod.ts | 30 +++++ .../api/public-form-submit-response.test.ts | 35 ++++++ .../src/type-alias-convention.pin.test.ts | 1 + .../view-submit-thank-you-record-slot.test.ts | 112 ++++++++++++++++++ packages/spec/src/ui/view.zod.ts | 106 ++++++++++++++++- 5 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 packages/spec/src/api/public-form-submit-response.test.ts create mode 100644 packages/spec/src/ui/view-submit-thank-you-record-slot.test.ts diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index 50c901126a1..d1519febc26 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -2692,6 +2692,34 @@ export const CreateDataResponseSchema = lazySchema(() => z.object({ notice: z.string().min(1).optional().describe(WRITE_ANSWER_NOTICE_DESCRIPTION), })); +/** + * Public Form Submit Response (#28005) + * + * The WHOLE body of `POST /forms/:slug/submit`, the anonymous public-form + * submission: the created record's `id`, plus — only when the form's + * `thank-you` panel names fields of the record as `{{record.field_name}}` — + * `fields`, holding exactly those fields' stored values. + * + * The answer echoes nothing else the insert stored, and that is the point of + * the shape. The stored row can carry values a `beforeInsert` / `afterInsert` + * hook derived, an elevated (`runAs: 'system'`) one included, from records + * the anonymous form grant may never read. The panel may therefore name only + * fields the ENGINE generates on insert (`RUNTIME_OWNED_FIELD_TYPES`, today + * `autonumber`); the authoring rule that judges a form's names refuses any + * other, and `thankYouRecordFieldNames` (`ui/view.zod.ts`) is the one reading + * of which names a panel holds. Strict, so a key beyond these two is a + * conformance failure rather than a silent widening of what an anonymous + * caller is shown. + */ +export const PublicFormSubmitResponseSchema = lazySchema(() => z.object({ + id: z.string().describe('The ID of the newly created record.'), + fields: z.record(z.string(), z.union([z.string(), z.number()])).optional().describe( + 'The stored values of exactly the fields the form\'s `thank-you` panel names as `{{record.field_name}}`, ' + + 'keyed by field name — each one a field the engine generates on insert (an `autonumber` field, e.g. a case ' + + 'number). Present only when the panel names at least one; never any other field of the record.', + ), +}).strict()); + /** * Clone Data Response (#11924) * @@ -4066,6 +4094,8 @@ export type GetDataRequest = z.input; export type GetDataResponse = z.input; export type CreateDataRequest = z.input; export type CreateDataResponse = z.input; +/** The `POST /forms/:slug/submit` body — see {@link PublicFormSubmitResponseSchema} (#28005). */ +export type PublicFormSubmitResponse = z.input; /** The `POST /data/:object/:id/clone` body — see {@link CloneDataResponseSchema} (#11924). */ export type CloneDataResponse = z.input; export type ValidateDataIssue = z.input; diff --git a/packages/spec/src/api/public-form-submit-response.test.ts b/packages/spec/src/api/public-form-submit-response.test.ts new file mode 100644 index 00000000000..5f58d6a60b8 --- /dev/null +++ b/packages/spec/src/api/public-form-submit-response.test.ts @@ -0,0 +1,35 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #28005 (ruling B) — the anonymous public-form submit answer: `id`, plus the + * engine-generated fields the form's `thank-you` panel names, and nothing else. + */ + +import { describe, it, expect } from 'vitest'; + +import { PublicFormSubmitResponseSchema } from './protocol.zod'; + +describe('PublicFormSubmitResponseSchema', () => { + it('accepts the id-only answer', () => { + expect(PublicFormSubmitResponseSchema.safeParse({ id: 'rec_1' }).success).toBe(true); + }); + + it('accepts the id plus the named fields', () => { + const answer = { id: 'rec_1', fields: { case_number: 'CASE-00042' } }; + const r = PublicFormSubmitResponseSchema.safeParse(answer); + expect(r.success).toBe(true); + expect(r.data).toEqual(answer); + }); + + it('refuses any other top-level key — the stored record never rides along', () => { + const r = PublicFormSubmitResponseSchema.safeParse({ id: 'rec_1', record: { subject: 'x' } }); + expect(r.success).toBe(false); + expect(r.error?.issues[0]?.code).toBe('unrecognized_keys'); + }); + + it('refuses an answer without an id, and a non-scalar field value', () => { + expect(PublicFormSubmitResponseSchema.safeParse({ fields: { case_number: 'C-1' } }).success).toBe(false); + expect(PublicFormSubmitResponseSchema.safeParse({ id: 'r', fields: { case_number: { v: 1 } } }).success) + .toBe(false); + }); +}); diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index 560ea561e21..fe5fc763f3a 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -616,6 +616,7 @@ export type Iso_api_protocol__MarkNotificationsReadRequestSchema = Assert, z.infer< typeof M28.MarkNotificationsReadResponseSchema > >>; export type Iso_api_protocol__MetadataAuthoringClientSchema = Assert, z.infer< typeof M28.MetadataAuthoringClientSchema > >>; export type Iso_api_protocol__MetadataAuthoringSurfaceSchema = Assert, z.infer< typeof M28.MetadataAuthoringSurfaceSchema > >>; +export type Iso_api_protocol__PublicFormSubmitResponseSchema = Assert, z.infer< typeof M28.PublicFormSubmitResponseSchema > >>; export type Iso_api_protocol__PublishMetaItemRequestSchema = Assert, z.infer< typeof M28.PublishMetaItemRequestSchema > >>; export type Iso_api_protocol__PublishMetaItemResponseSchema = Assert, z.infer< typeof M28.PublishMetaItemResponseSchema > >>; export type Iso_api_protocol__PublishPackageDraftsResponseSchema = Assert, z.infer< typeof M28.PublishPackageDraftsResponseSchema > >>; diff --git a/packages/spec/src/ui/view-submit-thank-you-record-slot.test.ts b/packages/spec/src/ui/view-submit-thank-you-record-slot.test.ts new file mode 100644 index 00000000000..a44aedd6d4b --- /dev/null +++ b/packages/spec/src/ui/view-submit-thank-you-record-slot.test.ts @@ -0,0 +1,112 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #28005 (ruling B) — the `thank-you` panel's record slot, pinned at the schema + * door. + * + * The panel's `title` / `message` take the redirect's token, + * `{{record.field_name}}`, and nothing else spelled with a brace. Parse time + * judges the SHAPE only; which field a token may name (engine-generated only) + * is the lint rule's, where the form's object is known + * (`@objectstack/lint` `validate-thank-you-record-fields.test.ts`). + * + * Each refusal pins the guidance an author reads — the offending token and the + * one spelling to write — not merely that the parse failed. + */ + +import { describe, it, expect } from 'vitest'; +import type { z } from 'zod'; + +import { FormViewSchema, thankYouRecordFieldNames } from './view.zod'; +import { projectPublishedJsonSchema } from '../../scripts/lib/refinement-projection'; + +const FORM_BASE = { type: 'simple', sections: [{ fields: ['subject'] }] }; + +function form(panel: Record): unknown { + return { ...FORM_BASE, submitBehavior: { kind: 'thank-you', ...panel } }; +} + +function accept(value: unknown): void { + const r = FormViewSchema.safeParse(value); + expect(r.success, `expected ACCEPTANCE, got ${JSON.stringify(r.error?.issues ?? '')}`).toBe(true); +} + +/** Reject `value` and return its issues, each a path + message. */ +function reject(value: unknown): Array<{ path: PropertyKey[]; message: string }> { + const r = FormViewSchema.safeParse(value); + expect(r.success, `expected REJECTION, got a successful parse of ${JSON.stringify(value)}`).toBe(false); + return (r.error?.issues ?? []).map((i) => ({ path: i.path, message: i.message })); +} + +describe('thank-you panel text takes `{{record.field_name}}` and no other brace', () => { + it('a message naming a case number parses, and the text arrives verbatim', () => { + const value = form({ title: 'Thanks', message: 'Your case number is {{record.case_number}}.' }); + accept(value); + const parsed = FormViewSchema.parse(value) as { submitBehavior?: { message?: string } }; + expect(parsed.submitBehavior?.message).toBe('Your case number is {{record.case_number}}.'); + }); + + it('a title token, several tokens, and text with no token all parse', () => { + accept(form({ title: 'Case {{record.case_number}} received' })); + accept(form({ message: '{{record.case_number}} / {{record.ticket_no}}' })); + accept(form({ title: 'Thank you!', message: 'We will be in touch.' })); + accept(form({})); + }); + + it.each([ + ['single braces', 'Your case is {record.case_number}', '{record.case_number}'], + ['spaces inside the braces', 'Your case is {{ record.case_number }}', '{{ record.case_number }}'], + ['no `record.` root', 'Your case is {{case_number}}', '{{case_number}}'], + ['an uppercase field segment', 'Your case is {{record.CaseNumber}}', '{{record.CaseNumber}}'], + ['a dotted path', 'Your case is {{record.account.name}}', '{{record.account.name}}'], + ['a stray closing brace', 'Done }', '}'], + ])('refuses %s, naming the offender and the one spelling', (_label, text, offender) => { + const issues = reject(form({ message: text })); + expect(issues).toHaveLength(1); + expect(issues[0].path).toEqual(['submitBehavior', 'message']); + expect(issues[0].message).toContain(`\`${offender}\` is not that shape`); + expect(issues[0].message).toContain('`{{record.field_name}}`'); + }); + + it('refuses a malformed title on the title, and names the member', () => { + const issues = reject(form({ title: 'Case {{record.Case}}' })); + expect(issues).toHaveLength(1); + expect(issues[0].path).toEqual(['submitBehavior', 'title']); + expect(issues[0].message).toMatch(/^A `thank-you` panel's `title`/); + }); + + it('the refusal carries no tracker id and no ruling date', () => { + const [issue] = reject(form({ message: '{case}' })); + expect(issue.message).not.toMatch(/#\d{3,6}|\b20\d\d-\d\d-\d\d\b|\bruling\b/i); + }); + + it('the published JSON Schema carries the shape as a `pattern` (no dropped refinement)', () => { + const json = JSON.stringify( + projectPublishedJsonSchema(FormViewSchema as unknown as z.ZodType, { io: 'input', unrepresentable: 'any' }), + ); + expect(json).toContain(JSON.stringify('^(?:[^{}]|\\{\\{record\\.[a-z_][a-z0-9_]*\\}\\})*$')); + }); +}); + +describe('thankYouRecordFieldNames — the one reading of which fields a panel names', () => { + it('reads title then message, each name once, in order of first appearance', () => { + expect(thankYouRecordFieldNames({ + kind: 'thank-you', + title: 'Case {{record.case_number}}', + message: '{{record.ticket_no}} and again {{record.case_number}}', + })).toEqual(['case_number', 'ticket_no']); + }); + + it('answers [] for any other kind, no panel text, no token, and non-objects', () => { + expect(thankYouRecordFieldNames({ kind: 'redirect', url: '/thanks?c={{record.case_number}}' })).toEqual([]); + expect(thankYouRecordFieldNames({ kind: 'thank-you' })).toEqual([]); + expect(thankYouRecordFieldNames({ kind: 'thank-you', message: 'Thanks!' })).toEqual([]); + expect(thankYouRecordFieldNames(undefined)).toEqual([]); + expect(thankYouRecordFieldNames('thank-you')).toEqual([]); + }); + + it('reads only well-formed tokens', () => { + expect(thankYouRecordFieldNames({ kind: 'thank-you', message: '{{ record.a }} {record.b} {{record.c}}' })) + .toEqual(['c']); + }); +}); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 05c15dc948a..ec80c3e279d 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -4470,6 +4470,88 @@ function checkSubmitRedirectUrl(raw: string): string | undefined { return undefined; } +/** + * ## The `thank-you` panel's record slot + * + * A public form's visitor often needs a reference once the form is sent — a + * web-to-case visitor needs the case number. The panel's `title` and `message` + * therefore take the SAME token the `redirect` member takes, + * `{{record.field_name}}` ({@link SUBMIT_REDIRECT_URL_TOKEN_RE}): one grammar + * for "a field of the record just submitted", never a second dialect. + * + * What the token may NAME is narrower than for a redirect. The anonymous submit + * answer echoes nothing the insert stored, because a `beforeInsert` / + * `afterInsert` hook — an elevated one included — can derive a stored value from + * records the anonymous form grant may never read. The panel may therefore name + * only a field whose value the ENGINE generates on insert: a field whose type is + * in `RUNTIME_OWNED_FIELD_TYPES` (`data/field.zod.ts`, today `autonumber`), the + * one statement of that set. The submit answer carries `id` plus exactly those + * named fields (`PublicFormSubmitResponseSchema`, `api/protocol.zod.ts`). + * + * Parse time knows the string and not the object, so the schema enforces the + * token SHAPE only — a malformed token is refused on its own terms, as the + * redirect refine does. Whether the named field is engine-generated is judged + * where the form's object is known: `@objectstack/lint`'s + * `validateThankYouRecordFields`, a gating authoring rule (`os validate` / + * `os build` / `os lint`, and the runtime publish gate on a `view` or `page` + * write). + */ +// The ruling is #28005 on the tracker, option B, ruled 2026-10-31 (ruling +// record 6545210989). Internal readers get the id and the date here; the +// customer-facing sentences carry neither (#22093). + +/** + * A `thank-you` panel text in which every brace belongs to a well-formed + * `{{record.field_name}}` token — the token of {@link SUBMIT_REDIRECT_URL_TOKEN_RE}, + * spelled again only because a `pattern` must be anchored and repeated. The two + * alternatives start on disjoint characters, so matching is linear. + */ +const THANK_YOU_TEXT_RE = /^(?:[^{}]|\{\{record\.[a-z_][a-z0-9_]*\}\})*$/; + +/** + * Word the refusal for one `thank-you` panel text that fails + * {@link THANK_YOU_TEXT_RE}. Returns the author-facing refusal, or `undefined` + * when every brace in it belongs to a well-formed `{{record.field_name}}` token. + */ +function checkThankYouText(raw: string, member: 'title' | 'message'): string | undefined { + const withoutTokens = raw.replace(SUBMIT_REDIRECT_URL_TOKEN_RE, ''); + if (!withoutTokens.includes('{') && !withoutTokens.includes('}')) return undefined; + const offender = withoutTokens.match(/\{\{?[^{}]*\}?\}?/)?.[0] ?? '}'; + return `A \`thank-you\` panel's \`${member}\` names a field of the submitted record ONLY as ` + + `\`{{record.field_name}}\` — \`${offender}\` is not that shape. Write the field's name in lowercase ` + + 'snake_case after `record.`, with no spaces inside the braces (`{{record.case_number}}`). Braces ' + + 'belong to that token alone, so reword text that needs a literal brace. Only a field the engine ' + + 'generates on insert (an `autonumber` field) may be named.'; +} + +/** + * The field names a `submitBehavior` block's `thank-you` panel interpolates as + * `{{record.field_name}}`, across `title` then `message`, each named once in + * order of first appearance. Any other kind, or a panel with no token, answers + * `[]`. + * + * This is the one reading of which fields the panel names: the authoring rule + * judges exactly these names, and the anonymous submit answer carries exactly + * these fields (`PublicFormSubmitResponseSchema.fields`). A pure function of the + * block — no schema, no object — so a server door and a renderer call the same + * code instead of each re-reading the token. + */ +export function thankYouRecordFieldNames(submitBehavior: unknown): string[] { + if (!submitBehavior || typeof submitBehavior !== 'object') return []; + const block = submitBehavior as { kind?: unknown; title?: unknown; message?: unknown }; + if (block.kind !== 'thank-you') return []; + const names: string[] = []; + for (const text of [block.title, block.message]) { + if (typeof text !== 'string') continue; + for (const token of text.matchAll(SUBMIT_REDIRECT_URL_TOKEN_RE)) { + // `{{record.` is 9 characters and `}}` is 2: the field segment between. + const name = token[0].slice(9, -2); + if (!names.includes(name)) names.push(name); + } + } + return names; +} + /** * ## Form-view predicates may not name the `features.*` scope root * @@ -4931,8 +5013,28 @@ export const FormViewSchema = lazySchema(() => strictObject({ aliases: { heading: 'title', text: 'message', body: 'message', description: 'message' }, }, { kind: z.literal('thank-you'), - title: z.string().optional(), - message: z.string().optional(), + // [#28005] `z.string()` stays the type, as on the redirect's `url`: the + // renderer receives the text the author wrote and fills each token from + // the submit answer. The shape is a `.regex()`, not a refine, so the + // published JSON Schema carries it as a `pattern` too; `checkThankYouText` + // words the refusal, and `thankYouRecordFieldNames` reads which fields a + // text names. + title: z.string() + .regex(THANK_YOU_TEXT_RE, { error: (issue) => checkThankYouText(String(issue.input), 'title') }) + .optional() + .describe( + 'Heading of the confirmation panel. May name a field of the submitted record as ' + + '`{{record.field_name}}`, and only a field the engine generates on insert (an `autonumber` ' + + 'field, e.g. a case number): a hook-derived value never reaches an anonymous visitor.', + ), + message: z.string() + .regex(THANK_YOU_TEXT_RE, { error: (issue) => checkThankYouText(String(issue.input), 'message') }) + .optional() + .describe( + 'Body of the confirmation panel. May name a field of the submitted record as ' + + '`{{record.field_name}}`, and only a field the engine generates on insert (an `autonumber` ' + + 'field, e.g. a case number): a hook-derived value never reaches an anonymous visitor.', + ), }), strictObject({ surface: 'this `redirect` submit behavior', From cc6bcee38bfadb907a63551af0d85c14918b2c3c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 15:23:40 +0000 Subject: [PATCH 2/8] feat(lint): thank-you panel names only engine-generated fields (wip) Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX --- packages/lint/src/authoring-rules.ts | 20 ++ packages/lint/src/index.ts | 2 + .../validate-thank-you-record-fields.test.ts | 197 ++++++++++++++++ .../src/validate-thank-you-record-fields.ts | 211 ++++++++++++++++++ 4 files changed, 430 insertions(+) create mode 100644 packages/lint/src/validate-thank-you-record-fields.test.ts create mode 100644 packages/lint/src/validate-thank-you-record-fields.ts diff --git a/packages/lint/src/authoring-rules.ts b/packages/lint/src/authoring-rules.ts index eeeff688735..5a7b522f5e6 100644 --- a/packages/lint/src/authoring-rules.ts +++ b/packages/lint/src/authoring-rules.ts @@ -129,6 +129,7 @@ import { validateFieldConsumers } from './validate-field-consumers.js'; import { validateSemanticRoles } from './validate-semantic-roles.js'; import { validateFormLayout } from './validate-form-layout.js'; import { validatePublicFormFileFields } from './validate-public-form-file-fields.js'; +import { validateThankYouRecordFields } from './validate-thank-you-record-fields.js'; import { validateSeedReplaySafety } from './validate-seed-replay-safety.js'; import { validateAssignmentRules } from './validate-assignment-rules.js'; import { validateSeedStateMachine } from './validate-seed-state-machine.js'; @@ -1612,6 +1613,25 @@ export const AUTHORING_RULES: readonly AuthoringRule[] = [ runtimeTypes: ['view'], run: (stack) => validatePublicFormFileFields(stack), }, + // Ruling B on #28005 — a form's `thank-you` panel may name, as + // `{{record.field_name}}`, only a field the engine generates on insert + // (`RUNTIME_OWNED_FIELD_TYPES`): any other stored value may come from a hook + // the anonymous form grant cannot read, and the public submit answer never + // carries it. Gating, so all three commands run it. + // + // Runtime publish gate: a `view` or `page` write's snapshot carries + // `objects` as context, which is every input this rule reads. `object` is + // not declared, for the reason the file-field rule above gives. + { + name: 'validateThankYouRecordFields', + tier: 'gating', + input: 'parsed', + commands: ALL, + source: 'packages/lint/src/validate-thank-you-record-fields.ts', + surfaces: CLI_AND_RUNTIME, + runtimeTypes: ['view', 'page'], + run: (stack) => validateThankYouRecordFields(stack), + }, // ADR-0078 Phase 3 (Tier-A `action-locations`) — an action that declares no // `locations` and that no view places by name renders on no surface at all. // objectui#3142 made that measurable: four renderers used to show an diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts index cc04ec408cd..254bde79297 100644 --- a/packages/lint/src/index.ts +++ b/packages/lint/src/index.ts @@ -268,6 +268,8 @@ export type { FormLayoutFinding, FormLayoutSeverity } from './validate-form-layo // A form open to anonymous intake that declares a file field (ruling C on #25572). export { validatePublicFormFileFields, PUBLIC_FORM_FILE_FIELD } from './validate-public-form-file-fields.js'; export type { PublicFormFileFieldFinding } from './validate-public-form-file-fields.js'; +export { validateThankYouRecordFields, THANK_YOU_RECORD_FIELD } from './validate-thank-you-record-fields.js'; +export type { ThankYouRecordFieldFinding } from './validate-thank-you-record-fields.js'; export { validateVisibilityPredicates, diff --git a/packages/lint/src/validate-thank-you-record-fields.test.ts b/packages/lint/src/validate-thank-you-record-fields.test.ts new file mode 100644 index 00000000000..d5064392f04 --- /dev/null +++ b/packages/lint/src/validate-thank-you-record-fields.test.ts @@ -0,0 +1,197 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Ruling B on #28005: a form's thank-you panel may name, as +// `{{record.field_name}}`, only a field the engine generates on insert +// (`RUNTIME_OWNED_FIELD_TYPES`). Pins the stack rule the three CLI commands run, +// the runtime publish gate on `view` and `page` writes, and that the admitted +// set is the spec's one set and no list of this rule's own. + +import { readFileSync } from 'node:fs'; + +import { describe, expect, it } from 'vitest'; + +import { RUNTIME_OWNED_FIELD_TYPES } from '@objectstack/spec/data'; + +import { AUTHORING_RULES } from './authoring-rules.js'; +import { runRuntimeAuthoringRules } from './runtime-gate.js'; +import { THANK_YOU_RECORD_FIELD, validateThankYouRecordFields } from './validate-thank-you-record-fields.js'; + +const support = { + name: 'support_case', + label: 'Support case', + fields: { + case_number: { type: 'autonumber', label: 'Case number', autonumberFormat: 'CASE-{0000}' }, + subject: { type: 'text', label: 'Subject' }, + priority_score: { type: 'formula', label: 'Priority score', expression: '1' }, + }, +}; + +const panel = (message: string) => ({ kind: 'thank-you', title: 'Thank you', message }); + +const formOf = (message: string) => ({ + type: 'simple', + data: { provider: 'object', object: 'support_case' }, + sections: [{ name: 'about', fields: ['subject'] }], + submitBehavior: panel(message), +}); + +const stackOf = (view: Record, objects: unknown = [support]) => ({ objects, views: [view] }); + +describe('validateThankYouRecordFields — the stack rule the CLI commands run', () => { + it('admits an autonumber field', () => { + expect(validateThankYouRecordFields(stackOf({ + name: 'case_views', + form: formOf('Your case number is {{record.case_number}}.'), + }))).toEqual([]); + }); + + it('refuses a text field, naming the field, its type and the reason', () => { + const findings = validateThankYouRecordFields(stackOf({ + name: 'case_views', + formViews: { intake: formOf('We got {{record.subject}} as {{record.case_number}}') }, + })); + expect(findings).toHaveLength(1); + expect(findings[0]).toMatchObject({ + severity: 'error', + rule: THANK_YOU_RECORD_FIELD, + where: 'view "case_views" · formViews.intake', + path: 'views[0].formViews.intake.submitBehavior', + }); + expect(findings[0].message).toContain('{{record.subject}}'); + expect(findings[0].message).toContain('a text field'); + expect(findings[0].message).toContain('hook'); + }); + + it('refuses a formula field the same way', () => { + const findings = validateThankYouRecordFields(stackOf({ name: 'v', form: formOf('{{record.priority_score}}') })); + expect(findings).toHaveLength(1); + expect(findings[0].message).toContain('{{record.priority_score}}'); + expect(findings[0].message).toContain('a formula field'); + }); + + it('refuses a name the object does not declare, and an injected system column', () => { + const findings = validateThankYouRecordFields(stackOf({ + name: 'v', + form: formOf('{{record.no_such_field}} {{record.created_at}} {{record.id}}'), + })); + expect(findings.map((f) => f.message.match(/is (.*?)\. A thank-you/)?.[1])).toEqual([ + 'not a field the object declares', + expect.stringContaining('a system column'), + expect.stringContaining('a system column'), + ]); + }); + + it('reads the title as well as the message, each name once', () => { + const form = formOf('{{record.subject}}'); + (form.submitBehavior as Record).title = 'Re: {{record.subject}}'; + expect(validateThankYouRecordFields(stackOf({ name: 'v', form }))).toHaveLength(1); + }); + + it('judges every form shape a view carries, public or not', () => { + const bad = formOf('{{record.subject}}'); + expect(validateThankYouRecordFields(stackOf({ name: 'v', viewKind: 'form', config: bad })).map((f) => f.path)) + .toEqual(['views[0].config.submitBehavior']); + expect(validateThankYouRecordFields(stackOf({ name: 'v', ...bad })).map((f) => f.path)) + .toEqual(['views[0].submitBehavior']); + const open = { ...bad, sharing: { enabled: true, allowAnonymous: true, publicLink: '/forms/case' } }; + expect(validateThankYouRecordFields(stackOf({ name: 'v', form: open }))).toHaveLength(1); + }); + + it('resolves the object from the view when the form names none', () => { + const form: Record = formOf('{{record.subject}}'); + delete form.data; + expect(validateThankYouRecordFields(stackOf({ name: 'v', object: 'support_case', form }))).toHaveLength(1); + }); + + it('reads an object whose fields are a list', () => { + const object = { name: 'support_case', fields: [{ name: 'subject', type: 'text' }] }; + expect(validateThankYouRecordFields(stackOf({ name: 'v', form: formOf('{{record.subject}}') }, [object]))) + .toHaveLength(1); + }); + + it('does not judge a panel with no token, another kind, or an object outside the stack', () => { + expect(validateThankYouRecordFields(stackOf({ name: 'v', form: formOf('Thanks!') }))).toEqual([]); + const redirect = { ...formOf(''), submitBehavior: { kind: 'redirect', url: '/t/{{record.subject}}' } }; + expect(validateThankYouRecordFields(stackOf({ name: 'v', form: redirect }))).toEqual([]); + expect(validateThankYouRecordFields(stackOf({ name: 'v', form: formOf('{{record.subject}}') }, []))).toEqual([]); + }); + + it('judges a page object-form panel against its objectName', () => { + const page = { + name: 'case_page', + type: 'app', + regions: [{ + name: 'main', + components: [{ + id: 'intake', + type: 'object-form', + properties: { objectName: 'support_case', mode: 'create', submitBehavior: panel('{{record.subject}}') }, + }], + }], + }; + const findings = validateThankYouRecordFields({ objects: [support], pages: [page] }); + expect(findings).toHaveLength(1); + expect(findings[0]).toMatchObject({ + where: 'page "case_page" · object-form "intake"', + path: 'pages[0].regions[0].components[0].properties.submitBehavior', + }); + const ok = structuredClone(page); + ok.regions[0].components[0].properties.submitBehavior = panel('{{record.case_number}}'); + expect(validateThankYouRecordFields({ objects: [support], pages: [ok] })).toEqual([]); + }); + + it('is registered gating, on all three commands and on the publish gate for view and page writes', () => { + const entry = AUTHORING_RULES.find((r) => r.name === 'validateThankYouRecordFields'); + expect(entry).toMatchObject({ + tier: 'gating', + commands: ['validate', 'build', 'lint'], + surfaces: ['cli', 'runtime-publish'], + runtimeTypes: ['view', 'page'], + }); + }); +}); + +describe('the admitted set is RUNTIME_OWNED_FIELD_TYPES, the one source', () => { + it('in production it is the spec set itself', () => { + expect([...RUNTIME_OWNED_FIELD_TYPES]).toContain('autonumber'); + const stack = stackOf({ name: 'v', form: formOf('{{record.case_number}}') }); + expect(validateThankYouRecordFields(stack)).toEqual(validateThankYouRecordFields(stack, RUNTIME_OWNED_FIELD_TYPES)); + }); + + it('widening the set (a test double) admits the new type with no other edit', () => { + const stack = stackOf({ name: 'v', form: formOf('{{record.priority_score}}') }); + expect(validateThankYouRecordFields(stack)).toHaveLength(1); + const widened = new Set([...RUNTIME_OWNED_FIELD_TYPES, 'formula']); + expect(validateThankYouRecordFields(stack, widened)).toEqual([]); + }); + + it('the rule source spells no field type of its own', () => { + const source = readFileSync(new URL('./validate-thank-you-record-fields.ts', import.meta.url), 'utf8'); + const code = source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, ''); + expect(code).toMatch(/RUNTIME_OWNED_FIELD_TYPES/); + expect(code).not.toMatch(/'autonumber'|"autonumber"/); + expect(code).toMatch(/thankYouRecordFieldNames/); + }); +}); + +describe('the runtime publish gate', () => { + it('refuses a view write naming a text field, resolving the type from the objects context', () => { + const { errors } = runRuntimeAuthoringRules({ + type: 'view', + item: { name: 'case_views', form: formOf('{{record.subject}}') }, + context: { objects: [support] }, + }); + const finding = errors.find((e) => e.rule === THANK_YOU_RECORD_FIELD); + expect(finding, JSON.stringify(errors)).toBeDefined(); + expect(finding!.path).toContain('form.submitBehavior'); + }); + + it('admits the same view naming the autonumber field', () => { + const { errors } = runRuntimeAuthoringRules({ + type: 'view', + item: { name: 'case_views', form: formOf('{{record.case_number}}') }, + context: { objects: [support] }, + }); + expect(errors.filter((e) => e.rule === THANK_YOU_RECORD_FIELD)).toEqual([]); + }); +}); diff --git a/packages/lint/src/validate-thank-you-record-fields.ts b/packages/lint/src/validate-thank-you-record-fields.ts new file mode 100644 index 00000000000..014ba032cba --- /dev/null +++ b/packages/lint/src/validate-thank-you-record-fields.ts @@ -0,0 +1,211 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * A form's `thank-you` panel may name only a field the engine generates on + * insert (ruling B on #28005, ruling record 6545210989). + * + * The panel's `title` / `message` interpolate a field of the record just + * submitted as `{{record.field_name}}`; the spec door holds the token's shape + * (`FormViewSchema`, `ui/view.zod.ts`) and `thankYouRecordFieldNames` + * (`@objectstack/spec/ui`) is the one reading of which fields a panel names. + * What the shape cannot decide is whether a named field is one an anonymous + * visitor may be shown. The submit answer of a public form echoes nothing the + * insert stored, because a `beforeInsert` / `afterInsert` hook — an elevated + * one included — can derive a stored value from records the anonymous form + * grant may never read. So the answer carries `id` plus exactly the named + * fields (`PublicFormSubmitResponseSchema`, `@objectstack/spec/api`), and a + * name is admitted only when its field's type is in `RUNTIME_OWNED_FIELD_TYPES` + * (`@objectstack/spec/data`, today `autonumber`): the engine mints that value + * itself, so no hook could have put it there. ⛔ This rule carries no list of + * its own; widening that set widens what it admits, with no edit here. + * + * Every other name is refused where it is written, naming the field and its + * type: a declared field of another type, a registry-injected column + * (`created_at`, `owner_id`, `id`, … — injected, not engine-numbered), and a + * name the object does not declare at all (the panel would print nothing). + * + * ## Which forms + * + * Every form that carries a `thank-you` panel, public or not: the slot is one + * slot with one admission, and a form that is internal today is made public by + * flipping its `sharing`, which is a write this rule would otherwise never see + * again. + * + * - A `views[]` entry's forms: the bare form-view shape itself, the nested + * `form`, every `formViews` entry and the flattened `config` of a + * `viewKind: 'form'` item. The object is the one `anonymousFormObjectName` + * resolves, the rung order both anonymous doors submit by. + * - A page `object-form` component (`properties.submitBehavior`, the form + * view's own block by reference). Its object is its `properties.objectName`, + * else the binding the page walk resolves. + * + * A form whose object does not resolve in the stack is not judged here (the + * object-reference rules own that answer), the same boundary + * `validate-public-form-file-fields.ts` draws. + * + * ## Where it runs + * + * A stack-level authoring rule (`authoring-rules.ts`): `os validate`, + * `os build` and `os lint` run it over the whole stack, and the runtime publish + * gate runs it on a `view` or `page` write, whose snapshot carries the + * `objects` collection as resolution context. An `object` write's snapshot + * carries no views or pages, so retyping a named field under a published panel + * is caught by the CLI commands and by the form's next publish. + */ + +import { RUNTIME_OWNED_FIELD_TYPES } from '@objectstack/spec/data'; +import { anonymousFormObjectName, thankYouRecordFieldNames } from '@objectstack/spec/ui'; + +import { collectionEntries } from './collection-entries.js'; +import { indexObjectGraph, type GraphObject } from './object-graph.js'; +import { walkPageComponents } from './page-walk.js'; + +export const THANK_YOU_RECORD_FIELD = 'thank-you-record-field'; + +export interface ThankYouRecordFieldFinding { + severity: 'error'; + /** Diagnostic rule id: always {@link THANK_YOU_RECORD_FIELD}. */ + rule: string; + /** Human-readable location, e.g. `view "case_intake" · form`. */ + where: string; + /** Config path of the panel, e.g. `views[0].form.submitBehavior`. */ + path: string; + message: string; + hint: string; +} + +type AnyRec = Record; + +/** The page component type whose `properties.submitBehavior` is a form view's block. */ +const OBJECT_FORM_COMPONENT_TYPE = 'object-form'; + +function isRec(v: unknown): v is AnyRec { + return !!v && typeof v === 'object' && !Array.isArray(v); +} + +function strName(v: unknown): string | undefined { + return typeof v === 'string' && v.length > 0 ? v : undefined; +} + +/** One form that carries a panel, located. */ +interface PanelSite { + submitBehavior: unknown; + objectName: string | undefined; + where: string; + path: string; + /** Who reads the refusal: `view "x" · form`, `page "y" · component …`. */ + owner: string; +} + +/** Every form a `views[]` entry carries, in scan order, with its slot. */ +function viewForms(view: AnyRec): Array<{ form: AnyRec; slot: string }> { + const forms: Array<{ form: AnyRec; slot: string }> = []; + if ('submitBehavior' in view) forms.push({ form: view, slot: '' }); + if (isRec(view.form)) forms.push({ form: view.form, slot: 'form' }); + if (isRec(view.formViews)) { + for (const [key, fv] of Object.entries(view.formViews)) { + if (isRec(fv)) forms.push({ form: fv, slot: `formViews.${key}` }); + } + } + if (view.viewKind === 'form' && isRec(view.config)) forms.push({ form: view.config, slot: 'config' }); + return forms; +} + +function panelSites(stack: AnyRec): PanelSite[] { + const sites: PanelSite[] = []; + for (const { rec: view, path: viewPath } of collectionEntries(stack.views, 'views')) { + const viewName = strName(view.name) ?? strName(view.object) ?? viewPath; + for (const { form, slot } of viewForms(view)) { + if (form.submitBehavior === undefined) continue; + const owner = `view "${viewName}"${slot ? ` · ${slot}` : ''}`; + sites.push({ + submitBehavior: form.submitBehavior, + objectName: anonymousFormObjectName(view, form), + where: owner, + path: `${viewPath}${slot ? `.${slot}` : ''}.submitBehavior`, + owner, + }); + } + } + for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, 'pages')) { + const pageName = strName(page.name) ?? pagePath; + for (const walked of walkPageComponents(page, pagePath)) { + const { component } = walked; + if (component.type !== OBJECT_FORM_COMPONENT_TYPE || !isRec(component.properties)) continue; + if (component.properties.submitBehavior === undefined) continue; + const id = strName(component.id); + const owner = `page "${pageName}" · object-form${id ? ` "${id}"` : ` at ${walked.path}`}`; + sites.push({ + submitBehavior: component.properties.submitBehavior, + objectName: strName(component.properties.objectName) ?? walked.objectName, + where: owner, + path: `${walked.path}.properties.submitBehavior`, + owner, + }); + } + } + return sites; +} + +/** Why `name` is not admitted on `object`, or `undefined` when it is runtime-owned. */ +function refusalReason(object: GraphObject, name: string, admitted: ReadonlySet): string | undefined { + const declared = object.fields.get(name); + if (declared) { + const type = declared.type; + if (type !== undefined && admitted.has(type)) return undefined; + return `a ${type ?? 'untyped'} field`; + } + if (object.injected.has(name)) { + const type = object.injected.get(name)?.type; + return `a system column${type ? ` (${type})` : ''} the platform adds to every record`; + } + return 'not a field the object declares'; +} + +/** + * Refuse every name a `thank-you` panel interpolates that is not an + * engine-generated field. Empty = clean. + * + * `admitted` is the spec's set and nothing else in production; the parameter + * exists so a test can widen it as a double and prove this rule follows the + * one source with no edit of its own (the spec dist is externalised in this + * package's tests, so it cannot be module-mocked). + */ +export function validateThankYouRecordFields( + stack: AnyRec, + admitted: ReadonlySet = RUNTIME_OWNED_FIELD_TYPES, +): ThankYouRecordFieldFinding[] { + const findings: ThankYouRecordFieldFinding[] = []; + const sites = panelSites(stack); + if (sites.length === 0) return findings; + const graph = indexObjectGraph(stack); + + for (const site of sites) { + const names = thankYouRecordFieldNames(site.submitBehavior); + if (names.length === 0 || !site.objectName) continue; + const object = graph.get(site.objectName); + if (!object) continue; + for (const name of names) { + const reason = refusalReason(object, name, admitted); + if (reason === undefined) continue; + findings.push({ + severity: 'error', + rule: THANK_YOU_RECORD_FIELD, + where: site.where, + path: site.path, + message: + `${site.owner}: the thank-you panel names {{record.${name}}}, which on object ` + + `"${site.objectName}" is ${reason}. A thank-you panel may name only a field the engine ` + + `generates on insert: any other value may have been set by a beforeInsert / afterInsert ` + + `hook from records an anonymous visitor's form grant cannot read, so a public form's ` + + `submit answer never carries it.`, + hint: + `Name a ${[...admitted].join(' or ')} field of "${site.objectName}" instead ` + + `(for example a case number), or ` + + `take {{record.${name}}} out of the panel's title and message.`, + }); + } + } + + return findings; +} From 1bc2dd6705fc6e72b4414dbca94ed72a9fa45410 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 15:27:38 +0000 Subject: [PATCH 3/8] test(lint): name the thank-you rule in the publish-gate rosters (wip) Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX --- packages/lint/src/authoring-rule-wiring.test.ts | 2 ++ .../runtime-gate.page-visibility-predicate-writes.test.ts | 4 ++++ packages/lint/src/runtime-gate.test.ts | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/packages/lint/src/authoring-rule-wiring.test.ts b/packages/lint/src/authoring-rule-wiring.test.ts index e315ea8acaf..8fd37410922 100644 --- a/packages/lint/src/authoring-rule-wiring.test.ts +++ b/packages/lint/src/authoring-rule-wiring.test.ts @@ -513,6 +513,8 @@ describe('authoring-rule registry wiring (#4409)', () => { 'validateReferenceIntegrity', // #25650 — a form open to anonymous intake declaring a file field. 'validatePublicFormFileFields', + // #28005 — a thank-you panel naming a field the engine does not generate. + 'validateThankYouRecordFields', ...FAMILY, ]); }); diff --git a/packages/lint/src/runtime-gate.page-visibility-predicate-writes.test.ts b/packages/lint/src/runtime-gate.page-visibility-predicate-writes.test.ts index 64741ff644e..b123ea683bd 100644 --- a/packages/lint/src/runtime-gate.page-visibility-predicate-writes.test.ts +++ b/packages/lint/src/runtime-gate.page-visibility-predicate-writes.test.ts @@ -134,6 +134,9 @@ describe('#24491 — the page door gives the build\'s visibility-predicate verdi 'validateComponentProps', 'validateComponentTypes', 'validatePrintPageBlocks', + // #28005 — an object-form's thank-you panel naming a field the engine + // does not generate. + 'validateThankYouRecordFields', ...FAMILY, // #24953 / #25002 — the props-bag CEL slots' bounds refusal, after the family. 'validatePagePredicateBounds', @@ -194,6 +197,7 @@ describe('#24491 — the page door gives the build\'s visibility-predicate verdi 'validatePresetComparands', 'validateReferenceIntegrity', 'validatePublicFormFileFields', + 'validateThankYouRecordFields', ...FAMILY, ]); const view = (visibleWhen: string, data?: unknown) => ({ diff --git a/packages/lint/src/runtime-gate.test.ts b/packages/lint/src/runtime-gate.test.ts index c289524b536..a2f630c41fb 100644 --- a/packages/lint/src/runtime-gate.test.ts +++ b/packages/lint/src/runtime-gate.test.ts @@ -342,6 +342,9 @@ describe('the views[] visibility-predicate family at the runtime publish gate (# // #25650 — a form open to anonymous intake declaring a file field; // the snapshot's `objects` context carries the field types it reads. 'validatePublicFormFileFields', + // #28005 — a thank-you panel naming a field the engine does not + // generate; the same `objects` context carries the types it reads. + 'validateThankYouRecordFields', 'validateVisibilityPredicates', 'validatePredicatePathRefs', ]); @@ -543,6 +546,7 @@ describe('the views[] visibility-predicate family at the runtime publish gate (# 'validatePresetComparands', // #8793 — dispatched for `view`, clean here 'validateReferenceIntegrity', // #9313 — its two list-view field members, clean here 'validatePublicFormFileFields', // #25650 — a public form's file fields, clean here + 'validateThankYouRecordFields', // #28005 — a thank-you panel's named fields, clean here 'validateVisibilityPredicates', 'validatePredicatePathRefs', ]); @@ -639,6 +643,7 @@ describe('the publish gate judges a schema-bound form at its own layer (#7815)', 'validatePresetComparands', // #8793 — dispatched for `view`, clean here 'validateReferenceIntegrity', // #9313 — its two list-view field members, clean here 'validatePublicFormFileFields', // #25650 — a public form's file fields, clean here + 'validateThankYouRecordFields', // #28005 — a thank-you panel's named fields, clean here 'validateVisibilityPredicates', 'validatePredicatePathRefs', ]); From 9d1b0b28eaa024ee02d74e2430de481f6c3fbcdd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 15:32:54 +0000 Subject: [PATCH 4/8] =?UTF-8?q?feat(spec):=20thank-you=20record=20slot=20?= =?UTF-8?q?=E2=80=94=20generated=20artefacts=20and=20changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX --- .changeset/28005-thank-you-record-slot.md | 17 +++++++++++++++++ content/docs/references/api/protocol.mdx | 18 +++++++++++++++--- content/docs/references/index.mdx | 10 +++++----- content/docs/references/ui/component.mdx | 8 ++++++++ content/docs/references/ui/view.mdx | 8 ++++++++ .../api.md | 2 +- packages/spec/api-surface/api.json | 2 ++ packages/spec/api-surface/ui.json | 1 + packages/spec/authorable-surface/api.json | 2 ++ packages/spec/declaration-map/api.json | 2 ++ packages/spec/export-origins/api.json | 2 ++ packages/spec/export-origins/ui.json | 1 + packages/spec/json-schema.manifest/api.json | 1 + 13 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 .changeset/28005-thank-you-record-slot.md diff --git a/.changeset/28005-thank-you-record-slot.md b/.changeset/28005-thank-you-record-slot.md new file mode 100644 index 00000000000..56b66b65923 --- /dev/null +++ b/.changeset/28005-thank-you-record-slot.md @@ -0,0 +1,17 @@ +--- +'@objectstack/spec': minor +'@objectstack/lint': minor +--- + +feat(spec): a form's thank-you panel can name the record's engine-generated fields, and the public submit answer has a declared shape + +Clause-②: yes (widening) +Metadata-protocol: added + +A public form's confirmation panel could not tell a web-to-case visitor their case number: `title` and `message` were plain text, and `POST /api/v1/forms/SLUG/submit` answers `{ id }` only. + +- **The slot.** A form's `submitBehavior: { kind: 'thank-you', title, message }` may name a field of the record just submitted as `{{record.field_name}}`, the same token the `redirect` behavior's `url` takes. Any other brace is refused at parse, naming the offending text: `{record.x}`, `{{ record.x }}` and `{{case_number}}` are not the token. +- **Only engine-generated fields.** A named field must be one whose value the engine generates on insert: its type is in `RUNTIME_OWNED_FIELD_TYPES` (today `autonumber`). Any other stored value may have been set by a `beforeInsert` / `afterInsert` hook from records an anonymous visitor cannot read. `os validate`, `os build`, `os lint` and the publish gate on a `view` or `page` write refuse any other name (rule `thank-you-record-field`), naming the field and its type. This covers every form view shape and a page `object-form` component. +- **The answer shape.** `PublicFormSubmitResponseSchema` (`@objectstack/spec/api`) declares the submit answer as `id` plus an optional `fields` map holding exactly the named fields, and nothing else. `thankYouRecordFieldNames` (`@objectstack/spec/ui`) reads which fields a panel names. + +The server fills `fields` and the console fills the tokens in follow-up releases. Until then, a panel that names a field shows the token text as written. diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 3c6ed44e69f..6b2a888e741 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1,7 +1,7 @@ --- title: Protocol schema — API Protocol reference navTitle: Protocol -description: "Protocol schemas of the ObjectStack API Protocol: AiAgentCapabilities and 139 more — each property with its type, default and a TypeScript example." +description: "Protocol schemas of the ObjectStack API Protocol: AiAgentCapabilities and 140 more — each property with its type, default and a TypeScript example." --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -13,8 +13,8 @@ description: "Protocol schemas of the ObjectStack API Protocol: AiAgentCapabilit ## TypeScript Usage ```typescript -import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AuditMetaItemRequestSchema, AuditMetaItemResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CloneDataResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DiffMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, FindReferencesToMetaResponseSchema, FlowRegistrationSchema, FlowRegistrationRefusalSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaDiagnosticsResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetPublishedMetaItemResponseSchema, GetRecordFieldAccessResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HistoryMetaItemRequestSchema, HistoryMetaItemResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListDraftsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, MetadataAuthoringClientSchema, MetadataAuthoringSurfaceSchema, MetadataConflictErrorSchema, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllPageHitSchema, SearchAllResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; -import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AuditMetaItemRequest, AuditMetaItemResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CloneDataResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DiffMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, FindReferencesToMetaResponse, FlowRegistration, FlowRegistrationRefusal, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaDiagnosticsResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetPublishedMetaItemResponse, GetRecordFieldAccessResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, HistoryMetaItemRequest, HistoryMetaItemResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListDraftsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, MetadataAuthoringClient, MetadataAuthoringSurface, MetadataConflictError, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllPageHit, SearchAllResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; +import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AuditMetaItemRequestSchema, AuditMetaItemResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CloneDataResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DiffMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, FindReferencesToMetaResponseSchema, FlowRegistrationSchema, FlowRegistrationRefusalSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaDiagnosticsResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetPublishedMetaItemResponseSchema, GetRecordFieldAccessResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HistoryMetaItemRequestSchema, HistoryMetaItemResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListDraftsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, MetadataAuthoringClientSchema, MetadataAuthoringSurfaceSchema, MetadataConflictErrorSchema, NotificationSchema, NotificationPreferencesSchema, PublicFormSubmitResponseSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllPageHitSchema, SearchAllResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; +import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AuditMetaItemRequest, AuditMetaItemResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CloneDataResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DiffMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, FindReferencesToMetaResponse, FlowRegistration, FlowRegistrationRefusal, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaDiagnosticsResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetPublishedMetaItemResponse, GetRecordFieldAccessResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, HistoryMetaItemRequest, HistoryMetaItemResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListDraftsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, MetadataAuthoringClient, MetadataAuthoringSurface, MetadataConflictError, Notification, NotificationPreferences, PublicFormSubmitResponse, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllPageHit, SearchAllResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; // Validate data const result = AiAgentCapabilitiesSchema.parse(data); @@ -2488,6 +2488,18 @@ The authoring surface of a metadata write. Closed: `copilot` is an AI assistant' | **push** | `boolean` | optional | Override push setting | +--- + +## PublicFormSubmitResponse + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **id** | `string` | ✅ | The ID of the newly created record. | +| **fields** | `Record` | optional | The stored values of exactly the fields the form's `thank-you` panel names as `{{record.field_name}}`, keyed by field name — each one a field the engine generates on insert (an `autonumber` field, e.g. a case number). Present only when the panel names at least one; never any other field of the record. | + + --- ## PublishMetaItemRequest diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 2215a7e8e78..cd4d71c58ce 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1571 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1572 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -21,7 +21,7 @@ counts are sums of the rows they head. Regenerate with | Module | Pages | Schemas | Description | | :--- | ---: | ---: | :--- | | [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | -| [API Protocol](/docs/references/api) | 34 | 452 | REST contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 34 | 453 | REST contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 172 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **204** | **1571** | 14 protocol modules | +| **Total** | **204** | **1572** | 14 protocol modules | --- @@ -63,7 +63,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations. ## API Protocol -**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 452 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 453 schemas** REST contracts, endpoints, routing, realtime, batch, discovery. @@ -92,7 +92,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery. | [`package-api-assembled.zod.ts`](/docs/references/api/package-api-assembled) | `AssembledInstalledPackage`, `GetInstalledPackageResponse`, `InstalledPackageAtEitherStage`, `ListInstalledPackagesResponse` | | [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `ImportUnmanagedPackageRequest`, `ImportUnmanagedPackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` | | [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `ValidationMode` | -| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `FlowRegistration`, `FlowRegistrationRefusal`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | +| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `FlowRegistration`, `FlowRegistrationRefusal`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublicFormSubmitResponse`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | | [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` | | [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` | | [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` | diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 71d96dfcdec..046fb6a818a 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -645,6 +645,14 @@ Sort field and direction pair | **group** | `string` | optional | Field group key (snake_case) whose members and presentation this section inherits, from the object's `fieldGroups`. Mutually exclusive with `fields`, and with every key the group itself declares (`name`, `label`, `description`, `visibleWhen`, and a `true` `collapsible` / `collapsed`) | | **fields** | `(string \| { field: string; type?: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| …>; options?: object[]; reference?: string; … } \| { name: string; label?: string; description?: string; type?: string; … })[]` | optional | The section's fields, in order — each a field name, the form view's `{ field, … }` entry overriding that object field, or an inline form field `{ name, type, … }`. Omit only when `group` supplies the members | +### Nested Shape: `ObjectFormProps.submitBehavior[kind='thank-you']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kind** | `'thank-you'` | ✅ | | +| **title** | `string` | optional | Heading of the confirmation panel. May name a field of the submitted record as `{{record.field_name}}`, and only a field the engine generates on insert (an `autonumber` field, e.g. a case number): a hook-derived value never reaches an anonymous visitor. | +| **message** | `string` | optional | Body of the confirmation panel. May name a field of the submitted record as `{{record.field_name}}`, and only a field the engine generates on insert (an `autonumber` field, e.g. a case number): a hook-derived value never reaches an anonymous visitor. | + ### Nested Shape: `ObjectFormProps.submitBehavior[kind='redirect']` | Property | Type | Required | Description | diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 85408a02d0e..257c53c5f3a 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -491,6 +491,14 @@ Form-view select option — the object-field option shape minus the per-option ` | **expiresAt** | `string` | optional | Expiration date/time in ISO 8601 format | | **allowAnonymous** | `boolean` | optional (default: `false`) | Allow access without authentication | +### Nested Shape: `FormView.submitBehavior[kind='thank-you']` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **kind** | `'thank-you'` | ✅ | | +| **title** | `string` | optional | Heading of the confirmation panel. May name a field of the submitted record as `{{record.field_name}}`, and only a field the engine generates on insert (an `autonumber` field, e.g. a case number): a hook-derived value never reaches an anonymous visitor. | +| **message** | `string` | optional | Body of the confirmation panel. May name a field of the submitted record as `{{record.field_name}}`, and only a field the engine generates on insert (an `autonumber` field, e.g. a case number): a hook-derived value never reaches an anonymous visitor. | + ### Nested Shape: `FormView.submitBehavior[kind='redirect']` | Property | Type | Required | Description | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md index 0f34efe8cca..ba08cfaa4fe 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md @@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate. | Dir | Sites | |---|---| -| `api/` | 461 | +| `api/` | 462 | diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json index d5104cd5366..c30c3a437eb 100644 --- a/packages/spec/api-surface/api.json +++ b/packages/spec/api-surface/api.json @@ -802,6 +802,8 @@ "PresignedUrlResponseSchema (const)", "ProvenanceWaiver (type)", "ProvenanceWaiverSchema (const)", + "PublicFormSubmitResponse (type)", + "PublicFormSubmitResponseSchema (const)", "PublishMetaItemRequest (type)", "PublishMetaItemRequestSchema (const)", "PublishMetaItemResponse (type)", diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index fb64982ab8f..18ecfd4b8c0 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -572,6 +572,7 @@ "resolveI18nLabel (function)", "selectViewMetadataBranch (function)", "stripViewConsoleDecorations (function)", + "thankYouRecordFieldNames (function)", "validateActionParams (function)", "viewForm (const)" ] diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 3249d7097b6..790a238b3e2 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -1356,6 +1356,8 @@ "api/ProvenanceWaiver:package", "api/ProvenanceWaiver:reason", "api/ProvenanceWaiver:registeredUnder", + "api/PublicFormSubmitResponse:fields", + "api/PublicFormSubmitResponse:id", "api/PublishMetaItemRequest:actor", "api/PublishMetaItemRequest:member", "api/PublishMetaItemRequest:message", diff --git a/packages/spec/declaration-map/api.json b/packages/spec/declaration-map/api.json index c90c0605468..4ac4f648eea 100644 --- a/packages/spec/declaration-map/api.json +++ b/packages/spec/declaration-map/api.json @@ -585,6 +585,8 @@ "PresignedUrlResponseSchema": "api/PresignedUrlResponse", "ProvenanceWaiver": "api/ProvenanceWaiver", "ProvenanceWaiverSchema": "api/ProvenanceWaiver", + "PublicFormSubmitResponse": "api/PublicFormSubmitResponse", + "PublicFormSubmitResponseSchema": "api/PublicFormSubmitResponse", "PublishMetaItemRequest": "api/PublishMetaItemRequest", "PublishMetaItemRequestSchema": "api/PublishMetaItemRequest", "PublishMetaItemResponse": "api/PublishMetaItemResponse", diff --git a/packages/spec/export-origins/api.json b/packages/spec/export-origins/api.json index d37f42c14b8..4ad7501a127 100644 --- a/packages/spec/export-origins/api.json +++ b/packages/spec/export-origins/api.json @@ -773,6 +773,8 @@ "PresignedUrlResponseSchema": "src/api/storage.zod.ts#PresignedUrlResponseSchema (const)", "ProvenanceWaiver": "src/api/error-code-ledger.zod.ts#ProvenanceWaiver (type)", "ProvenanceWaiverSchema": "src/api/error-code-ledger.zod.ts#ProvenanceWaiverSchema (const)", + "PublicFormSubmitResponse": "src/api/protocol.zod.ts#PublicFormSubmitResponse (type)", + "PublicFormSubmitResponseSchema": "src/api/protocol.zod.ts#PublicFormSubmitResponseSchema (const)", "PublishMetaItemRequest": "src/api/protocol.zod.ts#PublishMetaItemRequest (type)", "PublishMetaItemRequestSchema": "src/api/protocol.zod.ts#PublishMetaItemRequestSchema (const)", "PublishMetaItemResponse": "src/api/protocol.zod.ts#PublishMetaItemResponse (type)", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index db3ec47a59c..c504d4ab50f 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -555,6 +555,7 @@ "resolveI18nLabel": "src/ui/i18n-label-resolver.ts#resolveI18nLabel (function)", "selectViewMetadataBranch": "src/ui/view.zod.ts#selectViewMetadataBranch (function)", "stripViewConsoleDecorations": "src/ui/view.zod.ts#stripViewConsoleDecorations (function)", + "thankYouRecordFieldNames": "src/ui/view.zod.ts#thankYouRecordFieldNames (function)", "validateActionParams": "src/ui/action-params.zod.ts#validateActionParams (function)", "viewForm": "src/ui/view.form.ts#viewForm (const)" } diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json index f766597010d..96c4f8ae758 100644 --- a/packages/spec/json-schema.manifest/api.json +++ b/packages/spec/json-schema.manifest/api.json @@ -320,6 +320,7 @@ "api/PresenceUpdate", "api/PresignedUrlResponse", "api/ProvenanceWaiver", + "api/PublicFormSubmitResponse", "api/PublishMetaItemRequest", "api/PublishMetaItemResponse", "api/PublishPackageDraftsResponse", From 47c5f03c4ef9f3102465e447fb433af4525bdeae Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 15:40:38 +0000 Subject: [PATCH 5/8] test(spec): pin the submit answer alias as isomorphic (806) Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX --- packages/spec/src/type-alias-convention.pin.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/spec/src/type-alias-convention.pin.test.ts b/packages/spec/src/type-alias-convention.pin.test.ts index fe5fc763f3a..f45af50aa78 100644 --- a/packages/spec/src/type-alias-convention.pin.test.ts +++ b/packages/spec/src/type-alias-convention.pin.test.ts @@ -285,7 +285,7 @@ import type * as M194 from './automation/assignment-rule.zod.js'; import type * as M195 from './security/queue.zod.js'; // --------------------------------------------------------------------------- -// 805 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. +// 806 isomorphic aliases: `z.input` === `z.infer`, so no `XParsed` is declared. // // That number is machine-checked, not hand-kept. The runtime companion at the // bottom of this file recomputes the pin count from the source and asserts that @@ -1740,7 +1740,7 @@ describe('ADR-0122 type-alias convention', () => { // this title and the section header above the pin list — are now asserted // against the recomputed count below, so neither can go stale without a red // test naming it. - it('still declares all 805 isomorphic pins', () => { + it('still declares all 806 isomorphic pins', () => { // The truth of each pin is proved by tsc, not here — an `Assert>` // that stops holding is a compile error with the alias named. What tsc // cannot notice is a pin that was DELETED: removing the assertion removes @@ -2701,7 +2701,10 @@ describe('ADR-0122 type-alias convention', () => { // `tencentcloud-tc3-hmac-sha256` shape, no default and no transform // (Iso_shared_connectorAuth__ConnectorInstanceSignedTencentCloudTc3AuthSchema). // +1 added. - expect(pins).toHaveLength(805); + // 805 -> 806 is #28005: the public-form submit answer + // (`api/protocol.zod.ts`), strict, no default and no transform + // (Iso_api_protocol__PublicFormSubmitResponseSchema). +1 added. + expect(pins).toHaveLength(806); // The count is stated in PROSE twice as well — this case's title and the // section header above the pin list — and until commit c6b05c76a nothing read either From b968584778867941b61502d7df3346369b1eb8b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 15:44:04 +0000 Subject: [PATCH 6/8] test(lint): read the thank-you rule source without a private comment stripper Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX --- packages/lint/src/validate-thank-you-record-fields.test.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/lint/src/validate-thank-you-record-fields.test.ts b/packages/lint/src/validate-thank-you-record-fields.test.ts index d5064392f04..479d50660e6 100644 --- a/packages/lint/src/validate-thank-you-record-fields.test.ts +++ b/packages/lint/src/validate-thank-you-record-fields.test.ts @@ -167,10 +167,9 @@ describe('the admitted set is RUNTIME_OWNED_FIELD_TYPES, the one source', () => it('the rule source spells no field type of its own', () => { const source = readFileSync(new URL('./validate-thank-you-record-fields.ts', import.meta.url), 'utf8'); - const code = source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, ''); - expect(code).toMatch(/RUNTIME_OWNED_FIELD_TYPES/); - expect(code).not.toMatch(/'autonumber'|"autonumber"/); - expect(code).toMatch(/thankYouRecordFieldNames/); + expect(source).toContain("import { RUNTIME_OWNED_FIELD_TYPES } from '@objectstack/spec/data';"); + expect(source).toMatch(/thankYouRecordFieldNames \} from '@objectstack\/spec\/ui'/); + expect(source).not.toMatch(/'autonumber'|"autonumber"/); }); }); From eeb6adb2b1e13c5fd583e4d9bde9e6dd2755486a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 16:08:48 +0000 Subject: [PATCH 7/8] docs: CLI transcripts print 57 author-time rules Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX --- content/docs/deployment/cli.mdx | 2 +- content/docs/deployment/validating-metadata.mdx | 2 +- content/docs/getting-started/build-with-claude-code.mdx | 2 +- content/docs/ui/react-pages.mdx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/content/docs/deployment/cli.mdx b/content/docs/deployment/cli.mdx index bdda74dc7bf..091ef3b3ecb 100644 --- a/content/docs/deployment/cli.mdx +++ b/content/docs/deployment/cli.mdx @@ -771,7 +771,7 @@ os compile --json # JSON output for CI pipelines → Normalizing stack definition... → Lowering inline handlers... → Validating protocol compliance... - → Running author-time rules (56)... + → Running author-time rules (57)... → Checking that every required capability has a provider installable in this edition... → Collecting package docs (ADR-0046)... 0 collected → Writing artifact... diff --git a/content/docs/deployment/validating-metadata.mdx b/content/docs/deployment/validating-metadata.mdx index 6319d0bdcbc..6d4a7b7eb93 100644 --- a/content/docs/deployment/validating-metadata.mdx +++ b/content/docs/deployment/validating-metadata.mdx @@ -759,7 +759,7 @@ A clean run walks the registry and reports timing: Config: /path/to/support-desk/objectstack.config.ts Load time: 21ms → Validating against ObjectStack Protocol... - → Running author-time rules (56)... + → Running author-time rules (57)... → Checking that every required capability has a provider installable in this edition... → Checking package docs (ADR-0046)... diff --git a/content/docs/getting-started/build-with-claude-code.mdx b/content/docs/getting-started/build-with-claude-code.mdx index 22dcf5948ee..ec422157fc1 100644 --- a/content/docs/getting-started/build-with-claude-code.mdx +++ b/content/docs/getting-started/build-with-claude-code.mdx @@ -305,7 +305,7 @@ visible: 'status != "resolved"' ◆ Validate ──────────────────────────────────────── → Validating against ObjectStack Protocol... - → Running author-time rules (56)... + → Running author-time rules (57)... ✗ Author-time rules failed (1 issue) • stack · action 'resolve_ticket' visible: bare reference `status` — a diff --git a/content/docs/ui/react-pages.mdx b/content/docs/ui/react-pages.mdx index 146b302cfa1..f171cf7c228 100644 --- a/content/docs/ui/react-pages.mdx +++ b/content/docs/ui/react-pages.mdx @@ -404,7 +404,7 @@ objectstack validate ──────────────────────────────────────── → Loading configuration... → Validating against ObjectStack Protocol... - → Running author-time rules (56)... + → Running author-time rules (57)... → Checking that every required capability has a provider installable in this edition... → Checking package docs (ADR-0046)... From 8332f94c8e19e2abaac891843bc85d155c970515 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 20:39:35 +0000 Subject: [PATCH 8/8] chore(spec): regenerate generated artifacts after merging main (#28005) check:generated --fix on the merged tree: references, api/ui surfaces, export origins, declaration map, json-schema manifest and the api strictness-ledger count. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibwKTredm7FjB4Z6DeX --- content/docs/references/api/protocol.mdx | 29 +++- content/docs/references/index.mdx | 18 +-- content/docs/references/ui/component.mdx | 128 +++++++++++++--- content/docs/references/ui/view.mdx | 143 ++++++++++++++++-- .../api.md | 2 +- packages/spec/api-surface/api.json | 2 + packages/spec/api-surface/ui.json | 12 ++ packages/spec/authorable-surface/api.json | 2 + packages/spec/declaration-map/api.json | 2 + packages/spec/export-origins/api.json | 2 + packages/spec/export-origins/ui.json | 12 ++ packages/spec/json-schema.manifest/api.json | 1 + 12 files changed, 301 insertions(+), 52 deletions(-) diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 6b2a888e741..774195c8308 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1,7 +1,7 @@ --- title: Protocol schema — API Protocol reference navTitle: Protocol -description: "Protocol schemas of the ObjectStack API Protocol: AiAgentCapabilities and 140 more — each property with its type, default and a TypeScript example." +description: "Protocol schemas of the ObjectStack API Protocol: AiAgentCapabilities and 141 more — each property with its type, default and a TypeScript example." --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -13,8 +13,8 @@ description: "Protocol schemas of the ObjectStack API Protocol: AiAgentCapabilit ## TypeScript Usage ```typescript -import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AuditMetaItemRequestSchema, AuditMetaItemResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CloneDataResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DiffMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, FindReferencesToMetaResponseSchema, FlowRegistrationSchema, FlowRegistrationRefusalSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaDiagnosticsResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetPublishedMetaItemResponseSchema, GetRecordFieldAccessResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HistoryMetaItemRequestSchema, HistoryMetaItemResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListDraftsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, MetadataAuthoringClientSchema, MetadataAuthoringSurfaceSchema, MetadataConflictErrorSchema, NotificationSchema, NotificationPreferencesSchema, PublicFormSubmitResponseSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllPageHitSchema, SearchAllResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; -import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AuditMetaItemRequest, AuditMetaItemResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CloneDataResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DiffMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, FindReferencesToMetaResponse, FlowRegistration, FlowRegistrationRefusal, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaDiagnosticsResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetPublishedMetaItemResponse, GetRecordFieldAccessResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, HistoryMetaItemRequest, HistoryMetaItemResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListDraftsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, MetadataAuthoringClient, MetadataAuthoringSurface, MetadataConflictError, Notification, NotificationPreferences, PublicFormSubmitResponse, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllPageHit, SearchAllResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; +import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AuditMetaItemRequestSchema, AuditMetaItemResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CloneDataResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DiffMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, FindReferencesToMetaResponseSchema, FlowRegistrationSchema, FlowRegistrationRefusalSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaDiagnosticsResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetPublishedMetaItemResponseSchema, GetRecordFieldAccessResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HistoryMetaItemRequestSchema, HistoryMetaItemResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListDraftsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, MetadataAuthoringClientSchema, MetadataAuthoringSurfaceSchema, MetadataConflictErrorSchema, NotificationSchema, NotificationPreferencesSchema, PublicFormSubmitResponseSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsRequestSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllPageHitSchema, SearchAllResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api'; +import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AuditMetaItemRequest, AuditMetaItemResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CloneDataResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DiffMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, FindReferencesToMetaResponse, FlowRegistration, FlowRegistrationRefusal, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaDiagnosticsResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetPublishedMetaItemResponse, GetRecordFieldAccessResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, HistoryMetaItemRequest, HistoryMetaItemResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListDraftsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, MetadataAuthoringClient, MetadataAuthoringSurface, MetadataConflictError, Notification, NotificationPreferences, PublicFormSubmitResponse, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsRequest, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllPageHit, SearchAllResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api'; // Validate data const result = AiAgentCapabilitiesSchema.parse(data); @@ -1856,7 +1856,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | | **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns — the PROJECTION of the per-view field composition `columns` x `hiddenFields` x `fieldOrder`: this list is the candidate set AND the baseline order; `hiddenFields` subtracts from it and `fieldOrder` orders what survives. `hiddenFields` and `fieldOrder` cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's `ListView` grid derives the object's default columns). | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Filter criteria (JSON Rules) | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | @@ -1935,7 +1935,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own | **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | | **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns — the PROJECTION of the per-view field composition `columns` x `hiddenFields` x `fieldOrder`: this list is the candidate set AND the baseline order; `hiddenFields` subtracts from it and `fieldOrder` orders what survives. `hiddenFields` and `fieldOrder` cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's `ListView` grid derives the object's default columns). | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Filter criteria (JSON Rules) | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | @@ -2571,6 +2571,25 @@ The authoring surface of a metadata write. Closed: `copilot` is an AI assistant' | **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | How the gate treated this finding. `error` means the write was REFUSED (these appear on the 422, never on a 2xx); `warning` / `info` are advisory — the write succeeded and the finding is FYI. | +--- + +## PublishPackageDraftsRequest + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **message** | `string` | optional | The author's change note (ADR-0146 D1). The package commit records it, and so does every item history row this publish writes, as `change_note` (read back as each history event's `message`). Absent or empty, those rows record the sentence naming the package instead. | +| **backfill** | `{ object: string; field: string }[]` | optional | Opt-in: fields this publish adds whose literal `defaultValue` is written into the object's existing rows once the drafts are live, as the signed-in caller. Absent or `[]`, no row is touched. Each entry names an object and a field and nothing else, and names it once. The whole request is refused `400 VALIDATION_ERROR` before anything is published when an entry is malformed, repeated, or names a field that is not new in this publish or declares no literal default; `backfillApplied` in the response reports the rows written per field. | + +### Nested Shape: `PublishPackageDraftsRequest.backfill[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **object** | `string` | ✅ | Name of the object whose existing rows are written. | +| **field** | `string` | ✅ | Name of the field this publish adds to that object. | + + --- ## PublishPackageDraftsResponse diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index cd4d71c58ce..953017f7bcb 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1572 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1576 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -21,7 +21,7 @@ counts are sums of the rows they head. Regenerate with | Module | Pages | Schemas | Description | | :--- | ---: | ---: | :--- | | [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | -| [API Protocol](/docs/references/api) | 34 | 453 | REST contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 34 | 454 | REST contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | @@ -33,8 +33,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 172 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **204** | **1572** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 175 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **204** | **1576** | 14 protocol modules | --- @@ -63,7 +63,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations. ## API Protocol -**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 453 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 454 schemas** REST contracts, endpoints, routing, realtime, batch, discovery. @@ -92,7 +92,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery. | [`package-api-assembled.zod.ts`](/docs/references/api/package-api-assembled) | `AssembledInstalledPackage`, `GetInstalledPackageResponse`, `InstalledPackageAtEitherStage`, `ListInstalledPackagesResponse` | | [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `ImportUnmanagedPackageRequest`, `ImportUnmanagedPackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` | | [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `ValidationMode` | -| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `FlowRegistration`, `FlowRegistrationRefusal`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublicFormSubmitResponse`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | +| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `FlowRegistration`, `FlowRegistrationRefusal`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublicFormSubmitResponse`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsRequest`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | | [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` | | [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` | | [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` | @@ -371,7 +371,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 172 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 175 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -379,7 +379,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | :--- | :--- | | [`action.zod.ts`](/docs/references/ui/action) | `Action`, `ActionAi`, `ActionLocation`, `ActionParam`, `ActionType`, `InlineAction` | | [`action-params.zod.ts`](/docs/references/ui/action-params) | `ActionSession` | -| [`app.zod.ts`](/docs/references/ui/app) | `ActionNavItem`, `App`, `AppBranding`, `AppContextSelector`, `ComponentNavItem`, `DashboardNavItem`, `DocNavItem`, `GroupNavItem`, `NavigationArea`, `NavigationContribution`, `NavigationItem`, `ObjectNavItem`, `PageNavItem`, `ReportNavItem`, `UrlNavItem` | +| [`app.zod.ts`](/docs/references/ui/app) | `ActionNavItem`, `App`, `AppBranding`, `AppContextSelector`, `AppLandingEntry`, `ComponentNavItem`, `DashboardNavItem`, `DocNavItem`, `GroupNavItem`, `NavigationArea`, `NavigationContribution`, `NavigationItem`, `ObjectNavItem`, `PageNavItem`, `ReportNavItem`, `UrlNavItem` | | [`bulk-action.zod.ts`](/docs/references/ui/bulk-action) | `BulkActionDef`, `BulkActionExecution`, `BulkActionOperation`, `BulkActionParam` | | [`chart.zod.ts`](/docs/references/ui/chart) | `ChartAggregate`, `ChartAggregateFunction`, `ChartAnnotation`, `ChartAxis`, `ChartConfig`, `ChartDrillDown`, `ChartGroupBy`, `ChartInteraction`, `ChartSeries`, `ChartType` | | [`component.zod.ts`](/docs/references/ui/component) | `ActionButtonProps`, `ActionGroupProps`, `ActionIconProps`, `ActionMenuProps`, `ElementButtonProps`, `ElementDefinitionListProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementRepeaterProps`, `ElementTextInputProps`, `ElementTextProps`, `ObjectCalendarProps`, `ObjectFormProps`, `ObjectGanttProps`, `ObjectGridProps`, `ObjectKanbanProps`, `ObjectMapProps`, `ObjectMasterDetailFormProps`, `ObjectMetricProps`, `ObjectTimelineProps`, `ObjectTreeProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordAlertAction`, `RecordAlertProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordHistoryProps`, `RecordLineItemsProps`, `RecordPathProps`, `RecordQuickActionsProps`, `RecordReferenceRailProps`, `RecordRelatedListProps`, `ReferenceRailEntry` | @@ -392,7 +392,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` | | [`responsive.zod.ts`](/docs/references/ui/responsive) | `ResponsiveStyles`, `StyleMap` | | [`sharing.zod.ts`](/docs/references/ui/sharing) | `SharingConfig` | -| [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `EmptyState`, `FormButtonConfig`, `FormField`, `FormSection`, `FormSelectOption`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` | +| [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `EmptyState`, `FormButtonConfig`, `FormField`, `FormSection`, `FormSelectOption`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListMapConfig`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterElement`, `ViewFilterOrGroup`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` | --- diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 046fb6a818a..5be9453d723 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -486,7 +486,7 @@ const result = ActionButtonPropsSchema.parse(data); | **objectName** | `string` | optional | Object this calendar binds to. Optional because the component-level `dataSource` binding can supply the object instead | | **calendar** | `{ startDateField: string; endDateField?: string; titleField?: string; colorField?: string; … }` | optional | Calendar config `{ startDateField, endDateField?, titleField?, colorField?, allDayField?, initialDate? }` — the same block a list view's `calendar` declares; `startDateField` is required | | **defaultView** | `Enum<'month' \| 'week' \| 'day'>` | optional | Initial view mode | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Row order for the fetched events — the SortItem array form `[{ field, order }, ...]`, the one sort orthography every declared `sort` door on this platform shares; lowered to the wire `$orderby`. The legacy string clause (`name desc`) is refused — see migration `object-block-sort-item-array` | | **data** | `any[]` | optional | Pre-fetched records — skips the internal fetch | | **staticData** | `any[]` | optional | Static inline records | @@ -505,7 +505,7 @@ const result = ActionButtonPropsSchema.parse(data); | **allDayField** | `string` | optional | Field carrying the all-day flag for each event (names a boolean field, not a value): a record whose flag is true is drawn as an all-day band rather than at a clock time, and one whose flag is absent or false is not all-day. Omit to leave the renderer inference in place — an event with no end date draws as all-day | | **initialDate** | `string` | optional | Where the calendar opens: the month, week or day the active view mode shows that holds this date; omit it to open on today. A record anchor: `first_record` opens on the earliest start date (the startDateField value) among the records the view's filter selects, `next_record` on the earliest on or after the viewer's today (a `date` start field compares as a calendar day), and either opens on today when no record qualifies. Or a wrapped date-macro token, resolved each time the calendar opens: `{today}`, `{next_month_start}`, `{current_quarter_start}`, `{3_months_from_now}` (the full vocabulary is `DATE_MACRO_TOKENS` and the parameterised `DATE_MACRO_PARAM_RE` grammar in `@objectstack/spec/data`); a fixed calendar date is not accepted, because it goes stale as time passes. | -### Nested Shape: `ObjectCalendarProps.filter[number]` +### Nested Shape: `ObjectCalendarProps.filter[number][option 1]` View filter rule @@ -515,6 +515,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ObjectCalendarProps.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `ObjectCalendarProps.sort[number]` Sort field and direction pair @@ -683,7 +691,7 @@ Sort field and direction pair | **objectName** | `string` | optional | Object this gantt binds to — the THIRD record source `resolveRecordSourceConfig` resolves, after `data` and `staticData`. Optional because the component-level `dataSource` binding can supply the object instead | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source binding (ViewDataSchema — discriminated on `provider`: object \| value \| schema), read FIRST by `resolveRecordSourceConfig`. Static inline rows live at `{ provider: 'value', items: [...] }` or at `staticData`; the bare-array shortcut is refused | | **staticData** | `any[]` | optional | Inline records — read SECOND by `resolveRecordSourceConfig`, wrapped into a `{ provider: 'value' }` config | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Task order for the fetched bars — the SortItem array form `[{ field, order }, ...]`, the one sort orthography every declared `sort` door on this platform shares; lowered to the wire `$orderby`. The legacy string clause (`name desc`) is refused — see migration `object-block-sort-item-array` | | **gantt** | `{ startDateField: string; endDateField: string; titleField: string; progressField?: string; … }` | optional | Gantt-timeline configuration, the author face — the same block `ListViewSchema.gantt` declares, and the one the renderer validates this node against. Taken WHOLE when present: the flat top-level spelling beside it is ignored | | **navigation** | `{ mode?: Enum<'drawer' \| 'page' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Task-click navigation config — the same block `ListViewSchema.navigation` declares (`{ mode, size, openNewTab, preventNavigation }`), `mode` one of the four `NavigationModeSchema` values (`drawer` by default). The renderer's own default is `{ mode: 'drawer' }` when the key is absent | @@ -720,7 +728,7 @@ Sort field and direction pair | **schemaId** | `string` | ✅ | Schema identifier — typically the metadata type name | | **schema** | `Record` | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. | -### Nested Shape: `ObjectGanttProps.filter[number]` +### Nested Shape: `ObjectGanttProps.filter[number][option 1]` View filter rule @@ -730,6 +738,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ObjectGanttProps.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `ObjectGanttProps.sort[number]` Sort field and direction pair @@ -808,7 +824,7 @@ Sort field and direction pair | **emptyState** | `{ title?: string \| Record; message?: string \| Record; icon?: string; link?: object }` | optional | What the grid draws instead of an empty table: `{ title, message, icon, link }` — the list view's own empty-state shape | | **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | optional | Columns — all field-name strings, or all column entries `{ field, label?, width?, align?, hidden?, sortable?, … }`, the same union a list view's `columns` declares. One spelling per list: an array mixing strings and column objects is refused | | **fields** | `string[]` | optional | Field-name fallback the grid reads when `columns` is absent — bare field names (`['name', 'amount']`); write column decoration such as `label` or `width` on `columns` | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. THE key, singular — not the plural misspelling. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. THE key, singular — not the plural misspelling. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **defaultFilters** | `never` | optional | [REMOVED] `object-grid` property `defaultFilters` was removed in @objectstack/spec 18 (ADR-0087 D2) — it was the legacy second spelling of `filter`: the same rules, read only when `filter` lowered to nothing, so one intent had two spellings and a grid authoring both silently ignored this one. Use `filter`. Rename the key where `filter` is empty; the value (a ViewFilterRule array, `[{ field, operator, value }, ...]`) is unchanged. Where `filter` already has rules, delete this key: the grid never read it there. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Initial row order — the SortItem array form `[{ field, order }, ...]`, the one sort orthography every declared `sort` door on this platform shares; lowered to the wire `$orderby`. The legacy string clause (`name desc`) is refused — see migration `object-block-sort-item-array` | | **defaultSort** | `never` | optional | [REMOVED] `object-grid` property `defaultSort` was removed in @objectstack/spec 17 (ADR-0049) — it was the legacy second spelling of `sort`: a single `{ field, order }` pair read only when `sort` was absent, so one intent had two spellings and a grid authoring both silently ignored this one. Rename the key to `sort` and wrap the value in an array (`defaultSort: { field, order }` becomes `sort: [{ field, order }]`); the pair itself is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | @@ -870,7 +886,7 @@ Sort field and direction pair | **link** | `boolean` | optional | Functions as the primary navigation link (triggers View navigation) | | **action** | `string` | optional | Registered Action ID to execute when clicked | -### Nested Shape: `ObjectGridProps.filter[number]` +### Nested Shape: `ObjectGridProps.filter[number][option 1]` View filter rule @@ -880,6 +896,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ObjectGridProps.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `ObjectGridProps.sort[number]` Sort field and direction pair @@ -1005,7 +1029,7 @@ Sort field and direction pair | **objectName** | `string` | optional | Object this board binds to. Optional because the component-level `dataSource` binding can supply the object instead | | **groupBy** | `string` | optional | Field whose values become the board columns | | **columns** | `string[] \| { id: string; title: string; cards?: object[]; limit?: integer; … }[]` | optional | Swimlane definitions — all lane objects `{ id, title, cards?, limit?, className?, collapsed? }` (one per `groupBy` value), or all bare value strings (drawn only on a board with no `groupBy`). NOT a field projection; one spelling per list | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter, handed to the wire `$filter` — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Base query filter, handed to the wire `$filter` — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **limit** | `integer` | optional | Maximum number of records loaded onto the board (row cap); lowered to the query's top-level `$top` (renderer default 100). The component-level `dataSource.limit` wins when both are set; a bound view's `pagination.pageSize` fills this key only when it is unset — and on this face unset is the whole rule, because every cap this key accepts is one the binding gate already treats as authored | | **data** | `any[]` | optional | Static inline cards — bypasses the object query | | **cardTitle** | `string` | optional | Field rendered as each card title | @@ -1029,7 +1053,7 @@ Sort field and direction pair | **className** | `string` | optional | CSS class names applied to the lane | | **collapsed** | `boolean` | optional | Whether the lane first renders collapsed — a title spine with its cards withheld, which the viewer can reopen | -### Nested Shape: `ObjectKanbanProps.filter[number]` +### Nested Shape: `ObjectKanbanProps.filter[number][option 1]` View filter rule @@ -1039,6 +1063,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ObjectKanbanProps.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `ObjectKanbanProps.grouping` | Property | Type | Required | Description | @@ -1075,7 +1107,7 @@ View filter rule | **objectName** | `string` | optional | Object this map binds to — the THIRD record source `getDataConfig` resolves, after `data` and `staticData`. Optional because the component-level `dataSource` binding can supply the object instead | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source binding (ViewDataSchema — discriminated on `provider`: object \| value \| schema), read FIRST by `getDataConfig`. Static inline rows live at `{ provider: 'value', items: [...] }` or at `staticData`; the bare-array shortcut is refused | | **staticData** | `any[]` | optional | Inline records — read SECOND by `getDataConfig`, wrapped into a `{ provider: 'value' }` config | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Marker order for the fetched records — the SortItem array form `[{ field, order }, ...]`, the one sort orthography every declared `sort` door on this platform shares; lowered to the wire `$orderby`. The legacy string clause (`name desc`) is refused — see migration `object-block-sort-item-array` | | **map** | `{ latitudeField?: string; longitudeField?: string; locationField?: string; titleField?: string; … }` | optional | Map field config, the author face — the same block `ListViewSchema.map` declares, and the one the renderer validates this node against. Taken WHOLE when present: the flat top-level spelling beside it is ignored | | **mapStyle** | `string` | optional | MapLibre style URL or spec, overriding the public demo tiles. Read before `map.style`; NOT the base node `style`, which is an inline CSS record | @@ -1104,7 +1136,7 @@ View filter rule | **schemaId** | `string` | ✅ | Schema identifier — typically the metadata type name | | **schema** | `Record` | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. | -### Nested Shape: `ObjectMapProps.filter[number]` +### Nested Shape: `ObjectMapProps.filter[number][option 1]` View filter rule @@ -1114,6 +1146,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ObjectMapProps.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `ObjectMapProps.sort[number]` Sort field and direction pair @@ -1219,7 +1259,7 @@ Sort field and direction pair | **icon** | `string` | optional | Lucide icon name drawn in the metric tile header, inside the `colorVariant`-tinted square. Read on this component — `ObjectMetricWidget` forwards it to `MetricWidget`, which resolves it with `getLazyIcon` (the `LazyIcon` module: kebab-case or PascalCase, degrading to the `Database` glyph on an unknown name). | | **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Icon container color variant | | **aggregate** | `{ field?: string; function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>; groupBy?: string \| object }` | optional | Aggregation run against the object: `{ field?, function, groupBy? }` — `function` one of count / sum / avg / min / max / count_distinct (`field` needed for all but count), `groupBy` a field name or a `{ field, dateGranularity }` node, absent for one number over every row | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter the aggregation is scoped by — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Filter the aggregation is scoped by — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **format** | `string` | optional | Number format pattern (e.g. '0,0', '$0,0', '0%') | | **currency** | `string` | optional | ISO currency code (e.g. 'USD') — enables currency formatting | | **prefix** | `string` | optional | Static prefix before the formatted value | @@ -1230,7 +1270,7 @@ Sort field and direction pair | **trend** | `{ value: number; label?: string \| Record; direction?: Enum<'up' \| 'down' \| 'neutral'> }` | optional | Static trend badge `{ value, label?, direction? }` — `value` painted as a percentage, `direction` up / down / neutral. A `compareTo`-derived trend replaces it | | **drillDown** | `{ enabled?: boolean; title?: string; target?: Enum<'drawer' \| 'dialog' \| 'navigate'>; columns?: string[]; … }` | optional | Click-through drill config `{ enabled?, title?, target?, columns?, maxRows?, report? }` — opens the records behind the number, scoped by the metric's own `filter` — or draws the report `report` defines; a present block is on unless `enabled: false`. `filter` and `mode` are refused: a metric tile has no click context and no row | | **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'> }` | optional | Period-over-period comparison `{ kind }` — `previousPeriod` or `previousYear`, shifting the date macros in the tile's own `filter`. `dimension` is refused: this tile never reads a dataset time dimension | -| **polarity** | `Enum<'higher' \| 'lower' \| 'neutral'>` | optional | Which direction of change is good news: higher (rise green, fall red), lower (fall green, rise red) or neutral (no good/bad colour). Colours the trend badge, computed (`compareTo`) or static (`trend`); undeclared keeps rise green, fall red | +| **polarity** | `Enum<'higher' \| 'lower' \| 'neutral'>` | optional | Which direction of change is good news: higher (rise green, fall red), lower (fall green, rise red) or neutral (no good/bad colour). Colours the trend badge, computed (`compareTo`) or static (`trend`); undeclared keeps rise green, fall red. The badge and polarity read the aggregate before `invert`, so declare polarity against the raw value: an error rate shown as uptime is lower | ### Nested Shape: `ObjectMetricProps.aggregate` @@ -1240,7 +1280,7 @@ Sort field and direction pair | **function** | `Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct'>` | ✅ | Aggregation function — `count`, `sum`, `avg`, `min`, `max` or `count_distinct` | | **groupBy** | `string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string }` | optional | Field the rows are grouped by, or a `{ field, dateGranularity }` date-bucket node. Omit it for the one number over every row (`_all`) | -### Nested Shape: `ObjectMetricProps.filter[number]` +### Nested Shape: `ObjectMetricProps.filter[number][option 1]` View filter rule @@ -1250,6 +1290,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ObjectMetricProps.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `ObjectMetricProps.trend` | Property | Type | Required | Description | @@ -1286,7 +1334,7 @@ View filter rule | :--- | :--- | :--- | :--- | | **objectName** | `string` | optional | Object this timeline binds to. Optional because the component-level `dataSource` binding can supply the object instead — this block registers through `ElementDataSourceGate`, which lowers the binding onto this key before the renderer sees the node | | **timeline** | `{ startDateField: string; endDateField?: string; titleField: string; groupByField?: string; … }` | optional | Timeline configuration, the author face — the same block `ListViewSchema.timeline` declares: `{ startDateField, endDateField, titleField, groupByField, colorField, scale }`. The flat top-level spellings beside it are the runtime handoff, not a second authoring spelling | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Row order for the fetched entries — the SortItem array form `[{ field, order }, ...]`, the one sort orthography every declared `sort` door on this platform shares; lowered to the wire `$orderby`. The legacy string clause (`name desc`) is refused — see migration `object-block-sort-item-array` | | **limit** | `integer` | optional | Maximum number of records loaded onto the rail (row cap); lowered to the query's top-level `$top` (renderer default 100). A timeline renders one rail with no pagination control, so this is the author's window rather than a page size | | **data** | `any[]` | optional | Pre-fetched records — read FIRST as the rail's row source, ahead of the data-scope binding and the fetch, and composed into entries through the same `timeline` field bindings a fetched row takes; authoring it suppresses the object query entirely. Distinct from `items`, which is the already-composed entry shape and wins over this key when both are written | @@ -1311,7 +1359,7 @@ View filter rule | **colorField** | `string` | optional | Field to derive each item color from (it names a field, not a color): the option color declared on that field for the record value, else the value itself when it already is a color literal (hex, rgb() or hsl()), else the timeline default marker color | | **scale** | `Enum<'hour' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>` | optional (default: `"week"`) | Default timeline scale | -### Nested Shape: `ObjectTimelineProps.filter[number]` +### Nested Shape: `ObjectTimelineProps.filter[number][option 1]` View filter rule @@ -1321,6 +1369,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ObjectTimelineProps.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `ObjectTimelineProps.sort[number]` Sort field and direction pair @@ -1376,7 +1432,7 @@ Sort field and direction pair | **objectName** | `string` | optional | Object this tree binds to — the THIRD record source `resolveRecordSourceConfig` resolves, after `data` and `staticData`. Optional because either of the first two rungs resolves the source without it: `data` can name the object itself (`{ provider: 'object', object }`) and `staticData` needs none. ⚠️ NOT supplied by the component-level `dataSource` binding the sibling blocks name — this renderer registers no such gate | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source binding (ViewDataSchema — discriminated on `provider`: object \| value \| schema), read FIRST by `resolveRecordSourceConfig`. Static inline rows live at `{ provider: 'value', items: [...] }` or at `staticData`; the bare-array shortcut is refused | | **staticData** | `any[]` | optional | Inline records — read SECOND by `resolveRecordSourceConfig`, wrapped into a `{ provider: 'value' }` config | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Base query filter — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in this map shares; lowered to the wire `$filter`. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **tree** | `{ parentField?: string; labelField?: string; fields?: string[]; defaultExpandedDepth?: integer }` | optional | Tree/hierarchy configuration, the author face — the same block `ListViewSchema.tree` declares: `{ parentField?, labelField?, fields?, defaultExpandedDepth? }`. `parentField` auto-detects from the object schema when omitted | | **navigation** | `{ mode?: Enum<'drawer' \| 'page' \| 'new_window' \| 'none'>; preventNavigation?: boolean; openNewTab?: boolean; size?: Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>; … }` | optional | Row-click navigation config — the same block `ListViewSchema.navigation` declares (`{ mode, size, openNewTab, preventNavigation }`), `mode` one of the four `NavigationModeSchema` values (`drawer` by default) | @@ -1402,7 +1458,7 @@ Sort field and direction pair | **schemaId** | `string` | ✅ | Schema identifier — typically the metadata type name | | **schema** | `Record` | optional | Inline JSON Schema (Draft 2020-12). Optional when schemaId is resolvable. | -### Nested Shape: `ObjectTreeProps.filter[number]` +### Nested Shape: `ObjectTreeProps.filter[number][option 1]` View filter rule @@ -1412,6 +1468,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ObjectTreeProps.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `ObjectTreeProps.tree` | Property | Type | Required | Description | @@ -1832,7 +1896,7 @@ Type: `string` | **readonly** | `boolean` | optional | Render the grid read-only: cells locked, no Save button, and no adding or removing lines (renderer default: editable) | | **minRows** | `number` | optional | Minimum number of rows | | **maxRows** | `number` | optional | Maximum number of rows | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Additional criteria for the child rows — the ViewFilterRule array form `[{ field, operator, value }, ...]`, AND-combined with the parent relationship condition, never substituted for it: it can only narrow this record's lines | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Additional criteria for the child rows — the ViewFilterRule array form `[{ field, operator, value }, ...]`, AND-combined with the parent relationship condition, never substituted for it: it can only narrow this record's lines | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Load order for the child rows — the SortItem array form `[{ field, order }, ...]` (renderer default: storage order) | | **limit** | `integer` | optional | Row cap for the child fetch (renderer default: 500). The grid has no pagination, so this is the window of rows that are editable and saved together | @@ -1861,7 +1925,7 @@ Type: `string` | **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — the cell is read-only when TRUE, evaluated per row against the row as `record` plus the header as `parent` (e.g. P`parent.status == 'paid'`). | | **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — the cell is required when TRUE. Same `record` + `parent` scope as `readonlyWhen`. PRESENTATION ONLY: this flags the cell inline-invalid in the grid; nothing on the write path reads it. The server-enforced contract is the child FIELD's own `requiredWhen` — a transition gate, see `Field.requiredWhen` — which hydration copies onto an identity-only column, so declaring the requirement here alone enforces nothing. | -### Nested Shape: `RecordLineItemsProps.filter[number]` +### Nested Shape: `RecordLineItemsProps.filter[number][option 1]` View filter rule @@ -1871,6 +1935,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `RecordLineItemsProps.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `RecordLineItemsProps.sort[number]` Sort field and direction pair @@ -1971,11 +2043,11 @@ Sort field and direction pair | **objectName** | `string` | ✅ | Related object name (e.g., "task", "opportunity") | | **relationshipField** | `string` | ✅ | Field on related object that points to this record (e.g., "account_id") | | **relationshipValueField** | `string` | optional (default: `"id"`) | Parent-record field whose value relationshipField stores (default 'id'; e.g. 'name' for name-keyed junctions). | -| **parentDataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … }` | optional | The PARENT record this related list hangs off, bound by a query of its own: an object binding `{ object, view?, filter?, sort?, limit? }`, the shape `dataSource` takes. Read by the single-record rule: the FIRST row of the bound set by its `sort` is the parent (declare `limit: 1` to say one row is meant), and when no row answers, the block renders a loud absence, never an empty list, which would read as "this record has no related records". `relationshipValueField` is read on that row. When this key is absent, the record page's record context remains the parent. The node-level `dataSource` on this block keeps its published meaning, the CHILD rows the list draws (it fills `objectName`, `columns`, `filter`, `sort` and `limit`), and is never read as the parent. | +| **parentDataSource** | `{ object: string; view?: string; filter?: (object \| object)[]; sort?: object[]; … }` | optional | The PARENT record this related list hangs off, bound by a query of its own: an object binding `{ object, view?, filter?, sort?, limit? }`, the shape `dataSource` takes. Read by the single-record rule: the FIRST row of the bound set by its `sort` is the parent (declare `limit: 1` to say one row is meant), and when no row answers, the block renders a loud absence, never an empty list, which would read as "this record has no related records". `relationshipValueField` is read on that row. When this key is absent, the record page's record context remains the parent. The node-level `dataSource` on this block keeps its published meaning, the CHILD rows the list draws (it fills `objectName`, `columns`, `filter`, `sort` and `limit`), and is never read as the parent. | | **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | optional | Fields to display in the related list — either plain field-name strings, or the same per-column entries a saved list view declares (`ListColumn`: `field`, plus `label`, `width`, `align`, `hidden`, `sortable`, `summary`, …). A view-supplied list may arrive in the `ListColumn` spelling: objectui composes a saved view's `columns` onto this block verbatim, and this key declares the SAME union as `listViews[].columns`. One spelling per list — the two arms are exclusive, so an array mixing strings and column objects is refused. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list is just another surface that lists that object). Override chain: child highlightFields → field-level relatedListColumns (field-name strings only) → this inline list. | | **sort** | `string \| { field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sort order for related records | | **limit** | `integer` | optional (default: `5`) | Number of records to display initially | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Additional filter criteria for related records | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Additional filter criteria for related records | | **title** | `string \| Record` | optional | Custom title for the related list | | **showViewAll** | `boolean` | optional (default: `true`) | Show "View All" link to see all related records | | **actions** | `string[]` | optional | Action IDs available for related records | @@ -1991,7 +2063,7 @@ Sort field and direction pair | :--- | :--- | :--- | :--- | | **object** | `string` | ✅ | Object to query | | **view** | `string` | optional | Named view to apply | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Additional filter criteria — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in ComponentPropsMap shares; AND-combined with the filter of the named view. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Additional filter criteria — the ViewFilterRule array form `[{ field, operator, value }, ...]`, the one filter orthography every `filter` door in ComponentPropsMap shares; AND-combined with the filter of the named view. The MongoDB-style record form is refused — see migration `element-data-source-and-object-block-filter-rule-array` | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sort order | | **limit** | `integer` | optional | Max records to display | @@ -2014,7 +2086,7 @@ Sort field and direction pair | **link** | `boolean` | optional | Functions as the primary navigation link (triggers View navigation) | | **action** | `string` | optional | Registered Action ID to execute when clicked | -### Nested Shape: `RecordRelatedListProps.filter[number]` +### Nested Shape: `RecordRelatedListProps.filter[number][option 1]` View filter rule @@ -2024,11 +2096,19 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `RecordRelatedListProps.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `RecordRelatedListProps.add` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **picker** | `{ object: string; valueField?: string; labelField?: string; filter?: object[] }` | ✅ | Where the Add affordance sources records from. | +| **picker** | `{ object: string; valueField?: string; labelField?: string; filter?: (object \| object)[] }` | ✅ | Where the Add affordance sources records from. | | **linkField** | `string` | optional | Field on `objectName` that stores the picked record id (junction case). Omit for a 1:m re-parent. | | **label** | `string \| Record` | optional | Label for the Add button (default "Add"). | diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 257c53c5f3a..c49b87e0793 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -59,8 +59,8 @@ nothing. The one item-name grammar itself lives in ## TypeScript Usage ```typescript -import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, EmptyStateSchema, FormButtonConfigSchema, FormFieldSchema, FormSectionSchema, FormSelectOptionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, HttpMethodSubsetSchema, HttpRequestSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListMapConfigSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewItemWireSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui'; -import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, EmptyState, FormButtonConfig, FormField, FormSection, FormSelectOption, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, HttpMethodSubset, HttpRequest, KanbanConfig, ListChartConfig, ListColumn, ListMapConfig, ListView, NavigationConfig, NavigationMode, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemName, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui'; +import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, EmptyStateSchema, FormButtonConfigSchema, FormFieldSchema, FormSectionSchema, FormSelectOptionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, HttpMethodSubsetSchema, HttpRequestSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListMapConfigSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterElementSchema, ViewFilterOrGroupSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewItemWireSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui'; +import type { AddRecordConfig, AppearanceConfig, CalendarConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, EmptyState, FormButtonConfig, FormField, FormSection, FormSelectOption, FormView, GalleryConfig, GanttConfig, GanttQuickFilter, GroupingConfig, HttpMethodSubset, HttpRequest, KanbanConfig, ListChartConfig, ListColumn, ListMapConfig, ListView, NavigationConfig, NavigationMode, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, TreeConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterElement, ViewFilterOrGroup, ViewFilterRule, ViewItem, ViewItemName, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui'; // Validate data const result = AddRecordConfigSchema.parse(data); @@ -780,7 +780,7 @@ Map view configuration | **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | | **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns — the PROJECTION of the per-view field composition `columns` x `hiddenFields` x `fieldOrder`: this list is the candidate set AND the baseline order; `hiddenFields` subtracts from it and `fieldOrder` orders what survives. `hiddenFields` and `fieldOrder` cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's `ListView` grid derives the object's default columns). | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Filter criteria (JSON Rules) | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | @@ -863,7 +863,7 @@ Map view configuration | **link** | `boolean` | optional | Functions as the primary navigation link (triggers View navigation) | | **action** | `string` | optional | Registered Action ID to execute when clicked | -### Nested Shape: `ListView.filter[number]` +### Nested Shape: `ListView.filter[number][option 1]` View filter rule @@ -873,6 +873,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ListView.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `ListView.userFilters` | Property | Type | Required | Description | @@ -1161,7 +1169,7 @@ View filter rule | **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | | **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns — the PROJECTION of the per-view field composition `columns` x `hiddenFields` x `fieldOrder`: this list is the candidate set AND the baseline order; `hiddenFields` subtracts from it and `fieldOrder` orders what survives. `hiddenFields` and `fieldOrder` cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's `ListView` grid derives the object's default columns). | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Filter criteria (JSON Rules) | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | @@ -1244,7 +1252,7 @@ View filter rule | **link** | `boolean` | optional | Functions as the primary navigation link (triggers View navigation) | | **action** | `string` | optional | Registered Action ID to execute when clicked | -### Nested Shape: `ObjectListView.filter[number]` +### Nested Shape: `ObjectListView.filter[number][option 1]` View filter rule @@ -1254,6 +1262,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ObjectListView.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + ### Nested Shape: `ObjectListView.selection` | Property | Type | Required | Description | @@ -1695,7 +1711,7 @@ Tab configuration for multi-tab view interface | **label** | `string \| Record` | optional | Display label | | **icon** | `string` | optional | Tab icon name | | **view** | `string` | optional | Referenced list view name from listViews | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Tab-specific filter criteria | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Tab-specific filter criteria | | **order** | `integer` | optional | Tab display order | | **pinned** | `boolean` | optional (default: `false`) | Pin tab (cannot be removed by users) | | **isDefault** | `boolean` | optional (default: `false`) | Set as the default active tab | @@ -1735,7 +1751,7 @@ Tab configuration for multi-tab view interface | **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | | **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns — the PROJECTION of the per-view field composition `columns` x `hiddenFields` x `fieldOrder`: this list is the candidate set AND the baseline order; `hiddenFields` subtracts from it and `fieldOrder` orders what survives. `hiddenFields` and `fieldOrder` cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's `ListView` grid derives the object's default columns). | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Filter criteria (JSON Rules) | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | @@ -1814,7 +1830,7 @@ Tab configuration for multi-tab view interface | **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | | **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns — the PROJECTION of the per-view field composition `columns` x `hiddenFields` x `fieldOrder`: this list is the candidate set AND the baseline order; `hiddenFields` subtracts from it and `fieldOrder` orders what survives. `hiddenFields` and `fieldOrder` cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's `ListView` grid derives the object's default columns). | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Filter criteria (JSON Rules) | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | @@ -1936,6 +1952,99 @@ This schema accepts one of the following structures: --- +--- + +## ViewFilterElement + +One view filter element: a flat rule, or an any-of group of flat rules + +### Union Options + +This schema accepts one of the following structures: + +#### Option 1 + +View filter rule + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| 'starts_with' \| 'ends_with' \| 'greater_than' \| 'less_than' \| 'greater_than_or_equal' \| … +10 more>` | ✅ | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | + +### Allowed Values: `ViewFilterElement[option 1].operator` + +* `equals` +* `not_equals` +* `contains` +* `not_contains` +* `icontains` +* `starts_with` +* `ends_with` +* `greater_than` +* `less_than` +* `greater_than_or_equal` +* `less_than_or_equal` +* `in` +* `not_in` +* `is_empty` +* `is_not_empty` +* `is_null` +* `is_not_null` +* `before` +* `after` +* `between` + +--- + +#### Option 2 + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + +### Nested Shape: `ViewFilterElement[option 2].or[number]` + +View filter rule + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | + +--- + + +--- + +## ViewFilterOrGroup + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + +### Nested Shape: `ViewFilterOrGroup.or[number]` + +View filter rule + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **field** | `string` | ✅ | Field name to filter on | +| **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | +| **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | + + --- ## ViewFilterRule @@ -2016,7 +2125,7 @@ This schema accepts one of the following structures: | **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | | **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns — the PROJECTION of the per-view field composition `columns` x `hiddenFields` x `fieldOrder`: this list is the candidate set AND the baseline order; `hiddenFields` subtracts from it and `fieldOrder` orders what survives. `hiddenFields` and `fieldOrder` cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's `ListView` grid derives the object's default columns). | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Filter criteria (JSON Rules) | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | @@ -2188,7 +2297,7 @@ This schema accepts one of the following structures: | **type** | `Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>` | optional (default: `"grid"`) | | | **data** | `{ provider: 'object'; object: string } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }` | optional | Data source configuration (defaults to "object" provider) | | **columns** | `string[] \| { field: string; label?: string \| Record; width?: number; align?: Enum<'left' \| 'center' \| 'right'>; … }[]` | ✅ | Fields to display as columns — the PROJECTION of the per-view field composition `columns` x `hiddenFields` x `fieldOrder`: this list is the candidate set AND the baseline order; `hiddenFields` subtracts from it and `fieldOrder` orders what survives. `hiddenFields` and `fieldOrder` cannot add a field omitted here. An empty list declares no projection, so neither of them applies: which columns show is then left to the renderer (objectui's `ListView` grid derives the object's default columns). | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Filter criteria (JSON Rules) | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Filter criteria (JSON Rules) | | **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | | | **searchableFields** | `string[]` | optional | Fields enabled for search | | **filterableFields** | `never` | optional | [REMOVED] `view.list.filterableFields` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove) — the key was declared as a shorthand for `userFilters.fields` (the quick-filter bar), but the one renderer that read it used it as a whitelist of the fields the Filter panel offers instead, so one key carried two meanings and drew no quick filter. Delete the key; no per-view Filter-panel whitelist replaces it. For a curated end-user filter bar, declare `userFilters: { element: 'dropdown', fields: [{ field: '' }] }`. The Filter panel offers every field the user may read: field read security decides, and a field the object definition marks `hidden: true` is not offered. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | @@ -2376,13 +2485,13 @@ Tab configuration for multi-tab view interface | **label** | `string \| Record` | optional | Display label | | **icon** | `string` | optional | Tab icon name | | **view** | `string` | optional | Referenced list view name from listViews | -| **filter** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | optional | Tab-specific filter criteria | +| **filter** | `({ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] } \| { or: object[] })[]` | optional | Tab-specific filter criteria | | **order** | `integer` | optional | Tab display order | | **pinned** | `boolean` | optional (default: `false`) | Pin tab (cannot be removed by users) | | **isDefault** | `boolean` | optional (default: `false`) | Set as the default active tab | | **visible** | `boolean` | optional (default: `true`) | Tab visibility | -### Nested Shape: `ViewTab.filter[number]` +### Nested Shape: `ViewTab.filter[number][option 1]` View filter rule @@ -2392,6 +2501,14 @@ View filter rule | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>` | ✅ | Filter operator | | **value** | `string \| number \| boolean \| null \| (string \| number)[]` | optional | Filter value. The accepted SHAPE depends on the operator: `in` / `not_in` take an array (any length, including []), `between` takes exactly [min, max], every other operator takes a scalar. The unary operators (is_empty / is_not_empty / is_null / is_not_null) take their direction from the operator name and ignore this key. One operator bounds the VALUE as well as the shape: `icontains` takes a NON-EMPTY STRING, the comparand the Filter Protocol conformance table declares for it — an empty comparand constrains nothing and a non-string one would answer a query nobody wrote, and both are refused at the query path too. | +### Nested Shape: `ViewTab.filter[number][option 2]` + +[PLANNED — not yet rendered] An any-of group: `{ or: [rule, ...] }` matches a record when ANY of its rules matches. It sits in the filter list beside plain rules, and the list still ANDs every element, so `[{ or: [{ field: "status", operator: "equals", value: "open" }, { field: "completed_at", operator: "after", value: "{7_days_ago}" }] }]` selects the open records and the ones completed in the last 7 days. One level only: its members are flat rules. No console reads it yet: until the list query build reads the group, a view that stores one is refused when it loads (the list reports a malformed filter and shows no rows). + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **or** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'icontains' \| …>; value?: string \| number \| boolean \| null \| (string \| number)[] }[]` | ✅ | The rules of which ANY may match: a non-empty array of flat filter rules `{ field, operator, value }`, each judged exactly as a rule at the top level of the filter. A group cannot hold a group. | + --- diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md index ba08cfaa4fe..4c51f3708ac 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md @@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate. | Dir | Sites | |---|---| -| `api/` | 462 | +| `api/` | 464 | diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json index c30c3a437eb..5f15a205f88 100644 --- a/packages/spec/api-surface/api.json +++ b/packages/spec/api-surface/api.json @@ -808,6 +808,8 @@ "PublishMetaItemRequestSchema (const)", "PublishMetaItemResponse (type)", "PublishMetaItemResponseSchema (const)", + "PublishPackageDraftsRequest (type)", + "PublishPackageDraftsRequestSchema (const)", "PublishPackageDraftsResponse (type)", "PublishPackageDraftsResponseSchema (const)", "QueryAdapterConfig (type)", diff --git a/packages/spec/api-surface/ui.json b/packages/spec/api-surface/ui.json index 18ecfd4b8c0..68448dd4ad3 100644 --- a/packages/spec/api-surface/ui.json +++ b/packages/spec/api-surface/ui.json @@ -56,6 +56,9 @@ "AppContextSelector (type)", "AppContextSelectorParsed (type)", "AppContextSelectorSchema (const)", + "AppLandingEntry (type)", + "AppLandingEntryParsed (type)", + "AppLandingEntrySchema (const)", "AppParsed (type)", "AppSchema (const)", "AppearanceConfig (type)", @@ -429,6 +432,7 @@ "RowHeight (type)", "RowHeightSchema (const)", "SECTION_HEADING_COUNT_HOLE (const)", + "SINGLE_SERIES_CHART_TYPES (const)", "STRING_ARM_REGISTERED_TYPES (const)", "SelectionConfig (type)", "SelectionConfigParsed (type)", @@ -468,7 +472,13 @@ "ViewData (type)", "ViewDataParsed (type)", "ViewDataSchema (const)", + "ViewFilterElement (type)", + "ViewFilterElementParsed (type)", + "ViewFilterElementSchema (const)", "ViewFilterOperator (type)", + "ViewFilterOrGroup (type)", + "ViewFilterOrGroupParsed (type)", + "ViewFilterOrGroupSchema (const)", "ViewFilterRule (type)", "ViewFilterRuleParsed (type)", "ViewFilterRuleSchema (const)", @@ -515,6 +525,7 @@ "chartAggregateCategoryKey (function)", "chartAggregateResultKeys (function)", "chartAggregateValueKey (function)", + "checkAppLandingItems (function)", "checkDashboardWidgetChartMeasureArity (function)", "checkDashboardWidgetMetricMeasureArity (function)", "checkDashboardWidgetStageOrder (function)", @@ -555,6 +566,7 @@ "isKnownComponentType (function)", "isRecordContextBlockType (function)", "isViewContainerShaped (function)", + "isViewFilterOrGroup (function)", "listViewGroupKeyPredicate (function)", "listViewGroupOrderColumn (function)", "misplacedDefaultPageRefusal (function)", diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 790a238b3e2..605b34d1be2 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -1372,6 +1372,8 @@ "api/PublishMetaItemResponse:seq", "api/PublishMetaItemResponse:success", "api/PublishMetaItemResponse:version", + "api/PublishPackageDraftsRequest:backfill", + "api/PublishPackageDraftsRequest:message", "api/PublishPackageDraftsResponse:backfillApplied", "api/PublishPackageDraftsResponse:commitId", "api/PublishPackageDraftsResponse:failed", diff --git a/packages/spec/declaration-map/api.json b/packages/spec/declaration-map/api.json index 4ac4f648eea..64cdd51cec7 100644 --- a/packages/spec/declaration-map/api.json +++ b/packages/spec/declaration-map/api.json @@ -591,6 +591,8 @@ "PublishMetaItemRequestSchema": "api/PublishMetaItemRequest", "PublishMetaItemResponse": "api/PublishMetaItemResponse", "PublishMetaItemResponseSchema": "api/PublishMetaItemResponse", + "PublishPackageDraftsRequest": "api/PublishPackageDraftsRequest", + "PublishPackageDraftsRequestSchema": "api/PublishPackageDraftsRequest", "PublishPackageDraftsResponse": "api/PublishPackageDraftsResponse", "PublishPackageDraftsResponseSchema": "api/PublishPackageDraftsResponse", "QueryAdapterConfig": "api/QueryAdapterConfig", diff --git a/packages/spec/export-origins/api.json b/packages/spec/export-origins/api.json index 4ad7501a127..7f55a7bf09e 100644 --- a/packages/spec/export-origins/api.json +++ b/packages/spec/export-origins/api.json @@ -779,6 +779,8 @@ "PublishMetaItemRequestSchema": "src/api/protocol.zod.ts#PublishMetaItemRequestSchema (const)", "PublishMetaItemResponse": "src/api/protocol.zod.ts#PublishMetaItemResponse (type)", "PublishMetaItemResponseSchema": "src/api/protocol.zod.ts#PublishMetaItemResponseSchema (const)", + "PublishPackageDraftsRequest": "src/api/protocol.zod.ts#PublishPackageDraftsRequest (type)", + "PublishPackageDraftsRequestSchema": "src/api/protocol.zod.ts#PublishPackageDraftsRequestSchema (const)", "PublishPackageDraftsResponse": "src/api/protocol.zod.ts#PublishPackageDraftsResponse (type)", "PublishPackageDraftsResponseSchema": "src/api/protocol.zod.ts#PublishPackageDraftsResponseSchema (const)", "QueryAdapterConfig": "src/api/query-adapter.zod.ts#QueryAdapterConfig (type)", diff --git a/packages/spec/export-origins/ui.json b/packages/spec/export-origins/ui.json index c504d4ab50f..fac837449a4 100644 --- a/packages/spec/export-origins/ui.json +++ b/packages/spec/export-origins/ui.json @@ -53,6 +53,9 @@ "AppContextSelector": "src/ui/app.zod.ts#AppContextSelector (type)", "AppContextSelectorParsed": "src/ui/app.zod.ts#AppContextSelectorParsed (type)", "AppContextSelectorSchema": "src/ui/app.zod.ts#AppContextSelectorSchema (const)", + "AppLandingEntry": "src/ui/app.zod.ts#AppLandingEntry (type)", + "AppLandingEntryParsed": "src/ui/app.zod.ts#AppLandingEntryParsed (type)", + "AppLandingEntrySchema": "src/ui/app.zod.ts#AppLandingEntrySchema (const)", "AppParsed": "src/ui/app.zod.ts#AppParsed (type)", "AppSchema": "src/ui/app.zod.ts#AppSchema (const)", "AppearanceConfig": "src/ui/view.zod.ts#AppearanceConfig (type)", @@ -412,6 +415,7 @@ "RowHeight": "src/ui/view.zod.ts#RowHeight (type)", "RowHeightSchema": "src/ui/view.zod.ts#RowHeightSchema (const)", "SECTION_HEADING_COUNT_HOLE": "src/ui/component.zod.ts#SECTION_HEADING_COUNT_HOLE (const)", + "SINGLE_SERIES_CHART_TYPES": "src/ui/dashboard.zod.ts#SINGLE_SERIES_CHART_TYPES (const)", "STRING_ARM_REGISTERED_TYPES": "src/ui/component-type-vocabulary.ts#STRING_ARM_REGISTERED_TYPES (const)", "SelectionConfig": "src/ui/view.zod.ts#SelectionConfig (type)", "SelectionConfigParsed": "src/ui/view.zod.ts#SelectionConfigParsed (type)", @@ -451,7 +455,13 @@ "ViewData": "src/ui/view.zod.ts#ViewData (type)", "ViewDataParsed": "src/ui/view.zod.ts#ViewDataParsed (type)", "ViewDataSchema": "src/ui/view.zod.ts#ViewDataSchema (const)", + "ViewFilterElement": "src/ui/view.zod.ts#ViewFilterElement (type)", + "ViewFilterElementParsed": "src/ui/view.zod.ts#ViewFilterElementParsed (type)", + "ViewFilterElementSchema": "src/ui/view.zod.ts#ViewFilterElementSchema (const)", "ViewFilterOperator": "src/ui/view.zod.ts#ViewFilterOperator (type)", + "ViewFilterOrGroup": "src/ui/view.zod.ts#ViewFilterOrGroup (type)", + "ViewFilterOrGroupParsed": "src/ui/view.zod.ts#ViewFilterOrGroupParsed (type)", + "ViewFilterOrGroupSchema": "src/ui/view.zod.ts#ViewFilterOrGroupSchema (const)", "ViewFilterRule": "src/ui/view.zod.ts#ViewFilterRule (type)", "ViewFilterRuleParsed": "src/ui/view.zod.ts#ViewFilterRuleParsed (type)", "ViewFilterRuleSchema": "src/ui/view.zod.ts#ViewFilterRuleSchema (const)", @@ -498,6 +508,7 @@ "chartAggregateCategoryKey": "src/ui/chart-aggregate.ts#chartAggregateCategoryKey (function)", "chartAggregateResultKeys": "src/ui/chart-aggregate.ts#chartAggregateResultKeys (function)", "chartAggregateValueKey": "src/ui/chart-aggregate.ts#chartAggregateValueKey (function)", + "checkAppLandingItems": "src/ui/app.zod.ts#checkAppLandingItems (function)", "checkDashboardWidgetChartMeasureArity": "src/ui/dashboard.zod.ts#checkDashboardWidgetChartMeasureArity (function)", "checkDashboardWidgetMetricMeasureArity": "src/ui/dashboard.zod.ts#checkDashboardWidgetMetricMeasureArity (function)", "checkDashboardWidgetStageOrder": "src/ui/dashboard.zod.ts#checkDashboardWidgetStageOrder (function)", @@ -538,6 +549,7 @@ "isKnownComponentType": "src/ui/component-type-vocabulary.ts#isKnownComponentType (function)", "isRecordContextBlockType": "src/ui/react-blocks.ts#isRecordContextBlockType (function)", "isViewContainerShaped": "src/ui/assembled-views.zod.ts#isViewContainerShaped (function)", + "isViewFilterOrGroup": "src/ui/view.zod.ts#isViewFilterOrGroup (function)", "listViewGroupKeyPredicate": "src/ui/view-grouping-query.ts#listViewGroupKeyPredicate (function)", "listViewGroupOrderColumn": "src/ui/view-grouping-query.ts#listViewGroupOrderColumn (function)", "misplacedDefaultPageRefusal": "src/ui/default-record-page.ts#misplacedDefaultPageRefusal (function)", diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json index 96c4f8ae758..256bdd1570c 100644 --- a/packages/spec/json-schema.manifest/api.json +++ b/packages/spec/json-schema.manifest/api.json @@ -323,6 +323,7 @@ "api/PublicFormSubmitResponse", "api/PublishMetaItemRequest", "api/PublishMetaItemResponse", + "api/PublishPackageDraftsRequest", "api/PublishPackageDraftsResponse", "api/QueryAdapterConfig", "api/QueryAdapterTarget",