@pylonts/dsl 1.1.6 → 1.1.12

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 (88) hide show
  1. package/README.md +4 -0
  2. package/dist/action.d.ts +32 -0
  3. package/dist/action.js +14 -0
  4. package/dist/aggregate.d.ts +38 -0
  5. package/dist/aggregate.js +46 -0
  6. package/dist/business-flow.d.ts +9 -0
  7. package/dist/business-flow.js +72 -0
  8. package/dist/controller.d.ts +17 -9
  9. package/dist/controller.js +8 -2
  10. package/dist/convert.d.ts +28 -10
  11. package/dist/convert.js +16 -5
  12. package/dist/curd.d.ts +7 -10
  13. package/dist/curd.js +3 -1
  14. package/dist/dao.d.ts +81 -53
  15. package/dist/dao.js +291 -12
  16. package/dist/db.d.ts +6 -0
  17. package/dist/db.js +10 -0
  18. package/dist/domain-event.d.ts +48 -0
  19. package/dist/domain-event.js +24 -0
  20. package/dist/dsl.d.ts +17 -2
  21. package/dist/dsl.js +7 -0
  22. package/dist/dto.d.ts +6 -4
  23. package/dist/dto.js +5 -4
  24. package/dist/entity.d.ts +29 -0
  25. package/dist/entity.js +13 -0
  26. package/dist/exception.d.ts +9 -3
  27. package/dist/exception.js +25 -1
  28. package/dist/expr.d.ts +45 -0
  29. package/dist/expr.js +32 -0
  30. package/dist/filter.d.ts +45 -0
  31. package/dist/filter.js +21 -0
  32. package/dist/flow-script.d.ts +108 -0
  33. package/dist/flow-script.js +505 -0
  34. package/dist/flow.d.ts +294 -17
  35. package/dist/flow.js +803 -18
  36. package/dist/index.d.ts +6 -2
  37. package/dist/index.js +6 -2
  38. package/dist/mermaid-driver.js +264 -24
  39. package/dist/mysql-driver.js +3 -0
  40. package/dist/project.d.ts +10 -6
  41. package/dist/project.js +35 -4
  42. package/dist/repository.d.ts +26 -0
  43. package/dist/repository.js +8 -0
  44. package/dist/service.d.ts +14 -2
  45. package/dist/service.js +49 -0
  46. package/dist/third-service.d.ts +5 -0
  47. package/dist/third-service.js +1 -0
  48. package/dist/typebox-driver.js +4 -0
  49. package/dist/utils.d.ts +9 -2
  50. package/dist/utils.js +4 -0
  51. package/docs/aggregate.md +110 -0
  52. package/docs/curd.md +146 -111
  53. package/docs/dao-generation.md +478 -0
  54. package/docs/ddd-principles.md +75 -0
  55. package/docs/domain-event.md +137 -0
  56. package/docs/keyword-matcher.md +182 -0
  57. package/docs/project.md +17 -9
  58. package/docs/token.md +327 -0
  59. package/docs/trans-reentrant.md +85 -0
  60. package/package.json +25 -6
  61. package/src/action.ts +51 -10
  62. package/src/aggregate.ts +104 -0
  63. package/src/business-flow.ts +80 -0
  64. package/src/controller.ts +25 -11
  65. package/src/convert.ts +51 -15
  66. package/src/curd.ts +12 -6
  67. package/src/dao.ts +377 -63
  68. package/src/db.ts +13 -0
  69. package/src/domain-event.ts +74 -0
  70. package/src/dsl.ts +23 -2
  71. package/src/dto.ts +9 -6
  72. package/src/entity.ts +43 -0
  73. package/src/exception.ts +30 -5
  74. package/src/expr.ts +65 -0
  75. package/src/filter.ts +70 -0
  76. package/src/flow-script.ts +696 -0
  77. package/src/flow.ts +1129 -46
  78. package/src/index.ts +6 -2
  79. package/src/mermaid-driver.ts +256 -29
  80. package/src/mysql-driver.ts +3 -0
  81. package/src/project.ts +138 -97
  82. package/src/repository.ts +35 -0
  83. package/src/service.ts +68 -3
  84. package/src/third-service.ts +6 -0
  85. package/src/typebox-driver.ts +4 -0
  86. package/src/utils.ts +13 -2
  87. package/src/endpoint.ts +0 -18
  88. package/src/provider.ts +0 -68
