diff --git a/.changeset/issue-5373-retire-crud-schema.md b/.changeset/issue-5373-retire-crud-schema.md new file mode 100644 index 0000000000..1643a2efca --- /dev/null +++ b/.changeset/issue-5373-retire-crud-schema.md @@ -0,0 +1,44 @@ +--- +'@object-ui/types': minor +'@object-ui/core': minor +--- + +Retire `CRUDSchema` and the `type: 'crud'` node spelling (objectui#5373, +maintainer ruling 2026-08-20, route 2) under ADR-0049 enforce-or-remove. + +`crud` had four declaration faces and no registered renderer, for the whole +life of the key: the TS interface (`packages/types/src/crud.ts`), the zod +mirror (`packages/types/src/zod/crud.zod.ts`), a dedicated branch in +`validateSchema` that affirmatively PASSED it, and `CRUDBuilder` in +`@object-ui/core`. A node spelling it painted the OBJUI-001 "Unknown component +type" panel, and `content/docs/api/schema-reference.md` published it as +reference material — so a reader (or an AI author) who copied the page got a +red panel. + +Removed from `@object-ui/types`: the `CRUDSchema` interface and its zod +mirror, the four shapes that existed only to type its keys — `CRUDOperation`, +`CRUDFilter`, `CRUDToolbar`, `CRUDPagination` and their zod mirrors and +`…SchemaType` aliases — and `CRUDSchema` as a member of `CRUDComponentSchema`, +which is what took it off the node union `AnySchema`. `ActionSchema`, +`DetailSchema` and `CRUDDialogSchema` are unchanged and remain the union's +members. + +Removed from `@object-ui/core`: `CRUDBuilder` and the `crud()` factory. + +Authoring `crud` is now REFUSED BY NAME rather than passed or silently +ignored. `validateSchema` returns an `error` with `code: 'RETIRED_TYPE'` on +`schema.type` — at any depth, since it is what `validateChildren` recurses +with — so `assertValidSchema` throws and `isValidSchema` answers `false`. The +message names the migration: `object-grid` for the record table with its +toolbar, filters, pagination and row/batch actions, `object-form` for the +create/edit form, and `detail` for the record view. `api/schema-reference.md` +is rewritten around those shapes. + +Note on blast radius: the repository itself contains zero authored `crud` +nodes and zero registrations of the key (measured on the merge base against +the doc gate's own 659-key registry derivation, which reads `register` and +`registerLazy` alike). That is an IN-REPO zero, not an npm zero — a published +consumer that imported the `CRUDSchema` type, called `crud()` / `CRUDBuilder`, +or authored `type: 'crud'` will see a compile error or a validation error +respectively. Both are the intended, loud replacement for a shape that has +never rendered. diff --git a/content/docs/api/index.md b/content/docs/api/index.md index 8a684b496f..39825c0079 100644 --- a/content/docs/api/index.md +++ b/content/docs/api/index.md @@ -17,7 +17,7 @@ Complete reference for every ObjectUI schema type with annotated JSON examples c - **Layout** — `PageSchema`, `DivSchema`, `CardSchema`, `GridSchema`, `TabsSchema` - **Forms** — `FormSchema`, `InputSchema`, `SelectSchema`, `ButtonSchema` - **Data Display** — `TableSchema`, `ChartSchema`, `TreeViewSchema` -- **CRUD** — `CRUDSchema`, `ActionSchema`, `DetailSchema` +- **CRUD** — `ActionSchema`, `DetailSchema`, `CRUDDialogSchema` - **ObjectQL** — `ObjectGridSchema`, `ObjectFormSchema`, `ObjectViewSchema` - **Complex** — `KanbanSchema`, `DashboardSchema`, `CalendarViewSchema` - **Views** — `DetailViewSchema`, `ViewSwitcherSchema` diff --git a/content/docs/api/schema-reference.md b/content/docs/api/schema-reference.md index e3cebc60a3..b717a98cbf 100644 --- a/content/docs/api/schema-reference.md +++ b/content/docs/api/schema-reference.md @@ -424,7 +424,7 @@ did; authoring them is now refused by validation instead of silently ignored). | `data` | `any[]` | Array of row data objects. | | `footer` | `SchemaNode \| string` | Footer content below the table. | -**Related:** [CRUDSchema](#crudschema), [ObjectGridSchema](#objectgridschema) +**Related:** [ObjectGridSchema](#objectgridschema) --- @@ -523,88 +523,33 @@ A hierarchical tree component for nested data with expand/collapse and selection ## CRUD Schemas -### CRUDSchema +### CRUDSchema — retired -A complete CRUD (Create, Read, Update, Delete) interface with table, toolbar, filters, pagination, and batch/row actions. +`CRUDSchema` and the `crud` node type were **removed** in objectui#5373 under +ADR-0049 (enforce-or-remove). The type had four declaration faces — a TypeScript +interface, a zod mirror, a branch in the schema validator and a `CRUDBuilder` — +and no registered renderer, for the whole life of the key. A node that spelled it +painted the OBJUI-001 "Unknown component type" panel, so this page was teaching a +shape that could not render. -```json -{ - "type": "crud", - "title": "Products", - "resource": "products", - "api": "/api/products", - "selectable": "multiple", - "defaultSort": "name", - "defaultSortOrder": "asc", - "columns": [ - { "name": "name", "label": "Product Name", "sortable": true }, - { "name": "price", "label": "Price", "align": "right" }, - { "name": "stock", "label": "Stock", "sortable": true }, - { "name": "status", "label": "Status" } - ], - "fields": [ - { "name": "name", "label": "Product Name", "type": "text", "required": true }, - { "name": "price", "label": "Price", "type": "number", "required": true }, - { "name": "stock", "label": "Stock", "type": "number" }, - { "name": "status", "label": "Status", "type": "select", "options": [ - { "label": "Active", "value": "active" }, - { "label": "Draft", "value": "draft" } - ]} - ], - "operations": { - "create": true, - "read": true, - "update": true, - "delete": true, - "export": true - }, - "toolbar": { - "showSearch": true, - "showFilters": true, - "showExport": true, - "actions": [ - { "type": "action", "label": "Add Product", "level": "primary", "icon": "Plus" } - ] - }, - "filters": [ - { "name": "status", "label": "Status", "type": "select", "options": [ - { "label": "Active", "value": "active" }, - { "label": "Draft", "value": "draft" } - ]} - ], - "pagination": { - "pageSize": 20, - "pageSizeOptions": [10, 20, 50, 100] - }, - "rowActions": [ - { "type": "action", "label": "Edit", "icon": "Pencil", "actionType": "dialog" }, - { "type": "action", "label": "Delete", "icon": "Trash2", "level": "danger", "actionType": "confirm" } - ], - "batchActions": [ - { "type": "action", "label": "Delete Selected", "level": "danger", "actionType": "confirm" } - ] -} -``` +There is no drop-in replacement, because a CRUD screen is a composition rather +than one node. Build it from the shapes that do render: -| Property | Type | Description | -|----------|------|-------------| -| `title` | `string` | CRUD view title. | -| `resource` | `string` | Resource identifier for API calls. | -| `api` | `string` | Base API endpoint URL. | -| `columns` | `TableColumn[]` | **Required.** Column definitions for the table view. | -| `fields` | `FormField[]` | Field definitions for create/edit forms. | -| `operations` | `object` | Toggle CRUD operations: `create`, `read`, `update`, `delete`, `export`, `import`. | -| `toolbar` | `CRUDToolbar` | Toolbar configuration with search, filters, and custom actions. | -| `filters` | `CRUDFilter[]` | Filter definitions. | -| `pagination` | `CRUDPagination` | Pagination settings with `pageSize` and `pageSizeOptions`. | -| `selectable` | `boolean \| "single" \| "multiple"` | Row selection mode. | -| `rowActions` | `ActionSchema[]` | Actions available on each row. | -| `batchActions` | `ActionSchema[]` | Actions for selected rows. | -| `defaultSort` / `defaultSortOrder` | `string` / `"asc" \| "desc"` | Default sort field and direction. | -| `mode` | `"table" \| "grid" \| "list" \| "kanban"` | Display mode for the CRUD view. | -| `emptyState` | `SchemaNode` | Custom empty state content. | - -**Related:** [ActionSchema](#actionschema), [TableSchema](#tableschema), [ObjectGridSchema](#objectgridschema) +| What `CRUDSchema` promised | What to author instead | +|---|---| +| The record table, with toolbar, filters, pagination and row/batch actions | [ObjectGridSchema](#objectgridschema) | +| The create/edit form | [ObjectFormSchema](#objectformschema) | +| The single-record read view | [DetailSchema](#detailschema) / [DetailViewSchema](#detailviewschema) | +| Whole-object screens that bundle the above | [ObjectViewSchema](#objectviewschema) | + +The `defaultSort` and `defaultSortOrder` keys documented here were `CRUDSchema`'s +own — a flat field name plus a separate direction. They are gone with it. +[ObjectGridSchema](#objectgridschema) declares its own, differently shaped +`defaultSort` (an object with `field` and `order`); that key is unaffected. + +Authoring `crud` is now refused by name: `validateSchema` from `@object-ui/core` +returns a `RETIRED_TYPE` error on `schema.type` naming the migration above, and +`objectui check` reports the type as unknown. --- @@ -661,7 +606,7 @@ A powerful action definition supporting API calls, confirmations, dialogs, chain | `redirect` | `string` | URL to navigate to after action. | | `retry` | `object` | Retry config with `maxAttempts` and `delay`. | -**Related:** [CRUDSchema](#crudschema), [ButtonSchema](#buttonschema) +**Related:** [DetailSchema](#detailschema), [ButtonSchema](#buttonschema) --- @@ -722,7 +667,7 @@ A single-record detail view with grouped fields, actions, and tabs. | `showBack` | `boolean` | Show a back navigation button. | | `loading` | `boolean` | Show loading state. | -**Related:** [DetailViewSchema](#detailviewschema), [CRUDSchema](#crudschema) +**Related:** [DetailViewSchema](#detailviewschema), [ObjectGridSchema](#objectgridschema) --- @@ -790,7 +735,7 @@ A data grid that auto-fetches from an ObjectQL object definition. Includes searc | `frozenColumns` | `number` | Number of columns frozen on scroll. | | `navigation` | `ViewNavigationConfig` | SPA navigation configuration. | -**Related:** [ObjectViewSchema](#objectviewschema), [CRUDSchema](#crudschema), [TableSchema](#tableschema) +**Related:** [ObjectViewSchema](#objectviewschema), [TableSchema](#tableschema) --- @@ -966,7 +911,7 @@ A drag-and-drop Kanban board with columns and cards. | `onColumnAdd` | `function` | Callback when a new column is added. | | `onCardAdd` | `function` | Callback when a new card is added to a column. | -**Related:** [ObjectViewSchema](#objectviewschema), [CRUDSchema](#crudschema) +**Related:** [ObjectViewSchema](#objectviewschema), [ObjectGridSchema](#objectgridschema) --- @@ -1300,7 +1245,7 @@ import type { FormSchema, InputSchema, SelectSchema, ButtonSchema } from '@objec import type { TableSchema, ChartSchema, TreeViewSchema } from '@object-ui/types'; // CRUD -import type { CRUDSchema, ActionSchema, DetailSchema } from '@object-ui/types'; +import type { ActionSchema, DetailSchema } from '@object-ui/types'; // ObjectQL import type { ObjectGridSchema, ObjectFormSchema, ObjectViewSchema } from '@object-ui/types'; diff --git a/packages/app-shell/src/views/ReportView.dataSourceObjectKey.test.tsx b/packages/app-shell/src/views/ReportView.dataSourceObjectKey.test.tsx index 995d5885e8..fbf7e43784 100644 --- a/packages/app-shell/src/views/ReportView.dataSourceObjectKey.test.tsx +++ b/packages/app-shell/src/views/ReportView.dataSourceObjectKey.test.tsx @@ -24,8 +24,9 @@ * REJECTED, not ignored), and the binding's own predicate * `isElementDataSourceConfig` decides on `object` — so a `resource`-only * binding was never a binding on any other renderer. `resource` IS a real key - * elsewhere (`CRUDSchema.resource`, the `DataSource` adapter's first - * parameter, `LiveExportOptions.resource`); none of those is this surface. + * elsewhere (the `DataSource` adapter's first parameter, + * `LiveExportOptions.resource`); none of those is this surface. (`CRUDSchema.resource` + * was a fourth until objectui#5373 retired `CRUDSchema` under ADR-0049.) * * Per AGENTS.md #0.1 the fix belongs at the producer, never as a renderer-side * alias — and the producer census found no producer to fix: nothing in this diff --git a/packages/cli/src/__tests__/check-known-types.test.ts b/packages/cli/src/__tests__/check-known-types.test.ts index 3d54f90cc6..28144b3826 100644 --- a/packages/cli/src/__tests__/check-known-types.test.ts +++ b/packages/cli/src/__tests__/check-known-types.test.ts @@ -81,9 +81,26 @@ afterEach(() => { }); describe('objectui check — unknown schema types', () => { - it('warns about `crud`, which four declaration faces describe and no renderer registers', async () => { + it('warns about `crud`, a RETIRED spelling that must never re-enter the key set', async () => { // The defect objectui#5115 was filed for: this file passed in silence, and // then rendered the OBJUI-001 "Unknown component type" panel in the browser. + // + // This pin was flipped by objectui#5373, which retired `CRUDSchema` under + // ADR-0049. Its old name said `crud` was a type "four declaration faces + // describe and no renderer registers" — true when it was written, false + // now: the interface, the zod mirror, the validator branch and the builder + // are all gone, and `type: 'crud'` is refused BY NAME by + // `validateSchema` in `@object-ui/core`. + // + // What this pin can and cannot witness, stated so the next reader does not + // over-read it: `KNOWN_SCHEMA_TYPES` is derived from the REGISTRATION calls, + // and `crud` never had one — so this assertion held before the retirement + // and holds after it, and would keep holding if the retirement were + // reverted. It is a regression pin against `crud` being REGISTERED back + // into the key set, not a witness of the declarations being gone. The + // witnesses that do distinguish those two worlds are the refusal test in + // `@object-ui/core`'s `schema-validator.test.ts` and the union/barrel pins + // in `@object-ui/types`' `crud-retirement-5373.test.ts`. writeSchema('crud-page.json', { type: 'crud', resource: '/api/accounts' }); await check(cwd); expect(unknownTypeWarnings()).toEqual([ diff --git a/packages/core/src/builder/__tests__/schema-builder.test.ts b/packages/core/src/builder/__tests__/schema-builder.test.ts index 4fba57a36a..dcb8e1827c 100644 --- a/packages/core/src/builder/__tests__/schema-builder.test.ts +++ b/packages/core/src/builder/__tests__/schema-builder.test.ts @@ -1,20 +1,6 @@ import { describe, it, expect } from 'vitest'; -import type { CRUDOperation, CRUDSchema } from '@object-ui/types'; -import { form, crud, button, input, card, grid, flex } from '../../builder/schema-builder'; - -/** - * `CRUDSchema.operations.` is `boolean | CRUDOperation | undefined`, so an - * `.enabled` read off it does not compile. The `enable*()` builders always write - * the OBJECT form; this narrows to it and fails loudly if that ever stops being - * true, instead of casting the distinction away. - */ -function operationOf(schema: CRUDSchema, key: 'create' | 'update' | 'delete'): CRUDOperation { - const operation = schema.operations?.[key]; - if (typeof operation !== 'object' || operation === null) { - throw new Error(`operations.${key} is not the object form: ${String(operation)}`); - } - return operation; -} +import * as builder from '../../builder/schema-builder'; +import { form, button, input, card, grid, flex } from '../../builder/schema-builder'; describe('SchemaBuilder', () => { describe('form()', () => { @@ -87,68 +73,6 @@ describe('SchemaBuilder', () => { }); }); - describe('crud()', () => { - it('creates a basic CRUD schema', () => { - const schema = crud().id('test-crud').build(); - expect(schema).toBeDefined(); - expect(schema.type).toBe('crud'); - expect(schema.id).toBe('test-crud'); - }); - - it('supports resource definition', () => { - const schema = crud() - .resource('users') - .build(); - expect(schema.resource).toBe('users'); - }); - - // The fixtures below used to be authored as `{ name, label }` — a dialect - // `TableColumn` does not have (its keys are `accessorKey` / `header`). The - // builder stores whatever it is handed, so `columns![0].name` was asserting - // that the builder returns its own input, on a shape no CRUD renderer reads - // (objectui#4040). - it('supports column definitions', () => { - const schema = crud() - .column({ accessorKey: 'name', header: 'Name' }) - .build(); - expect(schema.columns).toHaveLength(1); - expect(schema.columns![0].accessorKey).toBe('name'); - expect(schema.columns![0].header).toBe('Name'); - }); - - it('supports bulk columns', () => { - const schema = crud() - .columns([ - { accessorKey: 'id', header: 'ID' }, - { accessorKey: 'name', header: 'Name' }, - ]) - .build(); - expect(schema.columns).toHaveLength(2); - }); - - it('supports CRUD operations', () => { - const schema = crud() - .api('/api/users') - .enableCreate() - .enableUpdate() - .enableDelete() - .build(); - expect(schema.operations).toBeDefined(); - expect(operationOf(schema, 'create').enabled).toBe(true); - expect(operationOf(schema, 'update').enabled).toBe(true); - expect(operationOf(schema, 'delete').enabled).toBe(true); - }); - - it('supports pagination', () => { - const schema = crud() - .pagination(25) - .build(); - expect(schema.pagination).toBeDefined(); - expect(schema.pagination!.enabled).toBe(true); - expect(schema.pagination!.pageSize).toBe(25); - }); - }); - describe('button()', () => { it('creates a button schema', () => { const schema = button().id('btn').build(); @@ -254,3 +178,15 @@ describe('SchemaBuilder', () => { }); }); }); + +describe('the retired `crud` builder (objectui#5373)', () => { + it('exports no `crud` factory — the ADR-0049 retirement removed CRUDBuilder', () => { + // Counter-probe in the SAME assertion pair, so this cannot pass by the + // module failing to load or by the names being read off the wrong object: + // the surviving factories must still be there. + expect(builder).not.toHaveProperty('crud'); + expect(builder).not.toHaveProperty('CRUDBuilder'); + expect(typeof builder.form).toBe('function'); + expect(typeof builder.grid).toBe('function'); + }); +}); diff --git a/packages/core/src/builder/schema-builder.ts b/packages/core/src/builder/schema-builder.ts index 0d8690d6ac..40acce12d2 100644 --- a/packages/core/src/builder/schema-builder.ts +++ b/packages/core/src/builder/schema-builder.ts @@ -20,9 +20,6 @@ import type { BaseSchema, FormSchema, FormField, - CRUDSchema, - TableColumn, - ActionSchema, ButtonSchema, InputSchema, CardSchema, @@ -162,153 +159,6 @@ export class FormBuilder extends SchemaBuilder { } } -/** - * CRUD builder - */ -export class CRUDBuilder extends SchemaBuilder { - constructor() { - super('crud'); - this.schema.columns = []; - } - - /** - * Set resource name - */ - resource(resource: string): this { - this.schema.resource = resource; - return this; - } - - /** - * Set API endpoint - */ - api(api: string): this { - this.schema.api = api; - return this; - } - - /** - * Set title - */ - title(title: string): this { - this.schema.title = title; - return this; - } - - /** - * Set description - */ - description(description: string): this { - this.schema.description = description; - return this; - } - - /** - * Add a column - */ - column(column: TableColumn): this { - this.schema.columns = [...(this.schema.columns || []), column]; - return this; - } - - /** - * Set all columns - */ - columns(columns: TableColumn[]): this { - this.schema.columns = columns; - return this; - } - - /** - * Set form fields - */ - fields(fields: FormField[]): this { - this.schema.fields = fields; - return this; - } - - /** - * Enable create operation - */ - enableCreate(label?: string): this { - if (!this.schema.operations) this.schema.operations = {}; - this.schema.operations.create = { - enabled: true, - label: label || 'Create', - api: this.schema.api, - method: 'POST' - }; - return this; - } - - /** - * Enable update operation - */ - enableUpdate(label?: string): this { - if (!this.schema.operations) this.schema.operations = {}; - this.schema.operations.update = { - enabled: true, - label: label || 'Update', - api: `${this.schema.api}/\${id}`, - method: 'PUT' - }; - return this; - } - - /** - * Enable delete operation - */ - enableDelete(label?: string, confirmText?: string): this { - if (!this.schema.operations) this.schema.operations = {}; - this.schema.operations.delete = { - enabled: true, - label: label || 'Delete', - api: `${this.schema.api}/\${id}`, - method: 'DELETE', - confirmText: confirmText || 'Are you sure?' - }; - return this; - } - - /** - * Set pagination - */ - pagination(pageSize: number = 20): this { - this.schema.pagination = { - enabled: true, - pageSize, - pageSizeOptions: [10, 20, 50, 100], - showTotal: true, - showSizeChanger: true - }; - return this; - } - - /** - * Enable row selection - */ - selectable(mode: 'single' | 'multiple' = 'multiple'): this { - this.schema.selectable = mode; - return this; - } - - /** - * Add a batch action - */ - batchAction(action: ActionSchema): this { - this.schema.batchActions = [...(this.schema.batchActions || []), action]; - return this; - } - - /** - * Add a row action - */ - rowAction(action: ActionSchema): this { - this.schema.rowActions = [...(this.schema.rowActions || []), action]; - return this; - } -} - /** * Button builder */ @@ -576,7 +426,6 @@ export class FlexBuilder extends SchemaBuilder { // Export factory functions export const form = () => new FormBuilder(); -export const crud = () => new CRUDBuilder(); export const button = () => new ButtonBuilder(); export const input = () => new InputBuilder(); export const card = () => new CardBuilder(); diff --git a/packages/core/src/validation/__tests__/schema-validator.test.ts b/packages/core/src/validation/__tests__/schema-validator.test.ts index 6e0c0ee12e..b2b74af512 100644 --- a/packages/core/src/validation/__tests__/schema-validator.test.ts +++ b/packages/core/src/validation/__tests__/schema-validator.test.ts @@ -20,21 +20,48 @@ describe('schema-validator', () => { expect(result.errors.length).toBeGreaterThan(0); }); - it('validates CRUD schema with columns', () => { + // objectui#5373 — the ADR-0049 retirement of `CRUDSchema`. These two tests + // used to assert the OPPOSITE: that a well-formed `type: 'crud'` node was + // VALID, and that an ill-formed one was merely warned about. Both were + // affirmations for a type no renderer has ever registered, and they are the + // reason the retirement had to reach this file — deleting the old branch on + // its own would have left `crud` falling through `validateBaseSchema` to a + // silent `valid: true`, which is a quieter version of the same defect. + it('REFUSES `type: \'crud\'` by name — the spelling was retired, not merely unregistered', () => { const result = validateSchema({ type: 'crud', columns: [{ name: 'id', label: 'ID' }], api: '/api/users', }); - expect(result.valid).toBe(true); + // Not `valid === false` alone: a refusal that named nothing would satisfy + // that too, and so would validation breaking outright. + expect(result.valid).toBe(false); + const refusal = result.errors.find((e) => e.code === 'RETIRED_TYPE'); + expect(refusal).toBeDefined(); + expect(refusal!.path).toBe('schema.type'); + expect(refusal!.message).toContain('`crud` was RETIRED'); + expect(refusal!.message).toContain('object-grid'); }); - it('warns about CRUD without columns', () => { - const result = validateSchema({ type: 'crud' }); - const hasColumnsIssue = [...result.errors, ...result.warnings].some( - (e) => e.message.toLowerCase().includes('column'), - ); - expect(hasColumnsIssue).toBe(true); + it('refuses a retired spelling nested in children, with the child\'s own path', () => { + const result = validateSchema({ + type: 'grid', + children: [{ type: 'button', label: 'OK' }, { type: 'crud', columns: [] }], + }); + expect(result.valid).toBe(false); + expect(result.errors.map((e) => e.path)).toContain('schema.children[1].type'); + }); + + // COUNTER-PROBE for both refusals above. Without it, "crud is refused" is + // equally satisfied by this validator rejecting everything — a registered + // node type travelling the identical path must still come back valid. + it('still accepts a registered node type in the same run', () => { + const result = validateSchema({ + type: 'object-grid', + objectApiName: 'account', + }); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); }); it('validates form with fields', () => { diff --git a/packages/core/src/validation/schema-validator.ts b/packages/core/src/validation/schema-validator.ts index c3f43b4718..6f09ac68ad 100644 --- a/packages/core/src/validation/schema-validator.ts +++ b/packages/core/src/validation/schema-validator.ts @@ -127,61 +127,66 @@ function validateBaseSchema(schema: any, path: string = 'schema'): SchemaNodeVal } /** - * Validate CRUD schema specific properties + * TOMBSTONE table — node `type` spellings this renderer has RETIRED, mapped to + * the prescription an author must follow instead (ADR-0049 enforce-or-remove). + * + * A retired spelling is not merely absent from this file. Absence here means + * {@link validateSchema} runs `validateBaseSchema` — which only asks that + * `type` be a non-empty string — finds nothing type-specific to say, and + * returns `valid: true`. That silence is the failure mode this table exists to + * prevent: the author is told their schema is fine and then gets the OBJUI-001 + * "Unknown component type" panel at render time, one layer too late to act on. + * So a retired spelling is refused BY NAME, with the migration in the message. + * + * `crud` (objectui#5373, maintainer ruling of 2026-08-20): `CRUDSchema` carried + * FOUR declaration faces — the TS interface, the zod mirror, a dedicated branch + * in this very function, and `CRUDBuilder` — and never once had a registered + * renderer, for the whole life of the key. Every face taught an author (and an + * AI author reading the published reference page) that the type existed. The + * branch that used to sit here is what made this validator the loudest of the + * four lies: it read `schema.type === 'crud'`, checked that `columns` was an + * array, and returned no error — an affirmative "your CRUD schema is valid" + * for a node that renders nothing. + * + * Keyed by the authored spelling, and quantified over the table rather than + * written per spelling, so the next retirement closes this face the day it + * lands here. */ -function validateCRUDSchema(schema: any, path: string = 'schema'): SchemaNodeValidationError[] { - const errors: SchemaNodeValidationError[] = []; - - if (schema.type === 'crud') { - // Check required properties for CRUD - if (!schema.columns || !Array.isArray(schema.columns)) { - errors.push({ - path: `${path}.columns`, - message: 'CRUD schema requires columns array', - type: 'error', - code: 'MISSING_COLUMNS' - }); - } - - if (!schema.api && !schema.dataSource) { - errors.push({ - path: `${path}.api`, - message: 'CRUD schema requires api or dataSource', - type: 'warning', - code: 'MISSING_DATA_SOURCE' - }); - } - - // Validate columns - if (schema.columns && Array.isArray(schema.columns)) { - schema.columns.forEach((column: any, index: number) => { - if (!column.name) { - errors.push({ - path: `${path}.columns[${index}]`, - message: 'Column requires name property', - type: 'error', - code: 'MISSING_COLUMN_NAME' - }); - } - }); - } +const RETIRED_NODE_TYPES: Readonly> = Object.freeze({ + crud: + "Node type `crud` was RETIRED (objectui#5373, ADR-0049 enforce-or-remove). " + + "`CRUDSchema` declared it in four places and no renderer ever registered it, " + + "so a node spelling it painted the OBJUI-001 \"Unknown component type\" panel. " + + "Compose the shapes that DO render instead: `object-grid` for the record " + + "table with its toolbar, filters, pagination and row/batch actions, " + + "`object-form` for the create/edit form, and `detail` for the record view.", +}); - // Validate fields if present - if (schema.fields && Array.isArray(schema.fields)) { - schema.fields.forEach((field: any, index: number) => { - if (!field.name) { - errors.push({ - path: `${path}.fields[${index}]`, - message: 'Field requires name property', - type: 'error', - code: 'MISSING_FIELD_NAME' - }); - } - }); - } - } - - return errors; +/** + * Refuse a retired node-type spelling by name. + * + * Severity is `error`, not `warning`, and that is the whole point of the + * retirement: a warning leaves `result.valid === true`, so `assertValidSchema` + * would still not throw and `isValidSchema` would still answer `true` — the + * same silence, one console line louder. + * + * `hasOwnProperty` rather than a plain index read: a `type` of `'constructor'` + * or `'toString'` reaches `Object.prototype` and would answer truthy. + * + * Reached for every node in the tree, not just the root — {@link validateSchema} + * is what `validateChildren` recurses with, so a retired spelling nested inside + * a `children` array is refused with its own path. + */ +function validateRetiredNodeType(schema: any, path: string = 'schema'): SchemaNodeValidationError[] { + const type = schema?.type; + if (typeof type !== 'string') return []; + if (!Object.prototype.hasOwnProperty.call(RETIRED_NODE_TYPES, type)) return []; + return [{ + path: `${path}.type`, + message: RETIRED_NODE_TYPES[type], + type: 'error', + code: 'RETIRED_TYPE' + }]; } /** @@ -402,7 +407,7 @@ export function validateSchema( allErrors.push(...validateBaseSchema(schema, path)); // Validate type-specific schemas - allErrors.push(...validateCRUDSchema(schema, path)); + allErrors.push(...validateRetiredNodeType(schema, path)); allErrors.push(...validateFormSchema(schema, path)); // Validate children recursively diff --git a/packages/plugin-form/README.md b/packages/plugin-form/README.md index 0f29b28abc..3476c172c1 100644 --- a/packages/plugin-form/README.md +++ b/packages/plugin-form/README.md @@ -723,7 +723,7 @@ nothing: | Written on a form schema | What actually happens | |---|---| | `dataSource` | **Discarded.** The basic form strips it in both directions — `dataSource: _dataSource` at `form.tsx:304` (`stripRendererOnlyProps`) and `:2168` — so it never reaches a widget and never reaches the DOM. The adapter the fields receive is the context one | -| `resource` | **Never read.** It is not declared on `FormSchema` or `ObjectFormSchema` at all. The key exists elsewhere in the protocol — on **`CRUDSchema`** (`packages/types/src/crud.ts`, `type: 'crud'`), where `CRUDBuilder` in `@object-ui/core` sets it — but no form renderer reads it under any spelling. On a form it names nothing | +| `resource` | **Never read.** It is not declared on `FormSchema` or `ObjectFormSchema` at all. It used to exist elsewhere in the protocol — on `CRUDSchema`, where `CRUDBuilder` set it — but objectui#5373 retired both under ADR-0049, so today the key names nothing anywhere in this package's surface, and no form renderer reads it under any spelling | Both survive compilation for the reason [Schema API](#schema-api) gives: `FormSchema` and `ObjectFormSchema` extend `BaseSchema`, which declares `[key: string]: any`, so an diff --git a/packages/types/src/__tests__/crud-retirement-5373.test.ts b/packages/types/src/__tests__/crud-retirement-5373.test.ts new file mode 100644 index 0000000000..93ece704b0 --- /dev/null +++ b/packages/types/src/__tests__/crud-retirement-5373.test.ts @@ -0,0 +1,70 @@ +/** + * 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. + */ + +/** + * The ADR-0049 retirement of `CRUDSchema` / `type: 'crud'` (objectui#5373, + * maintainer ruling of 2026-08-20), pinned at the two faces this package owns. + * + * Why these assertions and not a grep: `crud` had FOUR declaration faces and + * zero registered renderers for the whole life of the key, so every check that + * could have caught it was written against a face that agreed with the other + * three. The two pins here are chosen for one property — each DISTINGUISHES the + * two states of the world, i.e. each goes red if the retirement is reverted: + * + * - the zod union `CRUDComponentSchema` REFUSES a `crud` node. Restore the + * `CRUDSchema` mirror as a union member and `safeParse` succeeds again. + * - the zod barrel no longer EXPORTS the mirror or its four sub-shapes. + * Restore them and the `not.toHaveProperty` assertions fail. + * + * Every negative carries a control drawn from the same object in the same run — + * `detail` must still parse, `DetailSchema` / `CRUDDialogSchema` must still be + * exported — because "crud is refused" is otherwise equally satisfied by the + * union being broken outright or the barrel failing to load. + * + * The TS interface face cannot be pinned at runtime (types are erased) and is + * covered by `tsc`: `packages/types` type-checks, and no `CRUDSchema` import + * survives anywhere in the workspace. The validator face is pinned in + * `@object-ui/core`'s `schema-validator.test.ts`, and the builder face in its + * `schema-builder.test.ts`. + */ +import { describe, it, expect } from 'vitest'; + +import * as zodBarrel from '../zod/index.zod.js'; +import { CRUDComponentSchema } from '../zod/crud.zod.js'; + +describe('CRUDSchema retirement (objectui#5373, ADR-0049 enforce-or-remove)', () => { + it('the CRUD union refuses a `crud` node and still accepts a `detail` one', () => { + const authoredCrud = { + type: 'crud', + title: 'Products', + resource: 'products', + api: '/api/products', + columns: [{ name: 'name', label: 'Product Name' }], + }; + expect(CRUDComponentSchema.safeParse(authoredCrud).success).toBe(false); + // Control, same union, same run: a surviving member still parses, so the + // refusal above is a verdict rather than a union that rejects everything. + expect(CRUDComponentSchema.safeParse({ type: 'detail', title: 'Account' }).success).toBe(true); + }); + + it('the zod barrel exports neither the mirror nor its four sub-shapes', () => { + for (const gone of [ + 'CRUDSchema', + 'CRUDOperationSchema', + 'CRUDFilterSchema', + 'CRUDToolbarSchema', + 'CRUDPaginationSchema', + ]) { + expect(zodBarrel).not.toHaveProperty(gone); + } + // Controls: the CRUD module still exists and still exports its survivors. + expect(zodBarrel).toHaveProperty('DetailSchema'); + expect(zodBarrel).toHaveProperty('CRUDDialogSchema'); + expect(zodBarrel).toHaveProperty('CRUDComponentSchema'); + }); +}); diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index 5af90c03c0..3401984da0 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -75,7 +75,7 @@ import { AppActionSchema, AppComponentSchema, NavigationAreaSchema } from '../zo import { BaseSchema, ComponentConfigSchema, ComponentInputSchema, ComponentMetaSchema, KeyedI18nLabelSchema } from '../zod/base.zod.js'; import { BlockEditorSchema, BlockInstanceSchema, BlockLibraryItemSchema, BlockLibrarySchema, BlockMetadataSchema, BlockSchema, BlockSlotSchema, BlockVariableSchema, ComponentSchema } from '../zod/blocks.zod.js'; import { CalendarEventSchema, CalendarViewSchema, CarouselItemSchema, CarouselSchema, ChatbotSchema, ChatMessageSchema, ChatMessageSourceSchema, ChatToolInvocationSchema, DashboardComponentSchema, DashboardConfigSchema, DashboardWidgetConfigSchema, DashboardWidgetLayoutSchema, DashboardWidgetSchema, FilterBuilderSchema, FilterFieldSchema, KanbanCardSchema, KanbanColumnSchema, KanbanSchema } from '../zod/complex.zod.js'; -import { ActionCallbackSchema, CRUDDialogSchema, CRUDFilterSchema, CRUDOperationSchema, CRUDPaginationSchema, CRUDSchema, CRUDToolbarSchema, DetailSchema } from '../zod/crud.zod.js'; +import { ActionCallbackSchema, CRUDDialogSchema, DetailSchema } from '../zod/crud.zod.js'; import { AlertSchema, AvatarSchema, BadgeSchema, ChartDataSeriesSchema, ChartSchema, DataTableSchema, HtmlSchema, KbdSchema, ListItemSchema, ListSchema, MarkdownSchema, StaticTableColumnSchema, StatisticSchema, TableColumnSchema, TableSchema, TimelineEventSchema, TimelineSchema, TreeViewSchema } from '../zod/data-display.zod.js'; import { AccordionItemSchema, AccordionSchema, CollapsibleSchema, ToggleGroupItemSchema, ToggleGroupSchema } from '../zod/disclosure.zod.js'; import { EmptySchema, LoadingSchema, ProgressSchema, SkeletonSchema, SonnerSchema, SpinnerSchema, ToasterSchema, ToastSchema } from '../zod/feedback.zod.js'; @@ -92,7 +92,7 @@ import type { BaseSchema as Ts_BaseSchema, ComponentConfig as Ts_ComponentConfig import type { BlockEditorSchema as Ts_BlockEditorSchema, BlockInstanceSchema as Ts_BlockInstanceSchema, BlockLibraryItem as Ts_BlockLibraryItem, BlockLibrarySchema as Ts_BlockLibrarySchema, BlockMetadata as Ts_BlockMetadata, BlockSchema as Ts_BlockSchema, BlockSlot as Ts_BlockSlot, BlockVariable as Ts_BlockVariable, ComponentSchema as Ts_ComponentSchema } from '../blocks'; import type { CalendarEvent as Ts_CalendarEvent, CalendarViewSchema as Ts_CalendarViewSchema, CarouselItem as Ts_CarouselItem, CarouselSchema as Ts_CarouselSchema, ChatbotSchema as Ts_ChatbotSchema, ChatMessage as Ts_ChatMessage, ChatMessageSource as Ts_ChatMessageSource, ChatToolInvocation as Ts_ChatToolInvocation, DashboardComponentSchema as Ts_DashboardComponentSchema, DashboardWidgetLayout as Ts_DashboardWidgetLayout, DashboardWidgetSchema as Ts_DashboardWidgetSchema, FilterBuilderSchema as Ts_FilterBuilderSchema, FilterField as Ts_FilterField, KanbanCard as Ts_KanbanCard, KanbanColumn as Ts_KanbanColumn, KanbanSchema as Ts_KanbanSchema } from '../complex'; import type { DashboardConfig as Ts_DashboardConfig, DashboardWidgetConfig as Ts_DashboardWidgetConfig } from '../designer'; -import type { ActionCallback as Ts_ActionCallback, CRUDDialogSchema as Ts_CRUDDialogSchema, CRUDFilter as Ts_CRUDFilter, CRUDOperation as Ts_CRUDOperation, CRUDPagination as Ts_CRUDPagination, CRUDSchema as Ts_CRUDSchema, CRUDToolbar as Ts_CRUDToolbar, DetailSchema as Ts_DetailSchema } from '../crud'; +import type { ActionCallback as Ts_ActionCallback, CRUDDialogSchema as Ts_CRUDDialogSchema, DetailSchema as Ts_DetailSchema } from '../crud'; import type { AlertSchema as Ts_AlertSchema, AvatarSchema as Ts_AvatarSchema, BadgeSchema as Ts_BadgeSchema, ChartDataSeries as Ts_ChartDataSeries, ChartSchema as Ts_ChartSchema, DataTableSchema as Ts_DataTableSchema, HtmlSchema as Ts_HtmlSchema, KbdSchema as Ts_KbdSchema, ListItem as Ts_ListItem, ListSchema as Ts_ListSchema, MarkdownSchema as Ts_MarkdownSchema, StaticTableColumn as Ts_StaticTableColumn, StatisticSchema as Ts_StatisticSchema, TableColumn as Ts_TableColumn, TableSchema as Ts_TableSchema, TimelineEvent as Ts_TimelineEvent, TimelineSchema as Ts_TimelineSchema, TreeViewSchema as Ts_TreeViewSchema, BreadcrumbItem as Ts_BreadcrumbItem, BreadcrumbSchema as Ts_BreadcrumbSchema } from '../data-display'; import type { AccordionItem as Ts_AccordionItem, AccordionSchema as Ts_AccordionSchema, CollapsibleSchema as Ts_CollapsibleSchema, ToggleGroupItem as Ts_ToggleGroupItem, ToggleGroupSchema as Ts_ToggleGroupSchema } from '../disclosure'; import type { EmptySchema as Ts_EmptySchema, LoadingSchema as Ts_LoadingSchema, ProgressSchema as Ts_ProgressSchema, SkeletonSchema as Ts_SkeletonSchema, SonnerSchema as Ts_SonnerSchema, SpinnerSchema as Ts_SpinnerSchema, ToasterSchema as Ts_ToasterSchema, ToastSchema as Ts_ToastSchema } from '../feedback'; @@ -176,11 +176,6 @@ const MIRRORS = { 'complex.zod.ts#KanbanSchema': KanbanSchema, 'crud.zod.ts#ActionCallbackSchema': ActionCallbackSchema, 'crud.zod.ts#CRUDDialogSchema': CRUDDialogSchema, - 'crud.zod.ts#CRUDFilterSchema': CRUDFilterSchema, - 'crud.zod.ts#CRUDOperationSchema': CRUDOperationSchema, - 'crud.zod.ts#CRUDPaginationSchema': CRUDPaginationSchema, - 'crud.zod.ts#CRUDSchema': CRUDSchema, - 'crud.zod.ts#CRUDToolbarSchema': CRUDToolbarSchema, 'crud.zod.ts#DetailSchema': DetailSchema, 'data-display.zod.ts#AlertSchema': AlertSchema, 'data-display.zod.ts#AvatarSchema': AvatarSchema, @@ -343,11 +338,6 @@ interface Declared { 'complex.zod.ts#KanbanSchema': Ts_KanbanSchema; 'crud.zod.ts#ActionCallbackSchema': Ts_ActionCallback; 'crud.zod.ts#CRUDDialogSchema': Ts_CRUDDialogSchema; - 'crud.zod.ts#CRUDFilterSchema': Ts_CRUDFilter; - 'crud.zod.ts#CRUDOperationSchema': Ts_CRUDOperation; - 'crud.zod.ts#CRUDPaginationSchema': Ts_CRUDPagination; - 'crud.zod.ts#CRUDSchema': Ts_CRUDSchema; - 'crud.zod.ts#CRUDToolbarSchema': Ts_CRUDToolbar; 'crud.zod.ts#DetailSchema': Ts_DetailSchema; 'data-display.zod.ts#AlertSchema': Ts_AlertSchema; 'data-display.zod.ts#AvatarSchema': Ts_AvatarSchema; @@ -505,8 +495,6 @@ interface KnownDrift { 'complex.zod.ts#FilterBuilderSchema': 'fields'; /** DISJOINT vocabularies: TS declares `is_empty`/`is_not_empty`, the mirror declares `is_null`/`is_not_null`. One of the two is dead; which one is a ruling. */ 'complex.zod.ts#FilterFieldSchema': 'operators'; - /** TS declares an index signature whose value type includes `undefined`; the mirror`s `z.record` value type does not. The mirror refuses `{ create: undefined }` only. */ - 'crud.zod.ts#CRUDSchema': 'operations'; /** DISJOINT: TS declares `rowActions?: boolean` (show the column or not), the mirror declares `any[]` (the actions themselves). One of the two is dead; which is a ruling. (`selectable` was a second drifted key here until objectui#5927 widened the mirror to `boolean | 'single' | 'multiple'` — `resolveSelectionMode` in `renderers/complex/data-table.tsx` implements `'single'` as a real mode.) */ 'data-display.zod.ts#DataTableSchema': 'rowActions'; /** DISJOINT: TS `Date | Date[]`, mirror `string | Date`. The mirror refuses `Date[]`; the TS side refuses the ISO string the mirror accepts. */ diff --git a/packages/types/src/crud.ts b/packages/types/src/crud.ts index 97c848c44e..86d04b9b3c 100644 --- a/packages/types/src/crud.ts +++ b/packages/types/src/crud.ts @@ -17,8 +17,6 @@ */ import type { BaseSchema, SchemaNode } from './base.js'; -import type { FormField } from './form.js'; -import type { TableColumn } from './data-display.js'; /** * Action execution mode for chaining @@ -254,272 +252,6 @@ export interface ActionSchema extends BaseSchema { }; } -/** - * CRUD operation configuration - */ -export interface CRUDOperation { - /** - * Operation type - */ - type: 'create' | 'read' | 'update' | 'delete' | 'export' | 'import' | 'custom'; - /** - * Operation label - */ - label?: string; - /** - * Operation icon - */ - icon?: string; - /** - * Whether operation is enabled - * @default true - */ - enabled?: boolean; - /** - * API endpoint for this operation - */ - api?: string; - /** - * HTTP method - */ - method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; - /** - * Confirmation message - */ - confirmText?: string; - /** - * Success message - */ - successMessage?: string; - /** - * Visibility condition - */ - visibleOn?: string; - /** - * Disabled condition - */ - disabledOn?: string; -} - -/** - * Filter configuration for CRUD components - */ -export interface CRUDFilter { - /** - * Filter name (field name) - */ - name: string; - /** - * Filter label - */ - label?: string; - /** - * Filter type - */ - type?: 'input' | 'select' | 'date-picker' | 'date-range' | 'number-range'; - /** - * Filter operator - * @default 'equals' - */ - operator?: 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'gt' | 'gte' | 'lt' | 'lte' | 'between' | 'in'; - /** - * Options for select filter - */ - options?: Array<{ label: string; value: string | number }>; - /** - * Placeholder text - */ - placeholder?: string; - /** - * Default value - */ - defaultValue?: any; -} - -/** - * Toolbar configuration for CRUD components - */ -export interface CRUDToolbar { - /** - * Show create button - * @default true - */ - showCreate?: boolean; - /** - * Show refresh button - * @default true - */ - showRefresh?: boolean; - /** - * Show export button - * @default false - */ - showExport?: boolean; - /** - * Show import button - * @default false - */ - showImport?: boolean; - /** - * Show filter toggle - * @default true - */ - showFilter?: boolean; - /** - * Show search box - * @default true - */ - showSearch?: boolean; - /** - * Custom actions - */ - actions?: ActionSchema[]; -} - -/** - * CRUD pagination configuration - */ -export interface CRUDPagination { - /** - * Whether pagination is enabled - * @default true - */ - enabled?: boolean; - /** - * Default page size - * @default 10 - */ - pageSize?: number; - /** - * Page size options - * @default [10, 20, 50, 100] - */ - pageSizeOptions?: number[]; - /** - * Show total count - * @default true - */ - showTotal?: boolean; - /** - * Show page size selector - * @default true - */ - showSizeChanger?: boolean; -} - -/** - * Complete CRUD component - * Provides full Create, Read, Update, Delete functionality - */ -export interface CRUDSchema extends BaseSchema { - type: 'crud'; - /** - * CRUD title - */ - title?: string; - /** - * Resource name (singular) - * @example 'user', 'product', 'order' - */ - resource?: string; - /** - * API endpoint for list/search - */ - api?: string; - /** - * Table columns configuration - */ - columns: TableColumn[]; - /** - * Form fields for create/edit - */ - fields?: FormField[]; - /** - * Enabled operations - */ - operations?: { - create?: boolean | CRUDOperation; - read?: boolean | CRUDOperation; - update?: boolean | CRUDOperation; - delete?: boolean | CRUDOperation; - export?: boolean | CRUDOperation; - import?: boolean | CRUDOperation; - [key: string]: boolean | CRUDOperation | undefined; - }; - /** - * Toolbar configuration - */ - toolbar?: CRUDToolbar; - /** - * Filter configuration - */ - filters?: CRUDFilter[]; - /** - * Pagination configuration - */ - pagination?: CRUDPagination; - /** - * Default sort field - */ - defaultSort?: string; - /** - * Default sort order - * @default 'asc' - */ - defaultSortOrder?: 'asc' | 'desc'; - /** - * Row selection mode - */ - selectable?: boolean | 'single' | 'multiple'; - /** - * Batch actions for selected rows - */ - batchActions?: ActionSchema[]; - /** - * Row actions (displayed in each row) - */ - rowActions?: ActionSchema[]; - /** - * Custom empty state - */ - emptyState?: SchemaNode; - /** - * Whether to show loading state - * @default true - */ - loading?: boolean; - /** - * Custom loading component - */ - loadingComponent?: SchemaNode; - /** - * Table layout mode - * @default 'table' - */ - mode?: 'table' | 'grid' | 'list' | 'kanban'; - /** - * Grid columns (for grid mode) - * @default 3 - */ - gridColumns?: number; - /** - * Card template (for grid/list mode) - */ - cardTemplate?: SchemaNode; - /** - * Kanban columns (for kanban mode) - */ - kanbanColumns?: Array<{ - id: string; - title: string; - color?: string; - }>; - /** - * Kanban group field - */ - kanbanGroupField?: string; -} - /** * Detail view component * Displays detailed information about a single record @@ -637,6 +369,5 @@ export interface CRUDDialogSchema extends BaseSchema { */ export type CRUDComponentSchema = | ActionSchema - | CRUDSchema | DetailSchema | CRUDDialogSchema; diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index 9c679ff3b2..939176ba14 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -286,7 +286,7 @@ export interface TableColumn { * split the types). * * {@link TableColumn} above remains the rich shared shape that `data-table` - * honours (`DataTableSchema`, `CRUDSchema`, detail-view relations) — it is + * honours (`DataTableSchema`, detail-view relations) — it is * deliberately NOT narrowed. The static renderer * (`packages/components/src/renderers/complex/table.tsx`) reads exactly five * column keys: `header`, `accessorKey`, `className`, `cellClassName`, `width` diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b5815f0e84..79062b8e51 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -354,11 +354,6 @@ export type { // ============================================================================ export type { ActionSchema, - CRUDOperation, - CRUDFilter, - CRUDToolbar, - CRUDPagination, - CRUDSchema, DetailSchema, CRUDDialogSchema, CRUDComponentSchema, diff --git a/packages/types/src/zod/crud.zod.ts b/packages/types/src/zod/crud.zod.ts index d3bdf3ee9a..6a0519bdd1 100644 --- a/packages/types/src/zod/crud.zod.ts +++ b/packages/types/src/zod/crud.zod.ts @@ -113,92 +113,6 @@ export const ActionSchema: z.ZodType = z.lazy(() => BaseSchema.extend({ }).optional().describe('Retry configuration'), })); -/** - * CRUD Operation Schema - */ -export const CRUDOperationSchema = z.object({ - type: z.enum(['create', 'read', 'update', 'delete', 'export', 'import', 'custom']).describe('Operation type'), - label: z.string().optional().describe('Operation label'), - icon: z.string().optional().describe('Operation icon'), - enabled: z.boolean().optional().default(true).describe('Whether operation is enabled'), - api: z.string().optional().describe('API endpoint for this operation'), - method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().describe('HTTP method'), - confirmText: z.string().optional().describe('Confirmation message'), - successMessage: z.string().optional().describe('Success message'), - visibleOn: z.string().optional().describe('Visibility condition'), - disabledOn: z.string().optional().describe('Disabled condition'), -}); - -/** - * CRUD Filter Schema - */ -export const CRUDFilterSchema = z.object({ - name: z.string().describe('Filter name (field name)'), - label: z.string().optional().describe('Filter label'), - type: z.enum(['input', 'select', 'date-picker', 'date-range', 'number-range']).optional().describe('Filter type'), - operator: z.enum(['equals', 'contains', 'startsWith', 'endsWith', 'gt', 'gte', 'lt', 'lte', 'between', 'in']).optional().default('equals').describe('Filter operator'), - options: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.number()]) })).optional().describe('Options for select filter'), - placeholder: z.string().optional().describe('Placeholder text'), - defaultValue: z.any().optional().describe('Default value'), -}); - -/** - * CRUD Toolbar Schema - */ -export const CRUDToolbarSchema = z.object({ - showCreate: z.boolean().optional().default(true).describe('Show create button'), - showRefresh: z.boolean().optional().default(true).describe('Show refresh button'), - showExport: z.boolean().optional().default(false).describe('Show export button'), - showImport: z.boolean().optional().default(false).describe('Show import button'), - showFilter: z.boolean().optional().default(true).describe('Show filter toggle'), - showSearch: z.boolean().optional().default(true).describe('Show search box'), - actions: z.array(ActionSchema).optional().describe('Custom actions'), -}); - -/** - * CRUD Pagination Schema - */ -export const CRUDPaginationSchema = z.object({ - enabled: z.boolean().optional().default(true).describe('Whether pagination is enabled'), - pageSize: z.number().optional().default(10).describe('Default page size'), - pageSizeOptions: z.array(z.number()).optional().default([10, 20, 50, 100]).describe('Page size options'), - showTotal: z.boolean().optional().default(true).describe('Show total count'), - showSizeChanger: z.boolean().optional().default(true).describe('Show page size selector'), -}); - -/** - * CRUD Schema - */ -export const CRUDSchema = BaseSchema.extend({ - type: z.literal('crud'), - title: z.string().optional().describe('CRUD title'), - resource: z.string().optional().describe('Resource name (singular)'), - api: z.string().optional().describe('API endpoint for list/search'), - columns: z.array(z.any()).describe('Table columns configuration'), - fields: z.array(z.any()).optional().describe('Form fields for create/edit'), - operations: z.record(z.string(), z.union([z.boolean(), CRUDOperationSchema])).optional().describe('Enabled operations'), - toolbar: CRUDToolbarSchema.optional().describe('Toolbar configuration'), - filters: z.array(CRUDFilterSchema).optional().describe('Filter configuration'), - pagination: CRUDPaginationSchema.optional().describe('Pagination configuration'), - defaultSort: z.string().optional().describe('Default sort field'), - defaultSortOrder: z.enum(['asc', 'desc']).optional().default('asc').describe('Default sort order'), - selectable: z.union([z.boolean(), z.enum(['single', 'multiple'])]).optional().describe('Row selection mode'), - batchActions: z.array(ActionSchema).optional().describe('Batch actions for selected rows'), - rowActions: z.array(ActionSchema).optional().describe('Row actions (displayed in each row)'), - emptyState: SchemaNodeSchema.optional().describe('Custom empty state'), - loading: z.boolean().optional().default(true).describe('Whether to show loading state'), - loadingComponent: SchemaNodeSchema.optional().describe('Custom loading component'), - mode: z.enum(['table', 'grid', 'list', 'kanban']).optional().default('table').describe('Table layout mode'), - gridColumns: z.number().optional().default(3).describe('Grid columns (for grid mode)'), - cardTemplate: SchemaNodeSchema.optional().describe('Card template (for grid/list mode)'), - kanbanColumns: z.array(z.object({ - id: z.string(), - title: z.string(), - color: z.string().optional(), - })).optional().describe('Kanban columns (for kanban mode)'), - kanbanGroupField: z.string().optional().describe('Kanban group field'), -}); - /** * Detail Schema */ @@ -251,7 +165,6 @@ export const CRUDDialogSchema = BaseSchema.extend({ */ export const CRUDComponentSchema = z.union([ ActionSchema, - CRUDSchema, DetailSchema, CRUDDialogSchema, ]); @@ -262,10 +175,5 @@ export const CRUDComponentSchema = z.union([ export type ActionExecutionModeSchemaType = z.infer; export type ActionCallbackSchemaType = z.infer; export type ActionSchemaType = z.infer; -export type CRUDOperationSchemaType = z.infer; -export type CRUDFilterSchemaType = z.infer; -export type CRUDToolbarSchemaType = z.infer; -export type CRUDPaginationSchemaType = z.infer; -export type CRUDSchemaType = z.infer; export type DetailSchemaType = z.infer; export type CRUDDialogSchemaType = z.infer; diff --git a/packages/types/src/zod/index.zod.ts b/packages/types/src/zod/index.zod.ts index 4f35b1205a..b4db391dea 100644 --- a/packages/types/src/zod/index.zod.ts +++ b/packages/types/src/zod/index.zod.ts @@ -271,11 +271,6 @@ export { ActionExecutionModeSchema, ActionCallbackSchema, ActionSchema, - CRUDOperationSchema, - CRUDFilterSchema, - CRUDToolbarSchema, - CRUDPaginationSchema, - CRUDSchema, DetailSchema, CRUDDialogSchema, CRUDComponentSchema, diff --git a/scripts/__tests__/known-schema-types-derivation-5115.test.ts b/scripts/__tests__/known-schema-types-derivation-5115.test.ts index c9a21645fa..752d19231a 100644 --- a/scripts/__tests__/known-schema-types-derivation-5115.test.ts +++ b/scripts/__tests__/known-schema-types-derivation-5115.test.ts @@ -79,11 +79,12 @@ describe('KNOWN_SCHEMA_TYPES equals the registered universe', () => { }); describe('the two drifted types objectui#5115 was filed for', () => { - it('rejects `crud` — four declaration faces, no renderer', () => { - // `CRUDSchema` still has its interface, zod mirror, validator branch and - // builder. What it has never had is a registration, so the CLI must not - // claim the type exists. Whether CRUDSchema itself should survive is a - // separate question, deliberately untouched here. + it('rejects `crud` — retired under ADR-0049, and never registered before that', () => { + // `CRUDSchema` had an interface, a zod mirror, a validator branch and a + // builder when objectui#5115 was filed. What it never had is a + // registration, so the CLI must not claim the type exists. objectui#5373 + // resolved the open question this comment used to defer — the maintainer + // ruled retirement, and all four declaration faces are gone. expect(derived.keys.has('crud')).toBe(false); expect(isKnownSchemaType('crud')).toBe(false); }); diff --git a/scripts/check-doc-component-types.mjs b/scripts/check-doc-component-types.mjs index b64e095381..4e9bccde12 100644 --- a/scripts/check-doc-component-types.mjs +++ b/scripts/check-doc-component-types.mjs @@ -239,22 +239,14 @@ const OPEN_REGISTRATION_SITES = { const DOC_TYPE_EXEMPTIONS = { 'content/docs/api/schema-reference.md': { action: - 'ActionSchema discriminant under a CRUD schema\'s ACTION LISTS, never a rendered child — ' + - '`toolbar.actions[]`, `rowActions[]`, `batchActions[]` and a detail page\'s `actions[]` are ' + - 'each typed `ActionSchema[]` (packages/types/src/crud.ts:175, 379, 480, 484, 561, 612), and ' + - 'that interface declares `type: \'action\'` at crud.ts:89. Same vocabulary as the ' + - '`core/enhanced-actions.mdx` entry below.', - crud: - 'CRUDSchema discriminant — packages/types/src/crud.ts:418 declares it, zod/crud.zod.ts:158 ' + - 'validates it, core/src/validation/schema-validator.ts:135 has a branch for it and ' + - 'builder/schema-builder.ts:170 constructs it. FOUR declaration faces and NO registered ' + - 'renderer, and unlike its siblings in this table it IS on the render path (CRUDComponentSchema ' + - 'is in the node union at types/src/index.ts:852), so a node spelling it paints OBJUI-001. ' + - 'Ledgered rather than re-spelled because there is no registered spelling to move to: register ' + - 'a renderer / retire CRUDSchema under ADR-0049 / demote it off the node union are three ' + - 'different edits to this page, and picking one is a contract decision objectui#5115 left open ' + - 'after PR objectui#5128 closed only its CLI half. Filed as objectui#5373. DELETE this entry ' + - 'when that lands — the gate reports a stale exemption, so it cannot be forgotten.', + 'ActionSchema discriminant under an ACTION LIST, never a rendered child — an action\'s own ' + + '`dialog.actions[]` and `chain[]`, a detail page\'s `actions[]` and a CRUD dialog\'s ' + + '`actions[]` are each typed `ActionSchema[]` (packages/types/src/crud.ts:154, 175, 290, 341), ' + + 'and that interface declares `type: \'action\'` at crud.ts:68. Same vocabulary as the ' + + '`core/enhanced-actions.mdx` entry below. (This reason used to cite `CRUDSchema`\'s ' + + '`toolbar.actions[]` / `rowActions[]` / `batchActions[]` as the carriers; those keys were ' + + 'retired with `CRUDSchema` in objectui#5373, and the sites this exemption covers on the page ' + + 'now sit under ActionSchema and DetailSchema.)', string: 'PageNodeSchema variable declaration\'s data type inside `variables[]`, next to `name` / ' + '`defaultValue` — `PageVariable` (packages/types/src/layout.ts:566, re-exported from ' +