@pylonts/dsl 1.1.20 → 1.1.22
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 +8 -5
- package/dist/action.d.ts +76 -33
- package/dist/action.js +19 -17
- package/dist/aggregate.d.ts +3 -13
- package/dist/aggregate.js +19 -22
- package/dist/component.d.ts +3 -3
- package/dist/curd.d.ts +2 -2
- package/dist/dto.d.ts +18 -2
- package/dist/dto.js +75 -9
- package/dist/flow-script.js +73 -7
- package/dist/flow.d.ts +20 -3
- package/dist/flow.js +82 -11
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/journey.d.ts +23 -0
- package/dist/journey.js +26 -0
- package/dist/mermaid-driver.js +6 -1
- package/dist/navigation.d.ts +2 -2
- package/dist/page-action.d.ts +39 -0
- package/dist/page-action.js +17 -0
- package/dist/page-def.d.ts +9 -9
- package/dist/page-flow.d.ts +4 -4
- package/dist/popup.d.ts +3 -3
- package/dist/repository.d.ts +2 -2
- package/dist/task.d.ts +20 -0
- package/dist/task.js +21 -0
- package/dist/utils.d.ts +7 -0
- package/dist/utils.js +12 -4
- package/docs/aggregate-implementation.md +174 -0
- package/docs/aggregate.md +147 -110
- package/docs/concepts.md +109 -0
- package/docs/dto.md +130 -106
- package/docs/table.md +41 -0
- package/docs/task.md +81 -0
- package/docs/utils.md +19 -12
- package/package.json +1 -1
- package/src/action.ts +87 -52
- package/src/aggregate.ts +94 -103
- package/src/component.ts +3 -3
- package/src/curd.ts +2 -2
- package/src/dto.ts +87 -8
- package/src/flow-script.ts +68 -6
- package/src/flow.ts +99 -17
- package/src/index.ts +3 -0
- package/src/journey.ts +61 -0
- package/src/mermaid-driver.ts +5 -1
- package/src/navigation.ts +2 -2
- package/src/page-action.ts +59 -0
- package/src/page-def.ts +9 -9
- package/src/page-flow.ts +4 -4
- package/src/popup.ts +3 -3
- package/src/repository.ts +35 -35
- package/src/task.ts +52 -0
- package/src/utils.ts +18 -4
package/src/flow.ts
CHANGED
|
@@ -210,9 +210,49 @@ export interface Comparison {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/** The machine-readable condition behind a guard check or a branch edge
|
|
213
|
-
* (optional): a utils predicate call (its boolean result decides)
|
|
214
|
-
* comparison. */
|
|
215
|
-
export type GuardCondition = FlowCall | Comparison;
|
|
213
|
+
* (optional): a utils predicate call (its boolean result decides), a field
|
|
214
|
+
* comparison, or a composite (not/and/or over sub-conditions). */
|
|
215
|
+
export type GuardCondition = FlowCall | Comparison | ConditionGroup;
|
|
216
|
+
|
|
217
|
+
/** A composite condition: not (exactly one sub-condition), and/or (two or
|
|
218
|
+
* more). Renderers parenthesize sub-groups, so nesting stays unambiguous. */
|
|
219
|
+
export interface ConditionGroup {
|
|
220
|
+
kind: 'not' | 'and' | 'or';
|
|
221
|
+
conds: GuardCondition[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function isConditionGroup(c: unknown): c is ConditionGroup {
|
|
225
|
+
return (
|
|
226
|
+
typeof c === 'object' &&
|
|
227
|
+
c !== null &&
|
|
228
|
+
((c as { kind?: unknown }).kind === 'not' || (c as { kind?: unknown }).kind === 'and' || (c as { kind?: unknown }).kind === 'or') &&
|
|
229
|
+
Array.isArray((c as { conds?: unknown }).conds)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Negation — the only way to invert a condition (e.g. !predicate). */
|
|
234
|
+
export function not(...conds: GuardCondition[]): ConditionGroup {
|
|
235
|
+
if (conds.length !== 1) {
|
|
236
|
+
throw new Error('not: takes exactly one condition');
|
|
237
|
+
}
|
|
238
|
+
return { kind: 'not', conds };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Conjunction — every sub-condition must hold. */
|
|
242
|
+
export function and(...conds: GuardCondition[]): ConditionGroup {
|
|
243
|
+
if (conds.length < 2) {
|
|
244
|
+
throw new Error('and: requires at least two conditions');
|
|
245
|
+
}
|
|
246
|
+
return { kind: 'and', conds };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Disjunction — at least one sub-condition holds. */
|
|
250
|
+
export function or(...conds: GuardCondition[]): ConditionGroup {
|
|
251
|
+
if (conds.length < 2) {
|
|
252
|
+
throw new Error('or: requires at least two conditions');
|
|
253
|
+
}
|
|
254
|
+
return { kind: 'or', conds };
|
|
255
|
+
}
|
|
216
256
|
|
|
217
257
|
/** True when the condition operand is the slot itself (slot-level null check),
|
|
218
258
|
* not a field access on it. Slot metadata (name) lives on the proxy target and
|
|
@@ -221,13 +261,28 @@ export function isFlowSlot(v: unknown): v is FlowSlot {
|
|
|
221
261
|
return typeof v === 'object' && v !== null && typeof (v as FlowSlot).name === 'string';
|
|
222
262
|
}
|
|
223
263
|
|
|
264
|
+
/** True when the slot carries a scalar Field (its declared type has a jsType)
|
|
265
|
+
* rather than a message — scalar slots support full comparisons
|
|
266
|
+
* (gt(slots.total, 100)); message slots only slot-level null checks. */
|
|
267
|
+
export function isScalarSlot(slot: FlowSlot): boolean {
|
|
268
|
+
const t = slot.type as { jsType?: unknown } | undefined;
|
|
269
|
+
return typeof t === 'object' && t !== null && t.jsType !== undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
224
272
|
function comparison(op: CompareOp, field: unknown, value?: string | number | EnumValue): Comparison {
|
|
225
273
|
if (isFlowSlot(field)) {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
274
|
+
const nullOp = op === 'isNull' || op === 'isNotNull';
|
|
275
|
+
if (nullOp) {
|
|
276
|
+
if (value !== undefined) {
|
|
277
|
+
throw new Error(`${op}: takes no value`);
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
if (!isScalarSlot(field)) {
|
|
281
|
+
throw new Error(`${op}: a slot-level check only supports isNull/isNotNull — field comparisons need slots.args.amt`);
|
|
282
|
+
}
|
|
283
|
+
if (value === undefined) {
|
|
284
|
+
throw new Error(`${op}: requires a value`);
|
|
285
|
+
}
|
|
231
286
|
}
|
|
232
287
|
return { kind: 'comparison', op, field, value };
|
|
233
288
|
}
|
|
@@ -869,19 +924,37 @@ function validateGuardChecks(schema: FlowSchema): void {
|
|
|
869
924
|
}
|
|
870
925
|
|
|
871
926
|
// A condition must be a utils predicate call (boolean result, no result
|
|
872
|
-
// slot)
|
|
873
|
-
// type match the operator
|
|
927
|
+
// slot), a field comparison whose op is known and whose value presence and
|
|
928
|
+
// type match the operator, or a composite (not/and/or) whose sub-conditions
|
|
929
|
+
// are each valid.
|
|
874
930
|
const COMPARE_OPS: readonly CompareOp[] = ['lt', 'le', 'gt', 'ge', 'eq', 'ne', 'isNull', 'isNotNull'];
|
|
875
931
|
|
|
876
932
|
function validateCondition(where: string, c: GuardCondition): void {
|
|
933
|
+
if (isConditionGroup(c)) {
|
|
934
|
+
if (c.kind === 'not') {
|
|
935
|
+
if (c.conds.length !== 1) {
|
|
936
|
+
throw new Error(`${where}: not() takes exactly one condition`);
|
|
937
|
+
}
|
|
938
|
+
} else if (c.conds.length < 2) {
|
|
939
|
+
throw new Error(`${where}: ${c.kind}() takes at least two conditions`);
|
|
940
|
+
}
|
|
941
|
+
for (const sub of c.conds) validateCondition(where, sub);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
877
944
|
if (!isCall(c)) {
|
|
878
945
|
if (isFlowSlot(c.field)) {
|
|
879
946
|
const nullOp = c.op === 'isNull' || c.op === 'isNotNull';
|
|
880
|
-
if (
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
947
|
+
if (nullOp) {
|
|
948
|
+
if (c.value !== undefined) {
|
|
949
|
+
throw new Error(`${where}: ${c.op} takes no value`);
|
|
950
|
+
}
|
|
951
|
+
} else {
|
|
952
|
+
if (!isScalarSlot(c.field)) {
|
|
953
|
+
throw new Error(`${where}: a slot-level check only supports isNull/isNotNull`);
|
|
954
|
+
}
|
|
955
|
+
if (c.value === undefined) {
|
|
956
|
+
throw new Error(`${where}: ${c.op} requires a value`);
|
|
957
|
+
}
|
|
885
958
|
}
|
|
886
959
|
return;
|
|
887
960
|
}
|
|
@@ -908,18 +981,27 @@ function validateCondition(where: string, c: GuardCondition): void {
|
|
|
908
981
|
}
|
|
909
982
|
return;
|
|
910
983
|
}
|
|
911
|
-
const m = c.method as { type?: string; name: string };
|
|
984
|
+
const m = c.method as Partial<UtilsMethodSchema> & { type?: string; name: string };
|
|
912
985
|
if (m.type !== 'utilsMethod') {
|
|
913
986
|
throw new Error(`${where}: check call must be a utils predicate, got ${m.type ?? m.name}`);
|
|
914
987
|
}
|
|
915
988
|
if (c.result !== undefined) {
|
|
916
989
|
throw new Error(`${where}: a predicate call cannot bind a result slot`);
|
|
917
990
|
}
|
|
991
|
+
if (m.result === undefined || m.result.jsType !== 'boolean') {
|
|
992
|
+
const declared = m.result === undefined ? 'void' : m.result.jsType;
|
|
993
|
+
throw new Error(
|
|
994
|
+
`${where}: utils predicate "${m.name}" must declare a boolean result (got ${declared}) — ` +
|
|
995
|
+
`predicates (can/is/has) return boolean; defense guards (assert/validate/ensure) throw internally and are invoked, not used in IF`,
|
|
996
|
+
);
|
|
997
|
+
}
|
|
918
998
|
}
|
|
919
999
|
|
|
920
1000
|
/** Slots a condition reads: a comparison's field slot (or the slot itself for
|
|
921
|
-
* slot-level checks)
|
|
1001
|
+
* slot-level checks), a predicate call's arg slots, or every sub-condition's
|
|
1002
|
+
* slots for a composite. */
|
|
922
1003
|
function conditionSlots(c: GuardCondition): FlowSlot[] {
|
|
1004
|
+
if (isConditionGroup(c)) return c.conds.flatMap(conditionSlots);
|
|
923
1005
|
if (!isCall(c)) return [isFlowSlot(c.field) ? c.field : c.field.slot];
|
|
924
1006
|
return c.args ?? [];
|
|
925
1007
|
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,8 @@ export * from './pattern.js';
|
|
|
16
16
|
export * from './patterns/retry.js';
|
|
17
17
|
export * from './flow.js';
|
|
18
18
|
export * from './action.js';
|
|
19
|
+
export * from './journey.js';
|
|
20
|
+
export * from './page-action.js';
|
|
19
21
|
export * from './event.js';
|
|
20
22
|
export * from './component.js';
|
|
21
23
|
export * from './convert.js';
|
|
@@ -31,6 +33,7 @@ export * from './aggregate.js';
|
|
|
31
33
|
export * from './repository.js';
|
|
32
34
|
export * from './domain-event.js';
|
|
33
35
|
export * from './third-service.js';
|
|
36
|
+
export * from './task.js';
|
|
34
37
|
export * from './token.js';
|
|
35
38
|
export * from './field-rule.js';
|
|
36
39
|
export * from './exception.js';
|
package/src/journey.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl.js';
|
|
2
|
+
import type { ActionSchema } from './action.js';
|
|
3
|
+
|
|
4
|
+
// Journey definitions: a cross-app business journey — one data's story along
|
|
5
|
+
// the time axis. A journey is a plain running list (流水账) of actions:
|
|
6
|
+
// page → page → controller → third → db → task, in business order.
|
|
7
|
+
//
|
|
8
|
+
// Design principles:
|
|
9
|
+
// - The protagonist (the data the journey is about) is IMPLICIT — derived
|
|
10
|
+
// from the actions, never declared. Like a TV protagonist: no label on the
|
|
11
|
+
// forehead.
|
|
12
|
+
// - Blueprint first, anchored later: actions start as names (text), then
|
|
13
|
+
// upgrade to real references (page/controller/third/db/task instances).
|
|
14
|
+
// The name-vs-reference ratio is the refinement level.
|
|
15
|
+
// - Every action is `type + properties + data` (ActionSchema). No from/to
|
|
16
|
+
// state transitions, no branches — a journey is a single line.
|
|
17
|
+
|
|
18
|
+
/** One business journey: a goal-directed line of actions. */
|
|
19
|
+
export interface JourneySchema extends SchemaBase {
|
|
20
|
+
/** Chinese title of the journey. */
|
|
21
|
+
title: string;
|
|
22
|
+
/** The business goal the journey achieves (what it is for). */
|
|
23
|
+
goal: string;
|
|
24
|
+
/** The running list of actions, in business order. */
|
|
25
|
+
actions: ActionSchema[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Defines a business journey. `name` is kebab-case (e.g. 'merchant-onboarding');
|
|
30
|
+
* the export symbol is the kebab-camel of the name (merchantOnboarding).
|
|
31
|
+
* File name is the name plus '.journey.ts' (journey_schema/merchant-onboarding.journey.ts).
|
|
32
|
+
*/
|
|
33
|
+
export function defineJourney(options: {
|
|
34
|
+
name: string;
|
|
35
|
+
title: string;
|
|
36
|
+
goal: string;
|
|
37
|
+
actions: ActionSchema[];
|
|
38
|
+
description?: string;
|
|
39
|
+
}): JourneySchema {
|
|
40
|
+
if (!/^[a-z][a-z0-9-]*$/.test(options.name)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`journey ${options.name}: name must be kebab-case (lowercase letters/digits/dashes)`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (!options.title) {
|
|
46
|
+
throw new Error(`journey ${options.name}: title is required`);
|
|
47
|
+
}
|
|
48
|
+
if (!options.goal) {
|
|
49
|
+
throw new Error(`journey ${options.name}: goal is required`);
|
|
50
|
+
}
|
|
51
|
+
if (!options.actions || options.actions.length === 0) {
|
|
52
|
+
throw new Error(`journey ${options.name}: actions must be non-empty (a journey is a line of actions)`);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
name: options.name,
|
|
56
|
+
title: options.title,
|
|
57
|
+
goal: options.goal,
|
|
58
|
+
description: options.description,
|
|
59
|
+
actions: options.actions,
|
|
60
|
+
};
|
|
61
|
+
}
|
package/src/mermaid-driver.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FlowEdge, FlowEnd, FlowNode, FlowSchema, FlowStep, FlowNodeOrEnd, GuardNode, TryNode, IfNode } from './flow.js';
|
|
2
|
-
import { isCall, isFlowSlot, methodOf } from './flow.js';
|
|
2
|
+
import { isCall, isConditionGroup, isFlowSlot, methodOf } from './flow.js';
|
|
3
3
|
import type { FlowMethodRef, FlowNodeMethodRef, GuardCondition } from './flow.js';
|
|
4
4
|
import { Page } from './page.js';
|
|
5
5
|
import { PageEdge, PageFlow } from './page-flow.js';
|
|
@@ -219,6 +219,10 @@ function renderDataLine(n: FlowNode | GuardNode | IfNode, outgoing: FlowEdge[]):
|
|
|
219
219
|
const writes = new Set<string>();
|
|
220
220
|
const addCondition = (c: GuardCondition | undefined): void => {
|
|
221
221
|
if (c === undefined) return;
|
|
222
|
+
if (isConditionGroup(c)) {
|
|
223
|
+
for (const s of c.conds) addCondition(s);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
222
226
|
if (!isCall(c)) {
|
|
223
227
|
reads.add(isFlowSlot(c.field) ? c.field.name : c.field.slot.name);
|
|
224
228
|
return;
|
package/src/navigation.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { RouteDataSchema } from './route.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { PageActionSchema } from './page-action.js';
|
|
3
3
|
import { PageSchema } from './page.js';
|
|
4
4
|
|
|
5
5
|
/** Navigation primitives: front-end routing actions that are not provider calls. */
|
|
6
|
-
export interface NavigationAction extends
|
|
6
|
+
export interface NavigationAction extends PageActionSchema {
|
|
7
7
|
type: 'navigation';
|
|
8
8
|
method: 'back' | 'push' | 'popup' | 'home';
|
|
9
9
|
/** Target page for push navigation. */
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SchemaBase } from './dsl.js';
|
|
2
|
+
import type { DtoField, DtoMessage, DtoArrayField, DtoObjectField } from './dto.js';
|
|
3
|
+
import type { RefSchema } from './ref.js';
|
|
4
|
+
import type { ControllerMethodSchema } from './controller.js';
|
|
5
|
+
|
|
6
|
+
// Page actions: what a user can do ON a page (e.g. submit, approve, reject).
|
|
7
|
+
// Distinguished from the cross-domain Action (journey): page actions live in
|
|
8
|
+
// the page skeleton (PageDef.actions / PageSchema.pageDef) and describe the
|
|
9
|
+
// page's own operation surface; journey actions describe what happens along a
|
|
10
|
+
// business line (page / controller / third / db / task). Subclasses use
|
|
11
|
+
// `type` as the discriminator.
|
|
12
|
+
|
|
13
|
+
/** An action a user can perform on a page (e.g. submit, approve, reject).
|
|
14
|
+
* Subclasses use `type` as the discriminator. */
|
|
15
|
+
export interface PageActionSchema extends SchemaBase {
|
|
16
|
+
type: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function definePageAction(name: string, description?: string): PageActionSchema {
|
|
20
|
+
return { name, description, type: 'gesture' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Parameter data source for a call argument. */
|
|
24
|
+
export type DataRef =
|
|
25
|
+
| { type: 'route'; key: string }
|
|
26
|
+
| { type: 'data'; key: string }
|
|
27
|
+
| { type: 'value'; value: unknown };
|
|
28
|
+
|
|
29
|
+
/** Create a route-parameter reference. */
|
|
30
|
+
export function route(key: string): DataRef {
|
|
31
|
+
return { type: 'route', key };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Create a page-data reference. */
|
|
35
|
+
export function data(key: string): DataRef {
|
|
36
|
+
return { type: 'data', key };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CallAction extends PageActionSchema {
|
|
40
|
+
type: 'call';
|
|
41
|
+
func: ControllerMethodSchema;
|
|
42
|
+
args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function call(func: ControllerMethodSchema, args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>): CallAction {
|
|
46
|
+
return { name: func.name, type: 'call', func, args };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Assign a call's result to a page data field.
|
|
50
|
+
* React: setState({ [field]: await ... }). Mini-program: this.setData({ [field]: ... }). */
|
|
51
|
+
export interface SetDataAction extends PageActionSchema {
|
|
52
|
+
type: 'setData';
|
|
53
|
+
call: CallAction;
|
|
54
|
+
field: DtoField;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function setData(call: CallAction, field: DtoField): SetDataAction {
|
|
58
|
+
return { name: 'setData', type: 'setData', call, field };
|
|
59
|
+
}
|
package/src/page-def.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ImportableSchemaBase, CollectionSchemaBase, SchemaBase } from './d
|
|
|
2
2
|
import { DtoField } from './dto.js';
|
|
3
3
|
import type { DtoMessage, DtoArrayField, DtoObjectField } from './dto.js';
|
|
4
4
|
import type { ComponentSchema } from './component.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type { PageActionSchema } from './page-action.js';
|
|
6
6
|
|
|
7
7
|
export type { RouteDataSchema } from './route.js';
|
|
8
8
|
export { defineRouteData } from './route.js';
|
|
@@ -14,24 +14,24 @@ export { defineRouteData } from './route.js';
|
|
|
14
14
|
/** Page lifecycle events that trigger data loading. */
|
|
15
15
|
export interface EventSchema extends SchemaBase {
|
|
16
16
|
type: 'onLoad' | 'onShow' | 'onHide' | 'onPullDownRefresh' | 'onReachBottom';
|
|
17
|
-
/**
|
|
18
|
-
actions?:
|
|
17
|
+
/** Page actions that fire when this event occurs. */
|
|
18
|
+
actions?: PageActionSchema[];
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export const events = {
|
|
22
|
-
onLoad(actions?:
|
|
22
|
+
onLoad(actions?: PageActionSchema[], description?: string): EventSchema {
|
|
23
23
|
return { name: 'onLoad', type: 'onLoad', actions, description };
|
|
24
24
|
},
|
|
25
|
-
onShow(actions?:
|
|
25
|
+
onShow(actions?: PageActionSchema[], description?: string): EventSchema {
|
|
26
26
|
return { name: 'onShow', type: 'onShow', actions, description };
|
|
27
27
|
},
|
|
28
|
-
onHide(actions?:
|
|
28
|
+
onHide(actions?: PageActionSchema[], description?: string): EventSchema {
|
|
29
29
|
return { name: 'onHide', type: 'onHide', actions, description };
|
|
30
30
|
},
|
|
31
|
-
onPullDownRefresh(actions?:
|
|
31
|
+
onPullDownRefresh(actions?: PageActionSchema[], description?: string): EventSchema {
|
|
32
32
|
return { name: 'onPullDownRefresh', type: 'onPullDownRefresh', actions, description };
|
|
33
33
|
},
|
|
34
|
-
onReachBottom(actions?:
|
|
34
|
+
onReachBottom(actions?: PageActionSchema[], description?: string): EventSchema {
|
|
35
35
|
return { name: 'onReachBottom', type: 'onReachBottom', actions, description };
|
|
36
36
|
},
|
|
37
37
|
};
|
|
@@ -64,7 +64,7 @@ export interface PageDef extends ImportableSchemaBase {
|
|
|
64
64
|
/** Lifecycle events that trigger data loading. */
|
|
65
65
|
events: EventSchema[];
|
|
66
66
|
/** Page actions (provider calls, navigation, etc.). */
|
|
67
|
-
actions:
|
|
67
|
+
actions: PageActionSchema[];
|
|
68
68
|
/** UI component declarations that make up the page skeleton. */
|
|
69
69
|
components: ComponentSchema[];
|
|
70
70
|
/** Whether the driver should generate page-level loading / error / empty state wrappers. */
|
package/src/page-flow.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SchemaBase } from './dsl.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { PageActionSchema } from './page-action.js';
|
|
3
3
|
import { Page, getRegisteredPages, clearRegisteredPages } from './page.js';
|
|
4
4
|
import type { PageDef } from './page-def.js';
|
|
5
5
|
import type { NavigationAction } from './navigation.js';
|
|
@@ -10,8 +10,8 @@ import type { NavigationAction } from './navigation.js';
|
|
|
10
10
|
// models the flow graph between pages.
|
|
11
11
|
|
|
12
12
|
export interface PageEdge extends SchemaBase {
|
|
13
|
-
/** Trigger action; undefined = default path (success/normal). */
|
|
14
|
-
when?:
|
|
13
|
+
/** Trigger page action; undefined = default path (success/normal). */
|
|
14
|
+
when?: PageActionSchema;
|
|
15
15
|
start: Page;
|
|
16
16
|
end: Page;
|
|
17
17
|
}
|
|
@@ -24,7 +24,7 @@ export interface PageFlow extends SchemaBase {
|
|
|
24
24
|
edges: PageEdge[];
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function pageEdge(start: Page, end: Page, when?:
|
|
27
|
+
export function pageEdge(start: Page, end: Page, when?: PageActionSchema, description?: string): PageEdge {
|
|
28
28
|
// Auto name for uniformity with SchemaBase; `when` stays the branch marker.
|
|
29
29
|
return { name: `${start.name}->${end.name}`, start, end, when, description };
|
|
30
30
|
}
|
package/src/popup.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PageActionSchema } from './page-action.js';
|
|
2
2
|
import type { RefSchema } from './ref.js';
|
|
3
3
|
|
|
4
4
|
/** Display a toast notification. React: toast/message UI. Mini-program: wx.showToast. */
|
|
5
|
-
export interface ToastAction extends
|
|
5
|
+
export interface ToastAction extends PageActionSchema {
|
|
6
6
|
type: 'toast';
|
|
7
7
|
message: string | RefSchema;
|
|
8
8
|
icon?: 'success' | 'error' | 'loading' | 'none';
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/** Display an alert dialog. React: modal. Mini-program: wx.showModal. */
|
|
12
|
-
export interface AlertAction extends
|
|
12
|
+
export interface AlertAction extends PageActionSchema {
|
|
13
13
|
type: 'alert';
|
|
14
14
|
title: string | RefSchema;
|
|
15
15
|
content: string | RefSchema;
|
package/src/repository.ts
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { SchemaBase } from './dsl.js';
|
|
2
|
-
import type { DomainAggregate } from './aggregate.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Aggregate-grained storage entry. The repository binds one aggregate and
|
|
6
|
-
* exposes the three fixed operation skeletons (load / save / delete) that the
|
|
7
|
-
* generator expands from the aggregate structure:
|
|
8
|
-
*
|
|
9
|
-
* save(order) = tx { rootDao.upsert + memberDao cascade by
|
|
10
|
-
* load(id) = rootDao.get + memberDao by
|
|
11
|
-
* delete(id) = tx { memberDao delete + rootDao delete }
|
|
12
|
-
*
|
|
13
|
-
* Callers face the domain concept (Order), never the tables. DAO stays
|
|
14
|
-
* single-table; Repository is the multi-table (aggregate) unit; Service is the
|
|
15
|
-
* use-case unit. Inter-aggregate joins are prohibited — repositories only
|
|
16
|
-
* reference each other by root ID.
|
|
17
|
-
*/
|
|
18
|
-
export interface RepositorySchema extends SchemaBase {
|
|
19
|
-
type: 'repository';
|
|
20
|
-
/** The aggregate this repository persists (shared instance). */
|
|
21
|
-
aggregate: DomainAggregate;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function defineRepository(options: {
|
|
25
|
-
name: string;
|
|
26
|
-
aggregate: DomainAggregate;
|
|
27
|
-
description?: string;
|
|
28
|
-
}): RepositorySchema {
|
|
29
|
-
return {
|
|
30
|
-
type: 'repository',
|
|
31
|
-
name: options.name,
|
|
32
|
-
description: options.description,
|
|
33
|
-
aggregate: options.aggregate,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
1
|
+
import { SchemaBase } from './dsl.js';
|
|
2
|
+
import type { DomainAggregate } from './aggregate.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Aggregate-grained storage entry. The repository binds one aggregate and
|
|
6
|
+
* exposes the three fixed operation skeletons (load / save / delete) that the
|
|
7
|
+
* generator expands from the aggregate structure:
|
|
8
|
+
*
|
|
9
|
+
* save(order) = tx { rootDao.upsert + memberDao cascade by FK / extends }
|
|
10
|
+
* load(id) = rootDao.get + memberDao by FK / extends
|
|
11
|
+
* delete(id) = tx { memberDao delete + rootDao delete }
|
|
12
|
+
*
|
|
13
|
+
* Callers face the domain concept (Order), never the tables. DAO stays
|
|
14
|
+
* single-table; Repository is the multi-table (aggregate) unit; Service is the
|
|
15
|
+
* use-case unit. Inter-aggregate joins are prohibited — repositories only
|
|
16
|
+
* reference each other by root ID.
|
|
17
|
+
*/
|
|
18
|
+
export interface RepositorySchema extends SchemaBase {
|
|
19
|
+
type: 'repository';
|
|
20
|
+
/** The aggregate this repository persists (shared instance). */
|
|
21
|
+
aggregate: DomainAggregate;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function defineRepository(options: {
|
|
25
|
+
name: string;
|
|
26
|
+
aggregate: DomainAggregate;
|
|
27
|
+
description?: string;
|
|
28
|
+
}): RepositorySchema {
|
|
29
|
+
return {
|
|
30
|
+
type: 'repository',
|
|
31
|
+
name: options.name,
|
|
32
|
+
description: options.description,
|
|
33
|
+
aggregate: options.aggregate,
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/task.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { CollectionSchemaBase } from './dsl.js';
|
|
2
|
+
|
|
3
|
+
// Task definitions: scheduled (timer) operations — the second member of the
|
|
4
|
+
// action closure (action = controller | task | third callback).
|
|
5
|
+
//
|
|
6
|
+
// A task is a contract: name + cron + what it does. It declares NO state
|
|
7
|
+
// changes — state changes are expressed by journey steps. Task implementation
|
|
8
|
+
// (scan logic, idempotency) lives in the implementation layer (e.g. pylon-flow
|
|
9
|
+
// steps).
|
|
10
|
+
//
|
|
11
|
+
// Task represents ONLY timers (cron-driven). Async/event-driven operations
|
|
12
|
+
// belong to the event system (EventObserver / EventNotifier, pending), not
|
|
13
|
+
// here.
|
|
14
|
+
|
|
15
|
+
export interface TaskSchema extends CollectionSchemaBase {
|
|
16
|
+
type: 'task';
|
|
17
|
+
/** Display label (Chinese) for the task. */
|
|
18
|
+
label: string;
|
|
19
|
+
/** Cron expression — the timer schedule. */
|
|
20
|
+
cron: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Defines a scheduled task. `name` must end with 'Task' (PascalCase,
|
|
25
|
+
* e.g. 'AutoRefundTask'); the export symbol equals the name. File name is the
|
|
26
|
+
* name minus the Task suffix, kebab-cased, plus '.task.ts'
|
|
27
|
+
* (AutoRefundTask → task_schema/auto-refund.task.ts).
|
|
28
|
+
*/
|
|
29
|
+
export function defineTask(options: {
|
|
30
|
+
name: string;
|
|
31
|
+
label: string;
|
|
32
|
+
cron: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
}): TaskSchema {
|
|
35
|
+
if (!/(Task)$/.test(options.name)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`task ${options.name}: name must end with 'Task' (PascalCase, e.g. 'AutoRefundTask')`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (!options.cron || options.cron.trim() === '') {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`task ${options.name}: cron is required (a task is a timer)`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
type: 'task',
|
|
47
|
+
name: options.name,
|
|
48
|
+
label: options.label,
|
|
49
|
+
description: options.description,
|
|
50
|
+
cron: options.cron,
|
|
51
|
+
};
|
|
52
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CollectionSchemaBase, Field, SchemaBase } from './dsl.js';
|
|
2
|
+
import { assertNoInlineContainers } from './dto.js';
|
|
2
3
|
import type { DtoField } from './dto.js';
|
|
4
|
+
import type { ExceptionSchema } from './exception.js';
|
|
3
5
|
import type { FrontAppSchema, ProjectApiSchema } from './project.js';
|
|
4
6
|
|
|
5
7
|
// Base utility modules — business-agnostic helpers with full method
|
|
@@ -22,6 +24,12 @@ export interface UtilsMethodSchema extends SchemaBase {
|
|
|
22
24
|
* computed amounts, ...). The method output is a fresh value, never a
|
|
23
25
|
* shared table column. Omit for void methods (pure actions). */
|
|
24
26
|
result?: Field;
|
|
27
|
+
/** Exceptions this method may throw — the failure contract of a defense
|
|
28
|
+
* guard (assert/validate/ensure: void, throws internally). Flows route
|
|
29
|
+
* invoked guards' throws into their escape set automatically, so a
|
|
30
|
+
* guard's throws must be declared by the calling service method (or
|
|
31
|
+
* caught in a TRY). Predicates (can/is/has, boolean) do not throw. */
|
|
32
|
+
throws?: ExceptionSchema[];
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/** Method input for defineUtils: type/schema/name are set by the builder. */
|
|
@@ -62,6 +70,9 @@ export function defineUtils(options: {
|
|
|
62
70
|
};
|
|
63
71
|
for (const key of Object.keys(options.methods)) {
|
|
64
72
|
const method = options.methods[key] as UtilsMethodDef;
|
|
73
|
+
// Same rule as DTO fields: args must not nest inline containers — use a
|
|
74
|
+
// DTO field reference (args: { items: SubmitRequest.fields.items }).
|
|
75
|
+
assertNoInlineContainers(options.name, method.args);
|
|
65
76
|
const methodSchema: UtilsMethodSchema = {
|
|
66
77
|
type: 'utilsMethod',
|
|
67
78
|
name: key,
|
|
@@ -69,14 +80,17 @@ export function defineUtils(options: {
|
|
|
69
80
|
schema,
|
|
70
81
|
args: method.args,
|
|
71
82
|
result: method.result,
|
|
83
|
+
throws: method.throws,
|
|
72
84
|
};
|
|
73
85
|
// Args: write back on the DtoField wrapper only (safe: wrappers are
|
|
74
|
-
// created per method via dtoField(), never shared).
|
|
75
|
-
//
|
|
76
|
-
// (domain rules)
|
|
77
|
-
// name/schema
|
|
86
|
+
// created per method via dtoField(), never shared). Shared instances —
|
|
87
|
+
// DTO fields passed by reference (args: { items: OrderSubmitRequest.fields.items })
|
|
88
|
+
// and fields wrapping table columns (domain rules) — stay untouched: the
|
|
89
|
+
// DTO owns its field name/schema, the shared column instance keeps its
|
|
90
|
+
// table identity.
|
|
78
91
|
for (const argKey of Object.keys(methodSchema.args)) {
|
|
79
92
|
const df = methodSchema.args[argKey] as DtoField;
|
|
93
|
+
if (df.schema !== undefined) continue;
|
|
80
94
|
df.name = argKey;
|
|
81
95
|
df.schema = schema;
|
|
82
96
|
if (df.field.schema === undefined) {
|