@pylonts/dsl 1.1.5 → 1.1.6

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 (62) hide show
  1. package/dist/controller.d.ts +27 -0
  2. package/dist/controller.js +11 -0
  3. package/dist/convert.d.ts +18 -6
  4. package/dist/convert.js +12 -2
  5. package/dist/curd.d.ts +0 -2
  6. package/dist/curd.js +7 -0
  7. package/dist/dao.d.ts +111 -2
  8. package/dist/dao.js +28 -2
  9. package/dist/db.js +3 -0
  10. package/dist/dsl.d.ts +16 -1
  11. package/dist/dsl.js +6 -0
  12. package/dist/dto.d.ts +6 -2
  13. package/dist/dto.js +20 -9
  14. package/dist/endpoint.d.ts +15 -0
  15. package/dist/endpoint.js +3 -0
  16. package/dist/exception.d.ts +14 -0
  17. package/dist/exception.js +9 -0
  18. package/dist/field-rule.d.ts +20 -0
  19. package/dist/field-rule.js +19 -0
  20. package/dist/flow.d.ts +24 -2
  21. package/dist/flow.js +16 -4
  22. package/dist/index.d.ts +7 -0
  23. package/dist/index.js +9 -0
  24. package/dist/mermaid-driver.js +32 -3
  25. package/dist/method.d.ts +11 -0
  26. package/dist/method.js +3 -0
  27. package/dist/mysql-driver.js +4 -0
  28. package/dist/provider.d.ts +6 -11
  29. package/dist/provider.js +2 -2
  30. package/dist/service.d.ts +16 -6
  31. package/dist/service.js +13 -2
  32. package/dist/third-service.d.ts +75 -0
  33. package/dist/third-service.js +96 -0
  34. package/dist/typebox-driver.d.ts +6 -0
  35. package/dist/typebox-driver.js +73 -12
  36. package/dist/utils.d.ts +25 -10
  37. package/dist/utils.js +28 -11
  38. package/docs/dto.md +73 -66
  39. package/docs/third-service.md +122 -0
  40. package/package.json +4 -1
  41. package/src/controller.ts +40 -0
  42. package/src/convert.ts +42 -15
  43. package/src/curd.ts +98 -93
  44. package/src/dao.ts +172 -13
  45. package/src/db.ts +186 -181
  46. package/src/dsl.ts +26 -1
  47. package/src/dto.ts +263 -247
  48. package/src/endpoint.ts +18 -0
  49. package/src/exception.ts +28 -0
  50. package/src/field-rule.ts +47 -0
  51. package/src/flow.ts +143 -103
  52. package/src/index.ts +43 -33
  53. package/src/mermaid-driver.ts +112 -84
  54. package/src/method.ts +20 -0
  55. package/src/mysql-driver.ts +4 -0
  56. package/src/provider.ts +67 -72
  57. package/src/service.ts +42 -20
  58. package/src/third-service.ts +186 -0
  59. package/src/typebox-driver.ts +82 -11
  60. package/src/utils.ts +63 -26
  61. package/dist/check-inheritance.d.ts +0 -9
  62. package/dist/check-inheritance.js +0 -58
