@pylonts/dsl 1.1.12 → 1.1.13
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/convert.d.ts +6 -8
- package/dist/curd.js +1 -1
- package/dist/dao.d.ts +10 -7
- package/dist/dao.js +20 -7
- package/dist/dsl.d.ts +18 -1
- package/dist/dsl.js +40 -0
- package/dist/dto.d.ts +13 -8
- package/dist/dto.js +68 -13
- package/dist/entity.d.ts +4 -3
- package/dist/entity.js +1 -1
- package/dist/filter.d.ts +6 -4
- package/dist/filter.js +1 -1
- package/dist/flow-script.js +8 -2
- package/dist/flow.d.ts +10 -2
- package/dist/flow.js +44 -4
- package/dist/mermaid-driver.js +2 -2
- package/dist/service.d.ts +13 -8
- package/dist/service.js +1 -1
- package/dist/third-service.d.ts +10 -53
- package/dist/third-service.js +3 -78
- package/dist/typebox-driver.d.ts +0 -6
- package/dist/typebox-driver.js +8 -36
- package/dist/utils.d.ts +2 -2
- package/docs/curd.md +146 -146
- package/docs/dao-generation.md +477 -477
- package/docs/project.md +31 -31
- package/docs/token.md +326 -326
- package/package.json +1 -1
- package/src/action.ts +51 -51
- package/src/controller.ts +53 -53
- package/src/convert.ts +76 -78
- package/src/curd.ts +104 -104
- package/src/dao.ts +504 -485
- package/src/dsl.ts +296 -257
- package/src/dto.ts +323 -266
- package/src/entity.ts +43 -42
- package/src/expr.ts +64 -64
- package/src/filter.ts +71 -69
- package/src/flow-script.ts +702 -695
- package/src/flow.ts +1272 -1226
- package/src/index.ts +46 -46
- package/src/mermaid-driver.ts +339 -339
- package/src/mysql-driver.ts +108 -108
- package/src/project.ts +138 -138
- package/src/service.ts +112 -107
- package/src/third-service.ts +68 -191
- package/src/typebox-driver.ts +234 -268
- package/src/utils.ts +74 -74
package/package.json
CHANGED
package/src/action.ts
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import { SchemaBase } from './dsl.js';
|
|
2
|
-
import type { DtoField, DtoMessage, DtoArrayField, DtoObjectField } from './dto.js';
|
|
3
|
-
import type { RefSchema } from './ref.js';
|
|
4
|
-
import type { ControllerMethodSchema } from './controller.js';
|
|
5
|
-
|
|
6
|
-
/** An action a user can perform on a page (e.g. submit, approve, reject).
|
|
7
|
-
* Subclasses use `type` as the discriminator. */
|
|
8
|
-
export interface ActionSchema extends SchemaBase {
|
|
9
|
-
type: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function defineAction(name: string, description?: string): ActionSchema {
|
|
13
|
-
return { name, description, type: 'gesture' };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Parameter data source for a call argument. */
|
|
17
|
-
export type DataRef =
|
|
18
|
-
| { type: 'route'; key: string }
|
|
19
|
-
| { type: 'data'; key: string }
|
|
20
|
-
| { type: 'value'; value: unknown };
|
|
21
|
-
|
|
22
|
-
/** Create a route-parameter reference. */
|
|
23
|
-
export function route(key: string): DataRef {
|
|
24
|
-
return { type: 'route', key };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Create a page-data reference. */
|
|
28
|
-
export function data(key: string): DataRef {
|
|
29
|
-
return { type: 'data', key };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface CallAction extends ActionSchema {
|
|
33
|
-
type: 'call';
|
|
34
|
-
func: ControllerMethodSchema;
|
|
35
|
-
args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function call(func: ControllerMethodSchema, args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>): CallAction {
|
|
39
|
-
return { name: func.name, type: 'call', func, args };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Assign a call's result to a page data field.
|
|
43
|
-
* React: setState({ [field]: await ... }). Mini-program: this.setData({ [field]: ... }). */
|
|
44
|
-
export interface SetDataAction extends ActionSchema {
|
|
45
|
-
type: 'setData';
|
|
46
|
-
call: CallAction;
|
|
47
|
-
field: DtoField;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function setData(call: CallAction, field: DtoField): SetDataAction {
|
|
51
|
-
return { name: 'setData', type: 'setData', call, field };
|
|
1
|
+
import { SchemaBase } from './dsl.js';
|
|
2
|
+
import type { DtoField, DtoMessage, DtoArrayField, DtoObjectField } from './dto.js';
|
|
3
|
+
import type { RefSchema } from './ref.js';
|
|
4
|
+
import type { ControllerMethodSchema } from './controller.js';
|
|
5
|
+
|
|
6
|
+
/** An action a user can perform on a page (e.g. submit, approve, reject).
|
|
7
|
+
* Subclasses use `type` as the discriminator. */
|
|
8
|
+
export interface ActionSchema extends SchemaBase {
|
|
9
|
+
type: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function defineAction(name: string, description?: string): ActionSchema {
|
|
13
|
+
return { name, description, type: 'gesture' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Parameter data source for a call argument. */
|
|
17
|
+
export type DataRef =
|
|
18
|
+
| { type: 'route'; key: string }
|
|
19
|
+
| { type: 'data'; key: string }
|
|
20
|
+
| { type: 'value'; value: unknown };
|
|
21
|
+
|
|
22
|
+
/** Create a route-parameter reference. */
|
|
23
|
+
export function route(key: string): DataRef {
|
|
24
|
+
return { type: 'route', key };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Create a page-data reference. */
|
|
28
|
+
export function data(key: string): DataRef {
|
|
29
|
+
return { type: 'data', key };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CallAction extends ActionSchema {
|
|
33
|
+
type: 'call';
|
|
34
|
+
func: ControllerMethodSchema;
|
|
35
|
+
args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function call(func: ControllerMethodSchema, args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>): CallAction {
|
|
39
|
+
return { name: func.name, type: 'call', func, args };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Assign a call's result to a page data field.
|
|
43
|
+
* React: setState({ [field]: await ... }). Mini-program: this.setData({ [field]: ... }). */
|
|
44
|
+
export interface SetDataAction extends ActionSchema {
|
|
45
|
+
type: 'setData';
|
|
46
|
+
call: CallAction;
|
|
47
|
+
field: DtoField;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function setData(call: CallAction, field: DtoField): SetDataAction {
|
|
51
|
+
return { name: 'setData', type: 'setData', call, field };
|
|
52
52
|
}
|
package/src/controller.ts
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import { SchemaBase } from './dsl.js';
|
|
2
|
-
import { FrontAppSchema, ProjectApiSchema } from './project.js';
|
|
3
|
-
import type { DtoMessage } from './dto.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 backend api module this controller belongs to (shared instance from
|
|
11
|
-
* project.config.ts apis). Controllers are always backend-side, so storage
|
|
12
|
-
* is controller_schema/{api.name}/{app.name}/controller/. */
|
|
13
|
-
api: ProjectApiSchema;
|
|
14
|
-
/** The frontend app this controller serves (shared instance from project.config). */
|
|
15
|
-
app: FrontAppSchema;
|
|
16
|
-
/** RPC methods, keyed by method name (key === method.name, enforced by the builder). */
|
|
17
|
-
methods: Record<string, ControllerMethodSchema>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function defineController(options: {
|
|
21
|
-
name: string;
|
|
22
|
-
api: ProjectApiSchema;
|
|
23
|
-
app: FrontAppSchema;
|
|
24
|
-
/** Method declarations: type/schema/name are injected by this builder. */
|
|
25
|
-
methods: Record<string, Omit<ControllerMethodSchema, 'type' | 'schema' | 'name'>>;
|
|
26
|
-
description?: string;
|
|
27
|
-
}): ControllerSchema {
|
|
28
|
-
if (!options.api.apps.includes(options.app)) {
|
|
29
|
-
throw new Error(`controller ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
|
|
30
|
-
}
|
|
31
|
-
const schema: ControllerSchema = {
|
|
32
|
-
type: 'controller',
|
|
33
|
-
name: options.name,
|
|
34
|
-
description: options.description,
|
|
35
|
-
api: options.api,
|
|
36
|
-
app: options.app,
|
|
37
|
-
methods: {},
|
|
38
|
-
};
|
|
39
|
-
for (const [key, method] of Object.entries(options.methods)) {
|
|
40
|
-
schema.methods[key] = { type: 'method', schema, name: key, ...method };
|
|
41
|
-
}
|
|
42
|
-
return schema;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** An RPC method exposed by a controller. Carries the shared API call
|
|
46
|
-
* signature: one request DTO in, one response shape out. Referenced by
|
|
47
|
-
* frontend page actions — both sides use the exact same instance, so
|
|
48
|
-
* drift is impossible. */
|
|
49
|
-
export interface ControllerMethodSchema extends SchemaBase {
|
|
50
|
-
type: 'method';
|
|
51
|
-
schema: ControllerSchema;
|
|
52
|
-
args: DtoMessage;
|
|
53
|
-
results: DtoMessage | number | boolean | string;
|
|
1
|
+
import { SchemaBase } from './dsl.js';
|
|
2
|
+
import { FrontAppSchema, ProjectApiSchema } from './project.js';
|
|
3
|
+
import type { DtoMessage } from './dto.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 backend api module this controller belongs to (shared instance from
|
|
11
|
+
* project.config.ts apis). Controllers are always backend-side, so storage
|
|
12
|
+
* is controller_schema/{api.name}/{app.name}/controller/. */
|
|
13
|
+
api: ProjectApiSchema;
|
|
14
|
+
/** The frontend app this controller serves (shared instance from project.config). */
|
|
15
|
+
app: FrontAppSchema;
|
|
16
|
+
/** RPC methods, keyed by method name (key === method.name, enforced by the builder). */
|
|
17
|
+
methods: Record<string, ControllerMethodSchema>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function defineController(options: {
|
|
21
|
+
name: string;
|
|
22
|
+
api: ProjectApiSchema;
|
|
23
|
+
app: FrontAppSchema;
|
|
24
|
+
/** Method declarations: type/schema/name are injected by this builder. */
|
|
25
|
+
methods: Record<string, Omit<ControllerMethodSchema, 'type' | 'schema' | 'name'>>;
|
|
26
|
+
description?: string;
|
|
27
|
+
}): ControllerSchema {
|
|
28
|
+
if (!options.api.apps.includes(options.app)) {
|
|
29
|
+
throw new Error(`controller ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
|
|
30
|
+
}
|
|
31
|
+
const schema: ControllerSchema = {
|
|
32
|
+
type: 'controller',
|
|
33
|
+
name: options.name,
|
|
34
|
+
description: options.description,
|
|
35
|
+
api: options.api,
|
|
36
|
+
app: options.app,
|
|
37
|
+
methods: {},
|
|
38
|
+
};
|
|
39
|
+
for (const [key, method] of Object.entries(options.methods)) {
|
|
40
|
+
schema.methods[key] = { type: 'method', schema, name: key, ...method };
|
|
41
|
+
}
|
|
42
|
+
return schema;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** An RPC method exposed by a controller. Carries the shared API call
|
|
46
|
+
* signature: one request DTO in, one response shape out. Referenced by
|
|
47
|
+
* frontend page actions — both sides use the exact same instance, so
|
|
48
|
+
* drift is impossible. */
|
|
49
|
+
export interface ControllerMethodSchema extends SchemaBase {
|
|
50
|
+
type: 'method';
|
|
51
|
+
schema: ControllerSchema;
|
|
52
|
+
args: DtoMessage;
|
|
53
|
+
results: DtoMessage | number | boolean | string;
|
|
54
54
|
}
|
package/src/convert.ts
CHANGED
|
@@ -1,79 +1,77 @@
|
|
|
1
|
-
import type { SchemaBase } from './dsl.js';
|
|
2
|
-
import type { DtoMessage } from './dto.js';
|
|
3
|
-
import type { TableSchema } from './db.js';
|
|
4
|
-
import type { EntitySchema } from './entity.js';
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// {
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
return schema;
|
|
1
|
+
import type { CollectionSchemaBase, SchemaBase } from './dsl.js';
|
|
2
|
+
import type { DtoMessage } from './dto.js';
|
|
3
|
+
import type { TableSchema } from './db.js';
|
|
4
|
+
import type { EntitySchema } from './entity.js';
|
|
5
|
+
import type { FrontAppSchema, ProjectApiSchema } from './project.js';
|
|
6
|
+
|
|
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 convert file binds to ONE source identity — a table (internal mapping,
|
|
12
|
+
// {Table}Convert.ts) or a third-party service (anti-corruption translation,
|
|
13
|
+
// {third-service}.convert.ts) — and holds N methods keyed by name (same
|
|
14
|
+
// shape as defineService / defineDao).
|
|
15
|
+
|
|
16
|
+
/** A source/target collection of a convert — dto, entity or table. Entity
|
|
17
|
+
* sources may carry aggregate fields (aggField), e.g. an aggregate result
|
|
18
|
+
* entity projected into a wire message. */
|
|
19
|
+
export type ConvertSourceSchema = DtoMessage | TableSchema | EntitySchema;
|
|
20
|
+
|
|
21
|
+
/** Method input for defineConvert: type/schema/name are set by the builder. */
|
|
22
|
+
export type ConvertMethodDef = Omit<ConvertMethodSchema, 'type' | 'schema' | 'name'>;
|
|
23
|
+
|
|
24
|
+
/** One conversion: multiple source collections → single target collection. */
|
|
25
|
+
export interface ConvertMethodSchema extends SchemaBase {
|
|
26
|
+
type: 'convertMethod';
|
|
27
|
+
/** The convert file this method belongs to. */
|
|
28
|
+
schema: ConvertSchema;
|
|
29
|
+
/** Source schemas — one or more, mixed dimensions. */
|
|
30
|
+
sources: ConvertSourceSchema[];
|
|
31
|
+
/** Target schema — the single integrated collection. */
|
|
32
|
+
target: ConvertSourceSchema;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Declares multi-source → single-target schema integrations grouped by source identity. */
|
|
36
|
+
export interface ConvertSchema extends CollectionSchemaBase {
|
|
37
|
+
type: 'convert';
|
|
38
|
+
/** The backend api module this convert belongs to (shared instance from
|
|
39
|
+
* project.config.ts apis). Storage is convert_schema/{api.name}/{app.name}/
|
|
40
|
+
* — same layout as service_schema. */
|
|
41
|
+
api: ProjectApiSchema;
|
|
42
|
+
/** The app (module) this convert belongs to — its artifact lands in modules/{app}/convert/. */
|
|
43
|
+
app: FrontAppSchema;
|
|
44
|
+
/** Methods keyed by name — the map key is written back as the method name. */
|
|
45
|
+
methods: Record<string, ConvertMethodSchema>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function defineConvert(options: {
|
|
49
|
+
name: string;
|
|
50
|
+
api: ProjectApiSchema;
|
|
51
|
+
app: FrontAppSchema;
|
|
52
|
+
methods: Record<string, ConvertMethodDef>;
|
|
53
|
+
description?: string;
|
|
54
|
+
}): ConvertSchema {
|
|
55
|
+
if (Object.keys(options.methods).length === 0) {
|
|
56
|
+
throw new Error(`convert '${options.name}': methods must not be empty`);
|
|
57
|
+
}
|
|
58
|
+
if (!options.api.apps.includes(options.app)) {
|
|
59
|
+
throw new Error(`convert '${options.name}': api '${options.api.name}' does not serve app '${options.app.name}'`);
|
|
60
|
+
}
|
|
61
|
+
const schema: ConvertSchema = {
|
|
62
|
+
type: 'convert',
|
|
63
|
+
name: options.name,
|
|
64
|
+
description: options.description,
|
|
65
|
+
api: options.api,
|
|
66
|
+
app: options.app,
|
|
67
|
+
methods: {},
|
|
68
|
+
};
|
|
69
|
+
for (const key of Object.keys(options.methods)) {
|
|
70
|
+
const method = options.methods[key] as ConvertMethodDef;
|
|
71
|
+
if (method.sources.length === 0) {
|
|
72
|
+
throw new Error(`convert '${options.name}': method '${key}' sources must not be empty`);
|
|
73
|
+
}
|
|
74
|
+
schema.methods[key] = { type: 'convertMethod', schema, ...method, name: key };
|
|
75
|
+
}
|
|
76
|
+
return schema;
|
|
79
77
|
}
|
package/src/curd.ts
CHANGED
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
import { SchemaBase, Field } from './dsl.js';
|
|
2
|
-
import { TableSchema } from './db.js';
|
|
3
|
-
import { FrontAppSchema } from './project.js';
|
|
4
|
-
import { ActionSchema } from './action.js';
|
|
5
|
-
import type { FilterSchema } from './filter.js';
|
|
6
|
-
import { toKebabCase } from '@pylonts/core';
|
|
7
|
-
|
|
8
|
-
// Admin-only CRUD page standard: binds one entity table to a frontend admin
|
|
9
|
-
// app, describing everything needed to generate the list page plus optional
|
|
10
|
-
// add/update/detail pages. Field-level columns are plain Field instances
|
|
11
|
-
// (table fields, cross-table refs allowed) — DTOs are derived by the generator,
|
|
12
|
-
// the schema itself never references DtoMessage.
|
|
13
|
-
|
|
14
|
-
/** Mode of an action page: modal dialog or standalone route. */
|
|
15
|
-
export type ActionPageMode = 'modal' | 'route';
|
|
16
|
-
|
|
17
|
-
/** One CRUD action page (add / update / detail). */
|
|
18
|
-
export interface ActionPage {
|
|
19
|
-
mode: ActionPageMode;
|
|
20
|
-
/** Fields rendered on this page. Required, non-empty — every field the
|
|
21
|
-
* frontend shows must be listed explicitly. */
|
|
22
|
-
columns: Field[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** List page configuration. */
|
|
26
|
-
export interface CurdListConfig {
|
|
27
|
-
/** List columns; required, non-empty. Every field the list shows must be
|
|
28
|
-
* listed explicitly. May include cross-table fields via foreign refs. */
|
|
29
|
-
columns: Field[];
|
|
30
|
-
/** Page filter (search form + keyword search). Conditions declared on the
|
|
31
|
-
* filter render the search form; the filter's keyword (when present)
|
|
32
|
-
* drives the keyword query endpoint. Optional — a page without a filter
|
|
33
|
-
* has no search form. */
|
|
34
|
-
filter?: FilterSchema;
|
|
35
|
-
/** Default sort. Required — column and direction are both mandatory. */
|
|
36
|
-
orderBy: { column: Field; direction: 'asc' | 'desc' };
|
|
37
|
-
/** Column header text overrides: Field.name → header text. */
|
|
38
|
-
columnTitles?: Record<string, string>;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Admin-only CRUD page standard: binds one entity table to a frontend
|
|
42
|
-
* admin app. Drives generation of the list page plus optional
|
|
43
|
-
* add/update/detail pages. */
|
|
44
|
-
export interface CurdSchema extends SchemaBase {
|
|
45
|
-
/** The admin frontend app this CRUD belongs to (shared instance, type must be 'admin'). */
|
|
46
|
-
app: FrontAppSchema;
|
|
47
|
-
/** The bound entity table (shared instance). */
|
|
48
|
-
table: TableSchema;
|
|
49
|
-
/** List page Chinese title. */
|
|
50
|
-
title: string;
|
|
51
|
-
/** Sidebar menu section (group) this CRUD page belongs to. */
|
|
52
|
-
section: string;
|
|
53
|
-
/** Extra user actions on this page (beyond the standard CRUD). */
|
|
54
|
-
actions?: ActionSchema[];
|
|
55
|
-
/** Add/update/detail action pages. */
|
|
56
|
-
actionPages?: {
|
|
57
|
-
add?: ActionPage;
|
|
58
|
-
update?: ActionPage;
|
|
59
|
-
detail?: ActionPage;
|
|
60
|
-
};
|
|
61
|
-
list: CurdListConfig;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function assertColumns(curd: CurdSchema, pageName: string, columns: Field[]): void {
|
|
65
|
-
if (columns.length === 0) {
|
|
66
|
-
throw new Error(`curd ${curd.name}: ${pageName}.columns must be non-empty`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function assertFieldsOwnTable(curd: CurdSchema, label: string, fields: Field[]): void {
|
|
71
|
-
for (const f of fields) {
|
|
72
|
-
if (f.schema !== curd.table) {
|
|
73
|
-
throw new Error(`curd ${curd.name}: ${label} field ${f.name} does not belong to table ${curd.table.name}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Defines an admin CRUD page standard. Runtime-validates admin app binding,
|
|
79
|
-
* non-empty columns and table field ownership (same style as defineTable). */
|
|
80
|
-
export function defineCurd(name: string, schema: Omit<CurdSchema, 'name'>): CurdSchema {
|
|
81
|
-
const curd: CurdSchema = { name, ...schema };
|
|
82
|
-
if (curd.app.type !== 'admin') {
|
|
83
|
-
throw new Error(`curd ${name}: app ${curd.app.name} must be type 'admin' (got '${curd.app.type}')`);
|
|
84
|
-
}
|
|
85
|
-
// The name is the admin route path — it must be the kebab-case table name
|
|
86
|
-
// so paths cannot drift from the table they serve.
|
|
87
|
-
const expectedName = toKebabCase(curd.table.name);
|
|
88
|
-
if (name !== expectedName) {
|
|
89
|
-
throw new Error(`curd name must be '${expectedName}' (kebab-case of table '${curd.table.name}'); got '${name}'`);
|
|
90
|
-
}
|
|
91
|
-
if (!curd.section) {
|
|
92
|
-
throw new Error(`curd ${name}: section is required (sidebar menu group, e.g. '商户管理')`);
|
|
93
|
-
}
|
|
94
|
-
assertColumns(curd, 'list', curd.list.columns);
|
|
95
|
-
for (const [pageName, page] of Object.entries(curd.actionPages ?? {})) {
|
|
96
|
-
if (page) assertColumns(curd, `actionPages.${pageName}`, page.columns);
|
|
97
|
-
}
|
|
98
|
-
if (curd.list.filter !== undefined && curd.list.filter.app !== curd.app) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`curd ${name}: list.filter '${curd.list.filter.name}' is bound to
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
assertFieldsOwnTable(curd, 'orderBy', [curd.list.orderBy.column]);
|
|
104
|
-
return curd;
|
|
1
|
+
import { SchemaBase, Field } from './dsl.js';
|
|
2
|
+
import { TableSchema } from './db.js';
|
|
3
|
+
import { FrontAppSchema } from './project.js';
|
|
4
|
+
import { ActionSchema } from './action.js';
|
|
5
|
+
import type { FilterSchema } from './filter.js';
|
|
6
|
+
import { toKebabCase } from '@pylonts/core';
|
|
7
|
+
|
|
8
|
+
// Admin-only CRUD page standard: binds one entity table to a frontend admin
|
|
9
|
+
// app, describing everything needed to generate the list page plus optional
|
|
10
|
+
// add/update/detail pages. Field-level columns are plain Field instances
|
|
11
|
+
// (table fields, cross-table refs allowed) — DTOs are derived by the generator,
|
|
12
|
+
// the schema itself never references DtoMessage.
|
|
13
|
+
|
|
14
|
+
/** Mode of an action page: modal dialog or standalone route. */
|
|
15
|
+
export type ActionPageMode = 'modal' | 'route';
|
|
16
|
+
|
|
17
|
+
/** One CRUD action page (add / update / detail). */
|
|
18
|
+
export interface ActionPage {
|
|
19
|
+
mode: ActionPageMode;
|
|
20
|
+
/** Fields rendered on this page. Required, non-empty — every field the
|
|
21
|
+
* frontend shows must be listed explicitly. */
|
|
22
|
+
columns: Field[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** List page configuration. */
|
|
26
|
+
export interface CurdListConfig {
|
|
27
|
+
/** List columns; required, non-empty. Every field the list shows must be
|
|
28
|
+
* listed explicitly. May include cross-table fields via foreign refs. */
|
|
29
|
+
columns: Field[];
|
|
30
|
+
/** Page filter (search form + keyword search). Conditions declared on the
|
|
31
|
+
* filter render the search form; the filter's keyword (when present)
|
|
32
|
+
* drives the keyword query endpoint. Optional — a page without a filter
|
|
33
|
+
* has no search form. */
|
|
34
|
+
filter?: FilterSchema;
|
|
35
|
+
/** Default sort. Required — column and direction are both mandatory. */
|
|
36
|
+
orderBy: { column: Field; direction: 'asc' | 'desc' };
|
|
37
|
+
/** Column header text overrides: Field.name → header text. */
|
|
38
|
+
columnTitles?: Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Admin-only CRUD page standard: binds one entity table to a frontend
|
|
42
|
+
* admin app. Drives generation of the list page plus optional
|
|
43
|
+
* add/update/detail pages. */
|
|
44
|
+
export interface CurdSchema extends SchemaBase {
|
|
45
|
+
/** The admin frontend app this CRUD belongs to (shared instance, type must be 'admin'). */
|
|
46
|
+
app: FrontAppSchema;
|
|
47
|
+
/** The bound entity table (shared instance). */
|
|
48
|
+
table: TableSchema;
|
|
49
|
+
/** List page Chinese title. */
|
|
50
|
+
title: string;
|
|
51
|
+
/** Sidebar menu section (group) this CRUD page belongs to. */
|
|
52
|
+
section: string;
|
|
53
|
+
/** Extra user actions on this page (beyond the standard CRUD). */
|
|
54
|
+
actions?: ActionSchema[];
|
|
55
|
+
/** Add/update/detail action pages. */
|
|
56
|
+
actionPages?: {
|
|
57
|
+
add?: ActionPage;
|
|
58
|
+
update?: ActionPage;
|
|
59
|
+
detail?: ActionPage;
|
|
60
|
+
};
|
|
61
|
+
list: CurdListConfig;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function assertColumns(curd: CurdSchema, pageName: string, columns: Field[]): void {
|
|
65
|
+
if (columns.length === 0) {
|
|
66
|
+
throw new Error(`curd ${curd.name}: ${pageName}.columns must be non-empty`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertFieldsOwnTable(curd: CurdSchema, label: string, fields: Field[]): void {
|
|
71
|
+
for (const f of fields) {
|
|
72
|
+
if (f.schema !== curd.table) {
|
|
73
|
+
throw new Error(`curd ${curd.name}: ${label} field ${f.name} does not belong to table ${curd.table.name}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Defines an admin CRUD page standard. Runtime-validates admin app binding,
|
|
79
|
+
* non-empty columns and table field ownership (same style as defineTable). */
|
|
80
|
+
export function defineCurd(name: string, schema: Omit<CurdSchema, 'name'>): CurdSchema {
|
|
81
|
+
const curd: CurdSchema = { name, ...schema };
|
|
82
|
+
if (curd.app.type !== 'admin') {
|
|
83
|
+
throw new Error(`curd ${name}: app ${curd.app.name} must be type 'admin' (got '${curd.app.type}')`);
|
|
84
|
+
}
|
|
85
|
+
// The name is the admin route path — it must be the kebab-case table name
|
|
86
|
+
// so paths cannot drift from the table they serve.
|
|
87
|
+
const expectedName = toKebabCase(curd.table.name);
|
|
88
|
+
if (name !== expectedName) {
|
|
89
|
+
throw new Error(`curd name must be '${expectedName}' (kebab-case of table '${curd.table.name}'); got '${name}'`);
|
|
90
|
+
}
|
|
91
|
+
if (!curd.section) {
|
|
92
|
+
throw new Error(`curd ${name}: section is required (sidebar menu group, e.g. '商户管理')`);
|
|
93
|
+
}
|
|
94
|
+
assertColumns(curd, 'list', curd.list.columns);
|
|
95
|
+
for (const [pageName, page] of Object.entries(curd.actionPages ?? {})) {
|
|
96
|
+
if (page) assertColumns(curd, `actionPages.${pageName}`, page.columns);
|
|
97
|
+
}
|
|
98
|
+
if (curd.list.filter !== undefined && curd.list.filter.app !== curd.app) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`curd ${name}: list.filter '${curd.list.filter.name}' is bound to ${curd.list.filter.app?.name ?? 'common'} but the curd belongs to app '${curd.app.name}'`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
assertFieldsOwnTable(curd, 'orderBy', [curd.list.orderBy.column]);
|
|
104
|
+
return curd;
|
|
105
105
|
}
|