@@ -0,0 +1,80 @@
1
+ // business-flow: the minimal form of a flow — a chart. Steps are [from, when,
2
+ // to] tuples (the condition sits between source and target; a two-element
3
+ // [from, to] step has no label); nodes are collected by name from the edges
4
+ // (a name shared by several steps is one node); the first step's from is the
5
+ // start; every node with no outgoing edge falls through to the flow's return
6
+ // end. A node with several outgoing edges is a decision — the mermaid driver
7
+ // draws it as a diamond, and every branch edge must carry a when label or the
8
+ // chart cannot be read (nor later upgraded to machine conditions).
9
+ //
10
+ // The chart compiles to the same graph IR as flow-script: mermaid rendering,
11
+ // reachability checks, and future consumers (an executor) all work unchanged.
12
+ // A chart is the skeleton — upgrade a decision to IF(cond) and a node to
13
+ // invoke(...) when the business logic arrives.
14
+
15
+ import { defineFlow, edge, node } from './flow.js';
16
+ import type { FlowEdge, FlowNode, FlowSchema, FlowStep } from './flow.js';
17
+
18
+ /** One chart step: from → to, the optional branch label between them
19
+ * (mandatory for decision branches). */
20
+ export type ChartStep = [from: string, to: string] | [from: string, when: string, to: string];
21
+
22
+ /** Compile a tuple chart into a FlowSchema. */
23
+ export function flowChart(
24
+ name: string,
25
+ steps: ChartStep[],
26
+ options: { description?: string; start?: string } = {},
27
+ ): FlowSchema {
28
+ if (steps.length === 0) {
29
+ throw new Error(`business-flow ${name}: at least one step is required`);
30
+ }
31
+ for (const step of steps) {
32
+ const from = step[0];
33
+ const to = step.length === 3 ? step[2] : step[1];
34
+ if (from === '' || to === '') {
35
+ throw new Error(`business-flow ${name}: node names must not be empty`);
36
+ }
37
+ }
38
+ if (options.start !== undefined && !steps.some((step) => step[0] === options.start)) {
39
+ throw new Error(`business-flow ${name}: start "${options.start}" is not the source of any step`);
40
+ }
41
+ const nodes = new Map<string, FlowNode>();
42
+ const byName = (n: string): FlowNode => {
43
+ let found = nodes.get(n);
44
+ if (found === undefined) {
45
+ found = node(n, {});
46
+ nodes.set(n, found);
47
+ }
48
+ return found;
49
+ };
50
+ const edges: FlowEdge[] = steps.map((step) => {
51
+ const from = step[0];
52
+ const to = step.length === 3 ? step[2] : step[1];
53
+ const when = step.length === 3 ? step[1] : undefined;
54
+ return edge(byName(from), byName(to), { when });
55
+ });
56
+ // Decision rule: every branch of a multi-outgoing node must carry a label.
57
+ const outgoing = new Map<FlowStep, FlowEdge[]>();
58
+ for (const e of edges) {
59
+ const list = outgoing.get(e.start);
60
+ if (list === undefined) outgoing.set(e.start, [e]);
61
+ else list.push(e);
62
+ }
63
+ for (const [n, list] of outgoing) {
64
+ if (list.length > 1 && list.some((e) => e.when === undefined)) {
65
+ throw new Error(`business-flow ${name}: decision node "${n.name}" — every outgoing branch edge needs a when label`);
66
+ }
67
+ }
68
+ return defineFlow(name, {
69
+ start: byName(options.start ?? steps[0][0]),
70
+ description: options.description,
71
+ edges: (flow) => {
72
+ // Every path ends somewhere: nodes with no outgoing edge return.
73
+ const sinks: FlowEdge[] = [];
74
+ for (const n of nodes.values()) {
75
+ if (!outgoing.has(n)) sinks.push(edge(n, flow.returnEnd));
76
+ }
77
+ return [...edges, ...sinks];
78
+ },
79
+ });
80
+ }
package/src/controller.ts CHANGED
@@ -1,40 +1,54 @@
1
1
  import { SchemaBase } from './dsl.js';
2
- import { FrontAppSchema } from './project.js';
3
- import type { EndpointSchema } from './endpoint.js';
2
+ import { FrontAppSchema, ProjectApiSchema } from './project.js';
3
+ import type { DtoMessage } from './dto.js';
4
4
 
5
5
  /** A backend RPC controller. Strong constraints:
6
6
  * - a backend module maps 1:1 to a frontend app (they are peers);
7
7
  * - a controller serves exactly one frontend app — no cross-module calls. */
8
8
  export interface ControllerSchema extends SchemaBase {
9
9
  type: 'controller';
10
+ /** The backend api module this controller belongs to (shared instance from
11
+ * project.config.ts apis). Controllers are always backend-side, so storage
12
+ * is controller_schema/{api.name}/{app.name}/controller/. */
13
+ api: ProjectApiSchema;
10
14
  /** The frontend app this controller serves (shared instance from project.config). */
11
15
  app: FrontAppSchema;
12
- /** RPC methods exposed by this controller. */
13
- methods: ControllerMethodSchema[];
16
+ /** RPC methods, keyed by method name (key === method.name, enforced by the builder). */
17
+ methods: Record<string, ControllerMethodSchema>;
14
18
  }
