@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.
Files changed (54) hide show
  1. package/README.md +8 -5
  2. package/dist/action.d.ts +76 -33
  3. package/dist/action.js +19 -17
  4. package/dist/aggregate.d.ts +3 -13
  5. package/dist/aggregate.js +19 -22
  6. package/dist/component.d.ts +3 -3
  7. package/dist/curd.d.ts +2 -2
  8. package/dist/dto.d.ts +18 -2
  9. package/dist/dto.js +75 -9
  10. package/dist/flow-script.js +73 -7
  11. package/dist/flow.d.ts +20 -3
  12. package/dist/flow.js +82 -11
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.js +3 -0
  15. package/dist/journey.d.ts +23 -0
  16. package/dist/journey.js +26 -0
  17. package/dist/mermaid-driver.js +6 -1
  18. package/dist/navigation.d.ts +2 -2
  19. package/dist/page-action.d.ts +39 -0
  20. package/dist/page-action.js +17 -0
  21. package/dist/page-def.d.ts +9 -9
  22. package/dist/page-flow.d.ts +4 -4
  23. package/dist/popup.d.ts +3 -3
  24. package/dist/repository.d.ts +2 -2
  25. package/dist/task.d.ts +20 -0
  26. package/dist/task.js +21 -0
  27. package/dist/utils.d.ts +7 -0
  28. package/dist/utils.js +12 -4
  29. package/docs/aggregate-implementation.md +174 -0
  30. package/docs/aggregate.md +147 -110
  31. package/docs/concepts.md +109 -0
  32. package/docs/dto.md +130 -106
  33. package/docs/table.md +41 -0
  34. package/docs/task.md +81 -0
  35. package/docs/utils.md +19 -12
  36. package/package.json +1 -1
  37. package/src/action.ts +87 -52
  38. package/src/aggregate.ts +94 -103
  39. package/src/component.ts +3 -3
  40. package/src/curd.ts +2 -2
  41. package/src/dto.ts +87 -8
  42. package/src/flow-script.ts +68 -6
  43. package/src/flow.ts +99 -17
  44. package/src/index.ts +3 -0
  45. package/src/journey.ts +61 -0
  46. package/src/mermaid-driver.ts +5 -1
  47. package/src/navigation.ts +2 -2
  48. package/src/page-action.ts +59 -0
  49. package/src/page-def.ts +9 -9
  50. package/src/page-flow.ts +4 -4
  51. package/src/popup.ts +3 -3
  52. package/src/repository.ts +35 -35
  53. package/src/task.ts +52 -0
  54. package/src/utils.ts +18 -4
package/dist/flow.d.ts CHANGED
@@ -94,13 +94,30 @@ export interface Comparison {
94
94
  value?: string | number | EnumValue;
95
95
  }
96
96
  /** The machine-readable condition behind a guard check or a branch edge
97
- * (optional): a utils predicate call (its boolean result decides) or a field
98
- * comparison. */
99
- export type GuardCondition = FlowCall | Comparison;
97
+ * (optional): a utils predicate call (its boolean result decides), a field
98
+ * comparison, or a composite (not/and/or over sub-conditions). */
99
+ export type GuardCondition = FlowCall | Comparison | ConditionGroup;
100
+ /** A composite condition: not (exactly one sub-condition), and/or (two or
101
+ * more). Renderers parenthesize sub-groups, so nesting stays unambiguous. */
102
+ export interface ConditionGroup {
103
+ kind: 'not' | 'and' | 'or';
104
+ conds: GuardCondition[];
105
+ }
106
+ export declare function isConditionGroup(c: unknown): c is ConditionGroup;
107
+ /** Negation — the only way to invert a condition (e.g. !predicate). */
108
+ export declare function not(...conds: GuardCondition[]): ConditionGroup;
109
+ /** Conjunction — every sub-condition must hold. */
110
+ export declare function and(...conds: GuardCondition[]): ConditionGroup;
111
+ /** Disjunction — at least one sub-condition holds. */
112
+ export declare function or(...conds: GuardCondition[]): ConditionGroup;
100
113
  /** True when the condition operand is the slot itself (slot-level null check),
101
114
  * not a field access on it. Slot metadata (name) lives on the proxy target and
102
115
  * reads without field interception; a field access resolves to { slot, field }. */