package/src/dto.ts CHANGED
@@ -1,248 +1,264 @@
1
- import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase } from './dsl.js';
2
- import { TableSchema } from './db.js';
3
- import type { ImportBase } from './import-base.js';
4
- import { toCamelCase } from './utils.js';
5
-
6
- // Interface (DTO) field definitions.
7
- // Naming convention: all DTO types and builders use the Dto prefix.
8
- // A DtoField stores the database Field and the API-only extras separately.
9
-
10
- /** Re-export — Operator lives on the DSL level (see dsl.ts). */
11
- export type { Operator } from './dsl.js';
12
-
13
- /** Re-export — ImportBase lives on its own module (see import-base.ts). */
14
- export type { ImportBase } from './import-base.js';
15
-
16
- /** Re-export — MockDescriptor lives on its own module (see mock.ts). */
17
- export type { MockDescriptor } from './mock.js';
18
-
19
- /**
20
- * Reference to an existing TypeBox base schema by its import location.
21
- * Serializable metadata: the driver renders `import { name } from 'from'`
22
- * and `Type.Intersect([name, ...])` — the runtime schema is never loaded by the DSL.
23
- */
24
- export interface ImportRef extends ImportBase {
25
- /**
26
- * Generic type arguments for the base schema (e.g. PageResult(OrderRow)).
27
- * Two forms, both local DTOs:
28
- * string — the DTO export name
29
- * DtoMessage — the DTO instance itself; the driver resolves it to its name
30
- */
31
- args?: (string | DtoMessage)[];
32
- }
33
-
34
- export type DtoArrayFieldDef = BaseField & {
35
- type: 'array';
36
- jsType: 'array';
37
- /** Element type: inline DtoField, or a reference to an existing DTO (rendered by name) */
38
- items: DtoField | DtoMessage;
39
- };
40
-
41
- export type DtoObjectFieldDef = BaseField & {
42
- type: 'object';
43
- jsType: 'object';
44
- properties: Record<string, DtoField>;
45
- };
46
-
47
- export class DtoField implements SchemaBase {
48
- /** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
49
- * 构造时未知,由 buildMessage map key 反写。 */
50
- name: string;
51
- /** 字段描述 */
52
- description?: string;
53
- /** 所属容器(buildMessage / defineRouteData / definePageData 反写) */
54
- schema?: CollectionSchemaBase;
55
- field: Field | DtoArrayFieldDef | DtoObjectFieldDef;
56
- pattern?: string;
57
- optional?: boolean;
58
- operator?: Operator;
59
- /** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
60
- default?: unknown;
61
-
62
- constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef) {
63
- this.name = '';
64
- this.field = field;
65
- }
66
-
67
- setPattern(value: string): this {
68
- this.pattern = value;
69
- return this;
70
- }
71
-
72
- setDescription(value: string): this {
73
- this.description = value;
74
- return this;
75
- }
76
-
77
- getDescription(): string | undefined {
78
- return this.description;
79
- }
80
-
81
- /** True when this field wraps a DB column (picked via from()); false for inline fields. */
82
- isColumn(): boolean {
83
- return this.field.schema?.type === 'table';
84
- }
85
-
86
- setOptional(value: boolean): this {
87
- this.optional = value;
88
- return this;
89
- }
90
-
91
- /** Set a default value — emitted as a TypeBox schema default annotation */
92
- setDefault(value: unknown): this {
93
- this.default = value;
94
- return this;
95
- }
96
-
97
- /** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
98
- setOperator(value: Operator): this {
99
- this.operator = value;
100
- return this;
101
- }
102
-
103
- /** optional 优先于 field.optional */
104
- isOptional(): boolean {
105
- if (this.optional !== undefined) return this.optional;
106
- return this.field.optional ?? false;
107
- }
108
- }
109
-
110
- export class DtoArrayField extends DtoField {
111
- declare field: DtoArrayFieldDef;
112
-
113
- /** Element type: inline DtoField or a referenced DtoMessage (rendered by name). */
114
- items(): DtoField | DtoMessage {
115
- return this.field.items;
116
- }
117
- }
118
-
119
- export class DtoObjectField extends DtoField {
120
- declare field: DtoObjectFieldDef;
121
-
122
- properties(): Record<string, DtoField> {
123
- return this.field.properties;
124
- }
125
- }
126
-
127
- export enum DtoDirection {
128
- Input = 'input',
129
- Output = 'output',
130
- Query = 'query',
131
- Pk = 'pk',
132
- }
133
-
134
- export class DtoMessage implements CollectionSchemaBase {
135
- type = 'dto';
136
- name: string;
137
- description?: string;
138
- /** 方向:输入或输出 */
139
- direction: DtoDirection;
140
- fields: Record<string, DtoField>;
141
- /** TypeBox base schemas to intersect with at generation time (e.g. PageRequest) */
142
- bases: ImportRef[] = [];
143
-
144
- constructor(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string) {
145
- this.name = name;
146
- this.direction = direction;
147
- this.fields = fields;
148
- this.description = description;
149
- }
150
-
151
- /** 引用已存在的 TypeBox base schema,例如 include({ from: '@pylonts/core', name: 'PageRequest' }) */
152
- include(...refs: ImportRef[]): this {
153
- this.bases.push(...refs);
154
- return this;
155
- }
156
- }
157
-
158
- export function dtoField(field: Field): DtoField {
159
- return new DtoField(field);
160
- }
161
-
162
- export function dtoArrayField(def: { items: DtoField | DtoMessage } & Omit<BaseField, 'name'>): DtoArrayField {
163
- // Items stay as-is: an inline DtoField is rendered inline, a DtoMessage is
164
- // referenced by name (the driver renders Type.Array(<DtoName>)).
165
- return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def });
166
- }
167
-
168
- export function dtoObjectField(def: { properties: Record<string, DtoField> } & Omit<BaseField, 'name'>): DtoObjectField {
169
- return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def });
170
- }
171
-
172
- function buildMessage(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string): DtoMessage {
173
- const message = new DtoMessage(name, direction, fields, description);
174
- // Write back the DTO field name from the map key (safe: DtoField instances
175
- // are created per DTO, never shared).
176
- for (const key of Object.keys(message.fields)) {
177
- const df = message.fields[key];
178
- if (!(df instanceof DtoField)) {
179
- const got = df === null || df === undefined ? String(df) : `${(df as object).constructor.name ?? typeof df}`;
180
- throw new Error(
181
- `[dto] DTO "${name}" field "${key}" must be a DtoField (created with dtoField()/dtoArrayField()/dtoObjectField()), got ${got}. ` +
182
- `Did you pass enumField(...) directly? Use dtoField(enumField({...})) instead.`
183
- );
184
- }
185
- df.name = key;
186
- df.schema = message;
187
- // Custom (inline) fields are owned by this DTO: write back name + schema.
188
- // Fields picked via from() share the database Field instance whose
189
- // name/schema already point to the table leave them untouched.
190
- if (df.field.schema === undefined) {
191
- df.field.name = key;
192
- df.field.schema = message;
193
- }
194
- }
195
- return message;
196
- }
197
-
198
- export function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
199
- const message = buildMessage(name, DtoDirection.Input, fields, description);
200
- // Rule A set optionality from the DB column rule (skips fields the author
201
- // already set): nullable / default optional, else required.
202
- // PK columns are always required.
203
- for (const field of Object.values(message.fields)) {
204
- if (field.optional !== undefined) continue;
205
- const f = field.field as Field;
206
- if (f.schema?.type !== 'table') continue;
207
- const table = f.schema as TableSchema;
208
- field.optional = table.isPk(f) ? false : f.optional !== false || f.default !== undefined;
209
- }
210
- return message;
211
- }
212
-
213
- export function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
214
- return buildMessage(name, DtoDirection.Output, fields, description);
215
- }
216
-
217
- export function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
218
- const message = buildMessage(name, DtoDirection.Query, fields, description);
219
- // Rule B: query/search fields are always optional.
220
- for (const field of Object.values(message.fields)) field.optional = true;
221
- return message;
222
- }
223
-
224
- export function buildPk(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
225
- const message = buildMessage(name, DtoDirection.Pk, fields, description);
226
- // Rule P: PK locator fields are required, other fields are optional.
227
- for (const field of Object.values(message.fields)) {
228
- if (field.optional !== undefined) continue;
229
- const f = field.field as Field;
230
- const table = f.schema as TableSchema | undefined;
231
- field.optional = table !== undefined && table.isPk(f) ? false : true;
232
- }
233
- return message;
234
- }
235
-
236
- /** Pick columns from a built table and wrap them as DTO fields (aligned with dto.from). */
237
- export function from(table: TableSchema, fields: Field[]): Record<string, DtoField> {
238
- const out: Record<string, DtoField> = {};
239
- for (const field of fields) {
240
- if (field.schema !== table) {
241
- throw new Error(`dto.from(${table.name}): field ${field.name} does not belong to this table`);
242
- }
243
- // DTO field name is camelCase (mer_id → merId); the underlying field.name
244
- // stays snake_case (DB column).
245
- out[toCamelCase(field.name)] = dtoField(field);
246
- }
247
- return out;
1
+ import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase } from './dsl.js';
2
+ import { TableSchema } from './db.js';
3
+ import type { ImportBase } from './import-base.js';
4
+ import type { ThirdMethodSchema } from './third-service.js';
5
+ import { toCamelCase } from '@pylonts/core';
6
+
7
+ // Interface (DTO) field definitions.
8
+ // Naming convention: all DTO types and builders use the Dto prefix.
9
+ // A DtoField stores the database Field and the API-only extras separately.
10
+
11
+ /** Re-export Operator lives on the DSL level (see dsl.ts). */
12
+ export type { Operator } from './dsl.js';
13
+
14
+ /** Re-export ImportBase lives on its own module (see import-base.ts). */
15
+ export type { ImportBase } from './import-base.js';
16
+
17
+ /** Re-export MockDescriptor lives on its own module (see mock.ts). */
18
+ export type { MockDescriptor } from './mock.js';
19
+
20
+ /**
21
+ * Reference to an existing TypeBox base schema by its import location.
22
+ * Serializable metadata: the driver renders `import { name } from 'from'`
23
+ * and `Type.Intersect([name, ...])` — the runtime schema is never loaded by the DSL.
24
+ */
25
+ export interface ImportRef extends ImportBase {
26
+ /**
27
+ * Generic type arguments for the base schema (e.g. PageResult(OrderRow)).
28
+ * Two forms, both local DTOs:
29
+ * string — the DTO export name
30
+ * DtoMessage — the DTO instance itself; the driver resolves it to its name
31
+ */
32
+ args?: (string | DtoMessage)[];
33
+ }
34
+
35
+ export type DtoArrayFieldDef = BaseField & {
36
+ type: 'array';
37
+ jsType: 'array';
38
+ /** Element type: inline DtoField, or a reference to an existing DTO (rendered by name) */
39
+ items: DtoField | DtoMessage;
40
+ };
41
+
42
+ export type DtoObjectFieldDef = BaseField & {
43
+ type: 'object';
44
+ jsType: 'object';
45
+ properties: Record<string, DtoField>;
46
+ };
47
+
48
+ export class DtoField implements SchemaBase {
49
+ /** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
50
+ * 构造时未知,由 buildMessage 从 map key 反写。 */
51
+ name: string;
52
+ /** 字段描述 */
53
+ description?: string;
54
+ /** 所属容器(buildMessage / defineRouteData / definePageData 反写) */
55
+ schema?: CollectionSchemaBase;
56
+ field: Field | DtoArrayFieldDef | DtoObjectFieldDef;
57
+ pattern?: string;
58
+ optional?: boolean;
59
+ operator?: Operator;
60
+ /** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
61
+ default?: unknown;
62
+
63
+ constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef) {
64
+ this.name = '';
65
+ this.field = field;
66
+ }
67
+
68
+ setPattern(value: string): this {
69
+ this.pattern = value;
70
+ return this;
71
+ }
72
+
73
+ setDescription(value: string): this {
74
+ this.description = value;
75
+ return this;
76
+ }
77
+
78
+ getDescription(): string | undefined {
79
+ return this.description;
80
+ }
81
+
82
+ /** True when this field wraps a DB column (picked via from()); false for inline fields. */
83
+ isColumn(): boolean {
84
+ return this.field.schema?.type === 'table';
85
+ }
86
+
87
+ setOptional(value: boolean): this {
88
+ this.optional = value;
89
+ return this;
90
+ }
91
+
92
+ /** Set a default value emitted as a TypeBox schema default annotation */
93
+ setDefault(value: unknown): this {
94
+ this.default = value;
95
+ return this;
96
+ }
97
+
98
+ /** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
99
+ setOperator(value: Operator): this {
100
+ this.operator = value;
101
+ return this;
102
+ }
103
+
104
+ /** optional 优先于 field.optional */
105
+ isOptional(): boolean {
106
+ if (this.optional !== undefined) return this.optional;
107
+ return this.field.optional ?? false;
108
+ }
109
+ }
110
+
111
+ export class DtoArrayField extends DtoField {
112
+ declare field: DtoArrayFieldDef;
113
+
114
+ /** Element type: inline DtoField or a referenced DtoMessage (rendered by name). */
115
+ items(): DtoField | DtoMessage {
116
+ return this.field.items;
117
+ }
118
+ }
119
+
120
+ export class DtoObjectField extends DtoField {
121
+ declare field: DtoObjectFieldDef;
122
+
123
+ properties(): Record<string, DtoField> {
124
+ return this.field.properties;
125
+ }
126
+ }
127
+
128
+ export enum DtoDirection {
129
+ Input = 'input',
130
+ Output = 'output',
131
+ Query = 'query',
132
+ Pk = 'pk',
133
+ }
134
+
135
+ export class DtoMessage implements CollectionSchemaBase {
136
+ type = 'dto';
137
+ name: string;
138
+ description?: string;
139
+ /** 方向:输入或输出 */
140
+ direction: DtoDirection;
141
+ fields: Record<string, DtoField>;
142
+ /** TypeBox base schemas to intersect with at generation time (e.g. PageRequest) */
143
+ bases: ImportRef[] = [];
144
+
145
+ constructor(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string) {
146
+ this.name = name;
147
+ this.direction = direction;
148
+ this.fields = fields;
149
+ this.description = description;
150
+ }
151
+
152
+ /** 引用已存在的 TypeBox base schema,例如 include({ from: '@pylonts/core', name: 'PageRequest' }) */
153
+ include(...refs: ImportRef[]): this {
154
+ this.bases.push(...refs);
155
+ return this;
156
+ }
157
+ }
158
+
159
+ export function dtoField(field: Field): DtoField {
160
+ return new DtoField(field);
161
+ }
162
+
163
+ export function dtoArrayField(def: { items: DtoField | DtoMessage } & Omit<BaseField, 'name'>): DtoArrayField {
164
+ // Items stay as-is: an inline DtoField is rendered inline, a DtoMessage is
165
+ // referenced by name (the driver renders Type.Array(<DtoName>)).
166
+ return new DtoArrayField({ name: '', type: 'array', jsType: 'array', ...def });
167
+ }
168
+
169
+ export function dtoObjectField(def: { properties: Record<string, DtoField> } & Omit<BaseField, 'name'>): DtoObjectField {
170
+ return new DtoObjectField({ name: '', type: 'object', jsType: 'object', ...def });
171
+ }
172
+
173
+ function buildMessage(name: string, direction: DtoDirection, fields: Record<string, DtoField>, description?: string): DtoMessage {
174
+ const message = new DtoMessage(name, direction, fields, description);
175
+ // Write back the DTO field name from the map key (safe: DtoField instances
176
+ // are created per DTO, never shared).
177
+ for (const key of Object.keys(message.fields)) {
178
+ const df = message.fields[key];
179
+ if (!(df instanceof DtoField)) {
180
+ const got = df === null || df === undefined ? String(df) : `${(df as object).constructor.name ?? typeof df}`;
181
+ throw new Error(
182
+ `[dto] DTO "${name}" field "${key}" must be a DtoField (created with dtoField()/dtoArrayField()/dtoObjectField()), got ${got}. ` +
183
+ `Did you pass enumField(...) directly? Use dtoField(enumField({...})) instead.`
184
+ );
185
+ }
186
+ df.name = key;
187
+ df.schema = message;
188
+ // Custom (inline) fields are owned by this DTO: write back name + schema.
189
+ // Fields picked via from() share the database Field instance whose
190
+ // name/schema already point to the table — leave them untouched.
191
+ if (df.field.schema === undefined) {
192
+ df.field.name = key;
193
+ df.field.schema = message;
194
+ }
195
+ }
196
+ return message;
197
+ }
198
+
199
+ export function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
200
+ const message = buildMessage(name, DtoDirection.Input, fields, description);
201
+ // Rule A — set optionality from the DB column rule (skips fields the author
202
+ // already set): nullable / default → optional, else required.
203
+ // PK columns are always required.
204
+ for (const field of Object.values(message.fields)) {
205
+ if (field.optional !== undefined) continue;
206
+ const f = field.field as Field;
207
+ if (f.schema?.type !== 'table') continue;
208
+ const table = f.schema as TableSchema;
209
+ field.optional = table.isPk(f) ? false : f.optional !== false || f.default !== undefined;
210
+ }
211
+ return message;
212
+ }
213
+
214
+ export function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
215
+ return buildMessage(name, DtoDirection.Output, fields, description);
216
+ }
217
+
218
+ export function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
219
+ const message = buildMessage(name, DtoDirection.Query, fields, description);
220
+ // Rule B: query/search fields are always optional.
221
+ for (const field of Object.values(message.fields)) field.optional = true;
222
+ return message;
223
+ }
224
+
225
+ export function buildPk(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
226
+ const message = buildMessage(name, DtoDirection.Pk, fields, description);
227
+ // Rule P: PK locator fields are required, other fields are optional.
228
+ for (const field of Object.values(message.fields)) {
229
+ if (field.optional !== undefined) continue;
230
+ const f = field.field as Field;
231
+ const table = f.schema?.type === 'table' ? (f.schema as TableSchema) : undefined;
232
+ field.optional = table !== undefined && table.isPk(f) ? false : true;
233
+ }
234
+ return message;
235
+ }
236
+
237
+ /** Field-collection source a DTO can project from: a DB table or a third-party method message. */
238
+ export type DtoFieldSource = TableSchema | ThirdMethodSchema;
239
+
240
+ // Type guard instead of a plain discriminant check: TableSchema.type is declared
241
+ // as a class property, so TS widens it to string and cannot narrow the union.
242
+ function isThirdMethod(source: DtoFieldSource): source is ThirdMethodSchema {
243
+ return source.type === 'thirdMethod';
244
+ }
245
+
246
+ function ownsField(source: DtoFieldSource, field: Field): boolean {
247
+ if (isThirdMethod(source)) return Object.values(source.fields).includes(field);
248
+ return Object.values(source.columns).includes(field);
249
+ }
250
+
251
+ /** Project fields from a field-collection source (table or third-party method
252
+ * message) and wrap them as DTO fields (aligned with dto.from). */
253
+ export function from(source: DtoFieldSource, fields: Field[]): Record<string, DtoField> {
254
+ const out: Record<string, DtoField> = {};
255
+ for (const field of fields) {
256
+ if (!ownsField(source, field)) {
257
+ throw new Error(`dto.from(${source.name}): field ${field.name} does not belong to this ${source.type}`);
258
+ }
259
+ // DB columns map to camelCase interface names (mer_id → merId); wire-format
260
+ // names are protocol names themselves and stay untouched.
261
+ out[isThirdMethod(source) ? field.name : toCamelCase(field.name)] = dtoField(field);
262
+ }
263
+ return out;
248
264
  }
