@pylonts/dsl 1.0.0 → 1.0.2
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 +17 -85
- package/dist/dictionary.d.ts +8 -0
- package/dist/dictionary.js +7 -0
- package/dist/dsl.d.ts +19 -5
- package/dist/dsl.js +24 -4
- package/dist/enum-driver.d.ts +2 -2
- package/dist/enum-driver.js +5 -7
- package/dist/flow.d.ts +28 -0
- package/dist/flow.js +68 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/mermaid-driver.d.ts +4 -0
- package/dist/mermaid-driver.js +72 -0
- package/dist/mysql-driver.d.ts +5 -1
- package/dist/mysql-driver.js +21 -4
- package/dist/page-flow.d.ts +21 -0
- package/dist/page-flow.js +25 -0
- package/dist/page.d.ts +26 -0
- package/dist/page.js +14 -0
- package/dist/pattern.d.ts +15 -0
- package/dist/pattern.js +13 -0
- package/dist/patterns/retry.d.ts +16 -0
- package/dist/patterns/retry.js +40 -0
- package/dist/project.d.ts +41 -0
- package/dist/project.js +11 -0
- package/dist/prototype.d.ts +18 -0
- package/dist/prototype.js +10 -0
- package/dist/typebox-driver.js +4 -8
- package/docs/dictionary.md +20 -0
- package/docs/driver.md +42 -0
- package/docs/dto.md +54 -0
- package/docs/enum.md +24 -0
- package/docs/mysql-connection.md +1 -0
- package/docs/project.md +24 -0
- package/docs/prototype.md +21 -0
- package/docs/table.md +90 -0
- package/package.json +3 -2
- package/src/dictionary.ts +15 -0
- package/src/dsl.ts +50 -8
- package/src/enum-driver.ts +7 -8
- package/src/flow.ts +104 -0
- package/src/index.ts +9 -0
- package/src/mermaid-driver.ts +81 -0
- package/src/mysql-driver.ts +26 -5
- package/src/page-flow.ts +52 -0
- package/src/page.ts +40 -0
- package/src/pattern.ts +27 -0
- package/src/patterns/retry.ts +55 -0
- package/src/project.ts +55 -0
- package/src/prototype.ts +30 -0
- package/src/typebox-driver.ts +4 -6
package/src/enum-driver.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EnumDef } from './dsl';
|
|
2
2
|
|
|
3
|
-
// Enum driver: renders an
|
|
3
|
+
// Enum driver: renders an EnumDef into a standalone TypeScript enum file.
|
|
4
4
|
// Shape matches the generated-enum product consumed by the TypeBox driver (Type.Enum):
|
|
5
5
|
//
|
|
6
6
|
// export enum UserStatus {
|
|
@@ -26,11 +26,10 @@ function labelName(jsName: string): string {
|
|
|
26
26
|
return `${jsName.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase()}_LABEL`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export function renderEnum(
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const labels = field.values.map((v) => ` [${name}.${v.symbol}]: ${renderString(v.label)},`);
|
|
29
|
+
export function renderEnum(def: EnumDef): string {
|
|
30
|
+
const name = def.jsName;
|
|
31
|
+
const members = def.values.map((v) => ` ${v.symbol} = ${renderValue(v.value)},`);
|
|
32
|
+
const labels = def.values.map((v) => ` [${name}.${v.symbol}]: ${renderString(v.label)},`);
|
|
34
33
|
return [
|
|
35
34
|
`export enum ${name} {`,
|
|
36
35
|
...members,
|
|
@@ -41,4 +40,4 @@ export function renderEnum(field: EnumField): string {
|
|
|
41
40
|
'};',
|
|
42
41
|
'',
|
|
43
42
|
].join('\n');
|
|
44
|
-
}
|
|
43
|
+
}
|
package/src/flow.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl';
|
|
2
|
+
|
|
3
|
+
// Flow model: nodes and edges are separate entities. A FlowNode is a plain
|
|
4
|
+
// named step; a FlowEdge references its start/end node objects directly (no
|
|
5
|
+
// ids). Because a node object may appear in many edges, the graph can share
|
|
6
|
+
// nodes, merge branches, and form cycles (e.g. poll-and-retry loops).
|
|
7
|
+
|
|
8
|
+
export interface FlowNode extends SchemaBase {
|
|
9
|
+
/** Optional sub-flow. When present, entering this node runs the sub-flow;
|
|
10
|
+
* after the sub-flow reaches any terminal node, the outer flow continues
|
|
11
|
+
* via this node's outgoing edges. Sub-flows nest recursively. */
|
|
12
|
+
flow?: FlowSchema;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FlowEdge extends SchemaBase {
|
|
16
|
+
/** Trigger condition; undefined = default path (success/normal). */
|
|
17
|
+
when?: string;
|
|
18
|
+
start: FlowNode;
|
|
19
|
+
end: FlowNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FlowSchema extends SchemaBase {
|
|
23
|
+
/** Entry node. */
|
|
24
|
+
start: FlowNode;
|
|
25
|
+
/** All nodes, collected from edges (deduplicated by object identity). */
|
|
26
|
+
nodes: FlowNode[];
|
|
27
|
+
/** Independent edges; a node may be start of many edges, so cycles are expressible. */
|
|
28
|
+
edges: FlowEdge[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function node(name: string, flow?: FlowSchema, description?: string): FlowNode {
|
|
32
|
+
return { name, flow, description };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function edge(start: FlowNode, end: FlowNode, when?: string, description?: string): FlowEdge {
|
|
36
|
+
// Auto name for uniformity with SchemaBase; `when` stays the branch marker.
|
|
37
|
+
return { name: `${start.name}->${end.name}`, start, end, when, description };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function defineFlow(
|
|
41
|
+
name: string,
|
|
42
|
+
schema: {
|
|
43
|
+
start: FlowNode;
|
|
44
|
+
edges: FlowEdge[];
|
|
45
|
+
description?: string;
|
|
46
|
+
},
|
|
47
|
+
): FlowSchema {
|
|
48
|
+
const seen = new Set<FlowNode>();
|
|
49
|
+
const nodes: FlowNode[] = [];
|
|
50
|
+
for (const e of schema.edges) {
|
|
51
|
+
for (const n of [e.start, e.end]) {
|
|
52
|
+
if (!seen.has(n)) {
|
|
53
|
+
seen.add(n);
|
|
54
|
+
nodes.push(n);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!seen.has(schema.start)) {
|
|
59
|
+
seen.add(schema.start);
|
|
60
|
+
nodes.push(schema.start);
|
|
61
|
+
}
|
|
62
|
+
const flow: FlowSchema = { name, description: schema.description, start: schema.start, nodes, edges: schema.edges };
|
|
63
|
+
validate(flow);
|
|
64
|
+
return flow;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Nodes that never appear as an edge start are terminals. Reverse BFS from all
|
|
68
|
+
// terminals marks every node that can reach a terminal; unmarked nodes sit on
|
|
69
|
+
// a path that never ends (e.g. a cycle without an exit) — reject them at
|
|
70
|
+
// definition time.
|
|
71
|
+
function validate(schema: FlowSchema): void {
|
|
72
|
+
const starts = new Set<FlowNode>();
|
|
73
|
+
for (const e of schema.edges) starts.add(e.start);
|
|
74
|
+
|
|
75
|
+
const reverse = new Map<FlowNode, FlowNode[]>();
|
|
76
|
+
for (const n of schema.nodes) reverse.set(n, []);
|
|
77
|
+
for (const e of schema.edges) {
|
|
78
|
+
reverse.get(e.end)!.push(e.start);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const reached = new Set<FlowNode>();
|
|
82
|
+
const queue: FlowNode[] = [];
|
|
83
|
+
for (const n of schema.nodes) {
|
|
84
|
+
if (!starts.has(n)) {
|
|
85
|
+
reached.add(n);
|
|
86
|
+
queue.push(n);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
while (queue.length > 0) {
|
|
90
|
+
const cur = queue.shift()!;
|
|
91
|
+
for (const prev of reverse.get(cur)!) {
|
|
92
|
+
if (!reached.has(prev)) {
|
|
93
|
+
reached.add(prev);
|
|
94
|
+
queue.push(prev);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const n of schema.nodes) {
|
|
100
|
+
if (!reached.has(n)) {
|
|
101
|
+
throw new Error(`flow ${schema.name}: node "${n.name}" cannot reach a terminal`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export * from './dsl';
|
|
2
2
|
export * from './dto';
|
|
3
|
+
export * from './project';
|
|
4
|
+
export * from './prototype';
|
|
5
|
+
export * from './dictionary';
|
|
3
6
|
export * from './mysql-driver';
|
|
4
7
|
export * from './enum-driver';
|
|
5
8
|
export * from './typebox-driver';
|
|
9
|
+
export * from './pattern';
|
|
10
|
+
export * from './patterns/retry';
|
|
11
|
+
export * from './flow';
|
|
12
|
+
export * from './page';
|
|
13
|
+
export * from './page-flow';
|
|
14
|
+
export * from './mermaid-driver';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { FlowEdge, FlowNode, FlowSchema } from './flow';
|
|
2
|
+
import { Page } from './page';
|
|
3
|
+
import { PageEdge, PageFlow } from './page-flow';
|
|
4
|
+
|
|
5
|
+
// Mermaid driver: converts a FlowSchema into a Mermaid flowchart (TD).
|
|
6
|
+
// Node ids are hierarchical (n0, n0_0, n0_0_0, ...) so nested sub-flows stay
|
|
7
|
+
// globally unique. A node with a sub-flow renders as a subgraph block whose
|
|
8
|
+
// internals are rendered recursively. ok edges render as -->, conditional
|
|
9
|
+
// edges render as -->|"WHEN"|.
|
|
10
|
+
|
|
11
|
+
function escapeLabel(s: string): string {
|
|
12
|
+
return s.replace(/"/g, '\\"').replace(/\n/g, '<br/>');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderFlowMermaid(schema: FlowSchema): string {
|
|
16
|
+
const lines: string[] = ['flowchart TD'];
|
|
17
|
+
const ids = new Map<FlowNode, string>();
|
|
18
|
+
renderFlow(schema, 'n', lines, ids);
|
|
19
|
+
lines.push('');
|
|
20
|
+
lines.push(' classDef start fill:#e6f4ea,stroke:#333,stroke-width:1px;');
|
|
21
|
+
lines.push(` class ${ids.get(schema.start)} start;`);
|
|
22
|
+
return lines.join('\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderFlow(schema: FlowSchema, prefix: string, lines: string[], ids: Map<FlowNode, string>): void {
|
|
26
|
+
schema.nodes.forEach((n, i) => ids.set(n, `${prefix}${i}`));
|
|
27
|
+
for (const n of schema.nodes) {
|
|
28
|
+
const id = ids.get(n)!;
|
|
29
|
+
if (n.flow) {
|
|
30
|
+
lines.push(` subgraph ${id}["${escapeLabel(n.name)}"]`);
|
|
31
|
+
renderFlow(n.flow, `${id}_`, lines, ids);
|
|
32
|
+
lines.push(' end');
|
|
33
|
+
} else {
|
|
34
|
+
lines.push(` ${id}["${escapeLabel(n.name)}"]`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const e of schema.edges) {
|
|
38
|
+
lines.push(renderEdge(e, ids));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderEdge(e: FlowEdge, ids: Map<FlowNode, string>): string {
|
|
43
|
+
const label = e.when ? `|"${escapeLabel(e.when)}"|` : '';
|
|
44
|
+
return ` ${ids.get(e.start)} -->${label} ${ids.get(e.end)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Page-driven flow renderer: groups pages by their app into swimlane
|
|
48
|
+
// subgraphs, then renders edges across the whole flow.
|
|
49
|
+
|
|
50
|
+
export function renderPageFlowMermaid(schema: PageFlow): string {
|
|
51
|
+
const lines: string[] = ['flowchart TD'];
|
|
52
|
+
const ids = new Map<Page, string>();
|
|
53
|
+
|
|
54
|
+
const byApp = new Map<string, Page[]>();
|
|
55
|
+
for (const p of schema.pages) {
|
|
56
|
+
const list = byApp.get(p.app.name) ?? [];
|
|
57
|
+
list.push(p);
|
|
58
|
+
byApp.set(p.app.name, list);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let appIdx = 0;
|
|
62
|
+
for (const [appName, pages] of byApp) {
|
|
63
|
+
lines.push(` subgraph app${appIdx}["${escapeLabel(appName)}"]`);
|
|
64
|
+
pages.forEach((p, i) => ids.set(p, `app${appIdx}_p${i}`));
|
|
65
|
+
for (const p of pages) {
|
|
66
|
+
lines.push(` ${ids.get(p)}["${escapeLabel(p.name)}"]`);
|
|
67
|
+
}
|
|
68
|
+
appIdx++;
|
|
69
|
+
lines.push(' end');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const e of schema.edges) {
|
|
73
|
+
const label = e.when ? `|"${escapeLabel(e.when.name)}"|` : '';
|
|
74
|
+
lines.push(` ${ids.get(e.start)} -->${label} ${ids.get(e.end)}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push(' classDef start fill:#e6f4ea,stroke:#333,stroke-width:1px;');
|
|
79
|
+
lines.push(` class ${ids.get(schema.start)} start;`);
|
|
80
|
+
return lines.join('\n');
|
|
81
|
+
}
|
package/src/mysql-driver.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Field, Index, TableSchema } from './dsl';
|
|
1
|
+
import { Field, ForeignKey, Index, TableSchema } from './dsl';
|
|
2
2
|
|
|
3
3
|
// MySQL driver: converts a TableSchema into a CREATE TABLE statement.
|
|
4
4
|
|
|
@@ -25,16 +25,17 @@ function columnType(field: Field): string {
|
|
|
25
25
|
return 'DATETIME';
|
|
26
26
|
case 'enum':
|
|
27
27
|
// Enum is stored as a plain column: string -> VARCHAR(20), integer -> TINYINT.
|
|
28
|
-
return field.valueType === 'integer' ? 'TINYINT' : 'VARCHAR(20)';
|
|
28
|
+
return field.enum.valueType === 'integer' ? 'TINYINT' : 'VARCHAR(20)';
|
|
29
29
|
case 'json':
|
|
30
30
|
return 'JSON';
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function columnDef(field: Field): string {
|
|
34
|
+
function columnDef(field: Field, autoIncrement?: Field): string {
|
|
35
35
|
const parts = [field.name, columnType(field)];
|
|
36
36
|
if (field.optional === false) parts.push('NOT NULL');
|
|
37
37
|
if (field.default !== undefined) parts.push(`DEFAULT '${field.default}'`);
|
|
38
|
+
if (field === autoIncrement) parts.push('AUTO_INCREMENT');
|
|
38
39
|
return parts.join(' ');
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -51,10 +52,30 @@ function indexClause(index: Index): string {
|
|
|
51
52
|
return `${kind} ${name} (${fields.map((f) => f.name).join(', ')})`;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
const
|
|
55
|
+
function foreignKeyClause(name: string, fk: ForeignKey): string {
|
|
56
|
+
const fields = Array.isArray(fk.fields) ? fk.fields : [fk.fields];
|
|
57
|
+
const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
|
|
58
|
+
const refTable = refs[0].schema?.name;
|
|
59
|
+
if (!refTable) throw new Error(`foreign key ${name}: references field has no schema`);
|
|
60
|
+
const fkCols = fields.map((f) => f.name).join(', ');
|
|
61
|
+
const refCols = refs.map((r) => r.name).join(', ');
|
|
62
|
+
return `CONSTRAINT \`${name}\` FOREIGN KEY (${fkCols}) REFERENCES \`${refTable}\` (${refCols})`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface BuildCreateTableSqlOptions {
|
|
66
|
+
/** 是否生成外键约束,默认 false(不生成) */
|
|
67
|
+
generateForeignKeys?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildCreateTableSql(schema: TableSchema, options: BuildCreateTableSqlOptions = {}): string {
|
|
71
|
+
const lines = Object.values(schema.fields).map((field) => columnDef(field, schema.autoIncrement));
|
|
56
72
|
const pk = primaryKeyClause(schema);
|
|
57
73
|
if (pk) lines.push(pk);
|
|
58
74
|
for (const index of schema.indexes ?? []) lines.push(indexClause(index));
|
|
75
|
+
if (options.generateForeignKeys) {
|
|
76
|
+
for (const [name, fk] of Object.entries(schema.foreignKeys ?? {})) {
|
|
77
|
+
lines.push(foreignKeyClause(name, fk));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
59
80
|
return `CREATE TABLE \`${schema.name}\` (\n ${lines.join(',\n ')}\n);`;
|
|
60
81
|
}
|
package/src/page-flow.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl';
|
|
2
|
+
import { ActionSchema, Page } from './page';
|
|
3
|
+
|
|
4
|
+
// Page-driven flow: every node is a page, and a page belongs to an app.
|
|
5
|
+
// Leaf nodes are pages too — a journey starts at a page and ends at a page.
|
|
6
|
+
// Page definitions (PageSchema/Page/actions) live in page.ts; this file
|
|
7
|
+
// models the flow graph between pages.
|
|
8
|
+
|
|
9
|
+
export interface PageEdge extends SchemaBase {
|
|
10
|
+
/** Trigger action; undefined = default path (success/normal). */
|
|
11
|
+
when?: ActionSchema;
|
|
12
|
+
start: Page;
|
|
13
|
+
end: Page;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PageFlow extends SchemaBase {
|
|
17
|
+
/** Entry page. */
|
|
18
|
+
start: Page;
|
|
19
|
+
/** All pages, collected from edges (deduplicated by object identity). */
|
|
20
|
+
pages: Page[];
|
|
21
|
+
edges: PageEdge[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function pageEdge(start: Page, end: Page, when?: ActionSchema, description?: string): PageEdge {
|
|
25
|
+
// Auto name for uniformity with SchemaBase; `when` stays the branch marker.
|
|
26
|
+
return { name: `${start.name}->${end.name}`, start, end, when, description };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function definePageFlow(
|
|
30
|
+
name: string,
|
|
31
|
+
schema: {
|
|
32
|
+
start: Page;
|
|
33
|
+
edges: PageEdge[];
|
|
34
|
+
description?: string;
|
|
35
|
+
},
|
|
36
|
+
): PageFlow {
|
|
37
|
+
const seen = new Set<Page>();
|
|
38
|
+
const pages: Page[] = [];
|
|
39
|
+
for (const e of schema.edges) {
|
|
40
|
+
for (const p of [e.start, e.end]) {
|
|
41
|
+
if (!seen.has(p)) {
|
|
42
|
+
seen.add(p);
|
|
43
|
+
pages.push(p);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!seen.has(schema.start)) {
|
|
48
|
+
seen.add(schema.start);
|
|
49
|
+
pages.push(schema.start);
|
|
50
|
+
}
|
|
51
|
+
return { name, description: schema.description, start: schema.start, pages, edges: schema.edges };
|
|
52
|
+
}
|
package/src/page.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl';
|
|
2
|
+
import { FrontApp } from './project';
|
|
3
|
+
|
|
4
|
+
// Page definitions: standalone page schemas and the page node type used by
|
|
5
|
+
// page-driven flows. Kept separate from page-flow.ts (the flow graph itself).
|
|
6
|
+
|
|
7
|
+
/** Standalone page definition. A page is a shared value object: it lists
|
|
8
|
+
* the actions a user can perform, and belongs to exactly one frontend app. */
|
|
9
|
+
export interface PageSchema extends SchemaBase {
|
|
10
|
+
/** The frontend app this page belongs to (shared instance from project.config). */
|
|
11
|
+
app: FrontApp;
|
|
12
|
+
/** Actions a user can perform on this page (e.g. submit, approve, reject). */
|
|
13
|
+
actions: ActionSchema[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** An action a user can perform on a page (e.g. submit, approve, reject). */
|
|
17
|
+
export interface ActionSchema extends SchemaBase {}
|
|
18
|
+
|
|
19
|
+
export function defineAction(name: string, description?: string): ActionSchema {
|
|
20
|
+
return { name, description };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function definePage(schema: {
|
|
24
|
+
name: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
app: FrontApp;
|
|
27
|
+
actions: ActionSchema[];
|
|
28
|
+
}): PageSchema {
|
|
29
|
+
return { ...schema };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A page node in a page-driven flow: every node is a page, and a page belongs to an app. */
|
|
33
|
+
export interface Page extends SchemaBase {
|
|
34
|
+
/** The frontend app this page belongs to (shared instance from project.config). */
|
|
35
|
+
app: FrontApp;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function page(app: FrontApp, name: string, description?: string): Page {
|
|
39
|
+
return { name, app, description };
|
|
40
|
+
}
|
package/src/pattern.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Pattern core: minimal definitions to bootstrap the Flow × Pattern DSL.
|
|
2
|
+
// A Pattern is a reusable solution ("how to guarantee success") declared as
|
|
3
|
+
// pure data. Concrete usage fills its params and action injection points.
|
|
4
|
+
|
|
5
|
+
export interface PatternParamDef {
|
|
6
|
+
type: 'int' | 'string';
|
|
7
|
+
min?: number;
|
|
8
|
+
default?: number | string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PatternDef {
|
|
12
|
+
name: string;
|
|
13
|
+
params: Record<string, PatternParamDef>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PatternRef {
|
|
17
|
+
ref: string;
|
|
18
|
+
args: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function definePattern(def: PatternDef): PatternDef {
|
|
22
|
+
return def;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ref(name: string, args: Record<string, unknown>): PatternRef {
|
|
26
|
+
return { ref: name, args };
|
|
27
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { PatternDef } from '../pattern';
|
|
2
|
+
|
|
3
|
+
// Retry pattern: blind mechanical retry for read-only operations.
|
|
4
|
+
// Valid because a read-only action is idempotent by nature: clicking a query
|
|
5
|
+
// button any number of times never changes the result, so retrying the same
|
|
6
|
+
// call is always safe. No idempotency key, no query-and-resume, no compensate.
|
|
7
|
+
|
|
8
|
+
export const retryPattern: PatternDef = {
|
|
9
|
+
name: 'retry',
|
|
10
|
+
params: {
|
|
11
|
+
max: { type: 'int', min: 1, default: 3 },
|
|
12
|
+
backoffMs: { type: 'int', min: 0, default: 0 },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface RetryAction {
|
|
17
|
+
/** Function to call, e.g. 'queryOrderList' */
|
|
18
|
+
call: string;
|
|
19
|
+
/** Type name of the single argument passed through, e.g. 'OrderQueryParams' */
|
|
20
|
+
params?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RetryRefArgs {
|
|
24
|
+
max?: number;
|
|
25
|
+
backoffMs?: number;
|
|
26
|
+
action: RetryAction;
|
|
27
|
+
/** Generated function name; defaults to '<call>WithRetry' */
|
|
28
|
+
fnName?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderRetry(args: RetryRefArgs): string {
|
|
32
|
+
if (args.max !== undefined && args.max < 1) throw new Error('retry: max must be >= 1');
|
|
33
|
+
if (!args.action.call) throw new Error('retry: action.call is required');
|
|
34
|
+
|
|
35
|
+
const max = args.max ?? 3;
|
|
36
|
+
const backoffMs = args.backoffMs ?? 0;
|
|
37
|
+
const call = args.action.call;
|
|
38
|
+
const paramType = args.action.params ?? 'unknown';
|
|
39
|
+
const fnName = args.fnName ?? `${call}WithRetry`;
|
|
40
|
+
const retryLine = backoffMs > 0 ? ` await sleep(${backoffMs});` : '';
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
`export async function ${fnName}(params: ${paramType}) {`,
|
|
44
|
+
` for (let attempt = 1; ; attempt++) {`,
|
|
45
|
+
` try {`,
|
|
46
|
+
` return await ${call}(params);`,
|
|
47
|
+
` } catch (err) {`,
|
|
48
|
+
` if (attempt >= ${max}) throw err;`,
|
|
49
|
+
retryLine,
|
|
50
|
+
` }`,
|
|
51
|
+
` }`,
|
|
52
|
+
`}`,
|
|
53
|
+
'',
|
|
54
|
+
].join('\n');
|
|
55
|
+
}
|
package/src/project.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl';
|
|
2
|
+
|
|
3
|
+
// Project topology definitions: describe the applications (frontends) and
|
|
4
|
+
// backend APIs of a repository, and which frontends each API serves.
|
|
5
|
+
|
|
6
|
+
/** Frontend form factor. Closed enum, extend when new form factors appear. */
|
|
7
|
+
export type FrontType = 'admin' | 'wxmini';
|
|
8
|
+
|
|
9
|
+
/** A frontend application (e.g. admin console, wechat mini program). */
|
|
10
|
+
export interface FrontApp extends SchemaBase {
|
|
11
|
+
type: FrontType;
|
|
12
|
+
/** Source directory relative to project root, e.g. 'web-admin/'. */
|
|
13
|
+
dir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A backend API service. apps references shared FrontApp instances. */
|
|
17
|
+
export interface ProjectApi extends SchemaBase {
|
|
18
|
+
/** Source directory relative to project root, e.g. 'api/'. */
|
|
19
|
+
dir: string;
|
|
20
|
+
/** Frontends this API serves. Direct instance references (see defineProject). */
|
|
21
|
+
apps: FrontApp[];
|
|
22
|
+
/** API base URL prefix shared by all apps it serves, e.g. '/mall'. '' = no prefix. */
|
|
23
|
+
contextPath?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A third-party system (e.g. wechat pay, unionpay). Owns its own
|
|
27
|
+
* implementation dir and contract (controller_types), just like an API,
|
|
28
|
+
* but is not part of this repo's served surface. */
|
|
29
|
+
export interface ThirdApi extends SchemaBase {
|
|
30
|
+
/** Source directory relative to project root, e.g. 'wechat/'. */
|
|
31
|
+
dir: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProjectSchema extends SchemaBase {
|
|
35
|
+
apps: FrontApp[];
|
|
36
|
+
apis: ProjectApi[];
|
|
37
|
+
thirdApis: ThirdApi[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Defines the project topology. FrontApp instances are shared value objects:
|
|
42
|
+
* api.apps references the same instances from project.apps, so an app served
|
|
43
|
+
* by multiple APIs is defined once and referenced many times.
|
|
44
|
+
*/
|
|
45
|
+
export function defineProject(
|
|
46
|
+
name: string,
|
|
47
|
+
schema: {
|
|
48
|
+
description?: string;
|
|
49
|
+
apps: FrontApp[];
|
|
50
|
+
apis: ProjectApi[];
|
|
51
|
+
thirdApis?: ThirdApi[];
|
|
52
|
+
},
|
|
53
|
+
): ProjectSchema {
|
|
54
|
+
return { name, ...schema, thirdApis: schema.thirdApis ?? [] };
|
|
55
|
+
}
|
package/src/prototype.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl';
|
|
2
|
+
|
|
3
|
+
// Prototype definitions: high-level design of a page. A prototype lists only
|
|
4
|
+
// the fields a page needs — no types, no bindings to apps/APIs/tables. Field
|
|
5
|
+
// details (DTO/table definitions) are written separately and connected later.
|
|
6
|
+
|
|
7
|
+
/** Display metadata for a prototype field. */
|
|
8
|
+
export interface PrototypeFieldMeta {
|
|
9
|
+
label: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PrototypeSchema extends SchemaBase {
|
|
14
|
+
/** Field requirements: key is the field name, value is display metadata. */
|
|
15
|
+
fields: Record<string, PrototypeFieldMeta>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Defines a page prototype. The field keys become the names referenced by
|
|
20
|
+
* later DTO/table definitions; here they only carry label/description.
|
|
21
|
+
*/
|
|
22
|
+
export function definePrototype(
|
|
23
|
+
name: string,
|
|
24
|
+
schema: {
|
|
25
|
+
description?: string;
|
|
26
|
+
fields: Record<string, PrototypeFieldMeta>;
|
|
27
|
+
},
|
|
28
|
+
): PrototypeSchema {
|
|
29
|
+
return { name, ...schema };
|
|
30
|
+
}
|
package/src/typebox-driver.ts
CHANGED
|
@@ -50,9 +50,8 @@ function renderBasic(field: Field, pattern: string | undefined, resolver: EnumRe
|
|
|
50
50
|
case 'json':
|
|
51
51
|
return 'Type.Unknown()';
|
|
52
52
|
case 'enum': {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.jsName} — pass an EnumResolver`);
|
|
53
|
+
const ref = resolver?.(field.enum.jsName);
|
|
54
|
+
if (!ref) throw new Error(`enum field ${field.name}: no import ref for ${field.enum.jsName} — pass an EnumResolver`);
|
|
56
55
|
return `Type.Enum(${ref.name})`;
|
|
57
56
|
}
|
|
58
57
|
default:
|
|
@@ -97,9 +96,8 @@ function collectEnumImports(
|
|
|
97
96
|
return;
|
|
98
97
|
}
|
|
99
98
|
if (f.field.type === 'enum') {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!ref) throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.jsName} — pass an EnumResolver`);
|
|
99
|
+
const ref = resolver?.(f.field.enum.jsName);
|
|
100
|
+
if (!ref) throw new Error(`enum field ${f.field.name}: no import ref for ${f.field.enum.jsName} — pass an EnumResolver`);
|
|
103
101
|
out.set(`${ref.from}#${ref.name}`, ref);
|
|
104
102
|
}
|
|
105
103
|
}
|