diff --git a/.changeset/listview-spec-derive.md b/.changeset/listview-spec-derive.md new file mode 100644 index 000000000..667796206 --- /dev/null +++ b/.changeset/listview-spec-derive.md @@ -0,0 +1,35 @@ +--- +"@object-ui/types": minor +--- + +feat(types): derive `ListViewSchema` from `@objectstack/spec/ui` instead of a hand-written copy (#2231) + +`@object-ui/types` shipped a hand-written mirror of the spec's UI ListView zod +(`packages/types/src/zod/objectql.zod.ts`) plus a parallel hand-written TS `interface` +(`objectql.ts`). Both had drifted from the authoritative `@objectstack/spec/ui` +`ListViewSchema`, with nothing enforcing they stay in sync. + +- The zod `ListViewSchema` now **derives** from the spec's `ListViewSchema`: spec-owned + fields (`filter`, `sort`, `selection`, `navigation`, `pagination`, `grouping`, + `rowColor`, `userActions`, `appearance`, `tabs`, `addRecord`, `rowHeight`, `sharing`, + `chart`/`tree` configs, `responsive`, `performance`, …) flow in **by reference** instead + of being re-typed. The component envelope (`type: 'list-view'` discriminator + + `objectName`) and the legacy objectui vocabulary (`viewType`, `fields`, `filters`, the + `show*` toolbar flags, `densityMode`, `color`, …) plus the configs whose objectui shape + is intentionally broader than spec's (`userFilters`, `sharing`, `aria`, + `conditionalFormatting`, `exportOptions`, `kanban`/`calendar`/`gantt`/`gallery`/ + `timeline`) remain as sanctioned local `.extend()`s. Existing payloads keep validating; + spec-canonical payloads (`columns`/`filter`/`userActions`) now validate too. +- The hand-written TS `interface ListViewSchema` is replaced by + `z.infer & ListViewRuntimeProps`, so the type can no longer drift + from the schema. Non-serializable runtime-only props (`onNavigate`, `onDensityChange`, + `refreshTrigger`) live in `ListViewRuntimeProps`. +- Added a drift-guard test (`list-view-spec-parity.test.ts`) that fails if the spec grows a + field objectui hasn't triaged, renames an aliased anchor (`type`/`columns`/`filter`), or + an objectui-only field is added outside the sanctioned-local set. +- Bumped the `@objectstack/spec` dependency `^14.6.0` → `^15.1.0` across the workspace + (15.1.0 carries the framework#3021 `lazySchema`/`z.toJSONSchema` identity fix that the + spec-derived Page/View inspectors depend on). + +Migrating the legacy vocabulary to the spec-canonical keys and adopting spec's narrower +sub-shapes are deferred follow-ups (see #2231). No runtime behavior change. diff --git a/apps/console/package.json b/apps/console/package.json index e8c40f3c5..7aa55cf28 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -88,7 +88,7 @@ "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", "@objectstack/client": "^14.6.0", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "@tailwindcss/postcss": "^4.3.2", "@tailwindcss/typography": "^0.5.20", "@testing-library/jest-dom": "^6.9.1", @@ -113,8 +113,8 @@ "vitest": "^4.1.10" }, "dependencies": { - "@object-ui/sdui-parser": "workspace:*", "@object-ui/react-runtime": "workspace:*", + "@object-ui/sdui-parser": "workspace:*", "sucrase": "^3.35.0" } } diff --git a/apps/site/package.json b/apps/site/package.json index 17c0793d9..64588f73d 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -31,7 +31,7 @@ "@object-ui/plugin-view": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "fumadocs-core": "16.11.3", "fumadocs-mdx": "15.0.13", "fumadocs-ui": "16.11.3", diff --git a/package.json b/package.json index a0d160b3b..0f3863772 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "devDependencies": { "@changesets/cli": "^2.31.0", "@eslint/js": "^10.0.1", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "@playwright/test": "^1.61.1", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", diff --git a/packages/app-shell/package.json b/packages/app-shell/package.json index e68e366fb..754038312 100644 --- a/packages/app-shell/package.json +++ b/packages/app-shell/package.json @@ -50,7 +50,7 @@ "@object-ui/types": "workspace:*", "@objectstack/formula": "^14.6.0", "@objectstack/lint": "^14.6.0", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "@sentry/react": "^10.65.0", "jsonc-parser": "^3.3.1", "lucide-react": "^1.24.0", diff --git a/packages/core/package.json b/packages/core/package.json index d0b927a29..2f4e62176 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,7 +33,7 @@ "dependencies": { "@object-ui/types": "workspace:*", "@objectstack/formula": "^14.6.0", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "lodash": "^4.18.1", "zod": "^4.4.3" }, diff --git a/packages/plugin-detail/package.json b/packages/plugin-detail/package.json index f4fe0e4dc..420de96c0 100644 --- a/packages/plugin-detail/package.json +++ b/packages/plugin-detail/package.json @@ -32,7 +32,7 @@ }, "dependencies": { "@object-ui/i18n": "workspace:*", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "lucide-react": "^1.24.0" }, "peerDependencies": { diff --git a/packages/plugin-form/package.json b/packages/plugin-form/package.json index 02a45d673..024c4dabf 100644 --- a/packages/plugin-form/package.json +++ b/packages/plugin-form/package.json @@ -27,7 +27,7 @@ "@object-ui/permissions": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "lucide-react": "^1.24.0" }, "peerDependencies": { diff --git a/packages/plugin-gantt/package.json b/packages/plugin-gantt/package.json index 77ca93ba7..ac655cf37 100644 --- a/packages/plugin-gantt/package.json +++ b/packages/plugin-gantt/package.json @@ -38,7 +38,7 @@ "@object-ui/plugin-detail": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "lucide-react": "^1.24.0", "qrcode": "^1.5.4", "sonner": "^2.0.7" diff --git a/packages/plugin-map/package.json b/packages/plugin-map/package.json index 83bdca6e7..9aed21aad 100644 --- a/packages/plugin-map/package.json +++ b/packages/plugin-map/package.json @@ -35,7 +35,7 @@ "@object-ui/core": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "lucide-react": "^1.24.0", "maplibre-gl": "^5.24.0", "react-map-gl": "^8.1.1", diff --git a/packages/plugin-timeline/package.json b/packages/plugin-timeline/package.json index 454d0fee3..36260c388 100644 --- a/packages/plugin-timeline/package.json +++ b/packages/plugin-timeline/package.json @@ -36,7 +36,7 @@ "@object-ui/mobile": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "class-variance-authority": "^0.7.1", "zod": "^4.4.3" }, diff --git a/packages/plugin-tree/package.json b/packages/plugin-tree/package.json index 2059ec143..c3a7a5078 100644 --- a/packages/plugin-tree/package.json +++ b/packages/plugin-tree/package.json @@ -35,7 +35,7 @@ "@object-ui/core": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "lucide-react": "^1.24.0" }, "peerDependencies": { diff --git a/packages/plugin-view/package.json b/packages/plugin-view/package.json index a0904c5e5..d64e4ef8d 100644 --- a/packages/plugin-view/package.json +++ b/packages/plugin-view/package.json @@ -21,7 +21,6 @@ }, "dependencies": { "@dnd-kit/core": "^6.3.1", - "@objectstack/spec": "^14.6.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@object-ui/components": "workspace:*", @@ -31,6 +30,7 @@ "@object-ui/plugin-grid": "workspace:*", "@object-ui/react": "workspace:*", "@object-ui/types": "workspace:*", + "@objectstack/spec": "^15.1.0", "class-variance-authority": "^0.7.1", "lucide-react": "^1.24.0" }, diff --git a/packages/react/package.json b/packages/react/package.json index dfc742625..f45273084 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -34,7 +34,7 @@ "@object-ui/data-objectstack": "workspace:*", "@object-ui/i18n": "workspace:*", "@object-ui/types": "workspace:*", - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "react-hook-form": "^7.81.0" }, "peerDependencies": { diff --git a/packages/types/package.json b/packages/types/package.json index 4d2d86946..0756807c1 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -90,7 +90,7 @@ "directory": "packages/types" }, "dependencies": { - "@objectstack/spec": "^14.6.0", + "@objectstack/spec": "^15.1.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/types/src/__tests__/list-view-spec-parity.test.ts b/packages/types/src/__tests__/list-view-spec-parity.test.ts new file mode 100644 index 000000000..5a1cce431 --- /dev/null +++ b/packages/types/src/__tests__/list-view-spec-parity.test.ts @@ -0,0 +1,143 @@ +/** + * 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. + */ + +/** + * ListView ↔ @objectstack/spec drift guard (issue #2231). + * + * objectui's `ListViewSchema` (packages/types/src/zod/objectql.zod.ts) is DERIVED from + * the spec's `ListViewSchema` (`@objectstack/spec/ui`): spec-owned fields flow in by + * reference, and only objectui-only / legacy fields are declared locally. That derivation + * means new spec fields are picked up automatically — but it can still silently break in + * three ways, which these tests catch: + * + * 1. The spec grows a field objectui never triaged (would flow in unnoticed, or — if the + * omit list references it — vanish). We assert every spec field is present in the + * objectui shape (or explicitly envelope-owned by BaseSchema). + * 2. The spec RENAMES/REMOVES a field objectui specifically aliases or relaxes + * (`type` → `viewType`, `columns` relaxed, `filter` alongside legacy `filters`). The + * alias would then point at nothing. We assert those anchors still exist upstream. + * 3. Someone adds an objectui-local field that shadows what should have been a spec field + * (re-opening the drift the unification closed). We assert every objectui-only key is + * in the explicitly-sanctioned local set below, forcing a conscious local-vs-upstream + * decision on every new field. + * + * When one of these fails, do NOT just edit the sets to make it green — decide whether the + * field belongs upstream in `@objectstack/spec` (promote it) or is a genuine objectui-only + * extension (add it to SANCTIONED_LOCAL with a rationale). See #2231. + */ +import { describe, it, expect } from 'vitest'; +import { ListViewSchema as SpecListViewSchema } from '@objectstack/spec/ui'; +import { ListViewSchema as OuiListViewSchema } from '../zod/objectql.zod.js'; +import { BaseSchema } from '../zod/base.zod.js'; + +const specShape = (SpecListViewSchema as unknown as { shape: Record }).shape; +const ouiShape = (OuiListViewSchema as unknown as { shape: Record }).shape; +const baseShape = (BaseSchema as unknown as { shape: Record }).shape; + +const specKeys = Object.keys(specShape); +const ouiKeys = new Set(Object.keys(ouiShape)); +// Component-envelope keys owned by BaseSchema (id, className, visible, etc.) — derived, so +// this test never needs editing when the envelope changes. +const ENVELOPE = new Set(Object.keys(baseShape)); + +/** + * objectui-only ListView fields — sanctioned local extensions on top of the spec base. + * Each is either legacy vocabulary kept for back-compat (migration to the spec-canonical + * key is deferred — #2231) or a genuinely objectui-only renderer concern. Adding to this + * set is a deliberate act: prefer promoting the field into `@objectstack/spec` instead. + */ +const SANCTIONED_LOCAL = new Set([ + // component binding (spec binds via data.provider:'object') + 'objectName', + // renamed spec `type` (view-kind enum); `type` itself is the component discriminator + 'viewType', + // legacy aliases for spec `columns` / `filter` + 'fields', + 'filters', + // legacy toolbar visibility flags (spec-canonical: `userActions`) + 'showSearch', + 'showSort', + 'showFilters', + 'showHideFields', + 'showGroup', + 'showColor', + 'showDensity', + 'showDescription', + 'allowExport', + // legacy density shorthand (spec-canonical: `rowHeight`) + 'densityMode', + // legacy row/text coloring shorthand (spec-canonical: `rowColor`) + 'color', + 'fieldTextColor', + 'prefixField', + // objectui renderer flags with no spec equivalent (yet) + 'wrapHeaders', + 'clickIntoRecordDetails', + 'addRecordViaForm', + 'addDeleteRecordsInline', + 'collapseAllByDefault', + 'operations', + 'options', +]); + +describe('ListView spec parity (#2231 drift guard)', () => { + it('covers every @objectstack/spec ListView field (spec cannot grow a field objectui ignores)', () => { + // Fails when the spec adds a field that objectui neither imports nor envelope-owns — + // i.e. a field that needs a local-vs-upstream triage decision. + const missing = specKeys.filter((k) => !ouiKeys.has(k) && !ENVELOPE.has(k)); + expect(missing).toEqual([]); + }); + + it('keeps the spec anchors objectui remaps/relaxes (`type`, `columns`, `filter`)', () => { + // Fails when the spec renames/removes a field objectui aliases, orphaning the alias. + expect(specShape).toHaveProperty('type'); + expect(specShape).toHaveProperty('columns'); + expect(specShape).toHaveProperty('filter'); + }); + + it('declares no objectui-only field outside the sanctioned-local set', () => { + // Fails when a new objectui-only field is added without deciding local-vs-upstream. + const rogue = [...ouiKeys].filter( + (k) => !specShape[k] && !ENVELOPE.has(k) && !SANCTIONED_LOCAL.has(k), + ); + expect(rogue).toEqual([]); + }); + + it('preserves the component discriminator + required objectName', () => { + const bad = OuiListViewSchema.safeParse({ objectName: 'accounts' }); // no type + expect(bad.success).toBe(false); + const good = OuiListViewSchema.safeParse({ type: 'list-view', objectName: 'accounts' }); + expect(good.success).toBe(true); + const noObject = OuiListViewSchema.safeParse({ type: 'list-view' }); // no objectName + expect(noObject.success).toBe(false); + }); + + it('accepts both legacy (fields/filters/show*) and spec-canonical (columns/filter/userActions) payloads', () => { + const legacy = OuiListViewSchema.safeParse({ + type: 'list-view', + objectName: 'accounts', + viewType: 'kanban', + fields: ['name', 'stage'], + filters: [['stage', '=', 'won']], + showSearch: true, + showHideFields: true, + densityMode: 'compact', + }); + expect(legacy.success).toBe(true); + + const canonical = OuiListViewSchema.safeParse({ + type: 'list-view', + objectName: 'accounts', + columns: ['name', 'stage'], + filter: [{ field: 'stage', operator: 'equals', value: 'won' }], + userActions: { search: true, filter: true }, + rowHeight: 'compact', + }); + expect(canonical.success).toBe(true); + }); +}); diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 310e4c490..6071775e7 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -19,8 +19,9 @@ */ import type { BaseSchema } from './base'; -import type { TableColumn } from './data-display'; import type { FormField } from './form'; +// ListView type is now derived from the zod schema (issue #2231) — see ListViewSchema below. +import type { ListViewInferred } from './zod/objectql.zod.js'; // ============================================================================ // Spec-Canonical Types — imported from @objectstack/spec/ui @@ -70,8 +71,6 @@ export type { PaginationConfig } from '@objectstack/spec/ui'; // Import spec types for local use in interfaces below import type { - HttpMethod, - HttpRequest, ViewData, ListColumn, SelectionConfig, @@ -80,8 +79,6 @@ import type { RowColorConfig, GalleryConfig, TimelineConfig, - UserActionsConfig, - AppearanceConfig, } from '@objectstack/spec/ui'; /** @@ -1615,479 +1612,46 @@ export interface ViewNavigationConfig { } /** - * Generic View Definition - * Aligned with @objectstack/spec View/ListView. - * Defines the data requirement, not just the visual component. + * ListView component node — DERIVED from the zod `ListViewSchema` (issue #2231), which + * itself derives from `@objectstack/spec/ui` `ListViewSchema`. Spec-owned fields are + * imported by the schema rather than re-typed here, so this type can no longer drift from + * the protocol. The former hand-written interface (~470 lines mirroring the spec by hand) + * is replaced by this alias. Non-serializable runtime-only props (callbacks, refresh + * trigger) are intersected in via {@link ListViewRuntimeProps} — they cannot live in the + * zod/JSON-schema. + * + * Legacy objectui vocabulary (`viewType`/`fields`/`filters`/`show*`/`densityMode`/…) and + * the broader-than-spec configs (`userFilters`/`sharing`/`aria`/`conditionalFormatting`/ + * `exportOptions`/`kanban`/`calendar`/`gantt`/`gallery`/`timeline`) remain as sanctioned + * local `.extend()`s on the schema; migration to the spec-canonical keys is deferred (#2231). */ -export interface ListViewSchema extends BaseSchema { - type: 'list-view'; - - /** View name identifier */ - name?: string; - - /** View display label */ - label?: string; - - /** Object Name */ - objectName: string; - - /** View Type (grid, kanban, etc.) @default 'grid' */ - viewType?: 'grid' | 'kanban' | 'gallery' | 'calendar' | 'timeline' | 'gantt' | 'map'; - - /** - * Data Source Configuration. - * Aligned with @objectstack/spec ViewDataSchema. - * Supports provider: 'object' (fetch from objectName), 'value' (inline items), 'api' (custom endpoint). - * If not provided, defaults to fetching from objectName via dataSource.find(). - */ - data?: ViewData; - - /** - * Grouping Configuration (Airtable-style). - * Groups rows by specified fields with collapsible sections. - * Aligned with @objectstack/spec GroupingConfigSchema. - */ - grouping?: GroupingConfig; - - /** - * Row Color Configuration (Airtable-style). - * Colors rows based on field values. - * Aligned with @objectstack/spec RowColorConfigSchema. - */ - rowColor?: RowColorConfig; - - /** Columns definition (string field names or full column config) */ - columns?: string[] | Array<{ - field: string; - label?: string; - width?: number | string; - align?: 'left' | 'center' | 'right'; - hidden?: boolean; - sortable?: boolean; - resizable?: boolean; - wrap?: boolean; - type?: string; - link?: boolean; - action?: string; - /** Pin column to left or right edge */ - pinned?: 'left' | 'right'; - /** Column footer summary/aggregation (e.g., 'count', 'sum', 'avg') */ - summary?: string | { type: 'count' | 'sum' | 'avg' | 'min' | 'max'; field?: string }; - }>; - - /** Fields to fetch/display (alias for simple string[] columns) */ - fields?: string[]; - - /** Filter conditions */ - filters?: Array; - - /** Sort order. Supports array of objects or legacy string format "field desc" */ - sort?: Array<{ field: string; order: 'asc' | 'desc' } | string>; - - /** Fields that support text search */ - searchableFields?: string[]; - - /** Fields available for filter UI */ - filterableFields?: string[]; - - /** Row selection mode */ - selection?: SelectionConfig; - - /** Pagination configuration */ - pagination?: PaginationConfig; - - /** Allow column resizing @default false */ - resizable?: boolean; - - /** Show alternating row colors @default false */ - striped?: boolean; - - /** Show cell borders @default false */ - bordered?: boolean; - - /** Show search box in toolbar @default true */ - showSearch?: boolean; - - /** Show sort controls in toolbar @default true */ - showSort?: boolean; - - /** Show filter controls in toolbar @default true */ - showFilters?: boolean; - - /** Show hide-fields button in toolbar @default false */ - showHideFields?: boolean; - - /** Show group button in toolbar @default true */ - showGroup?: boolean; - - /** Show color button in toolbar @default false */ - showColor?: boolean; - - /** Show density/row-height button in toolbar @default false */ - showDensity?: boolean; - - /** - * Collapse the appearance/grouping cluster (Group + Color + Density + Hide Fields) - * into a single "View settings" popover button. Reduces toolbar clutter on - * data-heavy lists. Filter / Sort / Export remain top-level chips. - * @default false - */ - compactToolbar?: boolean; - - /** Allow data export @default undefined */ - allowExport?: boolean; - - /** - * Enable/disable built-in operations. The toolbar honors `export` as a hard - * gate: when `operations.export === false` the export button is hidden and - * any export request is blocked, regardless of `exportOptions`. Other keys - * are accepted for parity with grid schemas. Default-allow: an omitted - * `export` key (or `undefined`) leaves export enabled. - */ - operations?: { - create?: boolean; - read?: boolean; - update?: boolean; - delete?: boolean; - export?: boolean; - import?: boolean; - }; - - /** Color field for row/card coloring */ - color?: string; - - /** Navigation config for row click behavior */ - navigation?: ViewNavigationConfig; +export type ListViewSchema = ListViewInferred & ListViewRuntimeProps; +/** + * Non-serializable runtime-only props for the ListView component. These never belong in + * the zod schema — functions and imperative refresh triggers are not serialisable view + * metadata — so they are kept separate and intersected into {@link ListViewSchema}. + */ +export interface ListViewRuntimeProps { /** - * Callback for page-level navigation (used by 'page' mode). + * Callback for page-level navigation (used by 'page' navigation mode). * Called with recordId and action ('view' | 'edit'). */ onNavigate?: (recordId: string | number, action?: string) => void; - - /** Kanban-specific configuration */ - kanban?: { - groupField: string; - titleField?: string; - cardFields?: string[]; - [key: string]: any; - }; - - /** Calendar-specific configuration */ - calendar?: { - startDateField: string; - endDateField?: string; - titleField?: string; - defaultView?: 'month' | 'week' | 'day' | 'agenda'; - [key: string]: any; - }; - - /** Gantt-specific configuration */ - gantt?: { - startDateField: string; - endDateField: string; - titleField?: string; - progressField?: string; - dependenciesField?: string; - [key: string]: any; - }; - - /** Gallery-specific configuration. Aligned with @objectstack/spec GalleryConfigSchema. */ - gallery?: ListViewGalleryConfig; - - /** Timeline-specific configuration. Aligned with @objectstack/spec TimelineConfigSchema. */ - timeline?: ListViewTimelineConfig; - - /** Visual Component overrides (legacy, prefer typed configs above) */ - options?: Record; - - /** - * Empty state configuration shown when no data is available. - * Aligned with @objectstack/spec ListViewSchema.emptyState. - */ - emptyState?: { - /** Title text for the empty state */ - title?: string; - /** Message/description for the empty state */ - message?: string; - /** Icon name (Lucide icon identifier) for the empty state */ - icon?: string; - }; /** - * Fields to hide from the current view. - * Hides columns in grid view or fields in other view types. - */ - hiddenFields?: string[]; - - /** - * Custom field display order. Fields listed first appear first. - * Fields not listed are appended in their original order. - */ - fieldOrder?: string[]; - - /** - * Export options configuration for exporting list data. - * Supports both ObjectUI object format and spec simple string[] format. - * Spec format: ['csv', 'xlsx'] (simple array of format strings) - * ObjectUI format: { formats, maxRecords, includeHeaders, fileNamePrefix } - */ - exportOptions?: Array<'csv' | 'xlsx' | 'json' | 'pdf'> | { - /** Formats available for export */ - formats?: Array<'csv' | 'xlsx' | 'json' | 'pdf'>; - /** Maximum number of records to export (0 = unlimited) */ - maxRecords?: number; - /** Include column headers in export */ - includeHeaders?: boolean; - /** Custom file name prefix */ - fileNamePrefix?: string; - }; - - /** - * Density mode for controlling row/item spacing. - * Aligned with @objectstack/spec DensityMode. - */ - densityMode?: 'compact' | 'comfortable' | 'spacious'; - - /** - * Row height for list/grid view rows. - * Aligned with @objectstack/spec RowHeight enum. - */ - rowHeight?: 'compact' | 'short' | 'medium' | 'tall' | 'extra_tall'; - - /** - * Optional callback fired when the user toggles row density via the - * toolbar. When provided, the host (typically ObjectView) is expected to - * persist the new value on the active view definition (e.g. via - * dataSource.updateViewConfig). Without this callback the toggle remains - * a session-local preference. + * Callback fired when the user toggles row density/height via the toolbar. Lets the host + * persist the choice (e.g. dataSource.updateViewConfig). Without it the toggle is local-only. */ onDensityChange?: (mode: 'compact' | 'comfortable' | 'spacious') => void; /** - * Conditional formatting rules for row/cell styling. - * Rules are evaluated in order; first matching rule wins. - * Supports both ObjectUI field/operator/value rules and spec expression-based { condition, style } rules. - */ - conditionalFormatting?: ConditionalFormattingRule[]; - - /** - * Enable inline editing for list view fields. - * When true, cells become editable on click/double-click. - */ - inlineEdit?: boolean; - - /** Wrap column headers in grid view @default false */ - wrapHeaders?: boolean; - - /** Navigate to record detail view when row is clicked @default true */ - clickIntoRecordDetails?: boolean; - - /** Add records via a form dialog @default false */ - addRecordViaForm?: boolean; - - /** Enable inline add/delete of records @default false */ - addDeleteRecordsInline?: boolean; - - /** Collapse all grouped sections by default @default false */ - collapseAllByDefault?: boolean; - - /** Field name for custom text color */ - fieldTextColor?: string; - - /** Prefix field displayed before the main title */ - prefixField?: string; - - /** Show field descriptions below headers @default false */ - showDescription?: boolean; - - /** - * ARIA attributes for accessibility. - * Applied to the root list container element. - */ - aria?: { - /** Accessible label for the list view */ - label?: string; - /** ID of the element that describes the list view */ - describedBy?: string; - /** Live region politeness for dynamic updates */ - live?: 'polite' | 'assertive' | 'off'; - }; - - /** - * View sharing configuration. - * Supports both ObjectUI format { visibility, enabled } and spec format { type, lockedBy }. - * Controls who can see this view and enables share UI. - */ - sharing?: { - /** Visibility level for the view (ObjectUI format) */ - visibility?: 'private' | 'team' | 'organization' | 'public'; - /** Whether sharing controls are shown in the toolbar */ - enabled?: boolean; - /** Sharing type (spec format: personal or collaborative) */ - type?: 'personal' | 'collaborative'; - /** User who locked the view (spec format) */ - lockedBy?: string; - }; - - /** - * User Filters Configuration (Airtable Interfaces-style). - * - * Supports three display modes configured by `element`: - * - 'dropdown': Each field renders as a dropdown selector badge (e.g., "Status ∨") - * - 'tabs': Named filter presets rendered as a tab bar (e.g., "Tab | my customers | All records") - */ - userFilters?: { - /** UI element type for displaying filters */ - element: 'dropdown' | 'tabs'; - - /** - * Field-level filter definitions (used by 'dropdown' and 'toggle' modes). - * Each field appears as an independent filter control in the toolbar. - */ - fields?: Array<{ - /** Field name to filter on */ - field: string; - /** Display label (defaults to field label from objectDef) */ - label?: string; - /** Filter input type */ - type?: 'select' | 'multi-select' | 'boolean' | 'date-range' | 'text'; - /** Static options (overrides auto-derived from objectDef) */ - options?: Array<{ - label: string; - value: string | number | boolean; - color?: string; - }>; - /** Show record count per option */ - showCount?: boolean; - /** Default selected values */ - defaultValues?: (string | number | boolean)[]; - }>; - - /** - * Named filter presets (used by 'tabs' mode). - * Each tab represents a pre-configured filter combination. - * - * Canonical shape is `{ name, label, icon?, filter, isDefault? }` — aligned - * with the `{ field, operator, value }` rules used by every other filter in - * the protocol, so AI authoring and the Studio tabs editor emit one - * consistent form. The legacy `{ id, filters, default }` fields remain - * accepted (normalized at runtime by `normalizeTabPresets`) for back-compat. - */ - tabs?: Array<{ - /** Unique tab identifier (snake_case). Falls back to `id` if omitted. */ - name?: string; - /** Tab display label. */ - label: string; - /** Filter rules applied when this tab is active. */ - filter?: Array<{ field: string; operator: string; value?: unknown }>; - /** Icon name (Lucide icon identifier). */ - icon?: string; - /** Whether this tab is active by default. */ - isDefault?: boolean; - - /** @deprecated use `name`. */ - id?: string; - /** @deprecated use `filter` ({ field, operator, value }). */ - filters?: Array; - /** @deprecated use `isDefault`. */ - default?: boolean; - }>; - - /** Allow users to add new filter tabs at runtime */ - allowAddTab?: boolean; - /** Show "All records" tab (tabs mode) */ - showAllRecords?: boolean; - }; - - /** - * Row action identifiers (action names from ActionSchema). - * Aligned with @objectstack/spec ListViewSchema.rowActions. - */ - rowActions?: string[]; - - /** - * Bulk action identifiers (action names from ActionSchema). - * Aligned with @objectstack/spec ListViewSchema.bulkActions. - */ - bulkActions?: string[]; - - /** - * Rich bulk action definitions. See BulkActionDef. Takes precedence over - * `bulkActions` (string-id list) by opening the BulkActionDialog flow. - */ - bulkActionDefs?: BulkActionDef[]; - - /** - * Enable virtual scrolling for large datasets. - * Aligned with @objectstack/spec ListViewSchema.virtualScroll. - * @default false - */ - virtualScroll?: boolean; - - /** - * User actions configuration — which toolbar affordances end users get. - * - * Derived from the spec's `UserActionsConfig` per this file's "never redefine - * types, always import them" rule (this field used to be a hand-maintained - * mirror — a drift hazard). `Partial<>` because authoring is opt-in: you write - * only the toggles you want, the rest fall back to the spec's defaults. - * - * `editInline` is carried as a transitional extension only because the pinned - * @objectstack/spec release predates it (added at the source in - * framework#2468). Once the spec dep is bumped, `Partial` - * supplies `editInline` itself and this `&`-extension becomes redundant. - */ - userActions?: Partial & { - /** - * Allow end users to edit records inline (click a cell → type-aware editor, - * the same widgets the form uses). Default off: a list is read-only unless - * the author opts in. Read by InterfaceListPage → `inlineEdit`. - */ - editInline?: boolean; - }; - - /** - * Appearance configuration — derived from the spec's `AppearanceConfig` - * (per the "never redefine types" rule). `Partial<>` because authoring is - * opt-in. Note: `allowedVisualizations` is the spec's visualization enum, not - * a free `string[]`. - */ - appearance?: Partial; - - /** - * Add record configuration. - * Aligned with @objectstack/spec ListViewSchema.addRecord. - */ - addRecord?: { - enabled?: boolean; - position?: string; - mode?: string; - formView?: string; - }; - - /** - * Show total record count in the view. - * Aligned with @objectstack/spec ListViewSchema.showRecordCount. - * @default false - */ - showRecordCount?: boolean; - - /** - * Allow printing the view. - * Aligned with @objectstack/spec ListViewSchema.allowPrinting. - * @default false - */ - allowPrinting?: boolean; - - /** - * External refresh trigger. - * Increment this value to force the ListView to re-fetch data. - * Used by parent components (e.g., ObjectView) to signal that a mutation - * (create/update/delete) has occurred and the list should refresh. + * External refresh trigger. Increment this value to force the ListView to re-fetch data. + * Used by parent components (e.g. ObjectView) to signal that a mutation occurred. */ refreshTrigger?: number; } + export interface ObjectMapSchema extends BaseSchema { type: 'object-map'; /** ObjectQL object name */ diff --git a/packages/types/src/zod/README.md b/packages/types/src/zod/README.md index fe15a97d9..99fdc8584 100644 --- a/packages/types/src/zod/README.md +++ b/packages/types/src/zod/README.md @@ -11,6 +11,20 @@ This directory contains runtime validation schemas using [Zod](https://github.co - **Auto-completion and IntelliSense** through TypeScript integration - **Detailed error messages** for invalid configurations +### `ListViewSchema` is derived from `@objectstack/spec` (not hand-written) — #2231 + +`ListViewSchema` (`objectql.zod.ts`) is **composed from** the spec's +`@objectstack/spec/ui` `ListViewSchema`: spec-owned fields flow in by reference +(`BaseSchema.extend(SpecListViewFields.shape)`), so they track the protocol automatically. +Only the component envelope (`type: 'list-view'` + `objectName`), the legacy objectui +vocabulary (`viewType`/`fields`/`filters`/`show*`/`densityMode`/…), and the handful of +configs whose objectui shape is intentionally broader than spec's (`userFilters`, +`sharing`, `aria`, `conditionalFormatting`, `exportOptions`, and the per-view-type +`kanban`/`calendar`/`gantt`/`gallery`/`timeline`) are declared locally on top. The TS type +is `z.infer & ListViewRuntimeProps`. A drift-guard test +(`__tests__/list-view-spec-parity.test.ts`) fails if the spec grows a field objectui +hasn't triaged. **Do not hand-add spec-owned fields here** — import them from the spec. + ## Installation The Zod schemas are included in the `@object-ui/types` package: diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 70a74d257..9ee588bd1 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -17,6 +17,7 @@ */ import { z } from 'zod'; +import { ListViewSchema as SpecListViewSchema } from '@objectstack/spec/ui'; import { BaseSchema } from './base.zod.js'; /** @@ -281,109 +282,173 @@ const UserFiltersSchema = z.object({ }); /** - * ListView Schema + * ListView Schema — derived from `@objectstack/spec/ui` `ListViewSchema` (issue #2231). + * + * Spec-owned fields flow in **by reference** (see `SpecListViewFields`) so they auto-track + * the protocol instead of being re-typed here; the drift-guard test + * (`__tests__/list-view-spec-parity.test.ts`) fails if the spec grows a field objectui + * has not triaged. objectui-only / legacy fields are declared locally on top via + * `.extend()` (the final extend wins, so these override anything imported): + * - component envelope: `type: 'list-view'` discriminator + `objectName` binding; + * - legacy vocabulary kept for back-compat: `viewType` (renamed spec `type`), + * `fields`/`columns`, `filters`, the `show*` toolbar flags, `densityMode`, `color`, …; + * - configs whose objectui shape is intentionally broader than spec's (migration + * deferred): `userFilters`, `sharing`, `aria`, `conditionalFormatting`, + * `exportOptions`, and the per-view-type `kanban`/`calendar`/`gantt`/`gallery`/`timeline`. + * + * Migrating the legacy vocabulary to the spec-canonical keys (`type`/`columns`/`filter`/ + * `userActions`) and adopting spec's narrower sub-shapes is deferred — see #2231. */ -export const ListViewSchema = BaseSchema.extend({ - type: z.literal('list-view'), - objectName: z.string().describe('Object Name'), - viewType: z.enum(['grid', 'kanban', 'calendar', 'gantt', 'map', 'chart']).optional().describe('View Type'), - fields: z.array(z.string()).optional().describe('Fields to fetch'), - filters: z.array(z.union([z.array(z.any()), z.string()])).optional().describe('Filter conditions'), - sort: z.array(SortConfigSchema).optional().describe('Sort order'), - options: z.record(z.string(), z.any()).optional().describe('Component overrides'), - userFilters: UserFiltersSchema.optional().describe('User filters configuration'), - showSearch: z.boolean().optional().describe('Show search in toolbar'), - showSort: z.boolean().optional().describe('Show sort controls in toolbar'), - showFilters: z.boolean().optional().describe('Show filter controls in toolbar'), - showHideFields: z.boolean().optional().describe('Show hide-fields button in toolbar'), - showGroup: z.boolean().optional().describe('Show group button in toolbar'), - showColor: z.boolean().optional().describe('Show color button in toolbar'), - showDensity: z.boolean().optional().describe('Show density/row-height button in toolbar'), - allowExport: z.boolean().optional().describe('Allow data export'), - striped: z.boolean().optional().describe('Alternating row colors'), - bordered: z.boolean().optional().describe('Show cell borders'), - color: z.string().optional().describe('Color field for row/card coloring'), - inlineEdit: z.boolean().optional().describe('Enable inline editing'), - wrapHeaders: z.boolean().optional().describe('Wrap column headers'), - clickIntoRecordDetails: z.boolean().optional().describe('Navigate to detail on row click'), - addRecordViaForm: z.boolean().optional().describe('Add records via form dialog'), - addDeleteRecordsInline: z.boolean().optional().describe('Enable inline add/delete'), - collapseAllByDefault: z.boolean().optional().describe('Collapse all groups by default'), - fieldTextColor: z.string().optional().describe('Field for custom text color'), - prefixField: z.string().optional().describe('Prefix field before title'), - showDescription: z.boolean().optional().describe('Show field descriptions'), - navigation: z.object({ - mode: z.enum(['page', 'drawer', 'modal', 'split', 'popover', 'new_window', 'none']), - view: z.string().optional(), - preventNavigation: z.boolean().optional(), - openNewTab: z.boolean().optional(), - width: z.union([z.string(), z.number()]).optional(), - }).optional().describe('Navigation configuration'), - selection: z.object({ - type: z.enum(['none', 'single', 'multiple']), - }).optional().describe('Row selection mode'), - pagination: z.object({ - pageSize: z.number(), - pageSizeOptions: z.array(z.number()).optional(), - }).optional().describe('Pagination configuration'), - searchableFields: z.array(z.string()).optional().describe('Searchable fields'), - filterableFields: z.array(z.string()).optional().describe('Filterable fields'), - resizable: z.boolean().optional().describe('Allow column resizing'), - densityMode: z.enum(['compact', 'comfortable', 'spacious']).optional().describe('Density mode'), - rowHeight: z.enum(['compact', 'short', 'medium', 'tall', 'extra_tall']).optional().describe('Row height'), - hiddenFields: z.array(z.string()).optional().describe('Hidden fields'), - exportOptions: z.union([ - z.array(z.enum(['csv', 'xlsx', 'json', 'pdf'])), - z.object({ - formats: z.array(z.enum(['csv', 'xlsx', 'json', 'pdf'])).optional(), - maxRecords: z.number().optional(), - includeHeaders: z.boolean().optional(), - fileNamePrefix: z.string().optional(), - }), - ]).optional().describe('Export options'), - rowActions: z.array(z.string()).optional().describe('Row action identifiers'), - bulkActions: z.array(z.string()).optional().describe('Bulk action identifiers'), - sharing: z.object({ - visibility: z.enum(['private', 'team', 'organization', 'public']).optional(), - enabled: z.boolean().optional(), - type: z.enum(['personal', 'collaborative']).optional(), - lockedBy: z.string().optional(), - }).optional().describe('Sharing configuration'), - addRecord: z.object({ - enabled: z.boolean().optional(), - position: z.string().optional(), - mode: z.string().optional(), - formView: z.string().optional(), - }).optional().describe('Add record configuration'), - conditionalFormatting: z.array(z.union([ - z.object({ - field: z.string(), - operator: z.enum(['equals', 'not_equals', 'contains', 'greater_than', 'less_than', 'in']), - value: z.any(), - backgroundColor: z.string().optional(), - textColor: z.string().optional(), - borderColor: z.string().optional(), - expression: z.string().optional(), - }), - z.object({ - condition: z.string(), - style: z.record(z.string(), z.string()), - }), - ])).optional().describe('Conditional formatting rules'), - showRecordCount: z.boolean().optional().describe('Show total record count'), - allowPrinting: z.boolean().optional().describe('Allow printing'), - virtualScroll: z.boolean().optional().describe('Enable virtual scrolling'), - emptyState: z.object({ - title: z.string().optional(), - message: z.string().optional(), - icon: z.string().optional(), - }).optional().describe('Empty state configuration'), - aria: z.object({ - label: z.string().optional(), - describedBy: z.string().optional(), - live: z.enum(['polite', 'assertive', 'off']).optional(), - }).optional().describe('ARIA attributes'), -}); +// Spec view-config fields, minus: the component envelope (name/label/description → +// BaseSchema), the discriminator/renamed/relaxed keys (type/columns), and the configs +// kept as local overrides below. `.partial()` guarantees no *future* spec field can +// become required and silently invalidate existing objectui payloads. +const SpecListViewFields = SpecListViewSchema + .omit({ + type: true, + columns: true, + name: true, + label: true, + description: true, + userFilters: true, + sharing: true, + aria: true, + conditionalFormatting: true, + exportOptions: true, + kanban: true, + calendar: true, + gantt: true, + gallery: true, + timeline: true, + }) + .partial(); + +// View-kind enum reused from spec (unwrap its `.default('grid')`) so it cannot drift. +const ViewKindEnum = SpecListViewSchema.shape.type.removeDefault(); + +export const ListViewSchema = BaseSchema + // Import spec-owned fields by reference: data, filter, sort, searchableFields, + // filterableFields, resizable, striped, bordered, compactToolbar, selection, navigation, + // pagination, chart, tree, rowHeight, grouping, rowColor, hiddenFields, fieldOrder, + // rowActions, bulkActions, bulkActionDefs, virtualScroll, inlineEdit, userActions, + // appearance, tabs, addRecord, showRecordCount, allowPrinting, emptyState, responsive, + // performance. + .extend(SpecListViewFields.shape) + .extend({ + // Component discriminator — load-bearing for the ObjectQLComponentSchema union. + type: z.literal('list-view'), + // objectui-only object binding (spec binds via data.provider:'object'; migration deferred). + objectName: z.string().describe('Object Name'), + // Renamed spec `type` (view-kind); enum imported from spec so it can't drift. + viewType: ViewKindEnum.optional().describe('View Type'), + // Relaxed spec `columns` (spec requires it) + legacy `fields` alias for string[] columns. + columns: z.union([z.array(z.string()), z.array(ListColumnSchema)]).optional().describe('Columns definition'), + fields: z.array(z.string()).optional().describe('Legacy alias for string[] columns'), + // Legacy tuple/CEL filter format (spec-canonical `filter` is imported above). + filters: z.array(z.union([z.array(z.any()), z.string()])).optional().describe('Filter conditions (legacy)'), + // Legacy toolbar visibility flags (spec-canonical is `userActions`; runtime dual-reads). + showSearch: z.boolean().optional().describe('Show search in toolbar'), + showSort: z.boolean().optional().describe('Show sort controls in toolbar'), + showFilters: z.boolean().optional().describe('Show filter controls in toolbar'), + showHideFields: z.boolean().optional().describe('Show hide-fields button in toolbar'), + showGroup: z.boolean().optional().describe('Show group button in toolbar'), + showColor: z.boolean().optional().describe('Show color button in toolbar'), + showDensity: z.boolean().optional().describe('Show density/row-height button in toolbar'), + showDescription: z.boolean().optional().describe('Show field descriptions'), + allowExport: z.boolean().optional().describe('Allow data export'), + densityMode: z.enum(['compact', 'comfortable', 'spacious']).optional().describe('Density mode'), + color: z.string().optional().describe('Color field for row/card coloring'), + fieldTextColor: z.string().optional().describe('Field for custom text color'), + prefixField: z.string().optional().describe('Prefix field before title'), + wrapHeaders: z.boolean().optional().describe('Wrap column headers'), + clickIntoRecordDetails: z.boolean().optional().describe('Navigate to detail on row click'), + addRecordViaForm: z.boolean().optional().describe('Add records via form dialog'), + addDeleteRecordsInline: z.boolean().optional().describe('Enable inline add/delete'), + collapseAllByDefault: z.boolean().optional().describe('Collapse all groups by default'), + options: z.record(z.string(), z.any()).optional().describe('Component overrides (legacy)'), + operations: z.object({ + create: z.boolean().optional(), + read: z.boolean().optional(), + update: z.boolean().optional(), + delete: z.boolean().optional(), + export: z.boolean().optional(), + import: z.boolean().optional(), + }).optional().describe('Enabled operations'), + // ── Local overrides: objectui shapes are intentionally broader than spec (deferred) ── + userFilters: UserFiltersSchema.optional().describe('User filters configuration (accepts legacy tab shapes)'), + sharing: z.object({ + visibility: z.enum(['private', 'team', 'organization', 'public']).optional(), + enabled: z.boolean().optional(), + type: z.enum(['personal', 'collaborative']).optional(), + lockedBy: z.string().optional(), + }).optional().describe('Sharing configuration'), + aria: z.object({ + label: z.string().optional(), + describedBy: z.string().optional(), + live: z.enum(['polite', 'assertive', 'off']).optional(), + }).optional().describe('ARIA attributes'), + conditionalFormatting: z.array(z.union([ + z.object({ + field: z.string(), + operator: z.enum(['equals', 'not_equals', 'contains', 'greater_than', 'less_than', 'in']), + value: z.any(), + backgroundColor: z.string().optional(), + textColor: z.string().optional(), + borderColor: z.string().optional(), + expression: z.string().optional(), + }), + z.object({ + condition: z.string(), + style: z.record(z.string(), z.string()), + }), + ])).optional().describe('Conditional formatting rules'), + exportOptions: z.union([ + z.array(z.enum(['csv', 'xlsx', 'json', 'pdf'])), + z.object({ + formats: z.array(z.enum(['csv', 'xlsx', 'json', 'pdf'])).optional(), + maxRecords: z.number().optional(), + includeHeaders: z.boolean().optional(), + fileNamePrefix: z.string().optional(), + }), + ]).optional().describe('Export options'), + kanban: z.object({ + groupField: z.string(), + titleField: z.string().optional(), + cardFields: z.array(z.string()).optional(), + }).passthrough().optional().describe('Kanban-specific configuration'), + calendar: z.object({ + startDateField: z.string(), + endDateField: z.string().optional(), + titleField: z.string().optional(), + defaultView: z.enum(['month', 'week', 'day', 'agenda']).optional(), + }).passthrough().optional().describe('Calendar-specific configuration'), + gantt: z.object({ + startDateField: z.string(), + endDateField: z.string(), + titleField: z.string().optional(), + progressField: z.string().optional(), + dependenciesField: z.string().optional(), + }).passthrough().optional().describe('Gantt-specific configuration'), + gallery: z.object({ + coverField: z.string().optional(), + titleField: z.string().optional(), + imageField: z.string().optional(), + subtitleField: z.string().optional(), + }).passthrough().optional().describe('Gallery-specific configuration'), + timeline: z.object({ + startDateField: z.string().optional(), + endDateField: z.string().optional(), + titleField: z.string().optional(), + dateField: z.string().optional(), + }).passthrough().optional().describe('Timeline-specific configuration'), + }); + +/** + * Inferred TS type for the ListView component node (spec-derived; issue #2231). + * The hand-written `interface ListViewSchema` in `../objectql.ts` is now an alias of + * this inferred type intersected with the non-serializable runtime-only props. + */ +export type ListViewInferred = z.infer; /** * ObjectMap Schema diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e73e6ca9a..a4059db37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^10.0.1 version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) '@playwright/test': specifier: ^1.61.1 version: 1.61.1 @@ -265,8 +265,8 @@ importers: specifier: ^14.6.0 version: 14.6.0(ai@7.0.22(zod@4.4.3)) '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) '@tailwindcss/postcss': specifier: ^4.3.2 version: 4.3.2 @@ -397,8 +397,8 @@ importers: specifier: workspace:* version: link:../../packages/types '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) fumadocs-core: specifier: 16.11.3 version: 16.11.3(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.24.0(react@19.2.7))(next@16.2.10(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-router@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)(zod@4.4.3) @@ -692,8 +692,8 @@ importers: specifier: ^14.6.0 version: 14.6.0(ai@7.0.22(zod@4.4.3)) '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) '@sentry/react': specifier: ^10.65.0 version: 10.65.0(react@19.2.7) @@ -1075,8 +1075,8 @@ importers: specifier: ^14.6.0 version: 14.6.0(ai@7.0.22(zod@4.4.3)) '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) lodash: specifier: ^4.18.1 version: 4.18.1 @@ -1722,8 +1722,8 @@ importers: specifier: workspace:^ version: link:../permissions '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) lucide-react: specifier: ^1.24.0 version: 1.24.0(react@19.2.7) @@ -1838,8 +1838,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) lucide-react: specifier: ^1.24.0 version: 1.24.0(react@19.2.7) @@ -1893,8 +1893,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) lucide-react: specifier: ^1.24.0 version: 1.24.0(react@19.2.7) @@ -2131,8 +2131,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) lucide-react: specifier: ^1.24.0 version: 1.24.0(react@19.2.7) @@ -2320,8 +2320,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2372,8 +2372,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) lucide-react: specifier: ^1.24.0 version: 1.24.0(react@19.2.7) @@ -2436,8 +2436,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -2501,8 +2501,8 @@ importers: specifier: workspace:* version: link:../types '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) react: specifier: 19.2.7 version: 19.2.7 @@ -2607,8 +2607,8 @@ importers: packages/types: dependencies: '@objectstack/spec': - specifier: ^14.6.0 - version: 14.6.0(ai@7.0.22(zod@4.4.3)) + specifier: ^15.1.0 + version: 15.1.0(ai@7.0.22(zod@4.4.3)) zod: specifier: ^4.4.3 version: 4.4.3 @@ -3980,6 +3980,15 @@ packages: ai: optional: true + '@objectstack/spec@15.1.0': + resolution: {integrity: sha512-gL5toOEKTZ2whRJqQYfSa2G3D72jQJd4in3NtHlI02zR7olKzLx7z6+FbbcmI7cienYSxHMl3oDnRV/xFp2+aA==} + engines: {node: '>=18.0.0'} + peerDependencies: + ai: ^7.0.0 + peerDependenciesMeta: + ai: + optional: true + '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} @@ -7899,8 +7908,8 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isbot@5.2.0: - resolution: {integrity: sha512-gbZiGCb4B5xaoxg9mS7koAyRdvJnArk10VLSHOgz6rtBG93/pi1xOFaVvXMKZ7JXgyZ8zAbNRK5uIBdIUTFSqw==} + isbot@5.2.1: + resolution: {integrity: sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==} engines: {node: '>=18'} isexe@2.0.0: @@ -8629,8 +8638,8 @@ packages: monaco-editor@0.55.1: resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} - mongodb-connection-string-url@7.0.1: - resolution: {integrity: sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==} + mongodb-connection-string-url@7.0.2: + resolution: {integrity: sha512-ZoS07RoFqpKYQwAk59qmrx8+jJHNHU30UjlU96QktiGn1ltvDr+vCznLX5DiUBLEpMAHatHNWV1nM/74ul66kA==} engines: {node: '>=20.19.0'} mongodb@7.2.0: @@ -12136,6 +12145,12 @@ snapshots: optionalDependencies: ai: 7.0.22(zod@4.4.3) + '@objectstack/spec@15.1.0(ai@7.0.22(zod@4.4.3))': + dependencies: + zod: 4.4.3 + optionalDependencies: + ai: 7.0.22(zod@4.4.3) + '@open-draft/deferred-promise@2.2.0': {} '@open-draft/deferred-promise@3.0.0': {} @@ -13358,7 +13373,7 @@ snapshots: '@tanstack/history': 1.162.0 '@tanstack/react-store': 0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@tanstack/router-core': 1.171.6 - isbot: 5.2.0 + isbot: 5.2.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) optional: true @@ -16282,7 +16297,7 @@ snapshots: isarray@1.0.0: {} - isbot@5.2.0: + isbot@5.2.1: optional: true isexe@2.0.0: {} @@ -17262,7 +17277,7 @@ snapshots: dompurify: 3.4.12 marked: 14.0.0 - mongodb-connection-string-url@7.0.1: + mongodb-connection-string-url@7.0.2: dependencies: '@types/whatwg-url': 13.0.0 whatwg-url: 14.2.0 @@ -17272,7 +17287,7 @@ snapshots: dependencies: '@mongodb-js/saslprep': 1.4.12 bson: 7.3.1 - mongodb-connection-string-url: 7.0.1 + mongodb-connection-string-url: 7.0.2 optional: true motion-dom@12.42.2: