@pylonts/dsl 1.0.6 → 1.1.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.
Files changed (108) hide show
  1. package/README.md +2 -1
  2. package/dist/action.d.ts +7 -0
  3. package/dist/action.js +3 -0
  4. package/dist/asset.d.ts +48 -0
  5. package/dist/asset.js +31 -0
  6. package/dist/bases.d.ts +6 -2
  7. package/dist/bases.js +10 -6
  8. package/dist/check-inheritance.js +1 -4
  9. package/dist/component.d.ts +20 -0
  10. package/dist/component.js +1 -0
  11. package/dist/convert.d.ts +9 -0
  12. package/dist/convert.js +3 -0
  13. package/dist/curd.d.ts +62 -0
  14. package/dist/curd.js +31 -0
  15. package/dist/db-config.d.ts +8 -0
  16. package/dist/db-config.js +1 -0
  17. package/dist/db.d.ts +56 -0
  18. package/dist/db.js +100 -0
  19. package/dist/dictionary.d.ts +26 -5
  20. package/dist/dictionary.js +21 -8
  21. package/dist/dsl.d.ts +10 -49
  22. package/dist/dsl.js +12 -103
  23. package/dist/dto.d.ts +12 -9
  24. package/dist/dto.js +25 -34
  25. package/dist/enum-driver.d.ts +1 -1
  26. package/dist/enum-driver.js +1 -4
  27. package/dist/event.d.ts +8 -0
  28. package/dist/event.js +3 -0
  29. package/dist/flow.d.ts +1 -1
  30. package/dist/flow.js +3 -8
  31. package/dist/import-base.d.ts +15 -0
  32. package/dist/import-base.js +1 -0
  33. package/dist/index.d.ts +31 -17
  34. package/dist/index.js +31 -33
  35. package/dist/mermaid-driver.d.ts +2 -2
  36. package/dist/mermaid-driver.js +4 -7
  37. package/dist/mock.d.ts +12 -0
  38. package/dist/mock.js +1 -0
  39. package/dist/mysql-driver.d.ts +5 -1
  40. package/dist/mysql-driver.js +25 -10
  41. package/dist/navigation.d.ts +22 -0
  42. package/dist/navigation.js +15 -0
  43. package/dist/page-def.d.ts +40 -0
  44. package/dist/page-def.js +38 -0
  45. package/dist/page-flow.d.ts +5 -3
  46. package/dist/page-flow.js +109 -18
  47. package/dist/page.d.ts +37 -15
  48. package/dist/page.js +23 -13
  49. package/dist/pattern.js +2 -6
  50. package/dist/patterns/retry.d.ts +1 -1
  51. package/dist/patterns/retry.js +2 -6
  52. package/dist/popup.d.ts +18 -0
  53. package/dist/popup.js +8 -0
  54. package/dist/project.d.ts +25 -13
  55. package/dist/project.js +41 -6
  56. package/dist/prototype.d.ts +1 -1
  57. package/dist/prototype.js +1 -4
  58. package/dist/provider.d.ts +54 -0
  59. package/dist/provider.js +18 -0
  60. package/dist/ref.d.ts +14 -0
  61. package/dist/ref.js +6 -0
  62. package/dist/route.d.ts +8 -0
  63. package/dist/route.js +3 -0
  64. package/dist/typebox-driver.d.ts +3 -3
  65. package/dist/typebox-driver.js +24 -26
  66. package/dist/utils.d.ts +2 -0
  67. package/dist/utils.js +5 -4
  68. package/docs/curd.md +111 -0
  69. package/docs/dictionary.md +42 -31
  70. package/docs/driver.md +42 -42
  71. package/docs/dto.md +6 -6
  72. package/docs/enum.md +24 -24
  73. package/docs/project.md +2 -2
  74. package/docs/table.md +53 -16
  75. package/package.json +5 -3
  76. package/src/action.ts +11 -0
  77. package/src/asset.ts +63 -0
  78. package/src/bases.ts +30 -20
  79. package/src/component.ts +22 -0
  80. package/src/convert.ts +13 -0
  81. package/src/curd.ts +94 -0
  82. package/src/db-config.ts +8 -0
  83. package/src/db.ts +153 -0
  84. package/src/dictionary.ts +45 -19
  85. package/src/dsl.ts +13 -106
  86. package/src/dto.ts +25 -12
  87. package/src/enum-driver.ts +42 -42
  88. package/src/event.ts +12 -0
  89. package/src/flow.ts +103 -103
  90. package/src/import-base.ts +15 -0
  91. package/src/index.ts +31 -17
  92. package/src/mermaid-driver.ts +5 -4
  93. package/src/mock.ts +12 -0
  94. package/src/mysql-driver.ts +25 -6
  95. package/src/navigation.ts +29 -0
  96. package/src/page-def.ts +80 -0
  97. package/src/page-flow.ts +117 -15
  98. package/src/page.ts +57 -20
  99. package/src/patterns/retry.ts +54 -54
  100. package/src/popup.ts +25 -0
  101. package/src/project.ts +57 -14
  102. package/src/prototype.ts +29 -29
  103. package/src/provider.ts +73 -0
  104. package/src/ref.ts +19 -0
  105. package/src/route.ts +12 -0
  106. package/src/typebox-driver.ts +192 -187
  107. package/src/utils.ts +11 -6
  108. package/src/check-inheritance.ts +0 -86
