@pylonts/dsl 1.1.5 → 1.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +4 -0
  2. package/dist/action.d.ts +32 -0
  3. package/dist/action.js +14 -0
  4. package/dist/aggregate.d.ts +38 -0
  5. package/dist/aggregate.js +46 -0
  6. package/dist/business-flow.d.ts +9 -0
  7. package/dist/business-flow.js +72 -0
  8. package/dist/controller.d.ts +35 -0
  9. package/dist/controller.js +17 -0
  10. package/dist/convert.d.ts +37 -7
  11. package/dist/convert.js +23 -2
  12. package/dist/curd.d.ts +7 -12
  13. package/dist/curd.js +10 -1
  14. package/dist/dao.d.ts +140 -3
  15. package/dist/dao.js +307 -2
  16. package/dist/db.d.ts +6 -0
  17. package/dist/db.js +13 -0
  18. package/dist/domain-event.d.ts +48 -0
  19. package/dist/domain-event.js +24 -0
  20. package/dist/dsl.d.ts +32 -2
  21. package/dist/dsl.js +13 -0
  22. package/dist/dto.d.ts +8 -2
  23. package/dist/dto.js +21 -9
  24. package/dist/endpoint.d.ts +15 -0
  25. package/dist/endpoint.js +3 -0
  26. package/dist/entity.d.ts +29 -0
  27. package/dist/entity.js +13 -0
  28. package/dist/exception.d.ts +20 -0
  29. package/dist/exception.js +33 -0
  30. package/dist/expr.d.ts +45 -0
  31. package/dist/expr.js +32 -0
  32. package/dist/field-rule.d.ts +20 -0
  33. package/dist/field-rule.js +19 -0
  34. package/dist/filter.d.ts +45 -0
  35. package/dist/filter.js +21 -0
  36. package/dist/flow-script.d.ts +108 -0
  37. package/dist/flow-script.js +505 -0
  38. package/dist/flow.d.ts +309 -10
  39. package/dist/flow.js +819 -22
  40. package/dist/index.d.ts +12 -1
  41. package/dist/index.js +14 -1
  42. package/dist/mermaid-driver.js +278 -9
  43. package/dist/method.d.ts +11 -0
  44. package/dist/method.js +3 -0
  45. package/dist/mysql-driver.js +7 -0
  46. package/dist/project.d.ts +5 -4
  47. package/dist/project.js +14 -2
  48. package/dist/provider.d.ts +6 -11
  49. package/dist/provider.js +2 -2
  50. package/dist/repository.d.ts +26 -0
  51. package/dist/repository.js +8 -0
  52. package/dist/service.d.ts +30 -8
  53. package/dist/service.js +62 -2
  54. package/dist/third-service.d.ts +80 -0
  55. package/dist/third-service.js +97 -0
  56. package/dist/typebox-driver.d.ts +6 -0
  57. package/dist/typebox-driver.js +77 -12
  58. package/dist/utils.d.ts +32 -10
  59. package/dist/utils.js +32 -11
  60. package/docs/aggregate.md +110 -0
  61. package/docs/dao-generation.md +478 -0
  62. package/docs/ddd-principles.md +75 -0
  63. package/docs/domain-event.md +137 -0
  64. package/docs/dto.md +73 -66
  65. package/docs/keyword-matcher.md +182 -0
  66. package/docs/third-service.md +122 -0
  67. package/docs/token.md +327 -0
  68. package/docs/trans-reentrant.md +85 -0
  69. package/package.json +27 -5
  70. package/src/action.ts +51 -10
  71. package/src/aggregate.ts +104 -0
  72. package/src/business-flow.ts +80 -0
  73. package/src/controller.ts +54 -0
  74. package/src/convert.ts +78 -15
  75. package/src/curd.ts +104 -93
  76. package/src/dao.ts +486 -13
  77. package/src/db.ts +199 -181
  78. package/src/domain-event.ts +74 -0
  79. package/src/dsl.ts +48 -2
  80. package/src/dto.ts +266 -247
  81. package/src/entity.ts +43 -0
  82. package/src/exception.ts +53 -0
  83. package/src/expr.ts +65 -0
  84. package/src/field-rule.ts +47 -0
  85. package/src/filter.ts +70 -0
  86. package/src/flow-script.ts +696 -0
  87. package/src/flow.ts +1226 -103
  88. package/src/index.ts +47 -33
  89. package/src/mermaid-driver.ts +339 -84
  90. package/src/method.ts +20 -0
  91. package/src/mysql-driver.ts +7 -0
  92. package/src/project.ts +114 -97
  93. package/src/repository.ts +35 -0
  94. package/src/service.ts +107 -20
  95. package/src/third-service.ts +192 -0
  96. package/src/typebox-driver.ts +86 -11
  97. package/src/utils.ts +74 -26
  98. package/dist/check-inheritance.d.ts +0 -9
  99. package/dist/check-inheritance.js +0 -58
  100. package/src/provider.ts +0 -73
