@pylonts/dsl 1.1.5 → 1.1.11

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 (100) 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 +35 -0
  9. package/dist/controller.js +17 -0
  10. package/dist/convert.d.ts +37 -7
  11. package/dist/convert.js +23 -2
  12. package/dist/curd.d.ts +7 -12
  13. package/dist/curd.js +10 -1
  14. package/dist/dao.d.ts +140 -3
  15. package/dist/dao.js +307 -2
  16. package/dist/db.d.ts +6 -0
  17. package/dist/db.js +13 -0
  18. package/dist/domain-event.d.ts +48 -0
  19. package/dist/domain-event.js +24 -0
  20. package/dist/dsl.d.ts +32 -2
  21. package/dist/dsl.js +13 -0
  22. package/dist/dto.d.ts +8 -2
  23. package/dist/dto.js +21 -9
  24. package/dist/endpoint.d.ts +15 -0
  25. package/dist/endpoint.js +3 -0
  26. package/dist/entity.d.ts +29 -0
  27. package/dist/entity.js +13 -0
  28. package/dist/exception.d.ts +20 -0
  29. package/dist/exception.js +33 -0
  30. package/dist/expr.d.ts +45 -0
  31. package/dist/expr.js +32 -0
  32. package/dist/field-rule.d.ts +20 -0
  33. package/dist/field-rule.js +19 -0
  34. package/dist/filter.d.ts +45 -0
  35. package/dist/filter.js +21 -0
  36. package/dist/flow-script.d.ts +108 -0
  37. package/dist/flow-script.js +505 -0
  38. package/dist/flow.d.ts +309 -10
  39. package/dist/flow.js +819 -22
  40. package/dist/index.d.ts +12 -1
  41. package/dist/index.js +14 -1
  42. package/dist/mermaid-driver.js +278 -9
  43. package/dist/method.d.ts +11 -0
  44. package/dist/method.js +3 -0
  45. package/dist/mysql-driver.js +7 -0
  46. package/dist/project.d.ts +5 -4
  47. package/dist/project.js +14 -2
  48. package/dist/provider.d.ts +6 -11
  49. package/dist/provider.js +2 -2
  50. package/dist/repository.d.ts +26 -0
  51. package/dist/repository.js +8 -0
  52. package/dist/service.d.ts +30 -8
  53. package/dist/service.js +62 -2
  54. package/dist/third-service.d.ts +80 -0
  55. package/dist/third-service.js +97 -0
  56. package/dist/typebox-driver.d.ts +6 -0
  57. package/dist/typebox-driver.js +77 -12
  58. package/dist/utils.d.ts +32 -10
  59. package/dist/utils.js +32 -11
  60. package/docs/aggregate.md +110 -0
  61. package/docs/dao-generation.md +478 -0
  62. package/docs/ddd-principles.md +75 -0
  63. package/docs/domain-event.md +137 -0
  64. package/docs/dto.md +73 -66
  65. package/docs/keyword-matcher.md +182 -0
  66. package/docs/third-service.md +122 -0
  67. package/docs/token.md +327 -0
  68. package/docs/trans-reentrant.md +85 -0
  69. package/package.json +27 -5
  70. package/src/action.ts +51 -10
  71. package/src/aggregate.ts +104 -0
  72. package/src/business-flow.ts +80 -0
  73. package/src/controller.ts +54 -0
  74. package/src/convert.ts +78 -15
  75. package/src/curd.ts +104 -93
  76. package/src/dao.ts +486 -13
  77. package/src/db.ts +199 -181
  78. package/src/domain-event.ts +74 -0
  79. package/src/dsl.ts +48 -2
  80. package/src/dto.ts +266 -247
  81. package/src/entity.ts +43 -0
  82. package/src/exception.ts +53 -0
  83. package/src/expr.ts +65 -0
  84. package/src/field-rule.ts +47 -0
  85. package/src/filter.ts +70 -0
  86. package/src/flow-script.ts +696 -0
  87. package/src/flow.ts +1226 -103
  88. package/src/index.ts +47 -33
  89. package/src/mermaid-driver.ts +339 -84
  90. package/src/method.ts +20 -0
  91. package/src/mysql-driver.ts +7 -0
  92. package/src/project.ts +114 -97
  93. package/src/repository.ts +35 -0
  94. package/src/service.ts +107 -20
  95. package/src/third-service.ts +192 -0
  96. package/src/typebox-driver.ts +86 -11
  97. package/src/utils.ts +74 -26
  98. package/dist/check-inheritance.d.ts +0 -9
  99. package/dist/check-inheritance.js +0 -58
  100. package/src/provider.ts +0 -73
