@pylonts/dsl 1.1.4 → 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 (78) 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 +24 -1
  11. package/dist/dsl.js +16 -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 +8 -1
  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 +76 -14
  36. package/dist/utils.d.ts +25 -10
  37. package/dist/utils.js +28 -11
  38. package/docs/curd.md +110 -110
  39. package/docs/dto.md +9 -2
  40. package/docs/table.md +135 -135
  41. package/docs/third-service.md +122 -0
  42. package/package.json +4 -1
  43. package/src/action.ts +10 -10
  44. package/src/asset.ts +62 -62
  45. package/src/bases.ts +29 -29
  46. package/src/component.ts +21 -21
  47. package/src/controller.ts +40 -0
  48. package/src/convert.ts +34 -7
  49. package/src/curd.ts +7 -2
  50. package/src/dao.ts +162 -3
  51. package/src/db.ts +5 -0
  52. package/src/dsl.ts +49 -1
  53. package/src/dto.ts +25 -9
  54. package/src/endpoint.ts +18 -0
  55. package/src/event.ts +12 -12
  56. package/src/exception.ts +28 -0
  57. package/src/field-rule.ts +47 -0
  58. package/src/flow.ts +143 -103
  59. package/src/index.ts +11 -1
  60. package/src/mermaid-driver.ts +31 -3
  61. package/src/method.ts +20 -0
  62. package/src/mock.ts +12 -12
  63. package/src/mysql-driver.ts +8 -2
  64. package/src/navigation.ts +28 -28
  65. package/src/page-def.ts +79 -79
  66. package/src/page-flow.ts +153 -153
  67. package/src/page.ts +76 -76
  68. package/src/popup.ts +25 -25
  69. package/src/project.ts +97 -97
  70. package/src/provider.ts +7 -12
  71. package/src/ref.ts +18 -18
  72. package/src/route.ts +11 -11
  73. package/src/service.ts +28 -6
  74. package/src/third-service.ts +186 -0
  75. package/src/typebox-driver.ts +264 -192
  76. package/src/utils.ts +54 -17
  77. package/dist/check-inheritance.d.ts +0 -9
  78. package/dist/check-inheritance.js +0 -58
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonts/dsl",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "Schema definition DSL with drivers: MySQL DDL, TS enum, TypeBox schema codegen.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -24,6 +24,9 @@
24
24
  ],
25
25
  "author": "",
26
26
  "license": "MIT",
