diff --git a/.changeset/object-schema-metadata-derive-from-spec.md b/.changeset/object-schema-metadata-derive-from-spec.md new file mode 100644 index 0000000000..501ecb2388 --- /dev/null +++ b/.changeset/object-schema-metadata-derive-from-spec.md @@ -0,0 +1,34 @@ +--- +'@object-ui/types': minor +'@object-ui/app-shell': patch +'@object-ui/react': patch +--- + +`ObjectSchemaMetadata` is now derived from `@objectstack/spec`'s `ServiceObject` +instead of being a hand-written copy (objectui#5362; maintainer ruling +2026-08-20: the object document type belongs to the spec). + +What changes on the published type surface: + +- **Gained:** the full spec object-document surface, including the three keys + the runtime already reads but the old interface rejected as excess + properties: `icon`, `titleFormat`, `listViews` (plus `pluralLabel`, + `nameField`, `displayNameField`, `managedBy` as a spec key, and the rest of + the spec document). +- **Removed:** nine members the old interface declared that no objectui + runtime code reads and the spec document does not know: `extends`, + `triggers`, `primary_key`, `relationships`, `name_field` (the spec spelling + is `nameField`), `soft_delete`, `audit_trail`, `version`, `cache`. + `ObjectTrigger` and `ObjectRelationship` remain exported unchanged. +- **Kept:** `editMode` — the one measured client-side member the runtime reads + (`recordFormNavigation` / `AppContent`) — now declared on the new + `ObjectSchemaClientExtensions` interface, which the derivation intersects. + Note the spec's strict parse rejects `editMode` on published documents + (`unrecognized_keys`); it is a client-type member only. + +Spelling settlement: `listViews` (camelCase) is canonical — `list_views` +appears nowhere in `@objectstack/spec` 17.2.0. Runtime read sites in +`@object-ui/app-shell` and `@object-ui/react` keep a documented snake-spelling +READ fallback for stored pre-settlement documents (that stock has never been +censused — objectstack#7917); the CRUD guide and its pinned transcription now +author the canonical spelling. diff --git a/content/docs/guide/building-crud-app.md b/content/docs/guide/building-crud-app.md index e66bfe9e80..f86e164ac6 100644 --- a/content/docs/guide/building-crud-app.md +++ b/content/docs/guide/building-crud-app.md @@ -126,7 +126,7 @@ export const TaskSchema = { icon: 'check-circle-2', titleFormat: '{title}', fields, - list_views: { + listViews: { all: { label: 'All Tasks', columns: ['title', 'status', 'priority', 'assignee', 'due_date'], @@ -317,7 +317,7 @@ composes that view's `filter` and `sort` onto the query for you: ```tsx const [activeView, setActiveView] = useState('all'); -// View switcher buttons — the names match the `list_views` keys from Step 3. +// View switcher buttons — the names match the `listViews` keys from Step 3. @@ -334,7 +334,7 @@ Selecting **Active** re-queries with the `active` view's `filter` (`status != Done`) and `sort` (`priority asc`) applied — you never assemble `$filter` / `$orderby` by hand. A view name your backend does not publish is reported, not silently ignored: swap `activeView` for a name outside -`list_views` and the grid renders a configuration-error panel in place of the +`listViews` and the grid renders a configuration-error panel in place of the table, the same way an unresolved `objectName` does (Step 5). A page that instead fell back to the object's full, unfiltered scope would look like it worked while returning every record regardless of which button was pressed — diff --git a/content/docs/guide/console-architecture.md b/content/docs/guide/console-architecture.md index 8ce4638a54..d945c1895d 100644 --- a/content/docs/guide/console-architecture.md +++ b/content/docs/guide/console-architecture.md @@ -152,7 +152,7 @@ The `ActionRunner` supports: The shell's `ObjectView` — the one exported by `@object-ui/app-shell` and bound to the routes above — is a **thin wrapper** around `@object-ui/plugin-view`'s `ObjectView`: -- Resolves views from the object definition's `list_views` +- Resolves views from the object definition's `listViews` - Passes a `renderListView` callback for multi-view rendering (kanban, calendar, chart) - Handles shell-level concerns: URL routing, MetadataInspector, record detail overlay diff --git a/packages/app-shell/src/layout/AppHeader.tsx b/packages/app-shell/src/layout/AppHeader.tsx index e2a1cd76cb..f73ed34efa 100644 --- a/packages/app-shell/src/layout/AppHeader.tsx +++ b/packages/app-shell/src/layout/AppHeader.tsx @@ -498,6 +498,9 @@ export function AppHeader({ // humanized slug ("Kanban By Status") so the breadcrumb matches the // tab label users clicked. const viewName = pathParts[4]; + // `listViews` is canonical (#5362; @objectstack/spec declares only camelCase). The + // `list_views` leg is a compatibility READ for stored pre-settlement documents + // (that stock has never been censused: objectstack#7917). Never WRITE the snake key. const definedViews = (currentObject as any).listViews || (currentObject as any).list_views || {}; const viewDef = (definedViews as Record)[viewName]; const fallbackLabel = (viewDef && (viewDef.label || viewDef.title)) || humanizeSlug(viewName); diff --git a/packages/app-shell/src/providers/MetadataProvider.tsx b/packages/app-shell/src/providers/MetadataProvider.tsx index e8ec6bd072..f85101cedd 100644 --- a/packages/app-shell/src/providers/MetadataProvider.tsx +++ b/packages/app-shell/src/providers/MetadataProvider.tsx @@ -417,6 +417,10 @@ export function mergeViewsIntoObjects(objects: any[], views: any[]): any[] { return objects.map(obj => { const extra = byObject[obj.name]; if (!extra) return obj; + // `listViews` is canonical (#5362; @objectstack/spec declares only camelCase). The + // `list_views` / `form_views` legs are compatibility READS for stored pre-settlement + // documents (that stock has never been censused: objectstack#7917). Never WRITE snake keys + // — the merge below emits camelCase only. const existingListViews = obj.listViews || obj.list_views || {}; const existingFormViews = obj.formViews || obj.form_views || {}; const merged: any = { diff --git a/packages/app-shell/src/views/InterfaceListPage.tsx b/packages/app-shell/src/views/InterfaceListPage.tsx index 6eaa56076a..842fdb4a6d 100644 --- a/packages/app-shell/src/views/InterfaceListPage.tsx +++ b/packages/app-shell/src/views/InterfaceListPage.tsx @@ -53,6 +53,9 @@ function hasColumns(v: any): boolean { } function resolveSourceView(objectDef: any, sourceView?: string): any | undefined { + // `listViews` is canonical (#5362; @objectstack/spec declares only camelCase). The + // `list_views` leg is a compatibility READ for stored pre-settlement documents + // (that stock has never been censused: objectstack#7917). Never WRITE the snake key. const views: Record = objectDef?.listViews || objectDef?.list_views || {}; // ADR-0017 expansion can serve a default-view item with an empty config // while the full body lives on `objectDef.list` — prefer candidates that diff --git a/packages/app-shell/src/views/ObjectView.tsx b/packages/app-shell/src/views/ObjectView.tsx index e04073e6e2..262521054f 100644 --- a/packages/app-shell/src/views/ObjectView.tsx +++ b/packages/app-shell/src/views/ObjectView.tsx @@ -2,7 +2,7 @@ * Console ObjectView * * Thin wrapper around the plugin-view ObjectView that adds: - * - Multi-view resolution from objectDef.list_views + * - Multi-view resolution from objectDef.listViews * - MetadataInspector toggle * - Drawer for record detail preview * - useObjectActions for toolbar create button @@ -1298,6 +1298,9 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an setViewOverrides({}); return; } + // `listViews` is canonical (#5362; @objectstack/spec declares only camelCase). The + // `list_views` leg is a compatibility READ for stored pre-settlement documents + // (that stock has never been censused: objectstack#7917). Never WRITE the snake key. const definedViews = (objectDef.listViews || objectDef.list_views || {}) as Record; const ids = Object.keys(definedViews); // Include the primary view id so overrides apply to it too. Its identity diff --git a/packages/plugin-grid/src/__tests__/guideCrudAppRenders.test.tsx b/packages/plugin-grid/src/__tests__/guideCrudAppRenders.test.tsx index 9508543df4..4ca4885065 100644 --- a/packages/plugin-grid/src/__tests__/guideCrudAppRenders.test.tsx +++ b/packages/plugin-grid/src/__tests__/guideCrudAppRenders.test.tsx @@ -58,7 +58,9 @@ import '../index'; const GUIDE = path.resolve(__dirname, '../../../../content/docs/guide/building-crud-app.md'); -// `list_views` mirrors the guide's own Step 3 `TaskSchema.list_views` exactly +// `listViews` mirrors the guide's own Step 3 `TaskSchema.listViews` exactly +// (canonical camelCase since #5362; the runtime's snake fallback is pinned by +// `ElementDataSourceGate.test.tsx`, not by this guide transcription) // (same two ids, same `active` filter/sort) — the fourth axis below measures // whether Step 7's `dataSource: { object, view }` binding resolves against a // backend that publishes what the guide instructs the reader to declare, not @@ -73,7 +75,7 @@ const TASK_SCHEMA = { assignee: { name: 'assignee', type: 'text', label: 'Assignee' }, due_date: { name: 'due_date', type: 'date', label: 'Due Date' }, }, - list_views: { + listViews: { all: { label: 'All Tasks', columns: ['title', 'status', 'priority', 'assignee', 'due_date'], @@ -343,7 +345,7 @@ describe('object-grid — Step 7’s rewritten dataSource binding actually chang it('a view name the backend does not publish renders a configuration-error panel, not a silently unfiltered grid', async () => { // The behavioural trade the maintainer ruled acceptable (2026-08-22): - // `archived` is not one of `TASK_SCHEMA.list_views`'s keys. + // `archived` is not one of `TASK_SCHEMA.listViews`'s keys. const [step7] = guideSchemas('object-grid').slice(2); const unpublished = { ...step7, dataSource: { ...step7.dataSource, view: 'archived' } }; diff --git a/packages/react/src/element-data-source/__tests__/ElementDataSourceGate.test.tsx b/packages/react/src/element-data-source/__tests__/ElementDataSourceGate.test.tsx index c6430c948f..a5ad39afb7 100644 --- a/packages/react/src/element-data-source/__tests__/ElementDataSourceGate.test.tsx +++ b/packages/react/src/element-data-source/__tests__/ElementDataSourceGate.test.tsx @@ -168,6 +168,28 @@ describe('useElementDataSourceSchema — binding with a saved view', () => { expect(bound.viewType).toBe('kanban'); }); + it('still resolves a view served under snake `list_views` (stored-data compatibility, #5362)', async () => { + // `listViews` (camelCase) is the canonical spelling — @objectstack/spec + // declares nothing else, and every fixture above uses it. This pin is the + // OTHER half of that settlement: stored app data published before the + // canonization has never been censused (objectstack#7917), so the snake + // READ fallback in `useElementDataSource` must survive until that census + // exists. If this test is failing because the fallback was removed, the + // removal needs the census as evidence, not a cleanup rationale. + const snakeAdapter = { + find: vi.fn(), + getObjectSchema: vi.fn().mockResolvedValue({ name: 'account', list_views: { hot: HOT_VIEW } }), + }; + const result = await resolved( + { type: 'list-view', dataSource: { object: 'account', view: 'hot' } }, + FULL, + snakeAdapter, + ); + expect(result.current.status).toBe('resolved'); + expect(result.current.schema.columns).toEqual(['name', 'rating']); + expect(result.current.schema.filter).toEqual([['rating', '=', 'hot']]); + }); + it('lets an authored key win over the same key from the view', async () => { const result = await resolved( { diff --git a/packages/react/src/hooks/useElementDataSource.ts b/packages/react/src/hooks/useElementDataSource.ts index 6c05149bf0..dab70a5a82 100644 --- a/packages/react/src/hooks/useElementDataSource.ts +++ b/packages/react/src/hooks/useElementDataSource.ts @@ -101,6 +101,8 @@ async function fetchSavedViews( const embedded = canReadObjectDef ? dataSource!.getObjectSchema!(object).then( + // `listViews` is canonical (#5362); the snake leg is a compatibility READ for + // stored pre-settlement documents (never censused: objectstack#7917). (def) => (isRecord(def) ? (def.listViews ?? def.list_views) : undefined), () => undefined, ) diff --git a/packages/types/src/__tests__/object-schema-metadata-spec-derivation.test.ts b/packages/types/src/__tests__/object-schema-metadata-spec-derivation.test.ts new file mode 100644 index 0000000000..9bf2d39e82 --- /dev/null +++ b/packages/types/src/__tests__/object-schema-metadata-spec-derivation.test.ts @@ -0,0 +1,190 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `ObjectSchemaMetadata` ↔ `@objectstack/spec` derivation guard (objectui#5362). + * + * The maintainer ruled (2026-08-20, on #5362) that the object document type + * belongs to `@objectstack/spec` and objectui DERIVES it rather than keeping a + * hand-written second declaration. The hand-written interface this replaced + * declared none of `titleFormat` / `listViews` / `icon` — three keys the + * shipped runtime reads — while declaring nine members nothing reads and the + * spec does not know. This guard is the forcing function that keeps the + * settlement settled: + * + * 1. The derivation is structural — `ObjectSchemaMetadata` is the spec's + * `ServiceObject` plus the measured client delta, nothing else. Reverting + * the alias to a hand-written interface fails the `Equal` assertion and + * the three-key fixture below. + * 2. The three read-but-formerly-undeclared keys are admitted, at compile + * time (typed fixture) and at runtime (spec zod shape). + * 3. The SPELLING is settled: camelCase `listViews` is declared; snake + * `list_views` is not a key of the document type or the spec schema. + * The runtime keeps a snake READ fallback for stored app data published + * before this settlement (never censused — objectstack#7917); that + * tolerance lives at the read sites and must NOT leak back into the + * declared type, which is what the `@ts-expect-error` below pins. + * 4. The client-extension surface stays measured: every member must have a + * live runtime read. Growing `ObjectSchemaClientExtensions` fails the + * key-set assertion, forcing the promote-upstream / justify-here + * decision consciously (same forcing function as + * `object-view-spec-parity.test.ts`). + * + * When one of these fails, do NOT edit this file first. Decide whether the + * key belongs upstream in `@objectstack/spec` (promote it — the #5362 route + * for the three keys was exactly that, objectstack#10144) or is a genuine + * objectui-only runtime read (add it to the extension with its read site). + */ +import { describe, it, expect } from 'vitest'; +import { ObjectSchema as SpecObjectSchema } from '@objectstack/spec/data'; +import type { ServiceObject } from '@objectstack/spec/data'; +import type { + ObjectSchemaMetadata, + ObjectSchemaClientExtensions, +} from '../field-types.js'; + +// ── compile-time half ──────────────────────────────────────────────────────── +// Compiled by `tsconfig.test.json` via this package's `type-check` script — +// see that file's header for why tests must be in a compiled project at all +// (objectstack#4074: `satisfies` contracts that no tsc invocation ever read). + +type Equal = (() => T extends A ? 1 : 2) extends () => T extends B + ? 1 + : 2 + ? true + : false; +type Assert = T; + +// 1. Structural derivation — the alias IS spec type + client delta. +type _derivation = Assert< + Equal +>; + +// 4. The client delta stays exactly the measured set. `editMode` is read by +// `@object-ui/app-shell` (`utils/recordFormNavigation.ts`, routed by +// `AppContent`'s central `handleEdit` dispatcher). +type _extensionSurface = Assert< + Equal +>; + +// 2. The three keys #5362 measured as read-but-undeclared are admitted, and +// a typed document can carry the client extension alongside them. +const derivedAdmitsTheThreeKeys: ObjectSchemaMetadata = { + name: 'task', + fields: { + subject: { type: 'text', label: 'Subject', required: true }, + }, + icon: 'check-square', + titleFormat: '${subject}', + listViews: { + all: { label: 'All Tasks', columns: ['subject'] }, + }, + editMode: 'page', +}; + +// 3. Snake `list_views` is NOT part of the declared surface. If this +// directive ever reports as unused, the snake spelling has leaked back into +// the type — that is a regression of the #5362 settlement, not a cleanup. +const snakeSpellingIsNotDeclared: ObjectSchemaMetadata = { + name: 'task', + fields: {}, + // @ts-expect-error -- `list_views` was never a spec key; the spec declares only camelCase `listViews` (#5362). + list_views: {}, +}; + +// ── runtime half ───────────────────────────────────────────────────────────── + +/** Top-level keys of a zod object (same helper as the sibling parity guards). */ +function shapeKeys(schema: unknown): string[] { + const carrier = schema as { shape?: unknown; _def?: { shape?: unknown } }; + const shape = carrier?.shape ?? carrier?._def?.shape; + const resolved = typeof shape === 'function' ? (shape as () => object)() : shape; + return resolved && typeof resolved === 'object' ? Object.keys(resolved) : []; +} + +describe('ObjectSchemaMetadata spec derivation (#5362)', () => { + const keys = shapeKeys(SpecObjectSchema); + + it('reads a live spec shape (positive control for every absence below)', () => { + // A broken import or an empty shape would make every `not.toContain` + // below pass vacuously; pin the corpus first. + expect(keys.length).toBeGreaterThan(30); + expect(keys).toContain('name'); + expect(keys).toContain('fields'); + }); + + it('declares the three keys the runtime reads: icon, titleFormat, listViews', () => { + expect(keys).toContain('icon'); + expect(keys).toContain('titleFormat'); + expect(keys).toContain('listViews'); + }); + + it('canonizes camelCase listViews — snake list_views is not a spec key', () => { + expect(keys).not.toContain('list_views'); + }); + + it('does not declare the nine members the retired hand-written interface invented', () => { + // These were declared by the old mirror, read by nothing in this repo, + // and absent from the spec document — dropping them followed the spec. + for (const retired of [ + 'extends', + 'triggers', + 'primary_key', + 'relationships', + 'name_field', + 'soft_delete', + 'audit_trail', + 'version', + 'cache', + ]) { + expect(keys).not.toContain(retired); + } + // The spelling sibling: the identity the old `name_field` member wanted + // exists in the spec under its camelCase key. + expect(keys).toContain('nameField'); + }); + + it('does not declare the client extension — editMode stays an objectui-side member', () => { + // If this starts failing, the spec has adopted `editMode`: retire it from + // `ObjectSchemaClientExtensions` and let the derivation carry it. + expect(keys).not.toContain('editMode'); + }); + + it('accepts the spec-shaped document through the spec parse (value reachability, not just key presence)', () => { + const { editMode: _clientOnly, ...specShaped } = derivedAdmitsTheThreeKeys; + const parsed = SpecObjectSchema.safeParse(specShaped); + expect(parsed.success).toBe(true); + }); + + it('rejects the client extension at the spec parse — editMode is client-side ONLY', () => { + // The spec parse is strict on unrecognized keys (objectstack#4001), so a + // published document carrying `editMode` is rejected loudly, not dropped. + // This pins what "client extension" means: the member exists on the CLIENT + // type for documents served by client-side data sources, and it cannot + // ride a spec-validated publish path. If the spec ever adopts `editMode`, + // this test and the absence test above both flip — retire the extension + // member and let the derivation carry the key. + const parsed = SpecObjectSchema.safeParse(derivedAdmitsTheThreeKeys); + expect(parsed.success).toBe(false); + if (parsed.success) return; + const unrecognized = parsed.error.issues.find( + (issue) => issue.code === 'unrecognized_keys', + ); + expect(unrecognized).toBeDefined(); + expect( + (unrecognized as { keys?: string[] } | undefined)?.keys, + ).toEqual(['editMode']); + }); + + it('keeps the compile-time fixtures alive', () => { + expect(derivedAdmitsTheThreeKeys.listViews).toBeDefined(); + expect(derivedAdmitsTheThreeKeys.icon).toBe('check-square'); + expect(derivedAdmitsTheThreeKeys.titleFormat).toBe('${subject}'); + expect(snakeSpellingIsNotDeclared.name).toBe('task'); + }); +}); diff --git a/packages/types/src/field-types.ts b/packages/types/src/field-types.ts index 411cee0cc0..a7f281ff77 100644 --- a/packages/types/src/field-types.ts +++ b/packages/types/src/field-types.ts @@ -6,8 +6,6 @@ * LICENSE file in the root directory of this source tree. */ -import type { ManagedByBucket } from './managed-by.js'; - /** * @object-ui/types - Field Type Definitions * @@ -847,75 +845,48 @@ export interface ObjectTrigger { } /** - * Object schema definition - * Phase 3.1: Enhanced with inheritance, triggers, permissions, and caching + * Object document type — derived from `@objectstack/spec/data` rather than + * restated (objectui#5362; maintainer ruling 2026-08-20: the object document + * type belongs to `@objectstack/spec`, objectui derives rather than + * hand-copies — the same layer split objectui#3074 applied to + * `PageNodeSchema` and objectstack#4115 applied to `ObjectIndex` below). + * + * The hand-written interface this replaces had drifted in both directions: + * + * - It declared NONE of `titleFormat`, `listViews`, `icon` — three keys the + * shipped runtime reads (objectui#5362 lists the read sites), so a document + * annotated with this type got excess-property errors on keys the renderer + * then happily consumed. + * - It declared nine members no objectui runtime code reads and the spec's + * object document does not know: `extends`, `triggers`, `primary_key`, + * `relationships`, `name_field` (the spec key is `nameField`), + * `soft_delete`, `audit_trail`, `version`, `cache`. + * + * Spelling (objectui#5362): the spec declares only camelCase `listViews`; + * `list_views` appears nowhere in `@objectstack/spec` 17.2.0's `dist/`. The + * runtime keeps a snake-spelling READ fallback for stored app data published + * before this settlement (that stock has never been censused — + * objectstack#7917); the tolerance lives at the read sites, deliberately not + * in this type: new documents must author `listViews`. + * + * `ServiceObject` is the AUTHORING shape (`z.input` — pre-parse, defaults + * not yet applied), which is what a hand-authored or served object document + * is before validation. The post-parse shape is the spec's + * `ServiceObjectParsed`. */ -export interface ObjectSchemaMetadata { - /** - * Object name - */ - name: string; - - /** - * Display label - */ - label?: string; - - /** - * Object description - */ - description?: string; - - /** - * Fields definition - */ - fields: Record; - - /** - * Parent object to inherit from (Phase 3.1.2) - */ - extends?: string; - - /** - * Triggers configuration (Phase 3.1.3) - */ - triggers?: ObjectTrigger[]; - - /** - * Primary key field - */ - primary_key?: string; - - /** - * Indexes for optimization - */ - indexes?: ObjectIndex[]; - - /** - * Relationships with other objects - */ - relationships?: ObjectRelationship[]; - - /** - * Record naming pattern - */ - name_field?: string; - - /** - * Soft delete configuration - */ - soft_delete?: boolean; - - /** - * Audit trail configuration - */ - audit_trail?: boolean; - - /** - * Schema version - */ - version?: string; - +import type { ServiceObject } from '@objectstack/spec/data'; + +/** + * Client-side members the objectui runtime reads on the object document but + * `@objectstack/spec` does not declare. Every member here must cite a live + * runtime read — this interface is the measured client DELTA on top of the + * spec document, not a place to restate spec keys (restating them would + * recreate the hand-written fork objectui#5362 retired). The member list is + * pinned by `__tests__/object-schema-metadata-spec-derivation.test.ts`, so + * growing it is a conscious decision: promote the key upstream to the spec, + * or add it here with the runtime read that justifies it. + */ +export interface ObjectSchemaClientExtensions { /** * Default UI mode for record create/edit interactions. * @@ -930,55 +901,22 @@ export interface ObjectSchemaMetadata { * shareable links to the create/edit form. * * The host application reads this flag in its central `handleEdit` - * dispatcher (see `@object-ui/app-shell` `AppContent`) — switching the - * value requires no code changes. + * dispatcher (see `@object-ui/app-shell` `AppContent` and + * `utils/recordFormNavigation.ts`) — switching the value requires no code + * changes. * * @default 'modal' */ editMode?: 'modal' | 'page'; - - /** - * Object lifecycle bucket — sets the default CRUD affordances and the write - * policy (ADR-0103). The enforced policy is the *resolved affordance* - * (bucket default + `userActions`), computed by `resolveCrudAffordances` - * in `@object-ui/core` — not the bare bucket. - * - * - `'platform'` (default) — ObjectStack-owned business data; full CRUD. - * - `'config'` — admin-authored configuration; New / Edit / Delete, no import. - * - `'system-data'` — platform-defined schema holding admin/user-writable - * DATA (RBAC links, prefs, messaging config); full CRUD by default, - * `userActions` NARROWS. Renamed from the residual `'system'` in protocol - * 17 (objectstack#3355). - * - `'engine-owned'` — runtime rows a platform service owns end to end; no - * user writes. - * - `'append-only'` — immutable audit trail; View + Export only. - * - `'better-auth'` — identity tables owned by the auth driver; generic - * user-context writes are suppressed (they bypass password hashing, - * session validation, audit hooks) and flow through the auth API instead. - * - * @default 'platform' - */ - managedBy?: ManagedByBucket; - - /** - * Cache configuration (Phase 3.1.5) - */ - cache?: { - /** - * Enable metadata caching - */ - enabled?: boolean; - /** - * Cache TTL in seconds - */ - ttl?: number; - /** - * Cache invalidation strategy - */ - invalidation?: 'time' | 'event' | 'manual'; - }; } +/** + * Object schema definition — the object document a data source's + * `getObjectSchema(objectName)` serves, in its authoring shape, plus the + * measured objectui client extensions above. + */ +export type ObjectSchemaMetadata = ServiceObject & ObjectSchemaClientExtensions; + /** * Object index configuration — re-exported from `@objectstack/spec/data` * rather than restated (objectstack#4115). diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c23c760bf7..e12c938451 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -454,6 +454,7 @@ export type { FieldMetadata, ObjectTrigger, ObjectSchemaMetadata, + ObjectSchemaClientExtensions, ObjectIndex, ObjectRelationship, } from './field-types.js';