import { SchemaBase, Field } from './dsl.js'; import { TableSchema } from './db.js'; import { FrontAppSchema } from './project.js'; import { PageActionSchema } from './page-action.js'; import type { FilterSchema } from './filter.js'; import { toKebabCase } from '@pylonts/core'; // Admin-only CRUD page standard: binds one entity table to a frontend admin // app, describing everything needed to generate the list page plus optional // add/update/detail pages. Field-level columns are plain Field instances // (table fields, cross-table refs allowed) — DTOs are derived by the generator, // the schema itself never references DtoMessage. /** Mode of an action page: modal dialog or standalone route. */ export type ActionPageMode = 'modal' | 'route'; /** One CRUD action page (add / update / detail). */ export interface ActionPage { mode: ActionPageMode; /** Fields rendered on this page. Required, non-empty — every field the * frontend shows must be listed explicitly. */ columns: Field[]; } /** List page configuration. */ export interface CurdListConfig { /** List columns; required, non-empty. Every field the list shows must be * listed explicitly. May include cross-table fields via foreign refs. */ columns: Field[]; /** Page filter (search form + keyword search). Conditions declared on the * filter render the search form; the filter's keyword (when present) * drives the keyword query endpoint. Optional — a page without a filter * has no search form. */ filter?: FilterSchema; /** Default sort. Required — column and direction are both mandatory. */ orderBy: { column: Field; direction: 'asc' | 'desc' }; /** Column header text overrides: Field.name → header text. */ columnTitles?: Record; } /** Admin-only CRUD page standard: binds one entity table to a frontend * admin app. Drives generation of the list page plus optional * add/update/detail pages. */ export interface CurdSchema extends SchemaBase { /** The admin frontend app this CRUD belongs to (shared instance, type must be 'admin'). */ app: FrontAppSchema; /** The bound entity table (shared instance). */ table: TableSchema; /** List page Chinese title. */ title: string; /** Sidebar menu section (group) this CRUD page belongs to. */ section: string; /** Extra user actions on this page (beyond the standard CRUD). */ actions?: PageActionSchema[]; /** Add/update/detail action pages. */ actionPages?: { add?: ActionPage; update?: ActionPage; detail?: ActionPage; }; list: CurdListConfig; } function assertColumns(curd: CurdSchema, pageName: string, columns: Field[]): void { if (columns.length === 0) { throw new Error(`curd ${curd.name}: ${pageName}.columns must be non-empty`); } } function assertFieldsOwnTable(curd: CurdSchema, label: string, fields: Field[]): void { for (const f of fields) { if (f.schema !== curd.table) { throw new Error(`curd ${curd.name}: ${label} field ${f.name} does not belong to table ${curd.table.name}`); } } } /** Defines an admin CRUD page standard. Runtime-validates admin app binding, * non-empty columns and table field ownership (same style as defineTable). */ export function defineCurd(name: string, schema: Omit): CurdSchema { const curd: CurdSchema = { name, ...schema }; if (curd.app.type !== 'admin') { throw new Error(`curd ${name}: app ${curd.app.name} must be type 'admin' (got '${curd.app.type}')`); } // The name is the admin route path — it must be the kebab-case table name // so paths cannot drift from the table they serve. const expectedName = toKebabCase(curd.table.name); if (name !== expectedName) { throw new Error(`curd name must be '${expectedName}' (kebab-case of table '${curd.table.name}'); got '${name}'`); } if (!curd.section) { throw new Error(`curd ${name}: section is required (sidebar menu group, e.g. '商户管理')`); } assertColumns(curd, 'list', curd.list.columns); for (const [pageName, page] of Object.entries(curd.actionPages ?? {})) { if (page) assertColumns(curd, `actionPages.${pageName}`, page.columns); } if (curd.list.filter !== undefined && curd.list.filter.app !== curd.app) { throw new Error( `curd ${name}: list.filter '${curd.list.filter.name}' is bound to ${curd.list.filter.app?.name ?? 'common'} but the curd belongs to app '${curd.app.name}'`, ); } assertFieldsOwnTable(curd, 'orderBy', [curd.list.orderBy.column]); return curd; }