package/src/db.ts CHANGED
@@ -1,182 +1,200 @@
1
- // DB table definitions.
2
- // Shape: { type: 'table', <extension fields> }
3
-
4
- import type { EntityPhrase } from './dictionary.js';
5
- import type { CollectionSchemaBase, EnumDef, Field } from './dsl.js';
6
-
7
- export type Index = {
8
- name?: string;
9
- columns: Field | Field[];
10
- unique?: boolean;
11
- };
12
-
13
- export type ForeignKey = {
14
- columns: Field | Field[];
15
- references: Field | Field[];
16
- };
17
-
18
- export interface TableSchemaOptions<
19
- N extends string = string,
20
- C extends Record<string, Field> = Record<string, Field>,
21
- E extends Record<string, EnumDef> = Record<string, EnumDef>
22
- > {
23
- description?: string;
24
- paginated: boolean;
25
- actor?: boolean;
26
- generator?: string;
27
- autoIncrement?: Field;
28
- /** 本表用于关联显示的名称字段(如 name / username)。被外键引用时,自动用该字段做 label 展示。 */
29
- label?: Field;
30
- primaryKey?: Field | Field[];
31
- indexes?: Index[];
32
- foreignKeys?: Record<string, ForeignKey>;
33
- /** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
34
- phrase?: EntityPhrase;
35
- /** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
36
- enums: E;
37
- columns: C;
38
- }
39
-
40
- export class TableSchema<
41
- N extends string = string,
42
- C extends Record<string, Field> = Record<string, Field>,
43
- E extends Record<string, EnumDef> = Record<string, EnumDef>
44
- > implements CollectionSchemaBase {
45
- type = 'table';
46
- name: N;
47
- description?: string;
48
- /** 分页 */
49
- paginated: boolean;
50
- /** 系统操作者(如小程序为 C 端用户,管理端为运营) */
51
- actor?: boolean;
52
- /** id 生成器 */
53
- generator?: string;
54
- /** 自增主键字段 */
55
- autoIncrement?: Field;
56
- primaryKey?: Field | Field[];
57
- indexes?: Index[];
58
- /** 外键,引用其他表的字段 */
59
- foreignKeys?: Record<string, ForeignKey>;
60
- /** 本表用于关联显示的名称字段(如 name / username)。被外键引用时,自动用该字段做 label 展示。 */
61
- label?: Field;
62
- /** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
63
- phrase?: EntityPhrase;
64
- /** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
65
- enums: E;
66
- columns: C;
67
-
68
- constructor(name: N, options: TableSchemaOptions<N, C, E>) {
69
- if (!options.enums) {
70
- throw new Error(`table '${name}': enums is required — declare enums: {} when the table has no enums`);
71
- }
72
- if (options.paginated === undefined || options.paginated === null) {
73
- throw new Error(`table '${name}': paginated is required — discuss with user whether this table needs pagination, then set paginated: true or paginated: false`);
74
- }
75
- this.name = name;
76
- this.description = options.description;
77
- this.paginated = options.paginated;
78
- this.actor = options.actor;
79
- this.generator = options.generator;
80
- this.autoIncrement = options.autoIncrement;
81
- this.primaryKey = options.primaryKey;
82
- this.indexes = options.indexes;
83
- this.foreignKeys = options.foreignKeys;
84
- this.label = options.label;
85
- this.phrase = options.phrase;
86
- this.enums = options.enums;
87
- this.columns = options.columns;
88
- }
89
-
90
- /** True when the field is part of this table's primary key */
91
- isPk(fieldRef: Field): boolean {
92
- if (this.primaryKey === undefined) return false;
93
- return Array.isArray(this.primaryKey)
94
- ? this.primaryKey.includes(fieldRef)
95
- : this.primaryKey === fieldRef;
96
- }
97
- }
98
-
99
- export function defineTable<
100
- N extends string,
101
- C extends Record<string, Field>,
102
- E extends Record<string, EnumDef> = Record<string, EnumDef>
103
- >(
104
- name: N,
105
- schema: TableSchemaOptions<N, C, E>,
106
- ): TableSchema<N, C, E> {
107
- const table = new TableSchema<N, C, E>(name, schema);
108
- if (table.generator && table.autoIncrement) {
109
- throw new Error(`table '${name}': generator and autoIncrement are mutually exclusive`);
110
- }
111
- if (table.label && !Object.values(table.columns).includes(table.label)) {
112
- throw new Error(`table '${name}': label field '${table.label.name}' must be one of the table's columns`);
113
- }
114
- for (const key of Object.keys(table.columns)) {
115
- const field = table.columns[key] as Field;
116
- if (field.schema && field.schema !== table) {
117
- throw new Error(
118
- `field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`,
119
- );
120
- }
121
- }
122
- for (const key of Object.keys(table.columns)) {
123
- table.columns[key].name = key;
124
- table.columns[key].schema = table;
125
- }
126
- if (table.primaryKey) {
127
- const pkFields = Array.isArray(table.primaryKey) ? table.primaryKey : [table.primaryKey];
128
- const colRefs = Object.values(table.columns);
129
- for (const pk of pkFields) {
130
- if (!colRefs.includes(pk)) {
131
- throw new Error(
132
- `table '${name}': primaryKey field '${pk.name}' must be the exact same object as the corresponding column (extract it to a const and reuse)`,
133
- );
134
- }
135
- }
136
- }
137
- for (const [fkName, fk] of Object.entries(table.foreignKeys ?? {})) {
138
- const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
139
- const fields = Array.isArray(fk.columns) ? fk.columns : [fk.columns];
140
- const colRefs = Object.values(table.columns);
141
- for (const fkField of fields) {
142
- if (!colRefs.includes(fkField)) {
143
- throw new Error(
144
- `foreign key ${fkName}: column '${fkField.name}' must be the exact same object as the corresponding column in the table (extract it to a const and reuse)`,
145
- );
146
- }
147
- }
148
- for (let i = 0; i < refs.length; i++) {
149
- const ref = refs[i];
150
- if (!ref.schema) throw new Error(`foreign key ${fkName}: references field has no schema`);
151
- if (ref.schema === table) throw new Error(`foreign key ${fkName}: cannot reference own table ${table.name}`);
152
- const phrase = (ref.schema as TableSchema).phrase;
153
- if (!phrase) throw new Error(`foreign key ${fkName}: referenced table ${ref.schema.name} has no phrase, cannot check field naming`);
154
- const expected = `${phrase.name}_${ref.name}`;
155
- const fkField = fields[i];
156
- if (fkField.name !== expected) {
157
- throw new Error(
158
- `foreign key ${fkName}: field must be named ${expected} (phrase ${phrase.name} + ${ref.name}), got ${fkField.name}`,
159
- );
160
- }
161
- }
162
- }
163
- for (const [idxKey, index] of Object.entries(table.indexes ?? {})) {
164
- const cols = Array.isArray(index.columns) ? index.columns : [index.columns];
165
- const colRefs = Object.values(table.columns);
166
- for (const col of cols) {
167
- if (typeof col !== 'object' || col === null || typeof (col as Field).type !== 'string') {
168
- const idxName = index.name ?? idxKey;
169
- throw new Error(
170
- `index ${idxName}: columns must be Field instances (stringField()/intField()/...), got ${JSON.stringify(col)}`,
171
- );
172
- }
173
- if (!colRefs.includes(col)) {
174
- const idxName = index.name ?? idxKey;
175
- throw new Error(
176
- `index ${idxName}: column '${col.name}' must be the exact same object as the corresponding column in the table (extract it to a const and reuse)`,
177
- );
178
- }
179
- }
180
- }
181
- return table;
1
+ // DB table definitions.
2
+ // Shape: { type: 'table', <extension fields> }
3
+
4
+ import type { EntityPhrase } from './dictionary.js';
5
+ import type { CollectionSchemaBase, EnumDef, Field } from './dsl.js';
6
+
7
+ export type Index = {
8
+ name?: string;
9
+ columns: Field | Field[];
10
+ unique?: boolean;
11
+ };
12
+
13
+ export type ForeignKey = {
14
+ columns: Field | Field[];
15
+ references: Field | Field[];
16
+ };
17
+
18
+ export interface TableSchemaOptions<
19
+ N extends string = string,
20
+ C extends Record<string, Field> = Record<string, Field>,
21
+ E extends Record<string, EnumDef> = Record<string, EnumDef>
22
+ > {
23
+ description?: string;
24
+ paginated: boolean;
25
+ actor?: boolean;
26
+ generator?: string;
27
+ autoIncrement?: Field;
28
+ /** 本表用于关联显示的名称字段(如 name / username)。被外键引用时,自动用该字段做 label 展示。 */
29
+ label?: Field;
30
+ /** Optimistic lock version column: updates auto-manage `version = version + 1`
31
+ * and `WHERE version = ?` (value taken from the row). Must be an integer column. */
32
+ version?: Field;
33
+ primaryKey?: Field | Field[];
34
+ indexes?: Index[];
35
+ foreignKeys?: Record<string, ForeignKey>;
36
+ /** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
37
+ phrase?: EntityPhrase;
38
+ /** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
39
+ enums: E;
40
+ columns: C;
41
+ }
42
+
43
+ export class TableSchema<
44
+ N extends string = string,
45
+ C extends Record<string, Field> = Record<string, Field>,
46
+ E extends Record<string, EnumDef> = Record<string, EnumDef>
47
+ > implements CollectionSchemaBase {
48
+ type = 'table';
49
+ name: N;
50
+ description?: string;
51
+ /** 分页 */
52
+ paginated: boolean;
53
+ /** 系统操作者(如小程序为 C 端用户,管理端为运营) */
54
+ actor?: boolean;
55
+ /** id 生成器 */
56
+ generator?: string;
57
+ /** 自增主键字段 */
58
+ autoIncrement?: Field;
59
+ primaryKey?: Field | Field[];
60
+ indexes?: Index[];
61
+ /** 外键,引用其他表的字段 */
62
+ foreignKeys?: Record<string, ForeignKey>;
63
+ /** 本表用于关联显示的名称字段(如 name / username)。被外键引用时,自动用该字段做 label 展示。 */
64
+ label?: Field;
65
+ /** Optimistic lock version column: updates auto-manage `version = version + 1`
66
+ * and `WHERE version = ?` (value taken from the row). Must be an integer column. */
67
+ version?: Field;
68
+ /** 引用的实体短语(词典条目):本表归属的实体;关联表等多实体场景不需要 */
69
+ phrase?: EntityPhrase;
70
+ /** 本表引用的所有枚举定义(map,key 为枚举标识),显式声明供 gen-enums 收集 */
71
+ enums: E;
72
+ columns: C;
73
+
74
+ constructor(name: N, options: TableSchemaOptions<N, C, E>) {
75
+ if (!options.enums) {
76
+ throw new Error(`table '${name}': enums is required — declare enums: {} when the table has no enums`);
77
+ }
78
+ if (options.paginated === undefined || options.paginated === null) {
79
+ throw new Error(`table '${name}': paginated is required — discuss with user whether this table needs pagination, then set paginated: true or paginated: false`);
80
+ }
81
+ this.name = name;
82
+ this.description = options.description;
83
+ this.paginated = options.paginated;
84
+ this.actor = options.actor;
85
+ this.generator = options.generator;
86
+ this.autoIncrement = options.autoIncrement;
87
+ this.primaryKey = options.primaryKey;
88
+ this.indexes = options.indexes;
89
+ this.foreignKeys = options.foreignKeys;
90
+ this.label = options.label;
91
+ this.version = options.version;
92
+ this.phrase = options.phrase;
93
+ this.enums = options.enums;
94
+ this.columns = options.columns;
95
+ }
96
+
97
+ /** True when the field is part of this table's primary key */
98
+ isPk(fieldRef: Field): boolean {
99
+ if (this.primaryKey === undefined) return false;
100
+ return Array.isArray(this.primaryKey)
101
+ ? this.primaryKey.includes(fieldRef)
102
+ : this.primaryKey === fieldRef;
103
+ }
104
+ }
105
+
106
+ export function defineTable<
107
+ N extends string,
108
+ C extends Record<string, Field>,
109
+ E extends Record<string, EnumDef> = Record<string, EnumDef>
110
+ >(
111
+ name: N,
112
+ schema: TableSchemaOptions<N, C, E>,
113
+ ): TableSchema<N, C, E> {
114
+ const table = new TableSchema<N, C, E>(name, schema);
115
+ if (table.generator && table.autoIncrement) {
116
+ throw new Error(`table '${name}': generator and autoIncrement are mutually exclusive`);
117
+ }
118
+ if (table.label && !Object.values(table.columns).includes(table.label)) {
119
+ throw new Error(`table '${name}': label field '${table.label.name}' must be one of the table's columns`);
120
+ }
121
+ if (table.version && !Object.values(table.columns).includes(table.version)) {
122
+ throw new Error(`table '${name}': version field '${table.version.name}' must be one of the table's columns`);
123
+ }
124
+ if (table.version && table.version.jsType !== 'number') {
125
+ throw new Error(`table '${name}': version field '${table.version.name}' must be an integer column`);
126
+ }
127
+ for (const key of Object.keys(table.columns)) {
128
+ const field = table.columns[key] as Field;
129
+ if (field.schema && field.schema !== table) {
130
+ throw new Error(
131
+ `field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`,
132
+ );
133
+ }
134
+ if (field.type === 'array' || field.type === 'object') {
135
+ throw new Error(
136
+ `table '${name}': column '${key}' cannot be a nested ${field.type} field — wire-format nesting is not a table column`,
137
+ );
138
+ }
139
+ }
140
+ for (const key of Object.keys(table.columns)) {
141
+ table.columns[key].name = key;
142
+ table.columns[key].schema = table;
143
+ }
144
+ if (table.primaryKey) {
145
+ const pkFields = Array.isArray(table.primaryKey) ? table.primaryKey : [table.primaryKey];
146
+ const colRefs = Object.values(table.columns);
147
+ for (const pk of pkFields) {
148
+ if (!colRefs.includes(pk)) {
149
+ throw new Error(
150
+ `table '${name}': primaryKey field '${pk.name}' must be the exact same object as the corresponding column (extract it to a const and reuse)`,
151
+ );
152
+ }
153
+ }
154
+ }
155
+ for (const [fkName, fk] of Object.entries(table.foreignKeys ?? {})) {
156
+ const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
157
+ const fields = Array.isArray(fk.columns) ? fk.columns : [fk.columns];
158
+ const colRefs = Object.values(table.columns);
159
+ for (const fkField of fields) {
160
+ if (!colRefs.includes(fkField)) {
161
+ throw new Error(
162
+ `foreign key ${fkName}: column '${fkField.name}' must be the exact same object as the corresponding column in the table (extract it to a const and reuse)`,
163
+ );
164
+ }
165
+ }
166
+ for (let i = 0; i < refs.length; i++) {
167
+ const ref = refs[i];
168
+ if (!ref.schema) throw new Error(`foreign key ${fkName}: references field has no schema`);
169
+ if (ref.schema === table) throw new Error(`foreign key ${fkName}: cannot reference own table ${table.name}`);
170
+ const phrase = (ref.schema as TableSchema).phrase;
171
+ if (!phrase) throw new Error(`foreign key ${fkName}: referenced table ${ref.schema.name} has no phrase, cannot check field naming`);
172
+ const expected = `${phrase.name}_${ref.name}`;
173
+ const fkField = fields[i];
174
+ if (fkField.name !== expected) {
175
+ throw new Error(
176
+ `foreign key ${fkName}: field must be named ${expected} (phrase ${phrase.name} + ${ref.name}), got ${fkField.name}`,
177
+ );
178
+ }
179
+ }
180
+ }
181
+ for (const [idxKey, index] of Object.entries(table.indexes ?? {})) {
182
+ const cols = Array.isArray(index.columns) ? index.columns : [index.columns];
183
+ const colRefs = Object.values(table.columns);
184
+ for (const col of cols) {
185
+ if (typeof col !== 'object' || col === null || typeof (col as Field).type !== 'string') {
186
+ const idxName = index.name ?? idxKey;
187
+ throw new Error(
188
+ `index ${idxName}: columns must be Field instances (stringField()/intField()/...), got ${JSON.stringify(col)}`,
189
+ );
190
+ }
191
+ if (!colRefs.includes(col)) {
192
+ const idxName = index.name ?? idxKey;
193
+ throw new Error(
194
+ `index ${idxName}: column '${col.name}' must be the exact same object as the corresponding column in the table (extract it to a const and reuse)`,
195
+ );
196
+ }
197
+ }
198
+ }
199
+ return table;
182
200
  }
@@ -0,0 +1,74 @@
1
+ import { SchemaBase } from './dsl.js';
2
+ import type { DtoField } from './dto.js';
3
+ import type { FlowSchema } from './flow.js';
4
+
5
+ /**
6
+ * Domain event declaration: a fact that has happened ("OrderCancelled"),
7
+ * carrying a data snapshot (not references). Declared as a first-class schema
8
+ * so the flow `publish` action can be compile-time checked against it (event
9
+ * name exists, payload fields match) and the outbox table + handler wiring can
10
+ * be generated.
11
+ *
12
+ * Command vs event: a command says "do something" (future tense, one receiver,
13
+ * caller senses failure); an event says "something happened" (past tense, zero
14
+ * to many subscribers, publisher does not care who handles it).
15
+ */
16
+ export interface DomainEventSchema extends SchemaBase {
17
+ type: 'domain-event';
18
+ /** Payload fields: data snapshot, not references. */
19
+ fields: Record<string, DtoField>;
20
+ }
21
+
22
+ export function defineDomainEvent(options: {
23
+ name: string;
24
+ fields: Record<string, DtoField>;
25
+ description?: string;
26
+ }): DomainEventSchema {
27
+ if (Object.keys(options.fields).length === 0) {
28
+ throw new Error(`domain event '${options.name}': fields must not be empty`);
29
+ }
30
+ return {
31
+ type: 'domain-event',
32
+ name: options.name,
33
+ description: options.description,
34
+ fields: options.fields,
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Event subscription: declares that a handler processes a domain event.
40
+ * The processing logic is a flow (the flow model is the execution model — a
41
+ * handler flow receives the event payload as its input slot). A plain async
42
+ * function is accepted as the runtime path until a flow executor exists; the
43
+ * declared flow is then compile-time checked (payload fields match the event)
44
+ * and drives generation.
45
+ */
46
+ export interface EventHandlerSchema extends SchemaBase {
47
+ type: 'event-handler';
48
+ /** Subscribed event name (must match a defineDomainEvent name). */
49
+ event: string;
50
+ /** Processing flow — receives the event payload as its input slot. */
51
+ flow?: FlowSchema;
52
+ /** Runtime handler function (used directly when no flow executor exists). */
53
+ handler?: (payload: Record<string, unknown>) => Promise<void> | void;
54
+ }
55
+
56
+ export function defineEventHandler(options: {
57
+ name: string;
58
+ event: string;
59
+ flow?: FlowSchema;
60
+ handler?: (payload: Record<string, unknown>) => Promise<void> | void;
61
+ description?: string;
62
+ }): EventHandlerSchema {
63
+ if (options.flow === undefined && options.handler === undefined) {
64
+ throw new Error(`event handler '${options.name}': at least one of flow or handler is required`);
65
+ }
66
+ return {
67
+ type: 'event-handler',
68
+ name: options.name,
69
+ description: options.description,
70
+ event: options.event,
71
+ flow: options.flow,
72
+ handler: options.handler,
73
+ };
74
+ }
package/src/dsl.ts CHANGED
@@ -4,9 +4,10 @@
4
4
  import type { DictionaryEntry } from './dictionary.js';
5
5
  import type { ImportBase } from './import-base.js';
6
6
  import type { MockDescriptor } from './mock.js';
7
+ import type { ComputeExpr } from './expr.js';
7
8
 
8
9
  /** Field query comparison operators (search field semantics). */
9
- export type Operator = 'eq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ne';
10
+ export type Operator = 'eq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ne' | 'in' | 'null' | 'notNull';
10
11
 
11
12
  export interface SchemaBase {
12
13
  name: string;
@@ -143,6 +144,32 @@ interface JsonField extends BaseField {
143
144
  jsType: 'object';
144
145
  }
145
146
 
147
+ /** Recursive array field — wire-format nesting (third-party messages), not a table column. */
148
+ interface ArrayField extends BaseField {
149
+ type: 'array';
150
+ jsType: 'array';
151
+ /** Element type: any Field, including nested array/object. */
152
+ items: Field;
153
+ }
154
+
155
+ /** Recursive object field — wire-format nesting (third-party messages), not a table column. */
156
+ interface ObjectField extends BaseField {
157
+ type: 'object';
158
+ jsType: 'object';
159
+ properties: Record<string, Field>;
160
+ }
161
+
162
+ /** Aggregate result column (count/sum/avg) of an aggregate query — a Field
163
+ * so that aggregate result entities can carry it like any other column.
164
+ * jsType follows the aggregate precision rule: count → number;
165
+ * sum/avg over an integer column → number, anything else → string. */
166
+ interface AggregateField extends BaseField {
167
+ type: 'aggregate';
168
+ jsType: 'number' | 'string';
169
+ /** The aggregate expression producing this column. */
170
+ expr: ComputeExpr;
171
+ }
172
+
146
173
  export type Field =
147
174
  | StringField
148
175
  | TextField
@@ -155,7 +182,10 @@ export type Field =
155
182
  | TimeField
156
183
  | DateTimeField
157
184
  | EnumField
158
- | JsonField;
185
+ | JsonField
186
+ | ArrayField
187
+ | ObjectField
188
+ | AggregateField;
159
189
 
160
190
  // Field builders: type and jsType are fixed, pass extra properties only.
161
191
  // The field name is written back from the map key later (see defineTable).
@@ -206,7 +236,23 @@ export function jsonField(extra: FieldExtras<JsonField> = {}): JsonField {
206
236
  return { name: '', type: 'json', jsType: 'object', ...extra };
207
237
  }
208
238
 
239
+ export function arrayField(extra: FieldExtras<ArrayField>): ArrayField {
240
+ return { name: '', type: 'array', jsType: 'array', ...extra };
241
+ }
242
+
243
+ export function objectField(extra: FieldExtras<ObjectField>): ObjectField {
244
+ return { name: '', type: 'object', jsType: 'object', ...extra };
245
+ }
246
+
209
247
  export function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): EnumField {
210
248
  const jsType = extra.enum.valueType === 'integer' ? 'number' : 'string';
211
249
  return { name: '', type: 'enum', jsType, ...extra };
250
+ }
251
+
252
+ /** Aggregate result column field: count → number; sum/avg over an integer
253
+ * column → number, anything else (decimal/bigint/rate…) → string.
254
+ * Used in aggregate-query result entities (see AggregateSchema). */
255
+ export function aggField(name: string, expr: ComputeExpr): AggregateField {
256
+ const jsType = expr.field === undefined || expr.field.type === 'integer' ? 'number' : 'string';
257
+ return { name, type: 'aggregate', jsType, expr };
212
258
  }