15
19
 
16
20
  export function defineController(options: {
17
21
  name: string;
22
+ api: ProjectApiSchema;
18
23
  app: FrontAppSchema;
19
- /** Method declarations: type/schema are injected by this builder. */
20
- methods: Array<Omit<ControllerMethodSchema, 'type' | 'schema'>>;
24
+ /** Method declarations: type/schema/name are injected by this builder. */
25
+ methods: Record<string, Omit<ControllerMethodSchema, 'type' | 'schema' | 'name'>>;
21
26
  description?: string;
22
27
  }): ControllerSchema {
28
+ if (!options.api.apps.includes(options.app)) {
29
+ throw new Error(`controller ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
30
+ }
23
31
  const schema: ControllerSchema = {
24
32
  type: 'controller',
25
33
  name: options.name,
26
34
  description: options.description,
35
+ api: options.api,
27
36
  app: options.app,
28
- methods: [],
37
+ methods: {},
29
38
  };
30
- schema.methods = options.methods.map((method) => ({ type: 'method', schema, ...method }));
39
+ for (const [key, method] of Object.entries(options.methods)) {
40
+ schema.methods[key] = { type: 'method', schema, name: key, ...method };
41
+ }
31
42
  return schema;
32
43
  }
33
44
 
34
- /** An RPC method exposed by a controller. */
45
+ /** An RPC method exposed by a controller. Carries the shared API call
46
+ * signature: one request DTO in, one response shape out. Referenced by
47
+ * frontend page actions — both sides use the exact same instance, so
48
+ * drift is impossible. */
35
49
  export interface ControllerMethodSchema extends SchemaBase {
36
50
  type: 'method';
37
51
  schema: ControllerSchema;
38
- /** Shared API signature — same instance the page-side provider references. */
39
- signature: EndpointSchema;
52
+ args: DtoMessage;
53
+ results: DtoMessage | number | boolean | string;
40
54
  }
package/src/convert.ts CHANGED
@@ -1,43 +1,79 @@
1
1
  import type { SchemaBase } from './dsl.js';
2
2
  import type { DtoMessage } from './dto.js';
3
3
  import type { TableSchema } from './db.js';
4
+ import type { EntitySchema } from './entity.js';
4
5
  import type { ThirdMethodSchema } from './third-service.js';
5
- import type { FrontAppSchema } from './project.js';
6
+ import type { FrontAppSchema, ProjectApiSchema } from './project.js';
6
7
 
7
8
  // Schema-collection integration: multiple source collections combine into
8
9
  // one target collection (e.g. two entity tables into one dto, or entity
9
10
  // columns plus a dto into one third-party wire message).
11
+ //
12
+ // A convert file binds to ONE source identity — a table (internal mapping,
13
+ // {Table}Convert.ts) or a third-party service (anti-corruption translation,
14
+ // {third-service}.convert.ts) — and holds N methods keyed by name (same
15
+ // shape as defineService / defineDao).
10
16
 
11
- /** A source/target collection of a convert — dto, entity or third-party message. */
12
- export type ConvertSourceSchema = DtoMessage | TableSchema | ThirdMethodSchema;
17
+ /** A source/target collection of a convert — dto, entity, table or
18
+ * third-party message. Entity sources may carry aggregate fields
19
+ * (aggField), e.g. an aggregate result entity projected into a wire
20
+ * message. */
21
+ export type ConvertSourceSchema = DtoMessage | TableSchema | ThirdMethodSchema | EntitySchema;
13
22
 
14
- /** Declares a multi-source single-target schema integration. */
15
- export interface ConvertSchema extends SchemaBase {
16
- type: 'convert';
17
- /** The app (module) this convert belongs to its artifact lands in modules/{app}/convert/. */
18
- app: FrontAppSchema;
23
+ /** Method input for defineConvert: type/schema/name are set by the builder. */
24
+ export type ConvertMethodDef = Omit<ConvertMethodSchema, 'type' | 'schema' | 'name'>;
25
+
26
+ /** One conversion: multiple source collections single target collection. */
27
+ export interface ConvertMethodSchema extends SchemaBase {
28
+ type: 'convertMethod';
29
+ /** The convert file this method belongs to. */
30
+ schema: ConvertSchema;
19
31
  /** Source schemas — one or more, mixed dimensions. */
20
32
  sources: ConvertSourceSchema[];
21
33
  /** Target schema — the single integrated collection. */
22
34
  target: ConvertSourceSchema;
23
35
  }
24
36
 
37
+ /** Declares multi-source → single-target schema integrations grouped by source identity. */
38
+ export interface ConvertSchema extends SchemaBase {
39
+ type: 'convert';
40
+ /** The backend api module this convert belongs to (shared instance from
41
+ * project.config.ts apis). Storage is convert_schema/{api.name}/{app.name}/
42
+ * — same layout as service_schema. */
43
+ api: ProjectApiSchema;
44
+ /** The app (module) this convert belongs to — its artifact lands in modules/{app}/convert/. */
45
+ app: FrontAppSchema;
46
+ /** Methods keyed by name — the map key is written back as the method name. */
47
+ methods: Record<string, ConvertMethodSchema>;
48
+ }
49
+
25
50
  export function defineConvert(options: {
26
51
  name: string;
52
+ api: ProjectApiSchema;
27
53
  app: FrontAppSchema;
28
- sources: ConvertSourceSchema[];
29
- target: ConvertSourceSchema;
54
+ methods: Record<string, ConvertMethodDef>;
30
55
  description?: string;
31
56
  }): ConvertSchema {
32
- if (options.sources.length === 0) {
33
- throw new Error(`convert '${options.name}': sources must not be empty`);
57
+ if (Object.keys(options.methods).length === 0) {
58
+ throw new Error(`convert '${options.name}': methods must not be empty`);
59
+ }
60
+ if (!options.api.apps.includes(options.app)) {
61
+ throw new Error(`convert '${options.name}': api '${options.api.name}' does not serve app '${options.app.name}'`);
34
62
  }
35
- return {
63
+ const schema: ConvertSchema = {
36
64
  type: 'convert',
37
65
  name: options.name,
38
66
  description: options.description,
67
+ api: options.api,
39
68
  app: options.app,
40
- sources: options.sources,
41
- target: options.target,
69
+ methods: {},
42
70
  };
71
+ for (const key of Object.keys(options.methods)) {
72
+ const method = options.methods[key] as ConvertMethodDef;
73
+ if (method.sources.length === 0) {
74
+ throw new Error(`convert '${options.name}': method '${key}' sources must not be empty`);
75
+ }
76
+ schema.methods[key] = { type: 'convertMethod', schema, ...method, name: key };
77
+ }
78
+ return schema;
43
79
  }
package/src/curd.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { SchemaBase, Field, Operator } from './dsl.js';
1
+ import { SchemaBase, Field } from './dsl.js';
2
2
  import { TableSchema } from './db.js';
3
3
  import { FrontAppSchema } from './project.js';
4
4
  import { ActionSchema } from './action.js';
5
+ import type { FilterSchema } from './filter.js';
5
6
  import { toKebabCase } from '@pylonts/core';
6
7
 
7
8
  // Admin-only CRUD page standard: binds one entity table to a frontend admin
@@ -26,12 +27,13 @@ export interface CurdListConfig {
26
27
  /** List columns; required, non-empty. Every field the list shows must be
27
28
  * listed explicitly. May include cross-table fields via foreign refs. */
28
29
  columns: Field[];
29
- /** Fuzzy keyword search on this table's columns. */
30
- keyword?: { columns: Field[] };
30
+ /** Page filter (search form + keyword search). Conditions declared on the
31
+ * filter render the search form; the filter's keyword (when present)
32
+ * drives the keyword query endpoint. Optional — a page without a filter
33
+ * has no search form. */
34
+ filter?: FilterSchema;
31
35
  /** Default sort. Required — column and direction are both mandatory. */
32
36
  orderBy: { column: Field; direction: 'asc' | 'desc' };
33
- /** Search condition fields; op defaults to 'eq'. */
34
- searchFields?: { field: Field; op?: Operator }[];
35
37
  /** Column header text overrides: Field.name → header text. */
36
38
  columnTitles?: Record<string, string>;
37
39
  }
@@ -93,7 +95,11 @@ export function defineCurd(name: string, schema: Omit<CurdSchema, 'name'>): Curd
93
95
  for (const [pageName, page] of Object.entries(curd.actionPages ?? {})) {
94
96
  if (page) assertColumns(curd, `actionPages.${pageName}`, page.columns);
95
97
  }
96
- assertFieldsOwnTable(curd, 'keyword', curd.list.keyword?.columns ?? []);
98
+ if (curd.list.filter !== undefined && curd.list.filter.app !== curd.app) {
99
+ throw new Error(
100
+ `curd ${name}: list.filter '${curd.list.filter.name}' is bound to app '${curd.list.filter.app.name}' but the curd belongs to app '${curd.app.name}'`,
101
+ );
102
+ }
97
103
  assertFieldsOwnTable(curd, 'orderBy', [curd.list.orderBy.column]);
98
104
  return curd;
99
105
  }