@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.
- package/dist/controller.d.ts +27 -0
- package/dist/controller.js +11 -0
- package/dist/convert.d.ts +18 -6
- package/dist/convert.js +12 -2
- package/dist/curd.d.ts +0 -2
- package/dist/curd.js +7 -0
- package/dist/dao.d.ts +111 -2
- package/dist/dao.js +28 -2
- package/dist/db.js +3 -0
- package/dist/dsl.d.ts +24 -1
- package/dist/dsl.js +16 -0
- package/dist/dto.d.ts +6 -2
- package/dist/dto.js +20 -9
- package/dist/endpoint.d.ts +15 -0
- package/dist/endpoint.js +3 -0
- package/dist/exception.d.ts +14 -0
- package/dist/exception.js +9 -0
- package/dist/field-rule.d.ts +20 -0
- package/dist/field-rule.js +19 -0
- package/dist/flow.d.ts +24 -2
- package/dist/flow.js +16 -4
- package/dist/index.d.ts +7 -0
- package/dist/index.js +9 -0
- package/dist/mermaid-driver.js +32 -3
- package/dist/method.d.ts +11 -0
- package/dist/method.js +3 -0
- package/dist/mysql-driver.js +8 -1
- package/dist/provider.d.ts +6 -11
- package/dist/provider.js +2 -2
- package/dist/service.d.ts +16 -6
- package/dist/service.js +13 -2
- package/dist/third-service.d.ts +75 -0
- package/dist/third-service.js +96 -0
- package/dist/typebox-driver.d.ts +6 -0
- package/dist/typebox-driver.js +76 -14
- package/dist/utils.d.ts +25 -10
- package/dist/utils.js +28 -11
- package/docs/curd.md +110 -110
- package/docs/dto.md +9 -2
- package/docs/table.md +135 -135
- package/docs/third-service.md +122 -0
- package/package.json +4 -1
- package/src/action.ts +10 -10
- package/src/asset.ts +62 -62
- package/src/bases.ts +29 -29
- package/src/component.ts +21 -21
- package/src/controller.ts +40 -0
- package/src/convert.ts +34 -7
- package/src/curd.ts +7 -2
- package/src/dao.ts +162 -3
- package/src/db.ts +5 -0
- package/src/dsl.ts +49 -1
- package/src/dto.ts +25 -9
- package/src/endpoint.ts +18 -0
- package/src/event.ts +12 -12
- package/src/exception.ts +28 -0
- package/src/field-rule.ts +47 -0
- package/src/flow.ts +143 -103
- package/src/index.ts +11 -1
- package/src/mermaid-driver.ts +31 -3
- package/src/method.ts +20 -0
- package/src/mock.ts +12 -12
- package/src/mysql-driver.ts +8 -2
- package/src/navigation.ts +28 -28
- package/src/page-def.ts +79 -79
- package/src/page-flow.ts +153 -153
- package/src/page.ts +76 -76
- package/src/popup.ts +25 -25
- package/src/project.ts +97 -97
- package/src/provider.ts +7 -12
- package/src/ref.ts +18 -18
- package/src/route.ts +11 -11
- package/src/service.ts +28 -6
- package/src/third-service.ts +186 -0
- package/src/typebox-driver.ts +264 -192
- package/src/utils.ts +54 -17
- package/dist/check-inheritance.d.ts +0 -9
- 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.
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
17
|
+
/** The app (module) this convert belongs to — its artifact lands in modules/{app}/convert/. */
|
|
9
18
|
app: FrontAppSchema;
|
|
10
|
-
/**
|
|
11
|
-
|
|
19
|
+
/** Source schemas — one 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(
|
|
15
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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 };
|