103
116
  export declare function isFlowSlot(v: unknown): v is FlowSlot;
117
+ /** True when the slot carries a scalar Field (its declared type has a jsType)
118
+ * rather than a message — scalar slots support full comparisons
119
+ * (gt(slots.total, 100)); message slots only slot-level null checks. */
120
+ export declare function isScalarSlot(slot: FlowSlot): boolean;
104
121
  export declare function lt(field: unknown, value: string | number): Comparison;
105
122
  export declare function le(field: unknown, value: string | number): Comparison;
106
123
  export declare function gt(field: unknown, value: string | number): Comparison;
package/dist/flow.js CHANGED
@@ -80,19 +80,61 @@ export function isCall(m) {
80
80
  export function methodOf(m) {
81
81
  return isCall(m) ? m.method : m;
82
82
  }
83
+ export function isConditionGroup(c) {
84
+ return (typeof c === 'object' &&
85
+ c !== null &&
86
+ (c.kind === 'not' || c.kind === 'and' || c.kind === 'or') &&
87
+ Array.isArray(c.conds));
88
+ }
89
+ /** Negation — the only way to invert a condition (e.g. !predicate). */
90
+ export function not(...conds) {
91
+ if (conds.length !== 1) {
92
+ throw new Error('not: takes exactly one condition');
93
+ }
94
+ return { kind: 'not', conds };
95
+ }
96
+ /** Conjunction — every sub-condition must hold. */
97
+ export function and(...conds) {
98
+ if (conds.length < 2) {
99
+ throw new Error('and: requires at least two conditions');
100
+ }
101
+ return { kind: 'and', conds };
102
+ }
103
+ /** Disjunction — at least one sub-condition holds. */
104
+ export function or(...conds) {
105
+ if (conds.length < 2) {
106
+ throw new Error('or: requires at least two conditions');
107
+ }
108
+ return { kind: 'or', conds };
109
+ }
83
110
  /** True when the condition operand is the slot itself (slot-level null check),
84
111
  * not a field access on it. Slot metadata (name) lives on the proxy target and
85
112
  * reads without field interception; a field access resolves to { slot, field }. */
86
113
  export function isFlowSlot(v) {
87
114
  return typeof v === 'object' && v !== null && typeof v.name === 'string';
88
115
  }
116
+ /** True when the slot carries a scalar Field (its declared type has a jsType)
117
+ * rather than a message — scalar slots support full comparisons
118
+ * (gt(slots.total, 100)); message slots only slot-level null checks. */
119
+ export function isScalarSlot(slot) {
120
+ const t = slot.type;
121
+ return typeof t === 'object' && t !== null && t.jsType !== undefined;
122
+ }
89
123
  function comparison(op, field, value) {
90
124
  if (isFlowSlot(field)) {
91
- if (op !== 'isNull' && op !== 'isNotNull') {
92
- throw new Error(`${op}: a slot-level check only supports isNull/isNotNull — field comparisons need slots.args.amt`);
125
+ const nullOp = op === 'isNull' || op === 'isNotNull';
126
+ if (nullOp) {
127
+ if (value !== undefined) {
128
+ throw new Error(`${op}: takes no value`);
129
+ }
93
130
  }
94
- if (value !== undefined) {
95
- throw new Error(`${op}: takes no value`);
131
+ else {
132
+ if (!isScalarSlot(field)) {
133
+ throw new Error(`${op}: a slot-level check only supports isNull/isNotNull — field comparisons need slots.args.amt`);
134
+ }
135
+ if (value === undefined) {
136
+ throw new Error(`${op}: requires a value`);
137
+ }
96
138
  }
97
139
  return { kind: 'comparison', op, field, value };
98
140
  }
@@ -495,18 +537,39 @@ function validateGuardChecks(schema) {
495
537
  }
496
538
  }
497
539
  // A condition must be a utils predicate call (boolean result, no result
498
- // slot) or a field comparison whose op is known and whose value presence and
499
- // type match the operator.
540
+ // slot), a field comparison whose op is known and whose value presence and
541
+ // type match the operator, or a composite (not/and/or) whose sub-conditions
542
+ // are each valid.
500
543
  const COMPARE_OPS = ['lt', 'le', 'gt', 'ge', 'eq', 'ne', 'isNull', 'isNotNull'];
501
544
  function validateCondition(where, c) {
545
+ if (isConditionGroup(c)) {
546
+ if (c.kind === 'not') {
547
+ if (c.conds.length !== 1) {
548
+ throw new Error(`${where}: not() takes exactly one condition`);
549
+ }
550
+ }
551
+ else if (c.conds.length < 2) {
552
+ throw new Error(`${where}: ${c.kind}() takes at least two conditions`);
553
+ }
554
+ for (const sub of c.conds)
555
+ validateCondition(where, sub);
556
+ return;
557
+ }
502
558
  if (!isCall(c)) {
503
559
  if (isFlowSlot(c.field)) {
504
560
  const nullOp = c.op === 'isNull' || c.op === 'isNotNull';
505
- if (!nullOp) {
506
- throw new Error(`${where}: a slot-level check only supports isNull/isNotNull`);
561
+ if (nullOp) {
562
+ if (c.value !== undefined) {
563
+ throw new Error(`${where}: ${c.op} takes no value`);
564
+ }
507
565
  }
508
- if (c.value !== undefined) {
509
- throw new Error(`${where}: ${c.op} takes no value`);
566
+ else {
567
+ if (!isScalarSlot(c.field)) {
568
+ throw new Error(`${where}: a slot-level check only supports isNull/isNotNull`);
569
+ }
570
+ if (c.value === undefined) {
571
+ throw new Error(`${where}: ${c.op} requires a value`);
572
+ }
510
573
  }
511
574
  return;
512
575
  }
@@ -540,10 +603,18 @@ function validateCondition(where, c) {
540
603
  if (c.result !== undefined) {
541
604
  throw new Error(`${where}: a predicate call cannot bind a result slot`);
542
605
  }
606
+ if (m.result === undefined || m.result.jsType !== 'boolean') {
607
+ const declared = m.result === undefined ? 'void' : m.result.jsType;
608
+ throw new Error(`${where}: utils predicate "${m.name}" must declare a boolean result (got ${declared}) — ` +
609
+ `predicates (can/is/has) return boolean; defense guards (assert/validate/ensure) throw internally and are invoked, not used in IF`);
610
+ }
543
611
  }
544
612
  /** Slots a condition reads: a comparison's field slot (or the slot itself for
545
- * slot-level checks) or a predicate call's arg slots. */
613
+ * slot-level checks), a predicate call's arg slots, or every sub-condition's
614
+ * slots for a composite. */
546
615
  function conditionSlots(c) {
616
+ if (isConditionGroup(c))
617
+ return c.conds.flatMap(conditionSlots);
547
618
  if (!isCall(c))
548
619
  return [isFlowSlot(c.field) ? c.field : c.field.slot];
549
620
  return c.args ?? [];
package/dist/index.d.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/dist/index.js 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';
@@ -0,0 +1,23 @@
1
+ import { SchemaBase } from './dsl.js';
2
+ import type { ActionSchema } from './action.js';
3
+ /** One business journey: a goal-directed line of actions. */
4
+ export interface JourneySchema extends SchemaBase {
5
+ /** Chinese title of the journey. */
6
+ title: string;
7
+ /** The business goal the journey achieves (what it is for). */
8
+ goal: string;
9
+ /** The running list of actions, in business order. */
10
+ actions: ActionSchema[];
11
+ }
12
+ /**
13
+ * Defines a business journey. `name` is kebab-case (e.g. 'merchant-onboarding');
14
+ * the export symbol is the kebab-camel of the name (merchantOnboarding).
15
+ * File name is the name plus '.journey.ts' (journey_schema/merchant-onboarding.journey.ts).
16
+ */
17
+ export declare function defineJourney(options: {
18
+ name: string;
19
+ title: string;
20
+ goal: string;
21
+ actions: ActionSchema[];
22
+ description?: string;
23
+ }): JourneySchema;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Defines a business journey. `name` is kebab-case (e.g. 'merchant-onboarding');
3
+ * the export symbol is the kebab-camel of the name (merchantOnboarding).
4
+ * File name is the name plus '.journey.ts' (journey_schema/merchant-onboarding.journey.ts).
5
+ */
6
+ export function defineJourney(options) {
7
+ if (!/^[a-z][a-z0-9-]*$/.test(options.name)) {
8
+ throw new Error(`journey ${options.name}: name must be kebab-case (lowercase letters/digits/dashes)`);
9
+ }
10
+ if (!options.title) {
11
+ throw new Error(`journey ${options.name}: title is required`);
12
+ }
13
+ if (!options.goal) {
14
+ throw new Error(`journey ${options.name}: goal is required`);
15
+ }
16
+ if (!options.actions || options.actions.length === 0) {
17
+ throw new Error(`journey ${options.name}: actions must be non-empty (a journey is a line of actions)`);
18
+ }
19
+ return {
20
+ name: options.name,
21
+ title: options.title,
22
+ goal: options.goal,
23
+ description: options.description,
24
+ actions: options.actions,
25
+ };
26
+ }
@@ -1,4 +1,4 @@
1
- import { isCall, isFlowSlot, methodOf } from './flow.js';
1
+ import { isCall, isConditionGroup, isFlowSlot, methodOf } from './flow.js';
2
2
  // Mermaid driver: converts a FlowSchema into a Mermaid flowchart (TD).
3
3
  // Node ids are hierarchical (n0, n0_0, n0_0_0, ...) so nested sub-flows stay
4
4
  // globally unique. A node with a sub-flow renders as a subgraph block whose
@@ -214,6 +214,11 @@ function renderDataLine(n, outgoing) {
214
214
  const addCondition = (c) => {
215
215
  if (c === undefined)
216
216
  return;
217
+ if (isConditionGroup(c)) {
218
+ for (const s of c.conds)
219
+ addCondition(s);
220
+ return;
221
+ }
217
222
  if (!isCall(c)) {
218
223
  reads.add(isFlowSlot(c.field) ? c.field.name : c.field.slot.name);
219
224
  return;
@@ -1,8 +1,8 @@
1
1
  import type { RouteDataSchema } from './route.js';
2
- import type { ActionSchema } from './action.js';
2
+ import type { PageActionSchema } from './page-action.js';
3
3
  import { PageSchema } from './page.js';
4
4
  /** Navigation primitives: front-end routing actions that are not provider calls. */
5
- export interface NavigationAction extends ActionSchema {
5
+ export interface NavigationAction extends PageActionSchema {
6
6
  type: 'navigation';
7
7
  method: 'back' | 'push' | 'popup' | 'home';
8
8
  /** Target page for push navigation. */
@@ -0,0 +1,39 @@
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
+ /** An action a user can perform on a page (e.g. submit, approve, reject).
6
+ * Subclasses use `type` as the discriminator. */
7
+ export interface PageActionSchema extends SchemaBase {
8
+ type: string;
9
+ }
10
+ export declare function definePageAction(name: string, description?: string): PageActionSchema;
11
+ /** Parameter data source for a call argument. */
12
+ export type DataRef = {
13
+ type: 'route';
14
+ key: string;
15
+ } | {
16
+ type: 'data';
17
+ key: string;
18
+ } | {
19
+ type: 'value';
20
+ value: unknown;
21
+ };
22
+ /** Create a route-parameter reference. */
23
+ export declare function route(key: string): DataRef;
24
+ /** Create a page-data reference. */
25
+ export declare function data(key: string): DataRef;
26
+ export interface CallAction extends PageActionSchema {
27
+ type: 'call';
28
+ func: ControllerMethodSchema;
29
+ args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>;
30
+ }
31
+ export declare function call(func: ControllerMethodSchema, args?: Record<string, DtoField | DtoMessage | DtoArrayField | DtoObjectField | RefSchema | string>): CallAction;
32
+ /** Assign a call's result to a page data field.
33
+ * React: setState({ [field]: await ... }). Mini-program: this.setData({ [field]: ... }). */
34
+ export interface SetDataAction extends PageActionSchema {
35
+ type: 'setData';
36
+ call: CallAction;
37
+ field: DtoField;
38
+ }
39
+ export declare function setData(call: CallAction, field: DtoField): SetDataAction;
@@ -0,0 +1,17 @@
1
+ export function definePageAction(name, description) {
2
+ return { name, description, type: 'gesture' };
3
+ }
4
+ /** Create a route-parameter reference. */
5
+ export function route(key) {
6
+ return { type: 'route', key };
7
+ }
8
+ /** Create a page-data reference. */
9
+ export function data(key) {
10
+ return { type: 'data', key };
11
+ }
12
+ export function call(func, args) {
13
+ return { name: func.name, type: 'call', func, args };
14
+ }
15
+ export function setData(call, field) {
16
+ return { name: 'setData', type: 'setData', call, field };
17
+ }
@@ -2,21 +2,21 @@ 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 { ActionSchema } from './action.js';
5
+ import type { PageActionSchema } from './page-action.js';
6
6
  export type { RouteDataSchema } from './route.js';
7
7
  export { defineRouteData } from './route.js';
8
8
  /** Page lifecycle events that trigger data loading. */
9
9
  export interface EventSchema extends SchemaBase {
10
10
  type: 'onLoad' | 'onShow' | 'onHide' | 'onPullDownRefresh' | 'onReachBottom';
11
- /** Actions that fire when this event occurs. */
12
- actions?: ActionSchema[];
11
+ /** Page actions that fire when this event occurs. */
12
+ actions?: PageActionSchema[];
13
13
  }
14
14
  export declare const events: {
15
- onLoad(actions?: ActionSchema[], description?: string): EventSchema;
16
- onShow(actions?: ActionSchema[], description?: string): EventSchema;
17
- onHide(actions?: ActionSchema[], description?: string): EventSchema;
18
- onPullDownRefresh(actions?: ActionSchema[], description?: string): EventSchema;
19
- onReachBottom(actions?: ActionSchema[], description?: string): EventSchema;
15
+ onLoad(actions?: PageActionSchema[], description?: string): EventSchema;
16
+ onShow(actions?: PageActionSchema[], description?: string): EventSchema;
17
+ onHide(actions?: PageActionSchema[], description?: string): EventSchema;
18
+ onPullDownRefresh(actions?: PageActionSchema[], description?: string): EventSchema;
19
+ onReachBottom(actions?: PageActionSchema[], description?: string): EventSchema;
20
20
  };
21
21
  /** Page data: a named collection of fields that defines the page's data shape. */
22
22
  export interface PageDataSchema extends CollectionSchemaBase {
@@ -30,7 +30,7 @@ export interface PageDef extends ImportableSchemaBase {
30
30
  /** Lifecycle events that trigger data loading. */
31
31
  events: EventSchema[];
32
32
  /** Page actions (provider calls, navigation, etc.). */
33
- actions: ActionSchema[];
33
+ actions: PageActionSchema[];
34
34
  /** UI component declarations that make up the page skeleton. */
35
35
  components: ComponentSchema[];
36
36
  /** Whether the driver should generate page-level loading / error / empty state wrappers. */
@@ -1,9 +1,9 @@
1
1
  import { SchemaBase } from './dsl.js';
2
- import type { ActionSchema } from './action.js';
2
+ import type { PageActionSchema } from './page-action.js';
3
3
  import { Page } from './page.js';
4
4
  export interface PageEdge extends SchemaBase {
5
- /** Trigger action; undefined = default path (success/normal). */
6
- when?: ActionSchema;
5
+ /** Trigger page action; undefined = default path (success/normal). */
6
+ when?: PageActionSchema;
7
7
  start: Page;
8
8
  end: Page;
9
9
  }
@@ -14,7 +14,7 @@ export interface PageFlow extends SchemaBase {
14
14
  pages: Page[];
15
15
  edges: PageEdge[];
16
16
  }
17
- export declare function pageEdge(start: Page, end: Page, when?: ActionSchema, description?: string): PageEdge;
17
+ export declare function pageEdge(start: Page, end: Page, when?: PageActionSchema, description?: string): PageEdge;
18
18
  export declare function definePageFlow(name: string, schema: {
19
19
  start: Page;
20
20
  pages: Page[];
package/dist/popup.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- import type { ActionSchema } from './action.js';
1
+ import type { PageActionSchema } from './page-action.js';
2
2
  import type { RefSchema } from './ref.js';
3
3
  /** Display a toast notification. React: toast/message UI. Mini-program: wx.showToast. */
4
- export interface ToastAction extends ActionSchema {
4
+ export interface ToastAction extends PageActionSchema {
5
5
  type: 'toast';
6
6
  message: string | RefSchema;
7
7
  icon?: 'success' | 'error' | 'loading' | 'none';
8
8
  }
9
9
  /** Display an alert dialog. React: modal. Mini-program: wx.showModal. */
10
- export interface AlertAction extends ActionSchema {
10
+ export interface AlertAction extends PageActionSchema {
11
11
  type: 'alert';
12
12
  title: string | RefSchema;
13
13
  content: string | RefSchema;
@@ -5,8 +5,8 @@ import type { DomainAggregate } from './aggregate.js';
5
5
  * exposes the three fixed operation skeletons (load / save / delete) that the
6
6
  * generator expands from the aggregate structure:
7
7
  *
8
- * save(order) = tx { rootDao.upsert + memberDao cascade by via FK }
9
- * load(id) = rootDao.get + memberDao by via FK
8
+ * save(order) = tx { rootDao.upsert + memberDao cascade by FK / extends }
9
+ * load(id) = rootDao.get + memberDao by FK / extends
10
10
  * delete(id) = tx { memberDao delete + rootDao delete }
11
11
  *
12
12
  * Callers face the domain concept (Order), never the tables. DAO stays
package/dist/task.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { CollectionSchemaBase } from './dsl.js';
2
+ export interface TaskSchema extends CollectionSchemaBase {
3
+ type: 'task';
4
+ /** Display label (Chinese) for the task. */
5
+ label: string;
6
+ /** Cron expression — the timer schedule. */
7
+ cron: string;
8
+ }
9
+ /**
10
+ * Defines a scheduled task. `name` must end with 'Task' (PascalCase,
11
+ * e.g. 'AutoRefundTask'); the export symbol equals the name. File name is the
12
+ * name minus the Task suffix, kebab-cased, plus '.task.ts'
13
+ * (AutoRefundTask → task_schema/auto-refund.task.ts).
14
+ */
15
+ export declare function defineTask(options: {
16
+ name: string;
17
+ label: string;
18
+ cron: string;
19
+ description?: string;
20
+ }): TaskSchema;
package/dist/task.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Defines a scheduled task. `name` must end with 'Task' (PascalCase,
3
+ * e.g. 'AutoRefundTask'); the export symbol equals the name. File name is the
4
+ * name minus the Task suffix, kebab-cased, plus '.task.ts'
5
+ * (AutoRefundTask → task_schema/auto-refund.task.ts).
6
+ */
7
+ export function defineTask(options) {
8
+ if (!/(Task)$/.test(options.name)) {
9
+ throw new Error(`task ${options.name}: name must end with 'Task' (PascalCase, e.g. 'AutoRefundTask')`);
10
+ }
11
+ if (!options.cron || options.cron.trim() === '') {
12
+ throw new Error(`task ${options.name}: cron is required (a task is a timer)`);
13
+ }
14
+ return {
15
+ type: 'task',
16
+ name: options.name,
17
+ label: options.label,
18
+ description: options.description,
19
+ cron: options.cron,
20
+ };
21
+ }
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { CollectionSchemaBase, Field, SchemaBase } from './dsl.js';
2
2
  import type { DtoField } from './dto.js';
3
+ import type { ExceptionSchema } from './exception.js';
3
4
  import type { FrontAppSchema, ProjectApiSchema } from './project.js';
4
5
  /** A utility method with a full signature. */
5
6
  export interface UtilsMethodSchema extends SchemaBase {
@@ -12,6 +13,12 @@ export interface UtilsMethodSchema extends SchemaBase {
12
13
  * computed amounts, ...). The method output is a fresh value, never a
13
14
  * shared table column. Omit for void methods (pure actions). */
14
15
  result?: Field;
16
+ /** Exceptions this method may throw — the failure contract of a defense
17
+ * guard (assert/validate/ensure: void, throws internally). Flows route
18
+ * invoked guards' throws into their escape set automatically, so a
19
+ * guard's throws must be declared by the calling service method (or
20
+ * caught in a TRY). Predicates (can/is/has, boolean) do not throw. */
21
+ throws?: ExceptionSchema[];
15
22
  }
16
23
  /** Method input for defineUtils: type/schema/name are set by the builder. */
17
24
  export type UtilsMethodDef = Omit<UtilsMethodSchema, 'type' | 'schema' | 'name'>;
package/dist/utils.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { assertNoInlineContainers } from './dto.js';
1
2
  export function defineUtils(options) {
2
3
  if (options.api !== undefined && options.app !== undefined && !options.api.apps.includes(options.app)) {
3
4
  throw new Error(`utils ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
@@ -12,6 +13,9 @@ export function defineUtils(options) {
12
13
  };
13
14
  for (const key of Object.keys(options.methods)) {
14
15
  const method = options.methods[key];
16
+ // Same rule as DTO fields: args must not nest inline containers — use a
17
+ // DTO field reference (args: { items: SubmitRequest.fields.items }).
18
+ assertNoInlineContainers(options.name, method.args);
15
19
  const methodSchema = {
16
20
  type: 'utilsMethod',
17
21
  name: key,
@@ -19,14 +23,18 @@ export function defineUtils(options) {
19
23
  schema,
20
24
  args: method.args,
21
25
  result: method.result,
26
+ throws: method.throws,
22
27
  };
23
28
  // Args: write back on the DtoField wrapper only (safe: wrappers are
24
- // created per method via dtoField(), never shared). Inline fields get
25
- // their underlying Field written back too; fields wrapping table columns
26
- // (domain rules) keep the shared column instance untouched — its
27
- // name/schema already point to the table.
29
+ // created per method via dtoField(), never shared). Shared instances
30
+ // DTO fields passed by reference (args: { items: OrderSubmitRequest.fields.items })
31
+ // and fields wrapping table columns (domain rules) stay untouched: the
32
+ // DTO owns its field name/schema, the shared column instance keeps its
33
+ // table identity.
28
34
  for (const argKey of Object.keys(methodSchema.args)) {
29
35
  const df = methodSchema.args[argKey];
36
+ if (df.schema !== undefined)
37
+ continue;
30
38
  df.name = argKey;
31
39
  df.schema = schema;
32
40
  if (df.field.schema === undefined) {