@pylonts/dsl 1.1.6 → 1.1.12
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/README.md +4 -0
- package/dist/action.d.ts +32 -0
- package/dist/action.js +14 -0
- package/dist/aggregate.d.ts +38 -0
- package/dist/aggregate.js +46 -0
- package/dist/business-flow.d.ts +9 -0
- package/dist/business-flow.js +72 -0
- package/dist/controller.d.ts +17 -9
- package/dist/controller.js +8 -2
- package/dist/convert.d.ts +28 -10
- package/dist/convert.js +16 -5
- package/dist/curd.d.ts +7 -10
- package/dist/curd.js +3 -1
- package/dist/dao.d.ts +81 -53
- package/dist/dao.js +291 -12
- package/dist/db.d.ts +6 -0
- package/dist/db.js +10 -0
- package/dist/domain-event.d.ts +48 -0
- package/dist/domain-event.js +24 -0
- package/dist/dsl.d.ts +17 -2
- package/dist/dsl.js +7 -0
- package/dist/dto.d.ts +6 -4
- package/dist/dto.js +5 -4
- package/dist/entity.d.ts +29 -0
- package/dist/entity.js +13 -0
- package/dist/exception.d.ts +9 -3
- package/dist/exception.js +25 -1
- package/dist/expr.d.ts +45 -0
- package/dist/expr.js +32 -0
- package/dist/filter.d.ts +45 -0
- package/dist/filter.js +21 -0
- package/dist/flow-script.d.ts +108 -0
- package/dist/flow-script.js +505 -0
- package/dist/flow.d.ts +294 -17
- package/dist/flow.js +803 -18
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/mermaid-driver.js +264 -24
- package/dist/mysql-driver.js +3 -0
- package/dist/project.d.ts +10 -6
- package/dist/project.js +35 -4
- package/dist/repository.d.ts +26 -0
- package/dist/repository.js +8 -0
- package/dist/service.d.ts +14 -2
- package/dist/service.js +49 -0
- package/dist/third-service.d.ts +5 -0
- package/dist/third-service.js +1 -0
- package/dist/typebox-driver.js +4 -0
- package/dist/utils.d.ts +9 -2
- package/dist/utils.js +4 -0
- package/docs/aggregate.md +110 -0
- package/docs/curd.md +146 -111
- package/docs/dao-generation.md +478 -0
- package/docs/ddd-principles.md +75 -0
- package/docs/domain-event.md +137 -0
- package/docs/keyword-matcher.md +182 -0
- package/docs/project.md +17 -9
- package/docs/token.md +327 -0
- package/docs/trans-reentrant.md +85 -0
- package/package.json +25 -6
- package/src/action.ts +51 -10
- package/src/aggregate.ts +104 -0
- package/src/business-flow.ts +80 -0
- package/src/controller.ts +25 -11
- package/src/convert.ts +51 -15
- package/src/curd.ts +12 -6
- package/src/dao.ts +377 -63
- package/src/db.ts +13 -0
- package/src/domain-event.ts +74 -0
- package/src/dsl.ts +23 -2
- package/src/dto.ts +9 -6
- package/src/entity.ts +43 -0
- package/src/exception.ts +30 -5
- package/src/expr.ts +65 -0
- package/src/filter.ts +70 -0
- package/src/flow-script.ts +696 -0
- package/src/flow.ts +1129 -46
- package/src/index.ts +6 -2
- package/src/mermaid-driver.ts +256 -29
- package/src/mysql-driver.ts +3 -0
- package/src/project.ts +138 -97
- package/src/repository.ts +35 -0
- package/src/service.ts +68 -3
- package/src/third-service.ts +6 -0
- package/src/typebox-driver.ts +4 -0
- package/src/utils.ts +13 -2
- package/src/endpoint.ts +0 -18
- package/src/provider.ts +0 -68
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// business-flow: the minimal form of a flow — a chart. Steps are [from, when,
|
|
2
|
+
// to] tuples (the condition sits between source and target; a two-element
|
|
3
|
+
// [from, to] step has no label); nodes are collected by name from the edges
|
|
4
|
+
// (a name shared by several steps is one node); the first step's from is the
|
|
5
|
+
// start; every node with no outgoing edge falls through to the flow's return
|
|
6
|
+
// end. A node with several outgoing edges is a decision — the mermaid driver
|
|
7
|
+
// draws it as a diamond, and every branch edge must carry a when label or the
|
|
8
|
+
// chart cannot be read (nor later upgraded to machine conditions).
|
|
9
|
+
//
|
|
10
|
+
// The chart compiles to the same graph IR as flow-script: mermaid rendering,
|
|
11
|
+
// reachability checks, and future consumers (an executor) all work unchanged.
|
|
12
|
+
// A chart is the skeleton — upgrade a decision to IF(cond) and a node to
|
|
13
|
+
// invoke(...) when the business logic arrives.
|
|
14
|
+
|
|
15
|
+
import { defineFlow, edge, node } from './flow.js';
|
|
16
|
+
import type { FlowEdge, FlowNode, FlowSchema, FlowStep } from './flow.js';
|
|
17
|
+
|
|
18
|
+
/** One chart step: from → to, the optional branch label between them
|
|
19
|
+
* (mandatory for decision branches). */
|
|
20
|
+
export type ChartStep = [from: string, to: string] | [from: string, when: string, to: string];
|
|
21
|
+
|
|
22
|
+
/** Compile a tuple chart into a FlowSchema. */
|
|
23
|
+
export function flowChart(
|
|
24
|
+
name: string,
|
|
25
|
+
steps: ChartStep[],
|
|
26
|
+
options: { description?: string; start?: string } = {},
|
|
27
|
+
): FlowSchema {
|
|
28
|
+
if (steps.length === 0) {
|
|
29
|
+
throw new Error(`business-flow ${name}: at least one step is required`);
|
|
30
|
+
}
|
|
31
|
+
for (const step of steps) {
|
|
32
|
+
const from = step[0];
|
|
33
|
+
const to = step.length === 3 ? step[2] : step[1];
|
|
34
|
+
if (from === '' || to === '') {
|
|
35
|
+
throw new Error(`business-flow ${name}: node names must not be empty`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (options.start !== undefined && !steps.some((step) => step[0] === options.start)) {
|
|
39
|
+
throw new Error(`business-flow ${name}: start "${options.start}" is not the source of any step`);
|
|
40
|
+
}
|
|
41
|
+
const nodes = new Map<string, FlowNode>();
|
|
42
|
+
const byName = (n: string): FlowNode => {
|
|
43
|
+
let found = nodes.get(n);
|
|
44
|
+
if (found === undefined) {
|
|
45
|
+
found = node(n, {});
|
|
46
|
+
nodes.set(n, found);
|
|
47
|
+
}
|
|
48
|
+
return found;
|
|
49
|
+
};
|
|
50
|
+
const edges: FlowEdge[] = steps.map((step) => {
|
|
51
|
+
const from = step[0];
|
|
52
|
+
const to = step.length === 3 ? step[2] : step[1];
|
|
53
|
+
const when = step.length === 3 ? step[1] : undefined;
|
|
54
|
+
return edge(byName(from), byName(to), { when });
|
|
55
|
+
});
|
|
56
|
+
// Decision rule: every branch of a multi-outgoing node must carry a label.
|
|
57
|
+
const outgoing = new Map<FlowStep, FlowEdge[]>();
|
|
58
|
+
for (const e of edges) {
|
|
59
|
+
const list = outgoing.get(e.start);
|
|
60
|
+
if (list === undefined) outgoing.set(e.start, [e]);
|
|
61
|
+
else list.push(e);
|
|
62
|
+
}
|
|
63
|
+
for (const [n, list] of outgoing) {
|
|
64
|
+
if (list.length > 1 && list.some((e) => e.when === undefined)) {
|
|
65
|
+
throw new Error(`business-flow ${name}: decision node "${n.name}" — every outgoing branch edge needs a when label`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return defineFlow(name, {
|
|
69
|
+
start: byName(options.start ?? steps[0][0]),
|
|
70
|
+
description: options.description,
|
|
71
|
+
edges: (flow) => {
|
|
72
|
+
// Every path ends somewhere: nodes with no outgoing edge return.
|
|
73
|
+
const sinks: FlowEdge[] = [];
|
|
74
|
+
for (const n of nodes.values()) {
|
|
75
|
+
if (!outgoing.has(n)) sinks.push(edge(n, flow.returnEnd));
|
|
76
|
+
}
|
|
77
|
+
return [...edges, ...sinks];
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
package/src/controller.ts
CHANGED
|
@@ -1,40 +1,54 @@
|
|
|
1
1
|
import { SchemaBase } from './dsl.js';
|
|
2
|
-
import { FrontAppSchema } from './project.js';
|
|
3
|
-
import type {
|
|
2
|
+
import { FrontAppSchema, ProjectApiSchema } from './project.js';
|
|
3
|
+
import type { DtoMessage } from './dto.js';
|
|
4
4
|
|
|
5
5
|
/** A backend RPC controller. Strong constraints:
|
|
6
6
|
* - a backend module maps 1:1 to a frontend app (they are peers);
|
|
7
7
|
* - a controller serves exactly one frontend app — no cross-module calls. */
|
|
8
8
|
export interface ControllerSchema extends SchemaBase {
|
|
9
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;
|
|
10
14
|
/** The frontend app this controller serves (shared instance from project.config). */
|
|
11
15
|
app: FrontAppSchema;
|
|
12
|
-
/** RPC methods
|
|
13
|
-
methods: ControllerMethodSchema
|
|
16
|
+
/** RPC methods, keyed by method name (key === method.name, enforced by the builder). */
|
|
17
|
+
methods: Record<string, ControllerMethodSchema>;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
export function defineController(options: {
|
|
17
21
|
name: string;
|
|
22
|
+
api: ProjectApiSchema;
|
|
18
23
|
app: FrontAppSchema;
|
|
19
|
-
/** Method declarations: type/schema are injected by this builder. */
|
|
20
|
-
methods:
|
|
24
|
+
/** Method declarations: type/schema/name are injected by this builder. */
|
|
25
|
+
methods: Record<string, Omit<ControllerMethodSchema, 'type' | 'schema' | 'name'>>;
|
|
21
26
|
description?: string;
|
|
22
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
|
+
}
|
|
23
31
|
const schema: ControllerSchema = {
|
|
24
32
|
type: 'controller',
|
|
25
33
|
name: options.name,
|
|
26
34
|
description: options.description,
|
|
35
|
+
api: options.api,
|
|
27
36
|
app: options.app,
|
|
28
|
-
methods:
|
|
37
|
+
methods: {},
|
|
29
38
|
};
|
|
30
|
-
|
|
39
|
+
for (const [key, method] of Object.entries(options.methods)) {
|
|
40
|
+
schema.methods[key] = { type: 'method', schema, name: key, ...method };
|
|
41
|
+
}
|
|
31
42
|
return schema;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
/** An RPC method exposed by a controller.
|
|
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. */
|
|
35
49
|
export interface ControllerMethodSchema extends SchemaBase {
|
|
36
50
|
type: 'method';
|
|
37
51
|
schema: ControllerSchema;
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
args: DtoMessage;
|
|
53
|
+
results: DtoMessage | number | boolean | string;
|
|
40
54
|
}
|
package/src/convert.ts
CHANGED
|
@@ -1,43 +1,79 @@
|
|
|
1
1
|
import type { SchemaBase } from './dsl.js';
|
|
2
2
|
import type { DtoMessage } from './dto.js';
|
|
3
3
|
import type { TableSchema } from './db.js';
|
|
4
|
+
import type { EntitySchema } from './entity.js';
|
|
4
5
|
import type { ThirdMethodSchema } from './third-service.js';
|
|
5
|
-
import type { FrontAppSchema } from './project.js';
|
|
6
|
+
import type { FrontAppSchema, ProjectApiSchema } from './project.js';
|
|
6
7
|
|
|
7
8
|
// Schema-collection integration: multiple source collections combine into
|
|
8
9
|
// one target collection (e.g. two entity tables into one dto, or entity
|
|
9
10
|
// columns plus a dto into one third-party wire message).
|
|
11
|
+
//
|
|
12
|
+
// A convert file binds to ONE source identity — a table (internal mapping,
|
|
13
|
+
// {Table}Convert.ts) or a third-party service (anti-corruption translation,
|
|
14
|
+
// {third-service}.convert.ts) — and holds N methods keyed by name (same
|
|
15
|
+
// shape as defineService / defineDao).
|
|
10
16
|
|
|
11
|
-
/** A source/target collection of a convert — dto, entity or
|
|
12
|
-
|
|
17
|
+
/** A source/target collection of a convert — dto, entity, table or
|
|
18
|
+
* third-party message. Entity sources may carry aggregate fields
|
|
19
|
+
* (aggField), e.g. an aggregate result entity projected into a wire
|
|
20
|
+
* message. */
|
|
21
|
+
export type ConvertSourceSchema = DtoMessage | TableSchema | ThirdMethodSchema | EntitySchema;
|
|
13
22
|
|
|
14
|
-
/**
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
/** Method input for defineConvert: type/schema/name are set by the builder. */
|
|
24
|
+
export type ConvertMethodDef = Omit<ConvertMethodSchema, 'type' | 'schema' | 'name'>;
|
|
25
|
+
|
|
26
|
+
/** One conversion: multiple source collections → single target collection. */
|
|
27
|
+
export interface ConvertMethodSchema extends SchemaBase {
|
|
28
|
+
type: 'convertMethod';
|
|
29
|
+
/** The convert file this method belongs to. */
|
|
30
|
+
schema: ConvertSchema;
|
|
19
31
|
/** Source schemas — one or more, mixed dimensions. */
|
|
20
32
|
sources: ConvertSourceSchema[];
|
|
21
33
|
/** Target schema — the single integrated collection. */
|
|
22
34
|
target: ConvertSourceSchema;
|
|
23
35
|
}
|
|
24
36
|
|
|
37
|
+
/** Declares multi-source → single-target schema integrations grouped by source identity. */
|
|
38
|
+
export interface ConvertSchema extends SchemaBase {
|
|
39
|
+
type: 'convert';
|
|
40
|
+
/** The backend api module this convert belongs to (shared instance from
|
|
41
|
+
* project.config.ts apis). Storage is convert_schema/{api.name}/{app.name}/
|
|
42
|
+
* — same layout as service_schema. */
|
|
43
|
+
api: ProjectApiSchema;
|
|
44
|
+
/** The app (module) this convert belongs to — its artifact lands in modules/{app}/convert/. */
|
|
45
|
+
app: FrontAppSchema;
|
|
46
|
+
/** Methods keyed by name — the map key is written back as the method name. */
|
|
47
|
+
methods: Record<string, ConvertMethodSchema>;
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
export function defineConvert(options: {
|
|
26
51
|
name: string;
|
|
52
|
+
api: ProjectApiSchema;
|
|
27
53
|
app: FrontAppSchema;
|
|
28
|
-
|
|
29
|
-
target: ConvertSourceSchema;
|
|
54
|
+
methods: Record<string, ConvertMethodDef>;
|
|
30
55
|
description?: string;
|
|
31
56
|
}): ConvertSchema {
|
|
32
|
-
if (options.
|
|
33
|
-
throw new Error(`convert '${options.name}':
|
|
57
|
+
if (Object.keys(options.methods).length === 0) {
|
|
58
|
+
throw new Error(`convert '${options.name}': methods must not be empty`);
|
|
59
|
+
}
|
|
60
|
+
if (!options.api.apps.includes(options.app)) {
|
|
61
|
+
throw new Error(`convert '${options.name}': api '${options.api.name}' does not serve app '${options.app.name}'`);
|
|
34
62
|
}
|
|
35
|
-
|
|
63
|
+
const schema: ConvertSchema = {
|
|
36
64
|
type: 'convert',
|
|
37
65
|
name: options.name,
|
|
38
66
|
description: options.description,
|
|
67
|
+
api: options.api,
|
|
39
68
|
app: options.app,
|
|
40
|
-
|
|
41
|
-
target: options.target,
|
|
69
|
+
methods: {},
|
|
42
70
|
};
|
|
71
|
+
for (const key of Object.keys(options.methods)) {
|
|
72
|
+
const method = options.methods[key] as ConvertMethodDef;
|
|
73
|
+
if (method.sources.length === 0) {
|
|
74
|
+
throw new Error(`convert '${options.name}': method '${key}' sources must not be empty`);
|
|
75
|
+
}
|
|
76
|
+
schema.methods[key] = { type: 'convertMethod', schema, ...method, name: key };
|
|
77
|
+
}
|
|
78
|
+
return schema;
|
|
43
79
|
}
|
package/src/curd.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { SchemaBase, Field
|
|
1
|
+
import { SchemaBase, Field } 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 type { FilterSchema } from './filter.js';
|
|
5
6
|
import { toKebabCase } from '@pylonts/core';
|
|
6
7
|
|
|
7
8
|
// Admin-only CRUD page standard: binds one entity table to a frontend admin
|
|
@@ -26,12 +27,13 @@ export interface CurdListConfig {
|
|
|
26
27
|
/** List columns; required, non-empty. Every field the list shows must be
|
|
27
28
|
* listed explicitly. May include cross-table fields via foreign refs. */
|
|
28
29
|
columns: Field[];
|
|
29
|
-
/**
|
|
30
|
-
|
|
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;
|
|
31
35
|
/** Default sort. Required — column and direction are both mandatory. */
|
|
32
36
|
orderBy: { column: Field; direction: 'asc' | 'desc' };
|
|
33
|
-
/** Search condition fields; op defaults to 'eq'. */
|
|
34
|
-
searchFields?: { field: Field; op?: Operator }[];
|
|
35
37
|
/** Column header text overrides: Field.name → header text. */
|
|
36
38
|
columnTitles?: Record<string, string>;
|
|
37
39
|
}
|
|
@@ -93,7 +95,11 @@ export function defineCurd(name: string, schema: Omit<CurdSchema, 'name'>): Curd
|
|
|
93
95
|
for (const [pageName, page] of Object.entries(curd.actionPages ?? {})) {
|
|
94
96
|
if (page) assertColumns(curd, `actionPages.${pageName}`, page.columns);
|
|
95
97
|
}
|
|
96
|
-
|
|
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 app '${curd.list.filter.app.name}' but the curd belongs to app '${curd.app.name}'`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
97
103
|
assertFieldsOwnTable(curd, 'orderBy', [curd.list.orderBy.column]);
|
|
98
104
|
return curd;
|
|
99
105
|
}
|