package/README.md CHANGED
@@ -22,6 +22,10 @@ project(地图:应用与 API 拓扑)
22
22
  - [table.md](./docs/table.md) — 定义表(详细设计)
23
23
  - [dto.md](./docs/dto.md) — 定义 DTO
24
24
  - [curd.md](./docs/curd.md) — 管理端 CRUD 页面标准(CurdSchema)
25
+ - [aggregate.md](./docs/aggregate.md) — 聚合/仓储 DSL 扩展规划(声明层已落地:DomainAggregate/RepositorySchema + 存储校验,生成器待选型)
26
+ - [domain-event.md](./docs/domain-event.md) — 领域事件 DSL 扩展规划(声明层已落地:DomainEventSchema + event_schema 存储校验,发布/订阅待实现)
27
+ - [ddd-principles.md](./docs/ddd-principles.md) — DDD 落地原则(已决策)
28
+ - [trans-reentrant.md](./docs/trans-reentrant.md) — @Trans 可重入改造(已实现)
25
29
  - [enum.md](./docs/enum.md) — 定义枚举
26
30
  - [driver.md](./docs/driver.md) — driver 模式与产物生成
27
31
  - [mysql-connection.md](./docs/mysql-connection.md) — MySQL 连接说明
package/dist/action.d.ts CHANGED
@@ -1,7 +1,39 @@
1
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';
2
5
  /** An action a user can perform on a page (e.g. submit, approve, reject).
3
6
  * Subclasses use `type` as the discriminator. */
4
7
  export interface ActionSchema extends SchemaBase {
5
8
  type: string;
6
9
  }