@@ -0,0 +1,18 @@
1
+ import { SchemaBase } from './dsl.js';
2
+ import type { DtoMessage } from './dto.js';
3
+
4
+ /** Shared API call signature: one request DTO in, one response shape out.
5
+ * Referenced by both backend controller methods and frontend page providers —
6
+ * both sides must use the exact same instance, so drift is impossible. */
7
+ export interface EndpointSchema extends SchemaBase {
8
+ type: 'endpoint';
9
+ args: DtoMessage;
10
+ results: DtoMessage | number | boolean | string;
11
+ }
12
+
13
+ export function defineEndpoint(
14
+ name: string,
15
+ options: { args: DtoMessage; results: DtoMessage | number | boolean | string; description?: string },
16
+ ): EndpointSchema {
17
+ return { type: 'endpoint', name, args: options.args, results: options.results, description: options.description };
18
+ }
@@ -0,0 +1,28 @@
1
+ import type { SchemaBase } from './dsl.js';
2
+ import type { ImportBase } from './import-base.js';
3
+
4
+ // Exception declarations (logic-flatten-spec §5.3): each method declares the
5
+ // exceptions it throws, with retryability. The runtime class is referenced by
6
+ // import location — never loaded by the DSL.
7
+
8
+ /** Describes an exception a method can throw. */
9
+ export interface ExceptionSchema extends SchemaBase, Omit<ImportBase, 'type'> {
10
+ type: 'exception';
11
+ /** Whether the caller may safely retry after catching this exception. */
12
+ retryable: boolean;
13
+ }
14
+
15
+ export function defineException(options: {
16
+ name: string;
17
+ from: string;
18
+ retryable: boolean;
19
+ description?: string;
20
+ }): ExceptionSchema {
21
+ return {
22
+ type: 'exception',
23
+ name: options.name,
24
+ from: options.from,
25
+ retryable: options.retryable,
26
+ description: options.description,
27
+ };
28
+ }
@@ -0,0 +1,47 @@
1
+ import type { SchemaBase } from './dsl.js';
2
+
3
+ // Field rules: the third semantic dimension of a Field. A rule is a named
4
+ // transformation between two ends — scale (fen↔yuan), encrypt/decrypt,
5
+ // mask/unmask. The rule owns both ends; a binding says which field plays
6
+ // which end. Rules are unique per semantic name (checked at definition).
7
+
8
+ /** One end of a field rule (e.g. 'fen' / 'yuan', 'plain' / 'cipher', 'raw' / 'masked'). */
9
+ export interface FieldRuleEnd {
10
+ /** End name — written back by defineFieldRule from the ends map key. */
11
+ name: string;
12
+ description?: string;
13
+ }
14
+
15
+ /** A semantic transformation rule between two field representations. */
16
+ export interface FieldRuleSchema extends SchemaBase {
17
+ type: 'fieldRule';
18
+ /** Rule name — the semantic uniqueness key. */
19
+ name: string;
20
+ /** The two ends of the rule, keyed by end name. */
21
+ ends: Record<string, FieldRuleEnd>;
22
+ }
23
+
24
+ /** One rule per semantic name. Defining the same name twice throws. */
25
+ const ruleRegistry = new Map<string, FieldRuleSchema>();
26
+
27
+ export function defineFieldRule(options: {
28
+ name: string;
29
+ ends: Record<string, Omit<FieldRuleEnd, 'name'>>;
30
+ description?: string;
31
+ }): FieldRuleSchema {
32
+ if (ruleRegistry.has(options.name)) {
33
+ throw new Error(`fieldRule '${options.name}' is already defined — one rule per semantic`);
34
+ }
35
+ const ends: Record<string, FieldRuleEnd> = {};
36
+ for (const key of Object.keys(options.ends)) {
37
+ ends[key] = { name: key, description: options.ends[key].description };
38
+ }
39
+ const rule: FieldRuleSchema = {
40
+ type: 'fieldRule',
41
+ name: options.name,
42
+ description: options.description,
43
+ ends,
44
+ };
45
+ ruleRegistry.set(options.name, rule);
46
+ return rule;
47
+ }