// DSL field type definitions. // Shape: { type: , } import type { DictionaryEntry } from './dictionary.js'; import type { ImportBase } from './import-base.js'; import type { MockDescriptor } from './mock.js'; import type { ComputeExpr } from './expr.js'; /** Field query comparison operators (search field semantics). */ export type Operator = 'eq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ne' | 'in' | 'null' | 'notNull'; export interface SchemaBase { name: string; description?: string; } /** Schema base for cross-file entities. importRef locates the generating module; inline schemas omit it. */ export interface ImportableSchemaBase extends SchemaBase { importRef?: ImportBase; } /** Field collection schemas (DB table vs DTO message); `type` is the discriminator */ export interface CollectionSchemaBase extends SchemaBase { type: string; } export interface BaseField { name: string; /** 显示名称(中文标签) */ label?: string; /** 字段描述 */ description?: string; optional?: boolean; readOnly?: boolean; default?: string; /** 所属 schema(db 或 dto) */ schema?: CollectionSchemaBase; /** Pure-data mock descriptor for test data generation */ mock?: MockDescriptor; } interface StringField extends BaseField { type: 'string'; jsType: 'string'; minLength?: number; maxLength?: number; } interface TextField extends BaseField { type: 'text'; jsType: 'string'; } interface IntField extends BaseField { type: 'integer'; jsType: 'number'; min?: number; max?: number; } interface BigintField extends BaseField { type: 'bigint'; jsType: 'string'; // Value is transported as string to keep precision beyond 2^53. } interface DecimalField extends BaseField { type: 'decimal'; jsType: 'string'; precision: number; scale: number; // Value is transported as string to avoid binary float error. } type RateUnit = 'pct' | 'pm' | 'bp'; export function rateScale(unit: RateUnit): number { switch (unit) { case 'pct': return 2; case 'pm': return 3; case 'bp': return 4; } } interface RateField extends BaseField { type: 'rate'; jsType: 'string'; unit: RateUnit; // Precision/scale are derived from unit at DDL/TypeBox render time, // not stored on the field. } interface BooleanField extends BaseField { type: 'boolean'; jsType: 'boolean'; } interface DateField extends BaseField { type: 'date'; jsType: 'Date'; } interface TimeField extends BaseField { type: 'time'; jsType: 'string'; } interface DateTimeField extends BaseField { type: 'datetime'; jsType: 'Date'; } export interface EnumValue { value: string | number; symbol: string; label: string; } /** Shared enum definition. Pure value object, safe to reference from multiple fields/tables. */ export interface EnumDef { /** JS 定义名称,如 MerchantStatus */ jsName: string; valueType: 'string' | 'integer'; values: EnumValue[]; } export function defineEnum( jsName: string, valueType: 'string' | 'integer', values: EnumValue[], ): EnumDef { return { jsName, valueType, values }; } export interface EnumField extends BaseField { type: 'enum'; jsType: 'string' | 'number'; /** Reference to a shared enum definition (see defineEnum). */ enum: EnumDef; } interface JsonField extends BaseField { type: 'json'; jsType: 'object'; } /** Recursive array field — wire-format nesting (third-party messages), not a table column. */ interface ArrayField extends BaseField { type: 'array'; jsType: 'array'; /** Element type: any Field, including nested array/object. */ items: Field; } /** Recursive object field — wire-format nesting (third-party messages), not a table column. */ interface ObjectField extends BaseField { type: 'object'; jsType: 'object'; properties: Record; } /** Aggregate result column (count/sum/avg) of an aggregate query — a Field * so that aggregate result entities can carry it like any other column. * jsType follows the aggregate precision rule: count → number; * sum/avg over an integer column → number, anything else → string. */ export interface AggregateField extends BaseField { type: 'aggregate'; jsType: 'number' | 'string'; /** The aggregate expression producing this column. */ expr: ComputeExpr; } /** Whether the field is an aggregate result column — narrows to AggregateField. */ export function isAggregate(field: Field): field is AggregateField { return field.type === 'aggregate'; } export type Field = | StringField | TextField | IntField | BigintField | DecimalField | RateField | BooleanField | DateField | TimeField | DateTimeField | EnumField | JsonField | ArrayField | ObjectField | AggregateField; /** TS type of a field in generated code (row interfaces, dao params, filter * args): enum → its JS name, date/datetime → string (transported as ISO * strings), everything else → jsType. Aggregate fields carry their precision * rule in jsType already. The single source for this mapping — renderers * must not branch on field.type for a TS type string. */ export function fieldJsType(field: Field): string { if (field.type === 'enum') return field.enum.jsName; if (field.type === 'date' || field.type === 'datetime') return 'string'; return field.jsType; } /** Enum JS names referenced by a field, recursing into array/object * containers (wire-format nesting). First-occurrence order — callers that * need uniqueness collect into a Set. */ export function collectEnumRefs(field: Field, out: string[] = []): string[] { walkContainer(field, (leaf) => { if (leaf.type === 'enum') out.push(leaf.enum.jsName); }); return out; } /** Depth-first walk over a field's container structure: visit is called for * every field including containers (top-level, array items, object * properties). `key` is the object property name the field sits under * (undefined for the top-level field and for array items). */ export function walkContainer(field: Field, visit: (f: Field, key?: string) => void, key?: string): void { visit(field, key); if (field.type === 'array') { walkContainer(field.items, visit); } else if (field.type === 'object') { for (const [k, child] of Object.entries(field.properties)) walkContainer(child, visit, k); } } // Field builders: type and jsType are fixed, pass extra properties only. // The field name is written back from the map key later (see defineTable). type FieldExtras = Omit; export function stringField(extra: FieldExtras = {}): StringField { return { name: '', type: 'string', jsType: 'string', ...extra }; } export function textField(extra: FieldExtras = {}): TextField { return { name: '', type: 'text', jsType: 'string', ...extra }; } export function intField(extra: FieldExtras = {}): IntField { return { name: '', type: 'integer', jsType: 'number', ...extra }; } export function bigintField(extra: FieldExtras = {}): BigintField { return { name: '', type: 'bigint', jsType: 'string', ...extra }; } export function decimalField(extra: FieldExtras): DecimalField { return { name: '', type: 'decimal', jsType: 'string', ...extra }; } export function rateField(unit: RateUnit, extra: Omit, 'unit'> = {}): RateField { return { name: '', type: 'rate', jsType: 'string', unit, ...extra }; } export function booleanField(extra: FieldExtras = {}): BooleanField { return { name: '', type: 'boolean', jsType: 'boolean', ...extra }; } export function dateField(extra: FieldExtras = {}): DateField { return { name: '', type: 'date', jsType: 'Date', ...extra }; } export function timeField(extra: FieldExtras = {}): TimeField { return { name: '', type: 'time', jsType: 'string', ...extra }; } export function datetimeField(extra: FieldExtras = {}): DateTimeField { return { name: '', type: 'datetime', jsType: 'Date', ...extra }; } export function jsonField(extra: FieldExtras = {}): JsonField { return { name: '', type: 'json', jsType: 'object', ...extra }; } export function arrayField(extra: FieldExtras): ArrayField { return { name: '', type: 'array', jsType: 'array', ...extra }; } export function objectField(extra: FieldExtras): ObjectField { return { name: '', type: 'object', jsType: 'object', ...extra }; } export function enumField(extra: Omit): EnumField { const jsType = extra.enum.valueType === 'integer' ? 'number' : 'string'; return { name: '', type: 'enum', jsType, ...extra }; } /** Aggregate result column field: count → number; sum/avg over an integer * column → number, anything else (decimal/bigint/rate…) → string. * Used in aggregate-query result entities (see AggregateSchema). */ export function aggField(name: string, expr: ComputeExpr): AggregateField { const jsType = expr.field === undefined || expr.field.type === 'integer' ? 'number' : 'string'; return { name, type: 'aggregate', jsType, expr }; }