7
10
  export declare function defineAction(name: string, description?: string): ActionSchema;
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 ActionSchema {
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 ActionSchema {
35
+ type: 'setData';
36
+ call: CallAction;
37
+ field: DtoField;
38
+ }
39
+ export declare function setData(call: CallAction, field: DtoField): SetDataAction;
package/dist/action.js CHANGED
@@ -1,3 +1,17 @@
1
1
  export function defineAction(name, description) {
2
2
  return { name, description, type: 'gesture' };
3
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
+ }
@@ -0,0 +1,38 @@
1
+ import type { SchemaBase } from './dsl.js';
2
+ import type { TableSchema, ForeignKey } from './db.js';
3
+ /** How a member table attaches to the aggregate root. */
4
+ export interface AggregateMember {
5
+ /** The member table (shared instance from schema/*.table.ts). */
6
+ table: TableSchema;
7
+ /** The foreign key on the member table pointing to the root table.
8
+ * Must exist in table.foreignKeys and its references must be the root's PK
9
+ * columns. Defaults to the (unique) FK referencing the root table. */
10
+ via?: ForeignKey;
11
+ /** 1:1 member (unique constraint on via.columns) vs 1:N (default). */
12
+ one?: boolean;
13
+ }
14
+ /** A cross-member invariant, checked by generated repository code. */
15
+ export interface AggregateInvariant {
16
+ name: string;
17
+ /** Expression in the aggregate's field vocabulary (e.g. 'total == sum(items.price * items.qty)'). */
18
+ check: string;
19
+ }
20
+ export interface DomainAggregate extends SchemaBase {
21
+ type: 'aggregate';
22
+ /** The aggregate root table. */
23
+ root: TableSchema;
24
+ /** Member tables keyed by role name (e.g. 'items', 'address'). */
25
+ members: Record<string, AggregateMember>;
26
+ /** Cross-member invariants; optional. */
27
+ invariants?: AggregateInvariant[];
28
+ /** Inter-aggregate references: only by root ID, keyed by referenced role. */
29
+ references?: Record<string, string>;
30
+ }
31
+ export declare function defineAggregate(options: {
32
+ name: string;
33
+ root: TableSchema;
34
+ members?: Record<string, AggregateMember>;
35
+ invariants?: AggregateInvariant[];
36
+ references?: Record<string, string>;
37
+ description?: string;
38
+ }): DomainAggregate;
@@ -0,0 +1,46 @@
1
+ export function defineAggregate(options) {
2
+ const schema = {
3
+ type: 'aggregate',
4
+ name: options.name,
5
+ description: options.description,
6
+ root: options.root,
7
+ members: options.members ?? {},
8
+ invariants: options.invariants,
9
+ references: options.references,
10
+ };
11
+ // Root must have a primary key (aggregate identity).
12
+ if (options.root.primaryKey === undefined) {
13
+ throw new Error(`aggregate '${options.name}': root table '${options.root.name}' must have a primary key`);
14
+ }
15
+ // Each member must attach to the root via an existing FK referencing the root.
16
+ const rootPkRefs = Array.isArray(options.root.primaryKey)
17
+ ? options.root.primaryKey
18
+ : [options.root.primaryKey];
19
+ for (const [role, member] of Object.entries(schema.members)) {
20
+ const fks = Object.values(member.table.foreignKeys ?? {}).filter((fk) => {
21
+ const refs = Array.isArray(fk.references) ? fk.references : [fk.references];
22
+ return refs.every((r) => rootPkRefs.includes(r)) && refs.length === rootPkRefs.length;
23
+ });
24
+ if (fks.length === 0) {
25
+ throw new Error(`aggregate '${options.name}': member '${role}' table '${member.table.name}' has no foreign key referencing root '${options.root.name}' — declare one in the table's foreignKeys`);
26
+ }
27
+ if (member.via !== undefined) {
28
+ const viaKeys = Object.values(member.table.foreignKeys ?? {});
29
+ if (!viaKeys.includes(member.via)) {
30
+ throw new Error(`aggregate '${options.name}': member '${role}' via must be one of table '${member.table.name}' foreignKeys — got a non-FK object`);
31
+ }
32
+ const refs = Array.isArray(member.via.references) ? member.via.references : [member.via.references];
33
+ if (!(refs.length === rootPkRefs.length && refs.every((r) => rootPkRefs.includes(r)))) {
34
+ throw new Error(`aggregate '${options.name}': member '${role}' via must reference root '${options.root.name}' primary key columns`);
35
+ }
36
+ }
37
+ else {
38
+ // Default: the (single) FK referencing the root. More than one → must declare via.
39
+ if (fks.length > 1) {
40
+ throw new Error(`aggregate '${options.name}': member '${role}' table '${member.table.name}' has ${fks.length} foreign keys referencing root '${options.root.name}' — declare via explicitly`);
41
+ }
42
+ member.via = fks[0];
43
+ }
44
+ }
45
+ return schema;
46
+ }
@@ -0,0 +1,9 @@
1
+ import type { FlowSchema } from './flow.js';
2
+ /** One chart step: from → to, the optional branch label between them
3
+ * (mandatory for decision branches). */
4
+ export type ChartStep = [from: string, to: string] | [from: string, when: string, to: string];
5
+ /** Compile a tuple chart into a FlowSchema. */
6
+ export declare function flowChart(name: string, steps: ChartStep[], options?: {
7
+ description?: string;
8
+ start?: string;
9
+ }): FlowSchema;
@@ -0,0 +1,72 @@
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
+ import { defineFlow, edge, node } from './flow.js';
15
+ /** Compile a tuple chart into a FlowSchema. */
16
+ export function flowChart(name, steps, options = {}) {
17
+ if (steps.length === 0) {
18
+ throw new Error(`business-flow ${name}: at least one step is required`);
19
+ }
20
+ for (const step of steps) {
21
+ const from = step[0];
22
+ const to = step.length === 3 ? step[2] : step[1];
23
+ if (from === '' || to === '') {
24
+ throw new Error(`business-flow ${name}: node names must not be empty`);
25
+ }
26
+ }
27
+ if (options.start !== undefined && !steps.some((step) => step[0] === options.start)) {
28
+ throw new Error(`business-flow ${name}: start "${options.start}" is not the source of any step`);
29
+ }
30
+ const nodes = new Map();
31
+ const byName = (n) => {
32
+ let found = nodes.get(n);
33
+ if (found === undefined) {
34
+ found = node(n, {});
35
+ nodes.set(n, found);
36
+ }
37
+ return found;
38
+ };
39
+ const edges = steps.map((step) => {
40
+ const from = step[0];
41
+ const to = step.length === 3 ? step[2] : step[1];
42
+ const when = step.length === 3 ? step[1] : undefined;
43
+ return edge(byName(from), byName(to), { when });
44
+ });
45
+ // Decision rule: every branch of a multi-outgoing node must carry a label.
46
+ const outgoing = new Map();
47
+ for (const e of edges) {
48
+ const list = outgoing.get(e.start);
49
+ if (list === undefined)
50
+ outgoing.set(e.start, [e]);
51
+ else
52
+ list.push(e);
53
+ }
54
+ for (const [n, list] of outgoing) {
55
+ if (list.length > 1 && list.some((e) => e.when === undefined)) {
56
+ throw new Error(`business-flow ${name}: decision node "${n.name}" — every outgoing branch edge needs a when label`);
57
+ }
58
+ }
59
+ return defineFlow(name, {
60
+ start: byName(options.start ?? steps[0][0]),
61
+ description: options.description,
62
+ edges: (flow) => {
63
+ // Every path ends somewhere: nodes with no outgoing edge return.
64
+ const sinks = [];
65
+ for (const n of nodes.values()) {
66
+ if (!outgoing.has(n))
67
+ sinks.push(edge(n, flow.returnEnd));
68
+ }
69
+ return [...edges, ...sinks];
70
+ },
71
+ });
72
+ }
@@ -0,0 +1,35 @@
1
+ import { SchemaBase } from './dsl.js';
2
+ import { FrontAppSchema, ProjectApiSchema } from './project.js';
3
+ import type { DtoMessage } from './dto.js';
4
+ /** A backend RPC controller. Strong constraints:
5
+ * - a backend module maps 1:1 to a frontend app (they are peers);
6
+ * - a controller serves exactly one frontend app — no cross-module calls. */
7
+ export interface ControllerSchema extends SchemaBase {
8
+ type: 'controller';
9
+ /** The backend api module this controller belongs to (shared instance from
10
+ * project.config.ts apis). Controllers are always backend-side, so storage
11
+ * is controller_schema/{api.name}/{app.name}/controller/. */
12
+ api: ProjectApiSchema;
13
+ /** The frontend app this controller serves (shared instance from project.config). */
14
+ app: FrontAppSchema;
15
+ /** RPC methods, keyed by method name (key === method.name, enforced by the builder). */
16
+ methods: Record<string, ControllerMethodSchema>;
17
+ }
18
+ export declare function defineController(options: {
19
+ name: string;
20
+ api: ProjectApiSchema;
21
+ app: FrontAppSchema;
22
+ /** Method declarations: type/schema/name are injected by this builder. */
23
+ methods: Record<string, Omit<ControllerMethodSchema, 'type' | 'schema' | 'name'>>;
24
+ description?: string;
25
+ }): ControllerSchema;
26
+ /** An RPC method exposed by a controller. Carries the shared API call
27
+ * signature: one request DTO in, one response shape out. Referenced by
28
+ * frontend page actions — both sides use the exact same instance, so
29
+ * drift is impossible. */
30
+ export interface ControllerMethodSchema extends SchemaBase {
31
+ type: 'method';
32
+ schema: ControllerSchema;
33
+ args: DtoMessage;
34
+ results: DtoMessage | number | boolean | string;
35
+ }
@@ -0,0 +1,17 @@
1
+ export function defineController(options) {
2
+ if (!options.api.apps.includes(options.app)) {
3
+ throw new Error(`controller ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
4
+ }
5
+ const schema = {
6
+ type: 'controller',
7
+ name: options.name,
8
+ description: options.description,
9
+ api: options.api,
10
+ app: options.app,
11
+ methods: {},
12
+ };
13
+ for (const [key, method] of Object.entries(options.methods)) {
14
+ schema.methods[key] = { type: 'method', schema, name: key, ...method };
15
+ }
16
+ return schema;
17
+ }
package/dist/convert.d.ts CHANGED
@@ -1,12 +1,42 @@
1
1
  import type { SchemaBase } from './dsl.js';
2
- import type { FrontAppSchema } from './project.js';
3
- /** Declares post-call result page data field mapping.
4
- * Driver generates per-item transform (e.g. .map()) before setData. */
2
+ import type { DtoMessage } from './dto.js';
3
+ import type { TableSchema } from './db.js';
4
+ import type { EntitySchema } from './entity.js';
5
+ import type { ThirdMethodSchema } from './third-service.js';
6
+ import type { FrontAppSchema, ProjectApiSchema } from './project.js';
7
+ /** A source/target collection of a convert — dto, entity, table or
8
+ * third-party message. Entity sources may carry aggregate fields
9
+ * (aggField), e.g. an aggregate result entity projected into a wire
10
+ * message. */
11
+ export type ConvertSourceSchema = DtoMessage | TableSchema | ThirdMethodSchema | EntitySchema;
12
+ /** Method input for defineConvert: type/schema/name are set by the builder. */
13
+ export type ConvertMethodDef = Omit<ConvertMethodSchema, 'type' | 'schema' | 'name'>;
14
+ /** One conversion: multiple source collections → single target collection. */
15
+ export interface ConvertMethodSchema extends SchemaBase {
16
+ type: 'convertMethod';
17
+ /** The convert file this method belongs to. */
18
+ schema: ConvertSchema;
19
+ /** Source schemas — one or more, mixed dimensions. */
20
+ sources: ConvertSourceSchema[];
21
+ /** Target schema — the single integrated collection. */
22
+ target: ConvertSourceSchema;
23
+ }
24
+ /** Declares multi-source → single-target schema integrations grouped by source identity. */
5
25
  export interface ConvertSchema extends SchemaBase {
6
26
  type: 'convert';
7
- /** The frontend app this convert belongs to (shared instance from project.config). */
27
+ /** The backend api module this convert belongs to (shared instance from
28
+ * project.config.ts apis). Storage is convert_schema/{api.name}/{app.name}/
29
+ * — same layout as service_schema. */
30
+ api: ProjectApiSchema;
31
+ /** The app (module) this convert belongs to — its artifact lands in modules/{app}/convert/. */
8
32
  app: FrontAppSchema;
9
- /** { targetField: sourceField }renames or copies fields from call result. */
10
- fields: Record<string, string>;
33
+ /** Methods keyed by namethe map key is written back as the method name. */
34
+ methods: Record<string, ConvertMethodSchema>;
11
35
  }
12
- export declare function defineConvert(name: string, app: FrontAppSchema, fields: Record<string, string>): ConvertSchema;
36
+ export declare function defineConvert(options: {
37
+ name: string;
38
+ api: ProjectApiSchema;
39
+ app: FrontAppSchema;
40
+ methods: Record<string, ConvertMethodDef>;
41
+ description?: string;
42
+ }): ConvertSchema;
package/dist/convert.js CHANGED
@@ -1,3 +1,24 @@
1
- export function defineConvert(name, app, fields) {
2
- return { name, type: 'convert', app, fields };
1
+ export function defineConvert(options) {
2
+ if (Object.keys(options.methods).length === 0) {
3
+ throw new Error(`convert '${options.name}': methods must not be empty`);
4
+ }
5
+ if (!options.api.apps.includes(options.app)) {
6
+ throw new Error(`convert '${options.name}': api '${options.api.name}' does not serve app '${options.app.name}'`);
7
+ }
8
+ const schema = {
9
+ type: 'convert',
10
+ name: options.name,
11
+ description: options.description,
12
+ api: options.api,
13
+ app: options.app,
14
+ methods: {},
15
+ };
16
+ for (const key of Object.keys(options.methods)) {
17
+ const method = options.methods[key];
18
+ if (method.sources.length === 0) {
19
+ throw new Error(`convert '${options.name}': method '${key}' sources must not be empty`);
20
+ }
21
+ schema.methods[key] = { type: 'convertMethod', schema, ...method, name: key };
22
+ }
23
+ return schema;
3
24
  }
package/dist/curd.d.ts CHANGED
@@ -1,14 +1,13 @@
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
  /** Mode of an action page: modal dialog or standalone route. */
6
7
  export type ActionPageMode = 'modal' | 'route';
7
8
  /** One CRUD action page (add / update / detail). */
8
9
  export interface ActionPage {
9
10
  mode: ActionPageMode;
10
- /** For add/update: when true, render as modal on list page; when false/undefined, render as standalone route page. */
11
- modal?: boolean;
12
11
  /** Fields rendered on this page. Required, non-empty — every field the
13
12
  * frontend shows must be listed explicitly. */
14
13
  columns: Field[];
@@ -18,20 +17,16 @@ export interface CurdListConfig {
18
17
  /** List columns; required, non-empty. Every field the list shows must be
19
18
  * listed explicitly. May include cross-table fields via foreign refs. */
20
19
  columns: Field[];
21
- /** Fuzzy keyword search on this table's columns. */
22
- keyword?: {
23
- columns: Field[];
24
- };
20
+ /** Page filter (search form + keyword search). Conditions declared on the
21
+ * filter render the search form; the filter's keyword (when present)
22
+ * drives the keyword query endpoint. Optional — a page without a filter
23
+ * has no search form. */
24
+ filter?: FilterSchema;
25
25
  /** Default sort. Required — column and direction are both mandatory. */
26
26
  orderBy: {
27
27
  column: Field;
28
28
  direction: 'asc' | 'desc';
29
29
  };
30
- /** Search condition fields; op defaults to 'eq'. */
31
- searchFields?: {
32
- field: Field;
33
- op?: Operator;
34
- }[];
35
30
  /** Column header text overrides: Field.name → header text. */
36
31
  columnTitles?: Record<string, string>;
37
32
  }
package/dist/curd.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { toKebabCase } from '@pylonts/core';
1
2
  function assertColumns(curd, pageName, columns) {
2
3
  if (columns.length === 0) {
3
4
  throw new Error(`curd ${curd.name}: ${pageName}.columns must be non-empty`);
@@ -17,6 +18,12 @@ export function defineCurd(name, schema) {
17
18
  if (curd.app.type !== 'admin') {
18
19
  throw new Error(`curd ${name}: app ${curd.app.name} must be type 'admin' (got '${curd.app.type}')`);
19
20
  }
21
+ // The name is the admin route path — it must be the kebab-case table name
22
+ // so paths cannot drift from the table they serve.
23
+ const expectedName = toKebabCase(curd.table.name);
24
+ if (name !== expectedName) {
25
+ throw new Error(`curd name must be '${expectedName}' (kebab-case of table '${curd.table.name}'); got '${name}'`);
26
+ }
20
27
  if (!curd.section) {
21
28
  throw new Error(`curd ${name}: section is required (sidebar menu group, e.g. '商户管理')`);
22
29
  }
@@ -25,7 +32,9 @@ export function defineCurd(name, schema) {
25
32
  if (page)
26
33
  assertColumns(curd, `actionPages.${pageName}`, page.columns);
27
34
  }
28
- assertFieldsOwnTable(curd, 'keyword', curd.list.keyword?.columns ?? []);
35
+ if (curd.list.filter !== undefined && curd.list.filter.app !== curd.app) {
36
+ throw new Error(`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}'`);
37
+ }
29
38
  assertFieldsOwnTable(curd, 'orderBy', [curd.list.orderBy.column]);
30
39
  return curd;
31
40
  }
package/dist/dao.d.ts CHANGED
@@ -1,9 +1,146 @@
1
- import { SchemaBase } from './dsl.js';
2
- import { FrontAppSchema } from './project.js';
1
+ import { SchemaBase, Field } from './dsl.js';
2
+ import type { FrontAppSchema, ProjectApiSchema } from './project.js';
3
+ import { TableSchema } from './db.js';
4
+ import type { EntitySchema } from './entity.js';
5
+ import type { FilterSchema } from './filter.js';
6
+ import type { SetExpr } from './expr.js';
3
7
  /** A data-access layer bound to exactly one frontend app. */
4
8
  export interface DaoSchema extends SchemaBase {
5
9
  type: 'dao';
10
+ /** The backend api module this DAO belongs to (shared instance from
11
+ * project.config.ts apis). DAOs are always backend-side, so storage is
12
+ * dao_schema/{api.name}/{app.name}/dao/. */
13
+ api: ProjectApiSchema;
6
14
  /** The frontend app this DAO belongs to (shared instance from project.config). */
7
15
  app: FrontAppSchema;
16
+ /** The table this DAO operates on (single-table atomicity). */
17
+ table: TableSchema;
18
+ /** Methods keyed by name — the map key is written back as the method name. */
19
+ methods: Record<string, DaoMethodSchema>;
20
+ }
21
+ /** Method input for defineDao: name is written back from the methods map key. */
22
+ export type DaoMethodDef = Omit<FindSchema, 'schema' | 'name'> | Omit<GetSchema, 'schema' | 'name'> | Omit<InsertSchema, 'schema' | 'name'> | Omit<UpdateSchema, 'schema' | 'name'> | Omit<DeleteSchema, 'schema' | 'name'> | Omit<UpsertSchema, 'schema' | 'name'> | Omit<AggregateSchema, 'schema' | 'name'>;
23
+ /** The tenant column of the dao table when the app declares a tenant.
24
+ * Deterministic name derivation: `{tenant.phrase}_{tenant.pk}` (e.g. shop
25
+ * with pk id → `shop_id`) — checked directly against the table columns,
26
+ * no FK traversal. Tables without that column are global tables (valid:
27
+ * system config tables carry no tenant id). Exported for generator/linter. */
28
+ export declare function tenantFkOf(dao: DaoSchema): Field | undefined;
29
+ export declare function defineDao(options: {
30
+ name: string;
31
+ api: ProjectApiSchema;
32
+ app: FrontAppSchema;
33
+ table: TableSchema;
34
+ methods: Record<string, DaoMethodDef>;
35
+ description?: string;
36
+ }): DaoSchema;
37
+ /** Sort specification for find results. */
38
+ export interface OrderBySchema {
39
+ /** Column to sort by. */
40
+ column: Field;
41
+ /** Sort direction. */
42
+ sort: 'asc' | 'desc';
43
+ }
44
+ /** Query rows by criteria; returns a list of row objects. */
45
+ export interface FindSchema extends SchemaBase {
46
+ type: 'find';
47
+ schema: DaoSchema;
48
+ /** Query filter; omit = all rows. */
49
+ args?: FilterSchema;
50
+ /** Pagination marker: 'page' (page/pageSize) or 'limit' (position/limit).
51
+ * Only presence matters — parameter shapes are a generator convention. */
52
+ mode?: 'page' | 'limit';
53
+ /** Result sort; omit = no explicit order. */
54
+ orderBy?: OrderBySchema | OrderBySchema[];
55
+ /** Result row: selected columns (multi-table via external reference columns). */
56
+ results: EntitySchema;
57
+ }
58
+ /** Fetch a single row by exact key — any column or AND combination of
59
+ * columns (single PK, composite PK, getByXX are all legal declarations).
60
+ * May miss the row: the generated signature returns Row | null. */
61
+ export interface GetSchema extends SchemaBase {
62
+ type: 'get';
63
+ schema: DaoSchema;
64
+ /** Key column(s): one field or AND-exact-matched fields. */
65
+ args: Field | Field[];
66
+ /** Additional criteria AND-combined onto the key (e.g. state condition
67
+ * for optimistic reads). */
68
+ where?: FilterSchema;
69
+ /** Result row: selected columns (multi-table via external reference columns). */
70
+ results: EntitySchema;
71
+ }
72
+ /** Insert one row; returns the generated key (string). */
73
+ export interface InsertSchema extends SchemaBase {
74
+ type: 'insert';
75
+ schema: DaoSchema;
76
+ /** Row object. */
77
+ args: EntitySchema;
78
+ }
79
+ /** Update one row; returns number (affected rows). The where key is derived
80
+ * from the PK columns inside args; tenant/version columns are extracted
81
+ * from the row into WHERE and never SET (optimistic lock auto-manages
82
+ * `version = version + 1`). */
83
+ export interface UpdateSchema extends SchemaBase {
84
+ type: 'update';
85
+ schema: DaoSchema;
86
+ /** Row object containing the PK columns plus the columns to set. */
87
+ args: EntitySchema;
88
+ /** Expression-set columns (`col = expr`) beyond direct assignment. */
89
+ set?: SetExpr[];
90
+ /** Additional criteria beyond the derived PK (AND-combined). */
91
+ where?: FilterSchema;
92
+ }
93
+ /** Delete one row by exact key; returns number (affected rows). */
94
+ export interface DeleteSchema extends SchemaBase {
95
+ type: 'delete';
96
+ schema: DaoSchema;
97
+ /** Key column(s): one field or AND-exact-matched fields. */
98
+ args: Field | Field[];
99
+ }
100
+ /** Insert-or-update (MySQL ON DUPLICATE KEY UPDATE): atomic idempotent write.
101
+ * Returns affected rows (1 = inserted, 2 = updated, 0 = unchanged).
102
+ * No id generation — every conflict key must be carried in args. Tables with
103
+ * an auto-increment pk are forbidden (MySQL auto_increment burns ids on
104
+ * duplicate-key updates). */
105
+ export interface UpsertSchema extends SchemaBase {
106
+ type: 'upsert';
107
+ schema: DaoSchema;
108
+ /** Row object: must carry every conflict key plus the columns to merge. */
109
+ args: EntitySchema;
110
+ /** Conflict keys: must equal the pk or a complete unique index column set. */
111
+ keys: Field | Field[];
112
+ }
113
+ export type DaoMethodSchema = FindSchema | GetSchema | InsertSchema | UpdateSchema | DeleteSchema | UpsertSchema | AggregateSchema;
114
+ /** Aggregate query (find upgraded at the select level): the result entity
115
+ * mixes plain columns (the GROUP BY dimensions) and aggregate fields
116
+ * (aggField — count/sum/avg).
117
+ *
118
+ * Usage — aggregate fields are defined in the entity file, the dao method
119
+ * only references the entity:
120
+ *
121
+ * ```ts
122
+ * // entity_schema/{api}/{app}/entity/order.entity.ts
123
+ * export const orderStatusStats = defineEntity({
124
+ * name: 'OrderStatusStats',
125
+ * api, app,
126
+ * columns: [
127
+ * order.columns.status, // GROUP BY dimension
128
+ * aggField('total', Compute.count()), // count → jsType 'number'
129
+ * aggField('sumAmt', Compute.sum(order.columns.amount)), // sum(decimal) → 'string'
130
+ * ],
131
+ * });
132
+ *
133
+ * // dao_schema/{api}/{app}/dao/order.dao.ts
134
+ * stats: { type: 'aggregate', args: orderFilter, results: orderStatusStats },
135
+ * ``` */
136
+ export interface AggregateSchema extends SchemaBase {
137
+ type: 'aggregate';
138
+ schema: DaoSchema;
139
+ /** Criteria filter, same shape as find. */
140
+ args?: FilterSchema;
141
+ /** Result entity: plain columns group the rows (one row per group);
142
+ * aggregate fields become the computed output columns. The entity — with
143
+ * its aggField definitions — lives in the entity file ({table}.entity.ts);
144
+ * the dao method only references it, never defines aggregate fields inline. */
145
+ results: EntitySchema;
8
146
  }
9
- export declare function defineDao(name: string, app: FrontAppSchema, description?: string): DaoSchema;