UNPKG

@scenemesh/entity-engine

Version:

一个“元数据驱动 + 组件适配 + 动态关系 + 视图管线”式的实体引擎。以 **Model + View + FieldType + SuiteAdapter + DataSource** 为五大支点,统一 CRUD / 查询 / 引用管理 / 视图渲染 / 扩展注册,支持在运行期无侵入拼装出 **表单、网格、主从、看板、仪表盘、流程/树形视图** 等多形态界面。

1 lines 348 kB
{"version":3,"sources":["../src/modules/build-in/module.json","../src/modules/build-in/config/config.ts","../src/modules/build-in/buildin.module.ts","../src/core/types/session.types.ts","../src/core/delegate/delegate.ts","../src/core/delegate/meta.serializer.ts","../src/core/delegate/delegate.registry.ts","../src/services/api/trpc/vanilla-client.ts","../src/lib/data/data-utils.ts","../src/core/datasources/trpc.datasource.ts","../src/core/datasources/datasource.factory.ts","../src/services/api/services/model.service.ts","../src/services/api/routers/utils/enhanced-query.ts","../src/services/api/services/mode.external.service.ts","../src/services/api/trpc.ts","../src/services/database/db.ts","../package.json","../src/core/event/event.registry.ts","../src/core/watcher/watcher.ts","../src/core/action/action.registry.ts","../src/lib/query/query.client.ts","../src/core/session/session.provider.ts","../src/core/session/session.manager.ts","../src/core/servlet/servlet.registry.ts","../src/build-ins/fieldtypes/basic.types.ts","../src/build-ins/fieldtypes/common.types.ts","../src/core/module/module.registry.ts","../src/core/engine/engine.initializer.ts","../src/core/fieldtypes/fieldtype.registry.ts","../src/core/engine/engine.primitive.ts","../src/services/api/routers/entity-model.ts","../src/services/api/routers/entity-service.ts","../src/services/api/root.ts","../src/services/api/trpc/trcp.handler.ts","../src/services/api/auth/auth.handler.ts","../src/services/api/utils/upload/upload.handler.ts","../src/services/api/services/model.context.ts","../src/services/api/services/model.handler.ts","../src/services/entrance/entrance.handler.ts"],"sourcesContent":["{\n \"name\": \"build-in-module\",\n \"version\": \"1.0.0\",\n \"description\": \"内建模块\",\n \"provider\": \"Scenemesh Team\",\n \"dependencies\": [],\n \"url\": \"http://github.com/scenemesh\"\n}","import type { IEntityView, IEntityModel } from '../../../types';\n\nconst models: IEntityModel[] = [\n {\n name: '__default__',\n title: '默认模型',\n description: 'Default Model',\n fields: [],\n },\n {\n name: '__config__',\n title: '配置模型',\n description: 'Config Model',\n fields: [\n {\n name: 'name',\n title: '配置名称',\n description: '配置名称',\n type: 'string',\n isRequired: true,\n searchable: true,\n },\n {\n name: 'version',\n title: '配置版本',\n description: '配置版本',\n type: 'number',\n isRequired: true,\n searchable: false,\n },\n {\n name: 'content',\n title: '配置内容',\n description: '配置内容',\n type: 'string',\n isRequired: true,\n searchable: false,\n },\n ],\n },\n {\n name: 'entity-change-log',\n title: '实体变更记录',\n description: 'Entity Change Log',\n fields: [\n {\n name: 'modelName',\n title: 'Model Name',\n description: 'Model Name',\n type: 'string',\n isRequired: true,\n searchable: true,\n },\n {\n name: 'objectId',\n title: 'Object ID',\n description: 'Object ID',\n type: 'string',\n isRequired: true,\n searchable: true,\n },\n {\n name: 'changedBy',\n title: 'Changed By',\n description: 'Changed By',\n type: 'string',\n isRequired: true,\n searchable: true,\n },\n {\n name: 'changeType',\n title: 'Change Type',\n description: 'Change Type',\n type: 'string',\n isRequired: true,\n searchable: true,\n },\n {\n name: 'changeDetails',\n title: 'Change Details',\n description: 'Change Details',\n type: 'json',\n isRequired: false,\n searchable: false,\n },\n ],\n },\n {\n name: 'ee-base-user',\n title: '用户模型',\n description: 'Entity User Model',\n fields: [\n {\n name: 'userName',\n title: '用户名称',\n description: '用户名称',\n type: 'string',\n isRequired: true,\n searchable: true,\n },\n {\n name: 'email',\n title: '邮箱',\n description: '邮箱地址',\n type: 'string',\n isRequired: true,\n searchable: true,\n },\n {\n name: 'password',\n title: '登录密码',\n description: '登录密码',\n type: 'string',\n isRequired: true,\n },\n {\n name: 'avatar',\n title: '头像',\n description: '头像',\n type: 'binary',\n isRequired: false,\n searchable: false,\n },\n ],\n },\n];\n\nconst views: IEntityView[] = [\n {\n name: 'entity-change-log-grid-view',\n title: '实体变更网格视图',\n description: 'Entity Change Log Grid View',\n modelName: 'entity-change-log',\n viewType: 'grid',\n items: [\n { name: 'modelName', title: 'Model Name', spanCols: 12 },\n { name: 'objectId', title: 'Object ID', spanCols: 12 },\n { name: 'changedBy', title: 'Changed By', spanCols: 12 },\n { name: 'changeType', title: 'Change Type', spanCols: 12 },\n { name: 'changeDetails', title: 'Change Details', spanCols: 12 },\n ],\n },\n {\n name: 'ee-base-user-grid-view',\n title: '用户网格视图',\n description: '用户网格视图',\n modelName: 'ee-base-user',\n viewType: 'grid',\n items: [\n { name: 'userName', title: '用户名称', spanCols: 12 },\n { name: 'email', title: '登录邮箱', spanCols: 12 },\n { name: 'avatar', title: '头像', widget: 'image', spanCols: 12 },\n ],\n },\n {\n name: 'ee-base-user-form-view',\n title: '用户网格视图',\n description: '用户网格视图',\n modelName: 'ee-base-user',\n viewType: 'form',\n items: [\n { name: 'userName', title: '用户名称', spanCols: 12 },\n { name: 'email', title: '登录邮箱', spanCols: 12 },\n { name: 'password', title: '登录密码', widget: 'password', spanCols: 12 },\n { name: 'avatar', title: '头像', widget: 'image', spanCols: 12 },\n ],\n },\n];\n\nexport { views, models };\n","import type { IEntityView, IEntityModel } from '../../types';\nimport type { IEntityModule, IEntityServlet, ImportEntityData } from '../../core';\nimport type { EntityView, EntityWidget, IEntityNamedRenderer } from '../../components';\n\nimport info from './module.json';\nimport { views, models } from './config/config';\n\nconst servletHandler: IEntityServlet = {\n path: '/ttt',\n methods: ['GET', 'POST'],\n async handle(req, res) {\n console.log('BuildinModule: handleRequest called');\n res.write(new Response(`Hello from BuildinModule servlet! ${req.endpoint}`));\n },\n};\nexport class BuildinModule implements IEntityModule {\n readonly info = info;\n\n async setupConfig(args: {\n models: IEntityModel[];\n views: IEntityView[];\n servlets: IEntityServlet[];\n }): Promise<void> {\n console.log('BuildinModule: loadConfig called');\n args.models.push(...models);\n args.views.push(...views);\n args.servlets.push(servletHandler);\n }\n\n async setupComponents(args: {\n views: EntityView[];\n renderers: IEntityNamedRenderer[];\n widgets: EntityWidget[];\n }): Promise<void> {\n const authView = await import('./views/auth/auth.view'); // allowImportingTsExtensions 开启时保留 .tsx\n args.views.push(new authView.default() as EntityView);\n\n const mod = await import('./renderers/auth/shell-settings-target-renderer'); // allowImportingTsExtensions 开启时保留 .tsx\n args.renderers.push(mod.default as IEntityNamedRenderer);\n\n const menu = await import('./renderers/auth/shell-settings-menu-renderer'); // allowImportingTsExtensions 开启时保留 .tsx\n args.renderers.push(menu.default as IEntityNamedRenderer);\n\n console.log('BuildinModule: loadComponents called');\n }\n\n async setupData(args: { entities: ImportEntityData[] }): Promise<void> {\n const demoUser: ImportEntityData = {\n id: '3706a32d89d04423bc84cc1f9366881d',\n modelName: 'ee-base-user',\n values: {\n userName: 'Demo User',\n email: 'demo@demo.com',\n password: 'fe01ce2a7fbac8fafaed7c982a04e229',\n role: [],\n },\n };\n args.entities.push(demoUser);\n }\n}\n","export type EntitySessionData = {\n readonly email: string;\n readonly name: string;\n readonly avatar?: string;\n readonly roles?: string[];\n readonly id: string;\n};\n\nexport interface IEntitySession {\n isAuthenticated: () => boolean;\n update(): void;\n readonly sessionId?: string;\n readonly userInfo?: EntitySessionData;\n readonly updateTime?: number;\n}\n\nexport class EntitySession implements IEntitySession {\n constructor(\n public readonly sessionId?: string,\n public readonly userInfo?: EntitySessionData,\n public readonly update: () => void = () => {}\n ) {}\n isAuthenticated(): boolean {\n return !!this.userInfo;\n }\n}\n\nexport interface IEntitySessionProvider {\n get providerType(): string;\n session(): Promise<IEntitySession>;\n}\n\nexport interface IEntitySessionManager {\n setProvider(provider: IEntitySessionProvider): void;\n getProvider(): IEntitySessionProvider | undefined;\n\n getSession(): Promise<IEntitySession>;\n}\n","import type {\n IEntityMetaRegistry,\n IEntityViewDelegate,\n IEntityFieldDelegate,\n IEntityModelDelegate,\n IEntityViewFieldDelegate,\n IModelFieldTyperRegistry,\n} from '../types';\nimport type {\n IEntityView,\n IEntityField,\n IEntityModel,\n EntityFieldType,\n IEntityQueryMeta,\n IEntityViewField,\n IEntityViewPanel,\n EntityViewDensity,\n IEntityQueryItemMeta,\n IEntityGridViewHilite,\n IEntityModelExternalConfig,\n} from '../../types';\n\nimport { z, type ZodTypeAny } from 'zod';\n\nimport { QueryOperator } from '../../types';\n\nexport class EntityFieldDelegate implements IEntityFieldDelegate {\n private _field: IEntityField;\n constructor(field: IEntityField) {\n this._field = field;\n }\n get name(): string {\n return this._field.name;\n }\n get title(): string {\n return this._field.title;\n }\n get type(): EntityFieldType {\n return this._field.type;\n }\n get typeOptions(): { [key: string]: any } | undefined {\n return this._field.typeOptions;\n }\n get description(): string | undefined {\n return this._field.description;\n }\n get defaultValue(): any {\n return this._field.defaultValue;\n }\n get isRequired(): boolean {\n return this._field.isRequired ?? false;\n }\n get isPrimaryKey(): boolean {\n return this._field.isPrimaryKey ?? false;\n }\n get isUnique(): boolean {\n return this._field.isUnique ?? false;\n }\n get editable(): boolean {\n return this._field.editable ?? true;\n }\n get searchable(): boolean {\n return this._field.searchable ?? false;\n }\n get refModel(): string | undefined {\n return this._field.refModel;\n }\n get refField(): string | undefined {\n return this._field.refField;\n }\n get schema(): ZodTypeAny | undefined {\n return this._field.schema;\n }\n get order(): number | undefined {\n return this._field.order;\n }\n}\n\nexport class EntityModelDelegate implements IEntityModelDelegate {\n private _model: IEntityModel;\n private _typeRegistry: IModelFieldTyperRegistry;\n constructor(model: IEntityModel, typeRegistry: IModelFieldTyperRegistry) {\n this._model = model;\n this._typeRegistry = typeRegistry;\n }\n get name(): string {\n return this._model.name;\n }\n get title(): string {\n return this._model.title;\n }\n get description(): string | undefined {\n return this._model.description;\n }\n get external(): boolean | undefined {\n return this._model.external;\n }\n get externalConfig(): IEntityModelExternalConfig | undefined {\n return this._model.externalConfig;\n }\n\n isSupportFeature(feature: string): boolean {\n if (!this._model.external) return true;\n const fs = this._model.externalConfig?.features || [];\n console.log('@@@@@@@@@@@@@@ Supported features:', this._model.name, fs);\n return fs.includes(feature);\n }\n\n findPrimaryKeyFields(): IEntityField[] {\n return this._model.fields.filter((field) => field.isPrimaryKey);\n }\n findUniqueFields(): IEntityField[] {\n return this._model.fields.filter((field) => field.isUnique);\n }\n findFieldByName(name: string): IEntityField | undefined {\n return this._model.fields.find((field) => field.name === name);\n }\n findFieldByTitle(title: string): IEntityField | undefined {\n return this._model.fields.find((field) => field.title === title);\n }\n findSearchableFields(): IEntityField[] {\n return this._model.fields.filter((field) => field.searchable);\n }\n get schema(): ZodTypeAny {\n return z.object({\n ...this._model.fields.reduce<Record<string, ZodTypeAny>>((acc, field) => {\n const fschema = this.getFieldSchema(field);\n if (fschema) {\n acc[field.name] = fschema;\n }\n return acc;\n }, {}),\n });\n }\n\n private getFieldSchema(field: IEntityField): ZodTypeAny | undefined {\n if (field.schema) {\n return field.schema;\n }\n\n const typer = this._typeRegistry.getFieldTyper(field.type);\n if (typer) {\n return typer.getDefaultSchema(field);\n }\n\n // if (field.type === 'string') {\n // return field.isRequired ? z.string().min(1) : z.string().optional();\n // }\n // if (field.type === 'number') {\n // return field.isRequired ? z.number() : z.number().optional();\n // }\n // if (field.type === 'boolean') {\n // return field.isRequired ? z.boolean() : z.boolean().optional();\n // }\n // if (field.type === 'date') {\n // return field.isRequired ? z.coerce.date() : z.coerce.date().optional();\n // }\n // if (field.type === 'enum') {\n // const options = (field.typeOptions?.options || []).map(\n // (opt: { value: any }) => opt.value || opt\n // );\n // return field.isRequired\n // ? z.enum(options, { message: '必须进行选择' })\n // : z.enum(options).optional();\n // }\n // if (field.type === 'array') {\n // const options = (field.typeOptions?.options || []).map(\n // (opt: { value: any }) => opt.value || opt\n // );\n // return field.isRequired\n // ? z.array(z.enum(options, { message: '必须进行选择' }))\n // : z.array(z.enum(options)).optional();\n // }\n // if (['one_to_many', 'many_to_many'].includes(field.type)) {\n // return field.isRequired ? z.array(z.string()) : z.array(z.string()).optional();\n // }\n // if (['many_to_one', 'one_to_one'].includes(field.type)) {\n // return field.isRequired ? z.string() : z.string().optional();\n // }\n // if (field.type === 'binary') {\n // return field.isRequired\n // ? z.object({\n // fileName: z.string().min(1, { message: 'File name is required' }),\n // fileType: z.string().min(1, { message: 'File type is required' }),\n // fileSize: z\n // .number()\n // .min(0, { message: 'File size must be a positive number' }),\n // filePath: z.string().min(1, { message: 'File path is required' }),\n // })\n // : z\n // .object({\n // fileName: z\n // .string()\n // .min(1, { message: 'File name is required' })\n // .optional(),\n // fileType: z\n // .string()\n // .min(1, { message: 'File type is required' })\n // .optional(),\n // fileSize: z\n // .number()\n // .min(0, { message: 'File size must be a positive number' })\n // .optional(),\n // filePath: z\n // .string()\n // .min(1, { message: 'File path is required' })\n // .optional(),\n // })\n // .optional();\n // }\n // if (field.type === 'json') {\n // return field.isRequired\n // ? z.record(z.string(), z.unknown())\n // : z.record(z.string(), z.unknown()).optional();\n // }\n\n return undefined;\n }\n\n toSupplementedValues(input: Record<string, any>): Record<string, any> {\n const obj = {\n ...input,\n ...this._model.fields.reduce<Record<string, any>>((acc, field) => {\n if (!input[field.name]) {\n acc[field.name] = input[field.name] || this.getFieldDefaultValue(field);\n return acc;\n } else {\n return acc;\n }\n }, {}),\n };\n return obj;\n }\n\n private getFieldDefaultValue(field: IEntityField): any {\n if (field.defaultValue !== undefined) {\n return field.defaultValue;\n }\n\n const typer = this._typeRegistry.getFieldTyper(field.type);\n if (typer) {\n return typer.getDefaultValue(field);\n }\n\n // if (field.type === 'string') {\n // return '';\n // }\n // if (field.type === 'number') {\n // return 0;\n // }\n // if (field.type === 'boolean') {\n // return false;\n // }\n // if (field.type === 'date') {\n // return new Date();\n // }\n // if (field.type === 'enum') {\n // if (!field.typeOptions?.options || field.typeOptions.options.length === 0) {\n // return undefined;\n // }\n // return field.typeOptions?.options[0];\n // }\n // if (field.type === 'array') {\n // // if (!field.typeOptions?.options || field.typeOptions.options.length === 0) {\n // // return undefined;\n // // }\n // return [];\n // }\n // if (['many_to_one', 'one_to_one'].includes(field.type)) {\n // return '';\n // }\n return undefined;\n }\n\n getQueryMeta(): IEntityQueryMeta {\n const itemMetas: IEntityQueryItemMeta[] = [];\n const searchableFields = this.fields.filter((f) => f.searchable);\n searchableFields.forEach((field) => {\n const meta = this.getFieldQueryItemMeta(field);\n if (meta) {\n itemMetas.push(meta);\n }\n });\n return {\n queryItemMetas: itemMetas,\n };\n }\n\n private getFieldQueryItemMeta(field: IEntityField): IEntityQueryItemMeta | undefined {\n if (!field.searchable) return undefined;\n\n // const typer = this._typeRegistry.getFieldTyper(field.type);\n // if (typer) {\n // return typer.getQueryItemMeta(field);\n // }\n\n const operators: QueryOperator[] = [];\n const options: { [key: string]: any }[] = [];\n\n const normalizeOptionItem = (item: any) => {\n if (item == null) return undefined;\n if (typeof item === 'object') {\n // 若已是 { label, value } 或 { value },补齐 label\n const value = 'value' in item ? item.value : item;\n const label = 'label' in item ? item.label : String(value);\n return { label, value };\n }\n // 原始值\n return { label: String(item), value: item };\n };\n\n if (field.type === 'string') {\n operators.push(\n QueryOperator.EQ,\n QueryOperator.CONTAINS,\n QueryOperator.STARTS_WITH,\n QueryOperator.ENDS_WITH,\n QueryOperator.IS_NOT_NULL,\n QueryOperator.IS_NULL\n );\n }\n if (field.type === 'number') {\n operators.push(\n QueryOperator.EQ,\n QueryOperator.GT,\n QueryOperator.LT,\n QueryOperator.IS_NOT_NULL,\n QueryOperator.IS_NULL\n );\n }\n if (field.type === 'boolean') {\n operators.push(QueryOperator.EQ, QueryOperator.IS_NOT_NULL, QueryOperator.IS_NULL);\n options.push({ label: '是', value: true });\n options.push({ label: '否', value: false });\n }\n if (field.type === 'date') {\n operators.push(\n QueryOperator.EQ,\n QueryOperator.GT,\n QueryOperator.LT,\n QueryOperator.BETWEEN,\n QueryOperator.IS_NOT_NULL,\n QueryOperator.IS_NULL\n );\n }\n if (field.type === 'enum') {\n operators.push(\n QueryOperator.EQ,\n QueryOperator.NE,\n QueryOperator.IS_NOT_NULL,\n QueryOperator.IS_NULL\n );\n const raw = field.typeOptions?.options;\n if (Array.isArray(raw)) {\n for (const r of raw) {\n const norm = normalizeOptionItem(r);\n if (norm) options.push(norm);\n }\n } else {\n // 单值或对象也兼容\n const norm = normalizeOptionItem(raw);\n if (norm) options.push(norm);\n }\n }\n if (field.type === 'array') {\n operators.push(\n QueryOperator.IN,\n QueryOperator.NOT_IN,\n QueryOperator.IS_NOT_NULL,\n QueryOperator.IS_NULL\n );\n const raw = field.typeOptions?.options;\n if (Array.isArray(raw)) {\n for (const r of raw) {\n const norm = normalizeOptionItem(r);\n if (norm) options.push(norm);\n }\n }\n }\n\n if (operators.length > 0) {\n return {\n field,\n operators,\n options,\n };\n }\n\n return undefined;\n }\n\n get fields(): IEntityField[] {\n return this._model.fields.map((field) => new EntityFieldDelegate(field));\n }\n}\n\nexport class EntityViewFieldDelegate implements IEntityViewFieldDelegate {\n private _field: IEntityViewField;\n constructor(field: IEntityViewField) {\n this._field = field;\n }\n get name(): string {\n return this._field.name;\n }\n get title(): string | undefined {\n return this._field.title;\n }\n\n get description(): string | undefined {\n return this._field.description;\n }\n get icon(): string | undefined {\n return this._field.icon;\n }\n get widget(): string | undefined {\n return this._field.widget;\n }\n get widgetOptions(): { [key: string]: any } | undefined {\n return this._field.widgetOptions;\n }\n get width(): number | undefined {\n return this._field.width;\n }\n get flex(): 1 | 0 | undefined {\n return this._field.flex;\n }\n get spanCols(): number | undefined {\n return this._field.spanCols;\n }\n get order(): number | undefined {\n return this._field.order;\n }\n get fields(): IEntityViewField[] | undefined {\n return this._field.fields?.map((field) => new EntityViewFieldDelegate(field));\n }\n get hiddenWhen(): string | undefined {\n return this._field.hiddenWhen;\n }\n get showWhen(): string | undefined {\n return this._field.showWhen;\n }\n get readOnlyWhen(): string | undefined {\n return this._field.readOnlyWhen;\n }\n get disabledWhen(): string | undefined {\n return this._field.disabledWhen;\n }\n get requiredWhen(): string | undefined {\n return this._field.requiredWhen;\n }\n}\n\nexport class EntityViewDelegate implements IEntityViewDelegate {\n private _view: IEntityView;\n private _metaRegistry: IEntityMetaRegistry;\n private _typeRegistry: IModelFieldTyperRegistry;\n constructor(\n view: IEntityView,\n metaRegistry: IEntityMetaRegistry,\n typeRegistry: IModelFieldTyperRegistry\n ) {\n this._view = view;\n this._metaRegistry = metaRegistry;\n this._typeRegistry = typeRegistry!;\n }\n get name(): string {\n return this._view.name;\n }\n get title(): string {\n return this._view.title;\n }\n get description(): string | undefined {\n return this._view.description;\n }\n get modelName(): string {\n return this._view.modelName;\n }\n get viewType(): string {\n return this._view.viewType;\n }\n get density(): EntityViewDensity | undefined {\n return this._view.density;\n }\n get viewOptions(): { [key: string]: any } | undefined {\n return this._view.viewOptions;\n }\n get items(): IEntityViewField[] {\n return this._view.items.map((item) => new EntityViewFieldDelegate(item));\n }\n get hilites(): IEntityGridViewHilite[] | undefined {\n return this._view.hilites;\n }\n get canEdit(): boolean | undefined {\n return this._view.canEdit;\n }\n get canNew(): boolean | undefined {\n return this._view.canNew;\n }\n get canDelete(): boolean | undefined {\n return this._view.canDelete;\n }\n\n toSupplementedView(viewOptions?: Record<string, any>): EntityViewDelegate {\n const view: IEntityView = {\n ...this._view,\n ...(viewOptions && Object.keys(viewOptions).length > 0 ? { viewOptions } : {}),\n density: this._view.density || 'medium',\n items: this._view.items.map((field) => this.supplementField(field)),\n };\n return new EntityViewDelegate(view, this._metaRegistry, this._typeRegistry);\n }\n\n private supplementField(\n item: IEntityViewField | IEntityViewPanel\n ): IEntityViewField | IEntityViewPanel {\n if ('fields' in item) {\n // panel\n const panel = item as IEntityViewPanel;\n return {\n ...panel,\n fields: panel.fields.map((field) => this.supplementField(field)),\n };\n } else {\n const field = item as IEntityViewField;\n const modelField = this._metaRegistry\n .getModel(this._view.modelName)\n ?.findFieldByName(field.name);\n if (!modelField) {\n return field;\n } else {\n return {\n ...field,\n title: field.title || modelField.title,\n description: field.description || modelField.description,\n order: field.order || modelField.order || 0,\n flex: field.flex || 0,\n widget: this.getFieldWidget(field, modelField),\n };\n }\n }\n }\n private getFieldWidget(field: IEntityViewField, modelField: IEntityField): string {\n if (field.widget) {\n return field.widget;\n }\n if (!modelField) {\n return 'none';\n }\n\n const typer = this._typeRegistry.getFieldTyper(modelField.type);\n if (typer) {\n return typer.getDefaultWidgetType(this._view.viewType);\n }\n\n switch (modelField.type) {\n case 'string':\n return 'textfield';\n case 'number':\n return 'number';\n case 'boolean':\n return 'switch';\n case 'date':\n return 'date';\n case 'enum':\n case 'array':\n return 'select';\n case 'one_to_many':\n return 'reference';\n case 'many_to_many':\n return 'reference';\n case 'many_to_one':\n case 'one_to_one':\n return 'select';\n default:\n return 'none';\n }\n }\n}\n","import type { ZodTypeAny } from 'zod';\nimport type { IEntityView, IEntityModel } from '../../types';\n\nimport { zodToJsonSchema } from 'zod-to-json-schema';\nimport { convertJsonSchemaToZod } from 'zod-from-json-schema';\n\nexport function serializeFieldSchema(schema?: ZodTypeAny): any {\n if (schema) {\n try {\n return zodToJsonSchema(schema);\n } catch (e) {\n console.error('Failed to serialize Zod schema to JSON schema:', e);\n }\n }\n return undefined;\n}\n\nexport function deserializeFieldSchema(_data: any): ZodTypeAny | undefined {\n if (_data) {\n try {\n return convertJsonSchemaToZod(_data);\n } catch (e) {\n console.error('Failed to deserialize Zod schema from JSON schema:', e);\n }\n }\n return undefined;\n}\n\n// 将 IEntityModel 序列化为 JSON 字符串。\n// 注意: Zod schema (field.schema) 不是可序列化对象,这里剔除。\nexport function serializeEntityModel(model: IEntityModel): any {\n const safe = {\n name: model.name,\n title: model.title,\n description: model.description,\n external: model.external,\n externalConfig: { features: model.externalConfig?.features || [] },\n fields: model.fields.map((field) => ({\n name: field.name,\n title: field.title,\n type: field.type,\n typeOptions: field.typeOptions,\n description: field.description,\n defaultValue: field.defaultValue,\n isRequired: field.isRequired,\n isPrimaryKey: field.isPrimaryKey,\n isUnique: field.isUnique,\n editable: field.editable,\n searchable: field.searchable,\n refModel: field.refModel,\n refField: field.refField,\n order: field.order,\n // 添加 schemaSerialized 字段,保留原字段 schema 以便未来恢复\n schemaSerialized: serializeFieldSchema(field.schema),\n })),\n } as any;\n return safe;\n}\n\n// 从 JSON 字符串反序列化为 IEntityModel。遇到结构不合法时返回 undefined。\nexport function deserializeEntityModel(json: string): IEntityModel | undefined {\n if (!json) return undefined;\n try {\n const obj = JSON.parse(json);\n if (!obj || typeof obj !== 'object') return undefined;\n if (typeof obj.name !== 'string' || typeof obj.title !== 'string') return undefined;\n if (!Array.isArray(obj.fields)) return undefined;\n const fields = obj.fields\n .filter((f: any) => f && typeof f === 'object')\n .map((f: any) => {\n const {\n name,\n title,\n type,\n typeOptions,\n description,\n defaultValue,\n isRequired,\n isPrimaryKey,\n isUnique,\n editable,\n searchable,\n refModel,\n refField,\n order,\n schemaSerialized,\n } = f;\n if (\n typeof name !== 'string' ||\n typeof title !== 'string' ||\n typeof type !== 'string'\n ) {\n return undefined;\n }\n return {\n name,\n title,\n type,\n typeOptions,\n description,\n defaultValue,\n isRequired,\n isPrimaryKey,\n isUnique,\n editable,\n searchable,\n refModel,\n refField,\n order,\n schema: deserializeFieldSchema(schemaSerialized),\n };\n })\n .filter(Boolean);\n const model: IEntityModel = {\n name: obj.name,\n title: obj.title,\n external: obj.external,\n externalConfig: obj.externalConfig,\n description: typeof obj.description === 'string' ? obj.description : undefined,\n fields: fields as any,\n };\n return model;\n } catch {\n return undefined;\n }\n}\n\nexport function serializeEntityView(view: IEntityView): any {\n if (!view) return undefined;\n const VERSION = 1; // 当前视图序列化结构版本\n\n const cloneField = (field: any): any => {\n if (!field || typeof field !== 'object') return undefined;\n const {\n name,\n title,\n description,\n icon,\n widget,\n widgetOptions,\n width,\n flex,\n spanCols,\n order,\n fields,\n hiddenWhen,\n showWhen,\n readOnlyWhen,\n disabledWhen,\n requiredWhen,\n referenceView,\n referenceComp,\n } = field;\n if (typeof name !== 'string') return undefined;\n const safe: any = {\n name,\n title,\n description,\n icon,\n widget,\n widgetOptions: widgetOptions ? JSON.parse(JSON.stringify(widgetOptions)) : undefined,\n width,\n flex,\n spanCols,\n order,\n hiddenWhen,\n showWhen,\n readOnlyWhen,\n disabledWhen,\n requiredWhen,\n referenceView,\n referenceComp,\n };\n if (Array.isArray(fields)) {\n const child = fields.map((f) => cloneField(f)).filter(Boolean);\n if (child.length > 0) safe.fields = child;\n }\n return safe;\n };\n\n const {\n name,\n title,\n description,\n modelName,\n viewType,\n viewOptions,\n items,\n hilites,\n canEdit,\n canNew,\n canDelete,\n density,\n } = view;\n if (\n typeof name !== 'string' ||\n typeof title !== 'string' ||\n typeof modelName !== 'string' ||\n typeof viewType !== 'string'\n ) {\n throw new Error('Invalid IEntityView: missing required string fields');\n }\n const safeItems = Array.isArray(items) ? items.map((it) => cloneField(it)).filter(Boolean) : [];\n const safe: any = {\n __viewSerializerVersion: VERSION,\n name,\n title,\n description,\n modelName,\n viewType,\n viewOptions: viewOptions ? JSON.parse(JSON.stringify(viewOptions)) : undefined,\n items: safeItems,\n hilites: Array.isArray(hilites)\n ? hilites\n .filter((h) => h && typeof h === 'object' && typeof h.when === 'string')\n .map((h) => ({ when: h.when, color: h.color }))\n : undefined,\n canEdit,\n canNew,\n canDelete,\n density,\n };\n return safe;\n}\n\nexport function deserializeEntityView(json: string): IEntityView | undefined {\n if (!json) return undefined;\n try {\n const obj = JSON.parse(json);\n if (!obj || typeof obj !== 'object') return undefined;\n // 版本向前兼容:当前序列化版本为 1(见 serializeEntityView)。\n // 未来如果出现更高版本,可在此处做差异适配;暂时仅校验是数字或缺省。\n const {\n name,\n title,\n description,\n modelName,\n viewType,\n viewOptions,\n items,\n hilites,\n canEdit,\n canNew,\n canDelete,\n density,\n } = obj as any;\n if (\n typeof name !== 'string' ||\n typeof title !== 'string' ||\n typeof modelName !== 'string' ||\n typeof viewType !== 'string'\n ) {\n return undefined;\n }\n const reviveField = (field: any): any => {\n if (!field || typeof field !== 'object') return undefined;\n if (typeof field.name !== 'string') return undefined;\n const f: any = {\n name: field.name,\n title: typeof field.title === 'string' ? field.title : undefined,\n description: typeof field.description === 'string' ? field.description : undefined,\n icon: typeof field.icon === 'string' ? field.icon : undefined,\n widget: typeof field.widget === 'string' ? field.widget : undefined,\n widgetOptions:\n field.widgetOptions && typeof field.widgetOptions === 'object'\n ? field.widgetOptions\n : undefined,\n width: typeof field.width === 'number' ? field.width : undefined,\n flex: field.flex === 0 || field.flex === 1 ? field.flex : undefined,\n spanCols: typeof field.spanCols === 'number' ? field.spanCols : undefined,\n order: typeof field.order === 'number' ? field.order : undefined,\n hiddenWhen: typeof field.hiddenWhen === 'string' ? field.hiddenWhen : undefined,\n showWhen: typeof field.showWhen === 'string' ? field.showWhen : undefined,\n readOnlyWhen:\n typeof field.readOnlyWhen === 'string' ? field.readOnlyWhen : undefined,\n disabledWhen:\n typeof field.disabledWhen === 'string' ? field.disabledWhen : undefined,\n requiredWhen:\n typeof field.requiredWhen === 'string' ? field.requiredWhen : undefined,\n referenceView:\n field.referenceView && typeof field.referenceView === 'object'\n ? field.referenceView\n : undefined,\n referenceComp:\n field.referenceComp && typeof field.referenceComp === 'object'\n ? field.referenceComp\n : undefined,\n };\n if (Array.isArray(field.fields)) {\n const child = field.fields.map((c: any) => reviveField(c)).filter(Boolean);\n if (child.length > 0) f.fields = child;\n }\n return f;\n };\n const safeItems = Array.isArray(items)\n ? items.map((it: any) => reviveField(it)).filter(Boolean)\n : [];\n const safe: IEntityView = {\n name,\n title,\n description: typeof description === 'string' ? description : undefined,\n modelName,\n viewType,\n viewOptions: viewOptions && typeof viewOptions === 'object' ? viewOptions : undefined,\n items: safeItems,\n hilites: Array.isArray(hilites)\n ? hilites\n .filter((h: any) => h && typeof h.when === 'string')\n .map((h: any) => ({ when: h.when, color: h.color }))\n : undefined,\n canEdit: typeof canEdit === 'boolean' ? canEdit : undefined,\n canNew: typeof canNew === 'boolean' ? canNew : undefined,\n canDelete: typeof canDelete === 'boolean' ? canDelete : undefined,\n density:\n density === 'small' || density === 'medium' || density === 'large'\n ? density\n : undefined,\n };\n return safe;\n } catch {\n return undefined;\n }\n}\n","import type { IEntityView, IEntityModel } from '../../types';\nimport type {\n IEntityMetaRegistry,\n IEntityViewDelegate,\n IEntityEventEmitter,\n IEntityModelDelegate,\n IModelFieldTyperRegistry,\n} from '../types';\n\nimport { EntityViewDelegate, EntityModelDelegate } from './delegate';\nimport {\n serializeEntityView,\n serializeEntityModel,\n deserializeEntityView,\n deserializeEntityModel,\n} from './meta.serializer';\n\nexport class EntityMetaRegistry implements IEntityMetaRegistry {\n private _typeRegistry: IModelFieldTyperRegistry;\n private _models: Map<string, IEntityModelDelegate> = new Map();\n private _views: Map<string, IEntityViewDelegate> = new Map();\n private _eventEmitter: IEntityEventEmitter;\n\n constructor(typeRegistry: IModelFieldTyperRegistry, eventEmitter: IEntityEventEmitter) {\n this._typeRegistry = typeRegistry;\n this._eventEmitter = eventEmitter;\n }\n\n getModel(name: string): IEntityModelDelegate | undefined {\n return this._models.get(name);\n }\n\n get models(): IEntityModelDelegate[] {\n return Array.from(this._models.values());\n }\n\n get views(): IEntityViewDelegate[] {\n return Array.from(this._views.values());\n }\n\n getView(name: string): IEntityViewDelegate | undefined {\n return this._views.get(name);\n }\n\n cleanup(): void {\n this._models.clear();\n this._views.clear();\n }\n\n findView(modelName: string, viewType: string, name?: string): IEntityViewDelegate | undefined {\n let _view: IEntityViewDelegate | undefined = undefined;\n if (name) {\n _view = this._views.get(name);\n }\n if (!_view) {\n const values = Array.from(this._views.values());\n _view = values.find(\n (view) => view.modelName === modelName && view.viewType === viewType\n );\n }\n if (!_view) {\n const m = this.getModel(modelName);\n if (m) {\n const _viewData: IEntityView = {\n name: `${modelName}-${viewType}`,\n title: `${m.title}`,\n description: `${m.description}`,\n modelName: m.name,\n viewType,\n density: 'medium',\n items: m.fields.map((field) => ({\n name: field.name,\n title: field.title,\n description: field.description,\n order: field.order || 0,\n flex: 0,\n })),\n };\n _view = new EntityViewDelegate(_viewData, this, this._typeRegistry);\n }\n }\n return _view;\n }\n\n registerModel(model: IEntityModel): void {\n if (!model) {\n throw new Error('Model cannot be undefined or null');\n }\n if (!model.name) {\n throw new Error('Model must have a name');\n }\n this._models.set(model.name, new EntityModelDelegate(model, this._typeRegistry));\n }\n\n registerView(view: IEntityView): void {\n if (!view) {\n throw new Error('View cannot be undefined or null');\n }\n if (!view.modelName || !view.name || !view.viewType) {\n throw new Error('View must have modelName, name, and viewType defined');\n }\n this._views.set(view.name, new EntityViewDelegate(view, this, this._typeRegistry));\n }\n\n toPlainModelObject(model: IEntityModelDelegate): Record<string, any> {\n return serializeEntityModel(model);\n }\n\n toPlainViewObject(view: IEntityViewDelegate): Record<string, any> {\n return serializeEntityView(view.toSupplementedView());\n }\n\n updateOrRegisterByPlainObject(config: { models: any[]; views: any[] }): void {\n let ref = 0;\n if (Array.isArray(config.models)) {\n config.models.forEach((m) => {\n const model = deserializeEntityModel(JSON.stringify(m));\n if (model) {\n if (this.getModel(model.name)) {\n this._models.delete(model.name);\n }\n this.registerModel(model);\n ref++;\n }\n });\n }\n if (Array.isArray(config.views)) {\n config.views.forEach((v) => {\n const view = deserializeEntityView(JSON.stringify(v));\n console.log(`Deserialized view:`, view);\n if (view) {\n if (this.getView(view.name)) {\n this._views.delete(view.name);\n }\n this.registerView(view);\n ref++;\n }\n });\n }\n if (ref > 0) {\n this._eventEmitter.emit({\n name: 'config.updated',\n parameter: {\n modelIds: config.models?.map((m) => m.name) || [],\n viewIds: config.views?.map((v) => v.name) || [],\n },\n });\n }\n }\n\n toJSONString(): string {\n const models = this.models.map((model) => serializeEntityModel(model));\n const views = this.views.map((view) => serializeEntityView(view));\n return JSON.stringify({ models, views });\n }\n\n fromJSONString(json: string): void {\n if (!json) return;\n try {\n const obj = JSON.parse(json);\n if (!obj || typeof obj !== 'object') return;\n const { models, views } = obj as { models?: any[]; views?: any[] };\n\n // 先清空再注册,保持与输入一致\n this.cleanup();\n\n if (Array.isArray(models)) {\n models.forEach((m, idx) => {\n try {\n const model = deserializeEntityModel(JSON.stringify(m));\n if (model) {\n this.registerModel(model);\n }\n } catch (e) {\n console.warn('[EntityMetaRegistry] Failed to load model index', idx, e);\n }\n });\n }\n\n if (Array.isArray(views)) {\n views.forEach((v, idx) => {\n try {\n const view = deserializeEntityView(JSON.stringify(v));\n if (view) {\n // 只有在其引用的 model 已加载或为 __default__ 时注册\n if (view.modelName === '__default__' || this.getModel(view.modelName)) {\n this.registerView(view);\n } else {\n console.warn(\n `[EntityMetaRegistry] Skip view '${view.name}' because model '${view.modelName}' not found.`\n );\n }\n }\n } catch (e) {\n console.warn('[EntityMetaRegistry] Failed to load view index', idx, e);\n }\n });\n }\n } catch (e) {\n console.error('[EntityMetaRegistry] fromJSONString parse error:', e);\n }\n }\n}\n","import type { AppRouter } from '../root';\n\nimport superjson from 'superjson';\nimport { httpBatchLink, createTRPCClient } from '@trpc/client';\n\n// A minimal, framework-agnostic TRPC client factory for server-side usage.\n// Note: This file intentionally avoids importing any React/Next specific libs.\nexport function createVanillaTrpcClient(url: string) {\n return createTRPCClient<AppRouter>({\n links: [\n httpBatchLink({\n url,\n transformer: superjson,\n maxURLLength: 2048, // Set a safe max URL length\n }),\n ],\n });\n}\n","import type { EntityObject, EntityObjectReference } from '@prisma/client';\nimport type { IEntityObject, EntityTreeNode, IEntityObjectReference } from '../../types';\n\nexport function convertRawEntityObject(\n eo: Ent