27
+ "dependencies": {
28
+ "@pylonts/core": "file:../pylon"
29
+ },
27
30
  "devDependencies": {
28
31
  "typescript": "^7.0.2",
29
32
  "vitest": "^4.1.10"
package/src/action.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { SchemaBase } from './dsl.js';
2
-
3
- /** An action a user can perform on a page (e.g. submit, approve, reject).
4
- * Subclasses use `type` as the discriminator. */
5
- export interface ActionSchema extends SchemaBase {
6
- type: string;
7
- }
8
-
9
- export function defineAction(name: string, description?: string): ActionSchema {
10
- return { name, description, type: 'gesture' };
1
+ import { SchemaBase } from './dsl.js';
2
+
3
+ /** An action a user can perform on a page (e.g. submit, approve, reject).
4
+ * Subclasses use `type` as the discriminator. */
5
+ export interface ActionSchema extends SchemaBase {
6
+ type: string;
7
+ }
8
+
9
+ export function defineAction(name: string, description?: string): ActionSchema {
10
+ return { name, description, type: 'gesture' };
11
11
  }
package/src/asset.ts CHANGED
@@ -1,63 +1,63 @@
1
- /**
2
- * defineAsset — registry for reusable project assets (utils, components, flows, pages, hooks).
3
- *
4
- * Each asset declares its name, category, import path, tags, and optional usage example.
5
- * CLI scans all asset declarations, supports query (by tag/category) and generate (import links).
6
- *
7
- * // assets/utils.assets.ts
8
- * import { defineAsset } from '@pylonts/dsl';
9
- * export const formatAmt = defineAsset({
10
- * name: 'formatAmt',
11
- * category: 'util',
12
- * tags: ['amount', 'format'],
13
- * import: { name: 'formatAmt', from: '@/utils/amount' },
14
- * example: 'formatAmt(12345) => "12,345.00"',
15
- * });
16
- *
17
- * // CLI:
18
- * // pylonts gen asset list --tag form → all form-related assets
19
- * // pylonts gen asset import formatAmt → import { formatAmt } from '@/utils/amount';
20
- */
21
-
22
- export type AssetCategory = 'util' | 'component' | 'flow' | 'page' | 'hook';
23
-
24
- export interface AssetImport {
25
- /** Named export, e.g. 'formatAmt' */
26
- name: string;
27
- /** Module path, e.g. '@/utils/amount' */
28
- from: string;
29
- }
30
-
31
- export interface AssetConfig {
32
- name: string;
33
- category: AssetCategory;
34
- tags: string[];
35
- import: AssetImport;
36
- description?: string;
37
- /** One-liner usage example */
38
- example?: string;
39
- /** Link to detailed docs */
40
- see?: string;
41
- }
42
-
43
- export interface AssetDef {
44
- name: string;
45
- category: AssetCategory;
46
- tags: string[];
47
- import: AssetImport;
48
- description?: string;
49
- example?: string;
50
- see?: string;
51
- }
52
-
53
- export function defineAsset(config: AssetConfig): AssetDef {
54
- return {
55
- name: config.name,
56
- category: config.category,
57
- tags: config.tags,
58
- import: config.import,
59
- description: config.description,
60
- example: config.example,
61
- see: config.see,
62
- };
1
+ /**
2
+ * defineAsset — registry for reusable project assets (utils, components, flows, pages, hooks).
3
+ *
4
+ * Each asset declares its name, category, import path, tags, and optional usage example.
5
+ * CLI scans all asset declarations, supports query (by tag/category) and generate (import links).
6
+ *
7
+ * // assets/utils.assets.ts
8
+ * import { defineAsset } from '@pylonts/dsl';
9
+ * export const formatAmt = defineAsset({
10
+ * name: 'formatAmt',
11
+ * category: 'util',
12
+ * tags: ['amount', 'format'],
13
+ * import: { name: 'formatAmt', from: '@/utils/amount' },
14
+ * example: 'formatAmt(12345) => "12,345.00"',
15
+ * });
16
+ *
17
+ * // CLI:
18
+ * // pylonts gen asset list --tag form → all form-related assets
19
+ * // pylonts gen asset import formatAmt → import { formatAmt } from '@/utils/amount';
20
+ */
21
+
22
+ export type AssetCategory = 'util' | 'component' | 'flow' | 'page' | 'hook';
23
+
24
+ export interface AssetImport {
25
+ /** Named export, e.g. 'formatAmt' */
26
+ name: string;
27
+ /** Module path, e.g. '@/utils/amount' */
28
+ from: string;
29
+ }
30
+
31
+ export interface AssetConfig {
32
+ name: string;
33
+ category: AssetCategory;
34
+ tags: string[];
35
+ import: AssetImport;
36
+ description?: string;
37
+ /** One-liner usage example */
38
+ example?: string;
39
+ /** Link to detailed docs */
40
+ see?: string;
41
+ }
42
+
43
+ export interface AssetDef {
44
+ name: string;
45
+ category: AssetCategory;
46
+ tags: string[];
47
+ import: AssetImport;
48
+ description?: string;
49
+ example?: string;
50
+ see?: string;
51
+ }
52
+
53
+ export function defineAsset(config: AssetConfig): AssetDef {
54
+ return {
55
+ name: config.name,
56
+ category: config.category,
57
+ tags: config.tags,
58
+ import: config.import,
59
+ description: config.description,
60
+ example: config.example,
61
+ see: config.see,
62
+ };
63
63
  }
package/src/bases.ts CHANGED
@@ -1,30 +1,30 @@
1
- import type { DtoMessage, ImportBase, ImportRef } from './dto.js';
2
-
3
- // Named base-schema references for common protocol DTOs.
4
- //
5
- // These are ImportRef metadata (not re-exports of the actual TypeBox schemas):
6
- // the DSL stores { from, name } so the generator can emit the import line and
7
- // the identifier — the runtime schema object itself is never loaded by the DSL.
8
- //
9
- // `import { PageRequest } from '@pylonts/dsl'` therefore gives .include() an
10
- // already-resolved reference — no static analysis or name lookup needed.
11
-
12
- /** Paginated query request base — renders `import { PageRequest } from '@pylonts/core'` + Intersect */
13
- export const PageRequest: ImportBase = { from: '@pylonts/core', name: 'PageRequest' };
14
-
15
- /** Paginated list response base — renders `import { PageResult } from '@pylonts/core'` + `PageResult(<row>)` */
16
- export const PageResult = (row: DtoMessage): ImportRef => ({
17
- from: '@pylonts/core',
18
- name: 'PageResult',
19
- args: [row],
20
- });
21
-
22
- /** Paged rows type base without generic args — renders `import type { PagedRows } from '@pylonts/core'` */
23
- export const PagedRows: ImportBase = { from: '@pylonts/core', name: 'PagedRows', type: true };
24
-
25
- /** Paged rows type base — renders `import { PagedRows } from '@pylonts/core'` + `PagedRows(<row>)` */
26
- export const PageRows = (row: DtoMessage): ImportRef => ({
27
- from: '@pylonts/core',
28
- name: 'PagedRows',
29
- args: [row],
1
+ import type { DtoMessage, ImportBase, ImportRef } from './dto.js';
2
+
3
+ // Named base-schema references for common protocol DTOs.
4
+ //
5
+ // These are ImportRef metadata (not re-exports of the actual TypeBox schemas):
6
+ // the DSL stores { from, name } so the generator can emit the import line and
7
+ // the identifier — the runtime schema object itself is never loaded by the DSL.
8
+ //
9
+ // `import { PageRequest } from '@pylonts/dsl'` therefore gives .include() an
10
+ // already-resolved reference — no static analysis or name lookup needed.
11
+
12
+ /** Paginated query request base — renders `import { PageRequest } from '@pylonts/core'` + Intersect */
13
+ export const PageRequest: ImportBase = { from: '@pylonts/core', name: 'PageRequest' };
14
+
15
+ /** Paginated list response base — renders `import { PageResult } from '@pylonts/core'` + `PageResult(<row>)` */
16
+ export const PageResult = (row: DtoMessage): ImportRef => ({
17
+ from: '@pylonts/core',
18
+ name: 'PageResult',
19
+ args: [row],
20
+ });
21
+
22
+ /** Paged rows type base without generic args — renders `import type { PagedRows } from '@pylonts/core'` */
23
+ export const PagedRows: ImportBase = { from: '@pylonts/core', name: 'PagedRows', type: true };
24
+
25
+ /** Paged rows type base — renders `import { PagedRows } from '@pylonts/core'` + `PagedRows(<row>)` */
26
+ export const PageRows = (row: DtoMessage): ImportRef => ({
27
+ from: '@pylonts/core',
28
+ name: 'PagedRows',
29
+ args: [row],
30
30
  });
package/src/component.ts CHANGED
@@ -1,22 +1,22 @@
1
- import type { SchemaBase } from './dsl.js';
2
- import type { RefSchema } from './ref.js';
3
- import type { ActionSchema } from './action.js';
4
- import type { EventDataSchema } from './event.js';
5
-
6
- /** A component event trigger declaration. */
7
- export interface TriggerSchema extends SchemaBase {
8
- /** Data the event carries (e.g. e.detail). */
9
- eventData?: EventDataSchema;
10
- /** Actions that fire when the event occurs. */
11
- actions?: ActionSchema[];
12
- }
13
-
14
- /** A UI component declaration — a virtual schema that describes props and
15
- * event triggers, not a real renderable component.
16
- *
17
- * properties: data bindings via RefSchema (or literal values).
18
- * triggers: event name → TriggerSchema bindings. */
19
- export interface ComponentSchema extends SchemaBase {
20
- properties: Record<string, RefSchema | string | number | boolean>;
21
- triggers: Record<string, TriggerSchema>;
1
+ import type { SchemaBase } from './dsl.js';
2
+ import type { RefSchema } from './ref.js';
3
+ import type { ActionSchema } from './action.js';
4
+ import type { EventDataSchema } from './event.js';
5
+
6
+ /** A component event trigger declaration. */
7
+ export interface TriggerSchema extends SchemaBase {
8
+ /** Data the event carries (e.g. e.detail). */
9
+ eventData?: EventDataSchema;
10
+ /** Actions that fire when the event occurs. */
11
+ actions?: ActionSchema[];
12
+ }
13
+
14
+ /** A UI component declaration — a virtual schema that describes props and
15
+ * event triggers, not a real renderable component.
16
+ *
17
+ * properties: data bindings via RefSchema (or literal values).
18
+ * triggers: event name → TriggerSchema bindings. */
19
+ export interface ComponentSchema extends SchemaBase {
20
+ properties: Record<string, RefSchema | string | number | boolean>;
21
+ triggers: Record<string, TriggerSchema>;
22
22
  }
@@ -0,0 +1,40 @@
1
+ import { SchemaBase } from './dsl.js';
2
+ import { FrontAppSchema } from './project.js';
3
+ import type { EndpointSchema } from './endpoint.js';
4
+
5
+ /** A backend RPC controller. Strong constraints:
6
+ * - a backend module maps 1:1 to a frontend app (they are peers);
7
+ * - a controller serves exactly one frontend app — no cross-module calls. */
8
+ export interface ControllerSchema extends SchemaBase {
9
+ type: 'controller';
10
+ /** The frontend app this controller serves (shared instance from project.config). */
11
+ app: FrontAppSchema;
12
+ /** RPC methods exposed by this controller. */
13
+ methods: ControllerMethodSchema[];
14
+ }
15
+
16
+ export function defineController(options: {
17
+ name: string;
18
+ app: FrontAppSchema;
19
+ /** Method declarations: type/schema are injected by this builder. */
20
+ methods: Array<Omit<ControllerMethodSchema, 'type' | 'schema'>>;
21
+ description?: string;
22
+ }): ControllerSchema {
23
+ const schema: ControllerSchema = {
24
+ type: 'controller',
25
+ name: options.name,
26
+ description: options.description,
27
+ app: options.app,
28
+ methods: [],
29
+ };
30
+ schema.methods = options.methods.map((method) => ({ type: 'method', schema, ...method }));
31
+ return schema;
32
+ }
33
+
34
+ /** An RPC method exposed by a controller. */
35
+ export interface ControllerMethodSchema extends SchemaBase {
36
+ type: 'method';
37
+ schema: ControllerSchema;
38
+ /** Shared API signature — same instance the page-side provider references. */
39
+ signature: EndpointSchema;
40
+ }
package/src/convert.ts CHANGED
@@ -1,16 +1,43 @@
1
1
  import type { SchemaBase } from './dsl.js';
2
+ import type { DtoMessage } from './dto.js';
3
+ import type { TableSchema } from './db.js';
4
+ import type { ThirdMethodSchema } from './third-service.js';
2
5
  import type { FrontAppSchema } from './project.js';
3
6
 
4
- /** Declares post-call result page data field mapping.
5
- * Driver generates per-item transform (e.g. .map()) before setData. */
7
+ // Schema-collection integration: multiple source collections combine into
8
+ // one target collection (e.g. two entity tables into one dto, or entity
9
+ // columns plus a dto into one third-party wire message).
10
+
11
+ /** A source/target collection of a convert — dto, entity or third-party message. */
12
+ export type ConvertSourceSchema = DtoMessage | TableSchema | ThirdMethodSchema;
13
+
14
+ /** Declares a multi-source → single-target schema integration. */
6
15
  export interface ConvertSchema extends SchemaBase {
7
16
  type: 'convert';
8
- /** The frontend app this convert belongs to (shared instance from project.config). */
17
+ /** The app (module) this convert belongs to its artifact lands in modules/{app}/convert/. */
9
18
  app: FrontAppSchema;
10
- /** { targetField: sourceField } renames or copies fields from call result. */
11
- fields: Record<string, string>;
19
+ /** Source schemasone or more, mixed dimensions. */
20
+ sources: ConvertSourceSchema[];
21
+ /** Target schema — the single integrated collection. */
22
+ target: ConvertSourceSchema;
12
23
  }
13
24
 
14
- export function defineConvert(name: string, app: FrontAppSchema, fields: Record<string, string>): ConvertSchema {
15
- return { name, type: 'convert', app, fields };
25
+ export function defineConvert(options: {
26
+ name: string;
27
+ app: FrontAppSchema;
28
+ sources: ConvertSourceSchema[];
29
+ target: ConvertSourceSchema;
30
+ description?: string;
31
+ }): ConvertSchema {
32
+ if (options.sources.length === 0) {
33
+ throw new Error(`convert '${options.name}': sources must not be empty`);
34
+ }
35
+ return {
36
+ type: 'convert',
37
+ name: options.name,
38
+ description: options.description,
39
+ app: options.app,
40
+ sources: options.sources,
41
+ target: options.target,
42
+ };
16
43
  }
package/src/curd.ts CHANGED
@@ -2,6 +2,7 @@ import { SchemaBase, Field, Operator } from './dsl.js';
2
2
  import { TableSchema } from './db.js';
3
3
  import { FrontAppSchema } from './project.js';
4
4
  import { ActionSchema } from './action.js';
5
+ import { toKebabCase } from '@pylonts/core';
5
6
 
6
7
  // Admin-only CRUD page standard: binds one entity table to a frontend admin
7
8
  // app, describing everything needed to generate the list page plus optional
@@ -15,8 +16,6 @@ export type ActionPageMode = 'modal' | 'route';
15
16
  /** One CRUD action page (add / update / detail). */
16
17
  export interface ActionPage {
17
18
  mode: ActionPageMode;
18
- /** For add/update: when true, render as modal on list page; when false/undefined, render as standalone route page. */
19
- modal?: boolean;
20
19
  /** Fields rendered on this page. Required, non-empty — every field the
21
20
  * frontend shows must be listed explicitly. */
22
21
  columns: Field[];
@@ -81,6 +80,12 @@ export function defineCurd(name: string, schema: Omit<CurdSchema, 'name'>): Curd
81
80
  if (curd.app.type !== 'admin') {
82
81
  throw new Error(`curd ${name}: app ${curd.app.name} must be type 'admin' (got '${curd.app.type}')`);
83
82
  }
83
+ // The name is the admin route path — it must be the kebab-case table name
84
+ // so paths cannot drift from the table they serve.
85
+ const expectedName = toKebabCase(curd.table.name);
86
+ if (name !== expectedName) {
87
+ throw new Error(`curd name must be '${expectedName}' (kebab-case of table '${curd.table.name}'); got '${name}'`);
88
+ }
84
89
  if (!curd.section) {
85
90
  throw new Error(`curd ${name}: section is required (sidebar menu group, e.g. '商户管理')`);
86
91
  }
package/src/dao.ts CHANGED
@@ -1,13 +1,172 @@
1
- import { SchemaBase } from './dsl.js';
1
+ import { SchemaBase, Field, Operator } from './dsl.js';
2
2
  import { FrontAppSchema } from './project.js';
3
+ import type { DtoMessage } from './dto.js';
4
+ import { TableSchema } from './db.js';
3
5
 
4
6
  /** A data-access layer bound to exactly one frontend app. */
5
7
  export interface DaoSchema extends SchemaBase {
6
8
  type: 'dao';
7
9
  /** The frontend app this DAO belongs to (shared instance from project.config). */
8
10
  app: FrontAppSchema;
11
+ /** The table this DAO operates on (single-table atomicity). */
12
+ table: TableSchema;
13
+ /** Methods keyed by name — the map key is written back as the method name. */
14
+ methods: Record<string, DaoMethodSchema>;
9
15
  }
10
16
 
11
- export function defineDao(name: string, app: FrontAppSchema, description?: string): DaoSchema {
12
- return { name, type: 'dao', app, description };
17
+ /** Method input for defineDao: name is written back from the methods map key. */
18
+ export type DaoMethodDef =
19
+ | Omit<FindSchema, 'schema' | 'name'>
20
+ | Omit<GetSchema, 'schema' | 'name'>
21
+ | Omit<InsertSchema, 'schema' | 'name'>
22
+ | Omit<UpdateSchema, 'schema' | 'name'>
23
+ | Omit<DeleteSchema, 'schema' | 'name'>
24
+ | Omit<AggregateSchema, 'schema' | 'name'>;
25
+
26
+ export function defineDao(options: {
27
+ name: string;
28
+ app: FrontAppSchema;
29
+ table: TableSchema;
30
+ methods: Record<string, DaoMethodDef>;
31
+ description?: string;
32
+ }): DaoSchema {
33
+ const schema: DaoSchema = {
34
+ type: 'dao',
35
+ name: options.name,
36
+ description: options.description,
37
+ app: options.app,
38
+ table: options.table,
39
+ methods: {},
40
+ };
41
+ for (const key of Object.keys(options.methods)) {
42
+ const method = options.methods[key] as DaoMethodDef;
43
+ // Spread of a union loses discriminant correlation; the cast is safe
44
+ // (the builder only adds name and the back-reference field).
45
+ schema.methods[key] = { ...method, name: key, schema } as DaoMethodSchema;
46
+ }
47
+ return schema;
48
+ }
49
+
50
+ // DAO method kinds. Signature only: name + params + result. No logic —
51
+ // complex SQL goes into description (text), never into schema.
52
+
53
+ /** One AND-combined criterion: a column plus its comparison operator. */
54
+ export interface QueryField {
55
+ /** Column to compare. */
56
+ field: Field;
57
+ /** Comparison operator; defaults to 'eq'. */
58
+ op?: Operator;
59
+ }
60
+
61
+ /** Query criteria for find methods. */
62
+ export interface QuerySchema extends SchemaBase {
63
+ type: 'query';
64
+ /** Criteria; all fields are AND-combined. */
65
+ fields: QueryField[];
66
+ }
67
+
68
+ /** Sort specification for find results. */
69
+ export interface OrderBySchema {
70
+ /** Column to sort by. */
71
+ column: Field;
72
+ /** Sort direction. */
73
+ sort: 'asc' | 'desc';
74
+ }
75
+
76
+ /** Query rows by criteria; returns a list of row objects. */
77
+ export interface FindSchema extends SchemaBase {
78
+ type: 'find';
79
+ schema: DaoSchema;
80
+ /** Query criteria; omit = all rows. */
81
+ args?: QuerySchema;
82
+ /** Pagination marker: 'page' (page/pageSize) or 'limit' (position/limit).
83
+ * Only presence matters — parameter shapes are a generator convention. */
84
+ mode?: 'page' | 'limit';
85
+ /** Result sort; omit = no explicit order. */
86
+ orderBy?: OrderBySchema | OrderBySchema[];
87
+ /** Row type of the result list. */
88
+ results: EntitySchema;
89
+ }
90
+
91
+ /** Fetch a single row by primary key. Composite PK is not supported. */
92
+ export interface GetSchema extends SchemaBase {
93
+ type: 'get';
94
+ schema: DaoSchema;
95
+ /** Single PK scalar value. */
96
+ args: Field;
97
+ /** Row type. Get may miss the row: the generated signature returns Entity | null. */
98
+ results: EntitySchema;
13
99
  }
100
+
101
+ /** Insert one row; returns number (insert id). */
102
+ export interface InsertSchema extends SchemaBase {
103
+ type: 'insert';
104
+ schema: DaoSchema;
105
+ /** Row object. */
106
+ args: EntitySchema;
107
+ }
108
+
109
+ /** Update one row; returns number (affected rows). The where key is derived
110
+ * from the PK columns inside args. */
111
+ export interface UpdateSchema extends SchemaBase {
112
+ type: 'update';
113
+ schema: DaoSchema;
114
+ /** Row object containing the PK columns plus the columns to set. */
115
+ args: EntitySchema;
116
+ /** Additional criteria beyond the derived PK (AND-combined). */
117
+ where?: QuerySchema;
118
+ }
119
+
120
+ /** Delete one row by primary key; returns number (affected rows).
121
+ * Composite PK is not supported. */
122
+ export interface DeleteSchema extends SchemaBase {
123
+ type: 'delete';
124
+ schema: DaoSchema;
125
+ /** Single PK scalar value. */
126
+ args: Field;
127
+ }
128
+
129
+ export type DaoMethodSchema =
130
+ | FindSchema
131
+ | GetSchema
132
+ | InsertSchema
133
+ | UpdateSchema
134
+ | DeleteSchema
135
+ | AggregateSchema;
136
+
137
+ /** Aggregate expression result for aggregate queries. */
138
+ export interface ComputeExpr {
139
+ fn: 'sum' | 'avg' | 'count';
140
+ /** Column the function applies to; absent for count(*). */
141
+ field?: Field;
142
+ }
143
+
144
+ /** Aggregate expressions: Compute.sum(col) / Compute.avg(col) / Compute.count(). */
145
+ export const Compute = {
146
+ sum(field: Field): ComputeExpr {
147
+ return { fn: 'sum', field };
148
+ },
149
+ avg(field: Field): ComputeExpr {
150
+ return { fn: 'avg', field };
151
+ },
152
+ count(): ComputeExpr {
153
+ return { fn: 'count' };
154
+ },
155
+ };
156
+
157
+ /** Aggregate query (count/sum/avg): returns computed scalar values. */
158
+ export interface AggregateSchema extends SchemaBase {
159
+ type: 'aggregate';
160
+ schema: DaoSchema;
161
+ /** Criteria, same shape as find. */
162
+ args?: QuerySchema;
163
+ /** Computed results: key = result field name. */
164
+ results: Record<string, ComputeExpr>;
165
+ }
166
+
167
+ /** A database entity backed by a table. */
168
+ export interface EntitySchema extends SchemaBase {
169
+ type: 'entity';
170
+ /** The table columns of this entity. */
171
+ columns: Field[];
172
+ }
package/src/db.ts CHANGED
@@ -118,6 +118,11 @@ export function defineTable<
118
118
  `field ${key}: belongs to table ${field.schema.name}, cannot reuse in table ${table.name}`,
119
119
  );
120
120
  }
121
+ if (field.type === 'array' || field.type === 'object') {
122
+ throw new Error(
123
+ `table '${name}': column '${key}' cannot be a nested ${field.type} field — wire-format nesting is not a table column`,
124
+ );
125
+ }
121
126
  }
122
127
  for (const key of Object.keys(table.columns)) {
123
128
  table.columns[key].name = key;
package/src/dsl.ts CHANGED
@@ -71,6 +71,24 @@ interface DecimalField extends BaseField {
71
71
  // Value is transported as string to avoid binary float error.
72
72
  }
73
73
 
74
+ type RateUnit = 'pct' | 'pm' | 'bp';
75
+
76
+ export function rateScale(unit: RateUnit): number {
77
+ switch (unit) {
78
+ case 'pct': return 2;
79
+ case 'pm': return 3;
80
+ case 'bp': return 4;
81
+ }
82
+ }
83
+
84
+ interface RateField extends BaseField {
85
+ type: 'rate';
86
+ jsType: 'string';
87
+ unit: RateUnit;
88
+ // Precision/scale are derived from unit at DDL/TypeBox render time,
89
+ // not stored on the field.
90
+ }
91
+
74
92
  interface BooleanField extends BaseField {
75
93
  type: 'boolean';
76
94
  jsType: 'boolean';
@@ -125,18 +143,36 @@ interface JsonField extends BaseField {
125
143
  jsType: 'object';
126
144
  }
127
145
 
146
+ /** Recursive array field — wire-format nesting (third-party messages), not a table column. */
147
+ interface ArrayField extends BaseField {
148
+ type: 'array';
149
+ jsType: 'array';
150
+ /** Element type: any Field, including nested array/object. */
151
+ items: Field;
152
+ }
153
+
154
+ /** Recursive object field — wire-format nesting (third-party messages), not a table column. */
155
+ interface ObjectField extends BaseField {
156
+ type: 'object';
157
+ jsType: 'object';
158
+ properties: Record<string, Field>;
159
+ }
160
+
128
161
  export type Field =
129
162
  | StringField
130
163
  | TextField
131
164
  | IntField
132
165
  | BigintField
133
166
  | DecimalField
167
+ | RateField
134
168
  | BooleanField
135
169
  | DateField
136
170
  | TimeField
137
171
  | DateTimeField
138
172
  | EnumField
139
- | JsonField;
173
+ | JsonField
174
+ | ArrayField
175
+ | ObjectField;
140
176
 
141
177
  // Field builders: type and jsType are fixed, pass extra properties only.
142
178
  // The field name is written back from the map key later (see defineTable).
@@ -163,6 +199,10 @@ export function decimalField(extra: FieldExtras<DecimalField>): DecimalField {
163
199
  return { name: '', type: 'decimal', jsType: 'string', ...extra };
164
200
  }
165
201
 
202
+ export function rateField(unit: RateUnit, extra: Omit<FieldExtras<RateField>, 'unit'> = {}): RateField {
203
+ return { name: '', type: 'rate', jsType: 'string', unit, ...extra };
204
+ }
205
+
166
206
  export function booleanField(extra: FieldExtras<BooleanField> = {}): BooleanField {
167
207
  return { name: '', type: 'boolean', jsType: 'boolean', ...extra };
168
208
  }
@@ -183,6 +223,14 @@ export function jsonField(extra: FieldExtras<JsonField> = {}): JsonField {
183
223
  return { name: '', type: 'json', jsType: 'object', ...extra };
184
224
  }
185
225
 
226
+ export function arrayField(extra: FieldExtras<ArrayField>): ArrayField {
227
+ return { name: '', type: 'array', jsType: 'array', ...extra };
228
+ }
229
+
230
+ export function objectField(extra: FieldExtras<ObjectField>): ObjectField {
231
+ return { name: '', type: 'object', jsType: 'object', ...extra };
232
+ }
233
+
186
234
  export function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): EnumField {
187
235
  const jsType = extra.enum.valueType === 'integer' ? 'number' : 'string';
188
236
  return { name: '', type: 'enum', jsType, ...extra };