@pylonts/dsl 1.1.1 → 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.
- package/dist/action.d.ts +7 -0
- package/dist/action.js +3 -0
- package/dist/asset.d.ts +2 -2
- package/dist/asset.js +2 -2
- package/dist/component.d.ts +20 -0
- package/dist/component.js +1 -0
- package/dist/convert.d.ts +9 -0
- package/dist/convert.js +3 -0
- package/dist/curd.d.ts +3 -1
- package/dist/db.d.ts +4 -0
- package/dist/db.js +9 -0
- package/dist/dsl.d.ts +5 -0
- package/dist/dto.d.ts +2 -2
- package/dist/dto.js +1 -1
- package/dist/event.d.ts +8 -0
- package/dist/event.js +3 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/mermaid-driver.js +2 -1
- package/dist/mock.d.ts +3 -21
- package/dist/mock.js +1 -18
- package/dist/mysql-driver.d.ts +4 -0
- package/dist/mysql-driver.js +8 -3
- package/dist/navigation.d.ts +22 -0
- package/dist/navigation.js +15 -0
- package/dist/page-def.d.ts +40 -0
- package/dist/page-def.js +38 -0
- package/dist/page-flow.d.ts +4 -2
- package/dist/page-flow.js +107 -12
- package/dist/page.d.ts +32 -10
- package/dist/page.js +20 -5
- package/dist/popup.d.ts +18 -0
- package/dist/popup.js +8 -0
- package/dist/project.d.ts +7 -0
- package/dist/provider.d.ts +54 -0
- package/dist/provider.js +18 -0
- package/dist/ref.d.ts +14 -0
- package/dist/ref.js +6 -0
- package/dist/route.d.ts +8 -0
- package/dist/route.js +3 -0
- package/docs/curd.md +110 -110
- package/docs/dto.md +66 -66
- package/docs/table.md +4 -2
- package/package.json +2 -2
- package/src/action.ts +11 -0
- package/src/asset.ts +63 -63
- package/src/bases.ts +29 -29
- package/src/component.ts +22 -0
- package/src/convert.ts +13 -0
- package/src/curd.ts +93 -91
- package/src/db.ts +11 -0
- package/src/dsl.ts +188 -182
- package/src/dto.ts +247 -247
- package/src/enum-driver.ts +42 -42
- package/src/event.ts +12 -0
- package/src/flow.ts +103 -103
- package/src/index.ts +31 -21
- package/src/mermaid-driver.ts +2 -1
- package/src/mock.ts +12 -45
- package/src/mysql-driver.ts +8 -2
- package/src/navigation.ts +29 -0
- package/src/page-def.ts +80 -0
- package/src/page-flow.ts +116 -14
- package/src/page.ts +51 -14
- package/src/patterns/retry.ts +54 -54
- package/src/popup.ts +25 -0
- package/src/project.ts +97 -90
- package/src/prototype.ts +29 -29
- package/src/provider.ts +73 -0
- package/src/ref.ts +19 -0
- package/src/route.ts +12 -0
- package/src/utils.ts +10 -10
package/src/flow.ts
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
import { SchemaBase } from './dsl.js';
|
|
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
|
-
}
|
|
1
|
+
import { SchemaBase } from './dsl.js';
|
|
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
104
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
export * from './dsl.js';
|
|
2
|
-
export * from './db.js';
|
|
3
|
-
export * from './mock.js';
|
|
4
|
-
export * from './asset.js';
|
|
5
|
-
export * from './dto.js';
|
|
6
|
-
export * from './db-config.js';
|
|
7
|
-
export * from './bases.js';
|
|
8
|
-
export * from './utils.js';
|
|
9
|
-
export * from './project.js';
|
|
10
|
-
export * from './prototype.js';
|
|
11
|
-
export * from './dictionary.js';
|
|
12
|
-
export * from './mysql-driver.js';
|
|
13
|
-
export * from './enum-driver.js';
|
|
14
|
-
export * from './typebox-driver.js';
|
|
15
|
-
export * from './pattern.js';
|
|
16
|
-
export * from './patterns/retry.js';
|
|
17
|
-
export * from './flow.js';
|
|
18
|
-
export * from './
|
|
19
|
-
export * from './
|
|
20
|
-
export * from './
|
|
21
|
-
export * from './
|
|
1
|
+
export * from './dsl.js';
|
|
2
|
+
export * from './db.js';
|
|
3
|
+
export * from './mock.js';
|
|
4
|
+
export * from './asset.js';
|
|
5
|
+
export * from './dto.js';
|
|
6
|
+
export * from './db-config.js';
|
|
7
|
+
export * from './bases.js';
|
|
8
|
+
export * from './utils.js';
|
|
9
|
+
export * from './project.js';
|
|
10
|
+
export * from './prototype.js';
|
|
11
|
+
export * from './dictionary.js';
|
|
12
|
+
export * from './mysql-driver.js';
|
|
13
|
+
export * from './enum-driver.js';
|
|
14
|
+
export * from './typebox-driver.js';
|
|
15
|
+
export * from './pattern.js';
|
|
16
|
+
export * from './patterns/retry.js';
|
|
17
|
+
export * from './flow.js';
|
|
18
|
+
export * from './action.js';
|
|
19
|
+
export * from './event.js';
|
|
20
|
+
export * from './component.js';
|
|
21
|
+
export * from './convert.js';
|
|
22
|
+
export * from './ref.js';
|
|
23
|
+
export * from './route.js';
|
|
24
|
+
export * from './page.js';
|
|
25
|
+
export * from './curd.js';
|
|
26
|
+
export * from './page-flow.js';
|
|
27
|
+
export * from './provider.js';
|
|
28
|
+
export * from './page-def.js';
|
|
29
|
+
export * from './mermaid-driver.js';
|
|
30
|
+
export * from './navigation.js';
|
|
31
|
+
export * from './popup.js';
|
package/src/mermaid-driver.ts
CHANGED
|
@@ -66,7 +66,8 @@ export function renderPageFlowMermaid(schema: PageFlow): string {
|
|
|
66
66
|
lines.push(` subgraph app${appIdx}["${escapeLabel(appName)}"]`);
|
|
67
67
|
pages.forEach((p, i) => ids.set(p, `app${appIdx}_p${i}`));
|
|
68
68
|
for (const p of pages) {
|
|
69
|
-
|
|
69
|
+
const display = p.label ?? p.name;
|
|
70
|
+
lines.push(` ${ids.get(p)}["${escapeLabel(display)}"]`);
|
|
70
71
|
}
|
|
71
72
|
appIdx++;
|
|
72
73
|
lines.push(' end');
|
package/src/mock.ts
CHANGED
|
@@ -1,45 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure-data mock descriptor. Stored on DTO fields, consumed by
|
|
3
|
-
* or
|
|
4
|
-
*
|
|
5
|
-
* @example { fn: 'abc', count: 20 }
|
|
6
|
-
* @example { fn: 'amt', min: 10, max: 500 }
|
|
7
|
-
* @example { fn: 'phone' }
|
|
8
|
-
*/
|
|
9
|
-
export interface MockDescriptor {
|
|
10
|
-
fn: string;
|
|
11
|
-
[
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Result of a descriptor factory call: pure-data descriptor + lazy generator.
|
|
16
|
-
* generate is non-enumerable — JSON.stringify / spread won't pick it up.
|
|
17
|
-
*/
|
|
18
|
-
export interface MockSchema {
|
|
19
|
-
descriptor: MockDescriptor;
|
|
20
|
-
generate: () => unknown;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
type Params = Record<string, unknown>;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Define a mock descriptor factory.
|
|
27
|
-
*
|
|
28
|
-
* @param fn mock function name (for descriptor & dispatch)
|
|
29
|
-
* @param resolve converts descriptor params to a no-arg generator
|
|
30
|
-
* @returns (params?) => { descriptor, generate }
|
|
31
|
-
*/
|
|
32
|
-
export function defineMock(
|
|
33
|
-
fn: string,
|
|
34
|
-
resolve: (d: Params) => () => unknown,
|
|
35
|
-
): (params?: Params) => MockSchema {
|
|
36
|
-
return (params?: Params) => {
|
|
37
|
-
const d: MockDescriptor = { fn, ...params };
|
|
38
|
-
const result = { descriptor: d } as MockSchema;
|
|
39
|
-
Object.defineProperty(result, 'generate', {
|
|
40
|
-
value: resolve(d),
|
|
41
|
-
enumerable: false,
|
|
42
|
-
});
|
|
43
|
-
return result;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pure-data mock descriptor. Stored on DTO fields, consumed by pylonts lint mock
|
|
3
|
+
* or MockRegistry.resolve. No function references — serializable and gen-readable.
|
|
4
|
+
*
|
|
5
|
+
* @example { fn: 'abc', count: 20 }
|
|
6
|
+
* @example { fn: 'amt', min: 10, max: 500 }
|
|
7
|
+
* @example { fn: 'phone' }
|
|
8
|
+
*/
|
|
9
|
+
export interface MockDescriptor {
|
|
10
|
+
fn: string;
|
|
11
|
+
[args: string]: unknown;
|
|
12
|
+
}
|
package/src/mysql-driver.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { ForeignKey, Index, TableSchema } from './db.js';
|
|
|
3
3
|
|
|
4
4
|
// MySQL driver: converts a TableSchema into a CREATE TABLE statement.
|
|
5
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
|
+
|
|
6
11
|
function columnType(field: Field): string {
|
|
7
12
|
switch (field.type) {
|
|
8
13
|
case 'string':
|
|
@@ -34,8 +39,9 @@ function columnType(field: Field): string {
|
|
|
34
39
|
|
|
35
40
|
function renderDefault(field: Field): string {
|
|
36
41
|
if (field.default === undefined) return '';
|
|
37
|
-
//
|
|
38
|
-
|
|
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}`;
|
|
39
45
|
// Numeric columns take a bare literal, not a quoted one.
|
|
40
46
|
if (field.type === 'integer' || field.type === 'bigint' || field.type === 'decimal' || field.type === 'boolean') {
|
|
41
47
|
return ` DEFAULT ${field.default}`;
|
|
@@ -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
|
+
};
|
package/src/page-def.ts
ADDED
|
@@ -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
1
|
import { SchemaBase } from './dsl.js';
|
|
2
|
-
import { ActionSchema
|
|
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
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
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);
|
|
106
|
+
}
|
|
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);
|
|
45
128
|
}
|
|
46
129
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 };
|
|
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
|
+
}
|
|
50
152
|
}
|
|
51
|
-
return
|
|
153
|
+
return undefined;
|
|
52
154
|
}
|