@pylonts/dsl 1.0.1 → 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.
@@ -0,0 +1,24 @@
1
+ # 项目拓扑 (Project)
2
+
3
+ Project 是仓库的地图:描述有哪些前端应用、哪些后端 API,以及每个 API 服务哪些前端。
4
+
5
+ ```ts
6
+ import { defineProject } from '@pylonts/dsl';
7
+
8
+ const webAdmin = { name: 'web-admin', type: 'admin', dir: 'web-admin/', description: '管理后台' };
9
+ const miniUser = { name: 'mini-user', type: 'wxmini', dir: 'mini-user/', description: 'C端小程序' };
10
+ const miniVerify = { name: 'mini-verify', type: 'wxmini', dir: 'mini-verify/',description: '核销小程序' };
11
+
12
+ export const mall = defineProject('mall', {
13
+ description: '合作商户权益兑换商城',
14
+ apps: [webAdmin, miniUser, miniVerify],
15
+ apis: [
16
+ { name: 'mall-api', description: '商城主后端', dir: 'api/', contextPath: '/mall', apps: [webAdmin, miniUser, miniVerify] },
17
+ ],
18
+ });
19
+ ```
20
+
21
+ - `FrontApp`:`name` / `description` / `type`(admin | wxmini)/ `dir`(相对仓库根目录的源码目录)。
22
+ - `ProjectApi`:`name` / `description` / `dir` / `apps`(直接引用共享的 FrontApp 实例——一个 app 被多个 API 服务就定义一次、引用多次)/ `contextPath`(API 基础 URL 前缀,如 `/mall`,空串表示无前缀)。
23
+ - **直接对象引用优先**:`api.apps` 与 `project.apps` 指向同一实例,不写字符串。
24
+ - **contextPath 解析**:前端 app 的 API 前缀由服务它的 api 决定——`api.apps` 必须恰好包含该 app(零个或多个都报错),app 本身不声明 contextPath。
@@ -0,0 +1,21 @@
1
+ # 页面原型 (Prototype)
2
+
3
+ 原型是**单个实例**的页面/功能概要设计——一张页面的草图。只列出页面需要的字段,不涉及类型、不绑定 app/API/表,字段细节(DTO/表定义)在详细设计阶段另行编写、后续连接。
4
+
5
+ ```ts
6
+ import { definePrototype } from '@pylonts/dsl';
7
+
8
+ export const adminOrderList = definePrototype('AdminOrderList', {
9
+ description: '管理端订单列表页',
10
+ fields: {
11
+ order_no: { label: '订单号', description: '商家下单生成的订单编号' },
12
+ merchant: { label: '商户', description: '下单商户' },
13
+ amount: { label: '金额', description: '订单实付金额' },
14
+ created_at: { label: '下单时间' },
15
+ },
16
+ });
17
+ ```
18
+
19
+ - 字段 key 即字段名,后续 DTO/表定义直接引用同名;这里只携带 `label` / `description`。
20
+ - 原型是单实例的(一个东西的原型,不是整个项目的),同名页面在不同 app 中各自定义,互不干扰。
21
+ - 不做回写:原型字段无写回机制,各走各的。
package/docs/table.md ADDED
@@ -0,0 +1,90 @@
1
+ # 定义表 (TableSchema)
2
+
3
+ ## 字段类型
4
+
5
+ | 构建器 | 类型 | jsType | MySQL 列 | 备注 |
6
+ |---|---|---|---|---|
7
+ | `stringField` | string | string | VARCHAR | 必填 `maxLength` |
8
+ | `textField` | text | string | TEXT | |
9
+ | `intField` | integer | number | INT | |
10
+ | `bigintField` | bigint | string | BIGINT | 传输层走 string 保精度 |
11
+ | `decimalField` | decimal | string | DECIMAL | 必填 `precision` / `scale`,传输层走 string 避免浮点误差 |
12
+ | `booleanField` | boolean | boolean | TINYINT(1) | |
13
+ | `dateField` | date | Date | DATE | |
14
+ | `timeField` | time | string | TIME | |
15
+ | `datetimeField` | datetime | Date | DATETIME | |
16
+ | `enumField` | enum | string / number | VARCHAR(20) / TINYINT | 引用共享枚举定义,见 [enum.md](./enum.md) |
17
+ | `jsonField` | json | object | JSON | |
18
+
19
+ 通用扩展属性(构建器第二参数):`label`(中文标签)、`description`、`optional`、`readOnly`、`default`。
20
+
21
+ ## 定义表
22
+
23
+ ```ts
24
+ import { bigintField, defineTable, decimalField, stringField } from '@pylonts/dsl';
25
+
26
+ const id = bigintField({ readOnly: true, label: '主键' });
27
+
28
+ export const order = defineTable('order', {
29
+ description: '订单',
30
+ generator: 'auto_increment',
31
+ fields: {
32
+ id,
33
+ order_no: stringField({ label: '订单号', maxLength: 32, optional: false }),
34
+ amount: decimalField({ precision: 18, scale: 2, label: '金额' }),
35
+ },
36
+ primaryKey: id,
37
+ });
38
+ ```
39
+
40
+ - 字段名从 map key 反写,`fields` 里的 key 就是列名。
41
+ - 字段实例不可跨表复用(复用同一字段实例会抛错),枚举除外。
42
+
43
+ ## 索引
44
+
45
+ ```ts
46
+ indexes: [
47
+ { name: 'uk_uuid', fields: c_uuid, unique: true },
48
+ { fields: [c_enum, c_date] }, // 名字缺省时 = 字段名 join '_'
49
+ ],
50
+ ```
51
+
52
+ ## 外键与短语检查链
53
+
54
+ ```ts
55
+ import { definePhrase } from '@pylonts/dsl';
56
+
57
+ const BD = definePhrase({ label: 'BD推广员', description: '线下拓展商户的推广人员' });
58
+
59
+ const bdId = bigintField({ readOnly: true, label: 'BD ID' });
60
+
61
+ export const bd = defineTable('bd', {
62
+ description: 'BD',
63
+ phrase: BD, // 链接词典条目:本表归属的实体
64
+ fields: { id: bdId },
65
+ primaryKey: bdId,
66
+ });
67
+
68
+ const auditBdId = bigintField({ label: 'BD' });
69
+
70
+ export const audit = defineTable('audit', {
71
+ description: '审核',
72
+ fields: {
73
+ bd_id: auditBdId, // 列名必须 = phrase.name + '_' + 被引用字段名
74
+ },
75
+ foreignKeys: {
76
+ bd_bd_id: { fields: auditBdId, references: bdId },
77
+ },
78
+ });
79
+ ```
80
+
81
+ **规则(defineTable 时强制检查)**:外键字段名必须等于 `被引用表.phrase.name + "_" + 被引用字段名`。即引用 `bd.id` 的字段必须叫 `bd_id`——`bd` 来自词典(权威短语),`id` 是 `bd` 表主键。
82
+
83
+ - 被引用表未定义 `phrase` → 抛错(检查链要求每个被引用表都有短语)。
84
+ - 命名不匹配 → 抛错并提示期望名,例如:
85
+ `foreign key bad: field must be named bd_id (phrase bd + id), got merchant_id`
86
+ - 关联表等涉及多个实体的场景不需要 `phrase`,也不建外键。
87
+
88
+ ## 生成 SQL
89
+
90
+ 见 [driver.md](./driver.md)。
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@pylonts/dsl",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Schema definition DSL with drivers: MySQL DDL, TS enum, TypeBox schema codegen.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.ts",
7
7
  "types": "./dist/index.d.ts",