@@ -1,7 +1,13 @@
1
- import { Field, ForeignKey, Index, TableSchema } from './dsl';
1
+ import { Field } from './dsl.js';
2
+ import { ForeignKey, Index, TableSchema } from './db.js';
2
3
 
3
4
  // MySQL driver: converts a TableSchema into a CREATE TABLE statement.
4
5
 
6
+ /** Default value constant for created_at columns. */
7
+ export const CURRENT_TIMESTAMP = 'CURRENT_TIMESTAMP';
8
+ /** Default value constant for updated_at columns (with auto-update). */
9
+ export const CURRENT_TIMESTAMP_ON_UPDATE = 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP';
10
+
5
11
  function columnType(field: Field): string {
6
12
  switch (field.type) {
7
13
  case 'string':
@@ -31,11 +37,24 @@ function columnType(field: Field): string {
31
37
  }
32
38
  }
33
39
 
40
+ function renderDefault(field: Field): string {
41
+ if (field.default === undefined) return '';
42
+ // MySQL keyword expressions (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
43
+ // NULL, etc.) — render bare, no quotes.
44
+ if (/^[A-Z]/.test(field.default)) return ` DEFAULT ${field.default}`;
45
+ // Numeric columns take a bare literal, not a quoted one.
46
+ if (field.type === 'integer' || field.type === 'bigint' || field.type === 'decimal' || field.type === 'boolean') {
47
+ return ` DEFAULT ${field.default}`;
48
+ }
49
+ return ` DEFAULT '${field.default}'`;
50
+ }
51
+
34
52
  function columnDef(field: Field, autoIncrement?: Field): string {
35
53
  const parts = [field.name, columnType(field)];
36
54
  if (field.optional === false) parts.push('NOT NULL');
37
- if (field.default !== undefined) parts.push(`DEFAULT '${field.default}'`);
55
+ parts.push(renderDefault(field));
38
56
  if (field === autoIncrement) parts.push('AUTO_INCREMENT');
57
+ if (field.description) parts.push(`COMMENT '${field.description.replace(/'/g, "\\'")}'`);
39
58
  return parts.join(' ');
40
59
  }
41
60
 
@@ -46,14 +65,14 @@ function primaryKeyClause(schema: TableSchema): string | null {
46
65
  }
47
66
 
48
67
  function indexClause(index: Index): string {
49
- const fields = Array.isArray(index.fields) ? index.fields : [index.fields];
68
+ const fields = Array.isArray(index.columns) ? index.columns : [index.columns];
50
69
  const kind = index.unique ? 'UNIQUE KEY' : 'KEY';
51
70
  const name = index.name ?? fields.map((f) => f.name).join('_');
52
71
  return `${kind} ${name} (${fields.map((f) => f.name).join(', ')})`;
53
72
  }
54
73
 
55
74
  function foreignKeyClause(name: string, fk: ForeignKey): string {
56
- const fields = Array.isArray(fk.fields) ? fk.fields : [fk.fields];
75
+ const fields = Array.isArray(fk.columns) ? fk.columns : [fk.columns];
57
76
  const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
58
77
  const refTable = refs[0].schema?.name;
59
78
  if (!refTable) throw new Error(`foreign key ${name}: references field has no schema`);
@@ -68,7 +87,7 @@ export interface BuildCreateTableSqlOptions {
68
87
  }
69
88
 
70
89
  export function buildCreateTableSql(schema: TableSchema, options: BuildCreateTableSqlOptions = {}): string {
71
- const lines = Object.values(schema.fields).map((field) => columnDef(field, schema.autoIncrement));
90
+ const lines = Object.values(schema.columns).map((field) => columnDef(field, schema.autoIncrement));
72
91
  const pk = primaryKeyClause(schema);
73
92
  if (pk) lines.push(pk);
74
93
  for (const index of schema.indexes ?? []) lines.push(indexClause(index));
@@ -77,5 +96,5 @@ export function buildCreateTableSql(schema: TableSchema, options: BuildCreateTab
77
96
  lines.push(foreignKeyClause(name, fk));
78
97
  }
79
98
  }
80
- return `CREATE TABLE \`${schema.name}\` (\n ${lines.join(',\n ')}\n);`;
99
+ return `CREATE TABLE IF NOT EXISTS \`${schema.name}\` (\n ${lines.join(',\n ')}\n);`;
81
100
  }
@@ -0,0 +1,29 @@
1
+ import type { RouteDataSchema } from './route.js';
2
+ import type { ActionSchema } from './action.js';
3
+ import { PageSchema } from './page.js';
4
+
5
+ /** Navigation primitives: front-end routing actions that are not provider calls. */
6
+ export interface NavigationAction extends ActionSchema {
7
+ type: 'navigation';
8
+ method: 'back' | 'push' | 'popup' | 'home';
9
+ /** Target page for push navigation. */
10
+ target?: PageSchema;
11
+ /** Route params type for push navigation. Auto-derived from target.params if omitted. */
12
+ params?: RouteDataSchema;
13
+ }
14
+
15
+ export const navigation = {
16
+ back(description?: string): NavigationAction {
17
+ return { name: 'back', type: 'navigation', method: 'back', description };
18
+ },
19
+ push(options: { target: PageSchema; params?: RouteDataSchema; description?: string }): NavigationAction {
20
+ const { target, params, description } = options;
21
+ return { name: 'push', type: 'navigation', method: 'push', target, params: params ?? target.params, description };
22
+ },
23
+ popup(description?: string): NavigationAction {
24
+ return { name: 'popup', type: 'navigation', method: 'popup', description };
25
+ },
26
+ home(description?: string): NavigationAction {
27
+ return { name: 'home', type: 'navigation', method: 'home', description };
28
+ },
29
+ };
@@ -0,0 +1,80 @@
1
+ import type { ImportableSchemaBase, CollectionSchemaBase, SchemaBase } from './dsl.js';
2
+ import { DtoField } from './dto.js';
3
+ import type { DtoMessage, DtoArrayField, DtoObjectField } from './dto.js';
4
+ import type { ComponentSchema } from './component.js';
5
+ import type { ActionSchema } from './action.js';
6
+
7
+ export type { RouteDataSchema } from './route.js';
8
+ export { defineRouteData } from './route.js';
9
+
10
+ // PageDef: a page skeleton declaration. Describes what data a page needs
11
+ // and where each field comes from (route params, provider calls, or literals).
12
+ // Driver identifies the source by inspecting each field's .schema back-reference.
13
+
14
+ /** Page lifecycle events that trigger data loading. */
15
+ export interface EventSchema extends SchemaBase {
16
+ type: 'onLoad' | 'onShow' | 'onHide' | 'onPullDownRefresh' | 'onReachBottom';
17
+ /** Actions that fire when this event occurs. */
18
+ actions?: ActionSchema[];
19
+ }
20
+
21
+ export const events = {
22
+ onLoad(actions?: ActionSchema[], description?: string): EventSchema {
23
+ return { name: 'onLoad', type: 'onLoad', actions, description };
24
+ },
25
+ onShow(actions?: ActionSchema[], description?: string): EventSchema {
26
+ return { name: 'onShow', type: 'onShow', actions, description };
27
+ },
28
+ onHide(actions?: ActionSchema[], description?: string): EventSchema {
29
+ return { name: 'onHide', type: 'onHide', actions, description };
30
+ },
31
+ onPullDownRefresh(actions?: ActionSchema[], description?: string): EventSchema {
32
+ return { name: 'onPullDownRefresh', type: 'onPullDownRefresh', actions, description };
33
+ },
34
+ onReachBottom(actions?: ActionSchema[], description?: string): EventSchema {
35
+ return { name: 'onReachBottom', type: 'onReachBottom', actions, description };
36
+ },
37
+ };
38
+
39
+ /** Page data: a named collection of fields that defines the page's data shape. */
40
+ export interface PageDataSchema extends CollectionSchemaBase {
41
+ type: 'pageData';
42
+ fields: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField>;
43
+ }
44
+
45
+ export function definePageData(fields: PageDataSchema['fields']): PageDataSchema {
46
+ // Page data has no meaningful identity of its own; the page name already
47
+ // scopes it, so the schema name defaults to 'data'.
48
+ const data: PageDataSchema = { name: 'data', type: 'pageData', fields };
49
+ // Write back field names (same convention as buildMessage) so defineRef
50
+ // produces refs with a real field name instead of ''.
51
+ for (const key of Object.keys(data.fields)) {
52
+ const df = data.fields[key];
53
+ if (df instanceof DtoField) {
54
+ df.name = key;
55
+ df.schema = data;
56
+ }
57
+ }
58
+ return data;
59
+ }
60
+
61
+ /** Page skeleton definition. */
62
+ export interface PageDef extends ImportableSchemaBase {
63
+ data: PageDataSchema;
64
+ /** Lifecycle events that trigger data loading. */
65
+ events: EventSchema[];
66
+ /** Page actions (provider calls, navigation, etc.). */
67
+ actions: ActionSchema[];
68
+ /** UI component declarations that make up the page skeleton. */
69
+ components: ComponentSchema[];
70
+ /** Whether the driver should generate page-level loading / error / empty state wrappers. */
71
+ handleStates?: boolean;
72
+ }
73
+
74
+ /** Define a page skeleton. */
75
+ export function definePageDef(
76
+ name: string,
77
+ schema: Omit<PageDef, 'name'>,
78
+ ): PageDef {
79
+ return { name, ...schema };
80
+ }
package/src/page-flow.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { SchemaBase } from './dsl';
2
- import { ActionSchema, Page } from './page';
1
+ import { SchemaBase } from './dsl.js';
2
+ import type { ActionSchema } from './action.js';
3
+ import { Page, getRegisteredPages, clearRegisteredPages } from './page.js';
4
+ import type { PageDef } from './page-def.js';
5
+ import type { NavigationAction } from './navigation.js';
3
6
 
4
7
  // Page-driven flow: every node is a page, and a page belongs to an app.
5
8
  // Leaf nodes are pages too — a journey starts at a page and ends at a page.
@@ -16,7 +19,7 @@ export interface PageEdge extends SchemaBase {
16
19
  export interface PageFlow extends SchemaBase {
17
20
  /** Entry page. */
18
21
  start: Page;
19
- /** All pages, collected from edges (deduplicated by object identity). */
22
+ /** All pages explicitly declared by the caller. */
20
23
  pages: Page[];
21
24
  edges: PageEdge[];
22
25
  }
@@ -30,23 +33,122 @@ export function definePageFlow(
30
33
  name: string,
31
34
  schema: {
32
35
  start: Page;
36
+ pages: Page[];
33
37
  edges: PageEdge[];
34
38
  description?: string;
35
39
  },
36
40
  ): 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
- }
41
+ const { start, pages, edges: explicitEdges, description } = schema;
42
+
43
+ // Resolve the back action from any page's actions list (PageSchema at runtime)
44
+ const backAction = findBackAction(pages);
45
+
46
+ // Auto-generate back edges: for every explicit edge A→B (not itself a back),
47
+ // if B has no explicit back edge and no existing edge B→A, add B→A with back.
48
+ const hasExplicitBack = new Set<Page>();
49
+ for (const e of explicitEdges) {
50
+ if (e.when && e.when.name === 'back') hasExplicitBack.add(e.start);
51
+ }
52
+ const hasReverse = new Set<string>();
53
+ for (const e of explicitEdges) {
54
+ hasReverse.add(`${e.start.name}->${e.end.name}`);
55
+ }
56
+ const generatedBackEdges: PageEdge[] = [];
57
+ if (backAction) {
58
+ for (const e of explicitEdges) {
59
+ if (e.when && e.when.name === 'back') continue;
60
+ if (hasExplicitBack.has(e.end)) continue;
61
+ if (hasReverse.has(`${e.end.name}->${e.start.name}`)) continue;
62
+ generatedBackEdges.push({
63
+ name: `${e.end.name}->${e.start.name}`,
64
+ start: e.end,
65
+ end: e.start,
66
+ when: backAction,
67
+ description: 'auto-generated back edge',
68
+ });
69
+ hasExplicitBack.add(e.end);
70
+ }
71
+ }
72
+ const edges = [...explicitEdges, ...generatedBackEdges];
73
+
74
+ // Validate: every registered page must be in the flow's pages
75
+ const registered = getRegisteredPages();
76
+ const orphaned: string[] = [];
77
+ for (const rp of registered) {
78
+ // Compare by object identity — caller passes the same reference
79
+ if (!pages.includes(rp as unknown as Page)) {
80
+ orphaned.push(rp.name);
45
81
  }
46
82
  }
47
- if (!seen.has(schema.start)) {
48
- seen.add(schema.start);
49
- pages.push(schema.start);
83
+ if (orphaned.length > 0) {
84
+ throw new Error(
85
+ `[definePageFlow "${name}"] orphaned pages (defined but not in flow): ${orphaned.join(', ')}`,
86
+ );
87
+ }
88
+ clearRegisteredPages();
89
+
90
+ // Validate: start must be in pages
91
+ if (!pages.includes(start)) {
92
+ throw new Error(
93
+ `[definePageFlow "${name}"] start page "${start.name}" not in pages`,
94
+ );
95
+ }
96
+
97
+ // Validate: every page appears in at least one edge
98
+ const inEdge = new Set<Page>();
99
+ for (const e of edges) {
100
+ inEdge.add(e.start);
101
+ inEdge.add(e.end);
102
+ }
103
+ const isolated: string[] = [];
104
+ for (const p of pages) {
105
+ if (!inEdge.has(p)) isolated.push(p.name);
50
106
  }
51
- return { name, description: schema.description, start: schema.start, pages, edges: schema.edges };
107
+ if (isolated.length > 0) {
108
+ throw new Error(
109
+ `[definePageFlow "${name}"] isolated pages (no edges): ${isolated.join(', ')}`,
110
+ );
111
+ }
112
+
113
+ // Validate: all pages reachable from start
114
+ const adj = new Map<Page, Page[]>();
115
+ for (const p of pages) adj.set(p, []);
116
+ for (const e of edges) {
117
+ const neighbors = adj.get(e.start);
118
+ if (neighbors) neighbors.push(e.end);
119
+ }
120
+ const visited = new Set<Page>();
121
+ const stack: Page[] = [start];
122
+ while (stack.length > 0) {
123
+ const cur = stack.pop()!;
124
+ if (visited.has(cur)) continue;
125
+ visited.add(cur);
126
+ for (const next of adj.get(cur) ?? []) {
127
+ if (!visited.has(next)) stack.push(next);
128
+ }
129
+ }
130
+ const unreachable: string[] = [];
131
+ for (const p of pages) {
132
+ if (!visited.has(p)) unreachable.push(p.name);
133
+ }
134
+ if (unreachable.length > 0) {
135
+ throw new Error(
136
+ `[definePageFlow "${name}"] unreachable pages: ${unreachable.join(', ')}`,
137
+ );
138
+ }
139
+
140
+ return { name, description, start, pages, edges };
52
141
  }
142
+
143
+ /** Walk all pages' pageDef.actions (PageSchema runtime shape) to find a NavigationAction with method 'back'. */
144
+ function findBackAction(pages: Page[]): NavigationAction | undefined {
145
+ for (const p of pages) {
146
+ const pageDef = (p as unknown as Record<string, unknown>).pageDef as PageDef | undefined;
147
+ if (!pageDef?.actions) continue;
148
+ for (const a of pageDef.actions) {
149
+ const na = a as NavigationAction;
150
+ if (na.type === 'navigation' && na.method === 'back') return na;
151
+ }
152
+ }
153
+ return undefined;
154
+ }
package/src/page.ts CHANGED
@@ -1,40 +1,77 @@
1
- import { SchemaBase } from './dsl';
2
- import { FrontApp } from './project';
1
+ import { SchemaBase } from './dsl.js';
2
+ import { FrontAppSchema } from './project.js';
3
+ import type { PageDef } from './page-def.js';
4
+ import type { RouteDataSchema } from './route.js';
3
5
 
4
6
  // Page definitions: standalone page schemas and the page node type used by
5
7
  // page-driven flows. Kept separate from page-flow.ts (the flow graph itself).
6
8
 
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[];
9
+ // Module-level registry: every definePage/defineTabPage call registers its
10
+ // result so definePageFlow can check that no page is left out.
11
+ const _pageRegistry = new Set<PageSchema>();
12
+
13
+ export function getRegisteredPages(): ReadonlySet<PageSchema> {
14
+ return _pageRegistry;
14
15
  }
15
16
 
16
- /** An action a user can perform on a page (e.g. submit, approve, reject). */
17
- export interface ActionSchema extends SchemaBase {}
17
+ export function clearRegisteredPages(): void {
18
+ _pageRegistry.clear();
19
+ }
18
20
 
19
- export function defineAction(name: string, description?: string): ActionSchema {
20
- return { name, description };
21
+ /** Standalone page definition. A page is a shared value object: it belongs to
22
+ * exactly one frontend app. Actions live in the page skeleton (PageDef.actions),
23
+ * not on the page itself — the page only carries what the flow/topology needs. */
24
+ export interface PageSchema extends SchemaBase {
25
+ /** Short display name for topology diagrams. */
26
+ label: string;
27
+ /** The frontend app this page belongs to (shared instance from project.config). */
28
+ app: FrontAppSchema;
29
+ /** Full page skeleton definition (optional). */
30
+ pageDef?: PageDef;
31
+ /** Route params this page expects (e.g. detail page expects productId). */
32
+ params?: RouteDataSchema;
21
33
  }
22
34
 
23
35
  export function definePage(schema: {
24
36
  name: string;
37
+ label: string;
25
38
  description?: string;
26
- app: FrontApp;
27
- actions: ActionSchema[];
39
+ app: FrontAppSchema;
40
+ pageDef?: PageDef;
41
+ params?: RouteDataSchema;
28
42
  }): PageSchema {
29
- return { ...schema };
43
+ const p: PageSchema = { ...schema };
44
+ _pageRegistry.add(p);
45
+ return p;
46
+ }
47
+
48
+ /** Page with bottom tab navigation. tabs collects the child pages reachable via tab switch. */
49
+ export interface TabPageSchema extends PageSchema {
50
+ tabs: PageSchema[];
51
+ }
52
+
53
+ export function defineTabPage(schema: {
54
+ name: string;
55
+ label: string;
56
+ description?: string;
57
+ app: FrontAppSchema;
58
+ tabs: PageSchema[];
59
+ pageDef?: PageDef;
60
+ params?: RouteDataSchema;
61
+ }): TabPageSchema {
62
+ const p: TabPageSchema = { ...schema };
63
+ _pageRegistry.add(p);
64
+ return p;
30
65
  }
31
66
 
32
67
  /** A page node in a page-driven flow: every node is a page, and a page belongs to an app. */
33
68
  export interface Page extends SchemaBase {
69
+ /** Short display name for topology diagrams. */
70
+ label?: string;
34
71
  /** The frontend app this page belongs to (shared instance from project.config). */
35
- app: FrontApp;
72
+ app: FrontAppSchema;
36
73
  }
37
74
 
38
- export function page(app: FrontApp, name: string, description?: string): Page {
39
- return { name, app, description };
40
- }
75
+ export function page(app: FrontAppSchema, name: string, description?: string, label?: string): Page {
76
+ return { name, label, app, description };
77
+ }
@@ -1,55 +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');
1
+ import { PatternDef } from '../pattern.js';
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
55
  }
package/src/popup.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { ActionSchema } from './action.js';
2
+ import type { RefSchema } from './ref.js';
3
+
4
+ /** Display a toast notification. React: toast/message UI. Mini-program: wx.showToast. */
5
+ export interface ToastAction extends ActionSchema {
6
+ type: 'toast';
7
+ message: string | RefSchema;
8
+ icon?: 'success' | 'error' | 'loading' | 'none';
9
+ }
10
+
11
+ /** Display an alert dialog. React: modal. Mini-program: wx.showModal. */
12
+ export interface AlertAction extends ActionSchema {
13
+ type: 'alert';
14
+ title: string | RefSchema;
15
+ content: string | RefSchema;
16
+ }
17
+
18
+ export const popup = {
19
+ toast(message: string | RefSchema, icon?: 'success' | 'error' | 'loading' | 'none'): ToastAction {
20
+ return { name: 'toast', type: 'toast', message, icon };
21
+ },
22
+ alert(title: string | RefSchema, content: string | RefSchema): AlertAction {
23
+ return { name: 'alert', type: 'alert', title, content };
24
+ },
25
+ };