8
8
  "files": [
9
9
  "dist",
10
- "src"
10
+ "src",
11
+ "docs"
11
12
  ],
12
13
  "scripts": {
13
14
  "build": "tsc -p tsconfig.build.json",
package/src/dictionary.ts CHANGED
@@ -1,63 +1,15 @@
1
1
  import { SchemaBase } from './dsl';
2
2
 
3
3
  // Dictionary definitions: vocabulary shared across the team — what a term
4
- // means and what it is called. Entries are written back from the map key,
5
- // the same mechanism as defineTable fields.
4
+ // means and what it is called.
6
5
 
7
- /** A vocabulary entry: name is written back from the dictionary key. */
8
- export interface DictionaryEntry extends SchemaBase {}
9
-
10
- export interface DictionarySchema extends SchemaBase {
11
- entries: Record<string, DictionaryEntry>;
12
- }
13
-
14
- /** Creates a phrase entry. name is filled in by defineDictionary. */
15
- export function definePhrase(extra: Omit<DictionaryEntry, 'name'> = {}): DictionaryEntry {
16
- return { name: '', ...extra };
17
- }
18
-
19
- function defineDictionary(schema: {
20
- name: string;
21
- description?: string;
22
- entries: Record<string, DictionaryEntry>;
23
- }): DictionarySchema {
24
- const dictionary: DictionarySchema = { ...schema };
25
- for (const key of Object.keys(dictionary.entries)) {
26
- dictionary.entries[key].name = key;
27
- }
28
- return dictionary;
29
- }
30
-
31
- /** Defines the entity phrase dictionary: standard words aligned with entities. */
32
- export function defineEntityDictionary(schema: {
33
- description?: string;
34
- entries: Record<string, DictionaryEntry>;
35
- }): DictionarySchema {
36
- return defineDictionary({ name: 'entity', ...schema });
37
- }
38
-
39
- /**
40
- * Defines a business phrase package (one file per industry): entries are the
41
- * top-level map keys, no wrapping container.
42
- */
43
- export function defineBusinessDictionary(entries: Record<string, DictionaryEntry>): DictionarySchema {
44
- return defineDictionary({ name: 'business', entries });
6
+ /** A vocabulary entry. */
7
+ export interface DictionaryEntry extends SchemaBase {
8
+ /** Display label (Chinese) for the term. */
9
+ label?: string;
45
10
  }
46
11
 
47
- /**
48
- * Merges business phrase packages (one file per industry) into a single flat
49
- * map. Package metadata (name/description) is dropped; duplicate keys across
50
- * packages throw.
51
- */
52
- export function mergeBusinessDictionaries(packages: DictionarySchema[]): Record<string, DictionaryEntry> {
53
- const merged: Record<string, DictionaryEntry> = {};
54
- for (const pkg of packages) {
55
- for (const key of Object.keys(pkg.entries)) {
56
- if (merged[key]) {
57
- throw new Error(`business phrase ${key} is defined in multiple packages`);
58
- }
59
- merged[key] = pkg.entries[key];
60
- }
61
- }
62
- return merged;
12
+ /** Creates a phrase entry. */
13
+ export function definePhrase(extra: DictionaryEntry): DictionaryEntry {
14
+ return extra;
63
15
  }
package/src/dsl.ts CHANGED
@@ -211,6 +211,8 @@ export function defineTable(
211
211
  primaryKey?: Field | Field[];
212
212
  indexes?: Index[];
213
213
  foreignKeys?: Record<string, ForeignKey>;
214
+ /** 引用的实体短语(词典条目) */
215
+ phrase?: DictionaryEntry;
214
216
  fields: Record<string, Field>;
215
217
  },
216
218
  ): TableSchema {
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
@@ -5,4 +5,10 @@ export * from './prototype';
5
5
  export * from './dictionary';
6
6
  export * from './mysql-driver';
7
7
  export * from './enum-driver';
8
- export * from './typebox-driver';
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
+ }
@@ -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 CHANGED
@@ -19,11 +19,22 @@ export interface ProjectApi extends SchemaBase {
19
19
  dir: string;
20
20
  /** Frontends this API serves. Direct instance references (see defineProject). */
21
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;
22
32
  }
23
33
 
24
34
  export interface ProjectSchema extends SchemaBase {
25
35
  apps: FrontApp[];
26
36
  apis: ProjectApi[];
37
+ thirdApis: ThirdApi[];
27
38
  }
28
39
 
29
40
  /**
@@ -37,7 +48,8 @@ export function defineProject(
37
48
  description?: string;
38
49
  apps: FrontApp[];
39
50
  apis: ProjectApi[];
51
+ thirdApis?: ThirdApi[];
40
52
  },
41
53
  ): ProjectSchema {
42
- return { name, ...schema };
54
+ return { name, ...schema, thirdApis: schema.thirdApis ?? [] };
43
55
  }