@pylonts/dsl 1.1.6 → 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 (86) 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 +5 -4
  41. package/dist/project.js +14 -2
  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/dao-generation.md +478 -0
  53. package/docs/ddd-principles.md +75 -0
  54. package/docs/domain-event.md +137 -0
  55. package/docs/keyword-matcher.md +182 -0
  56. package/docs/token.md +327 -0
  57. package/docs/trans-reentrant.md +85 -0
  58. package/package.json +25 -6
  59. package/src/action.ts +51 -10
  60. package/src/aggregate.ts +104 -0
  61. package/src/business-flow.ts +80 -0
  62. package/src/controller.ts +25 -11
  63. package/src/convert.ts +51 -15
  64. package/src/curd.ts +12 -6
  65. package/src/dao.ts +377 -63
  66. package/src/db.ts +13 -0
  67. package/src/domain-event.ts +74 -0
  68. package/src/dsl.ts +23 -2
  69. package/src/dto.ts +9 -6
  70. package/src/entity.ts +43 -0
  71. package/src/exception.ts +30 -5
  72. package/src/expr.ts +65 -0
  73. package/src/filter.ts +70 -0
  74. package/src/flow-script.ts +696 -0
  75. package/src/flow.ts +1129 -46
  76. package/src/index.ts +6 -2
  77. package/src/mermaid-driver.ts +256 -29
  78. package/src/mysql-driver.ts +3 -0
  79. package/src/project.ts +114 -97
  80. package/src/repository.ts +35 -0
  81. package/src/service.ts +68 -3
  82. package/src/third-service.ts +6 -0
  83. package/src/typebox-driver.ts +4 -0
  84. package/src/utils.ts +13 -2
  85. package/src/endpoint.ts +0 -18
  86. package/src/provider.ts +0 -68
package/src/dsl.ts CHANGED
@@ -4,9 +4,10 @@
4
4
  import type { DictionaryEntry } from './dictionary.js';
5
5
  import type { ImportBase } from './import-base.js';
6
6
  import type { MockDescriptor } from './mock.js';
7
+ import type { ComputeExpr } from './expr.js';
7
8
 
8
9
  /** Field query comparison operators (search field semantics). */
9
- export type Operator = 'eq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ne';
10
+ export type Operator = 'eq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'ne' | 'in' | 'null' | 'notNull';
10
11
 
11
12
  export interface SchemaBase {
12
13
  name: string;
@@ -158,6 +159,17 @@ interface ObjectField extends BaseField {
158
159
  properties: Record<string, Field>;
159
160
  }
160
161
 
162
+ /** Aggregate result column (count/sum/avg) of an aggregate query — a Field
163
+ * so that aggregate result entities can carry it like any other column.
164
+ * jsType follows the aggregate precision rule: count → number;
165
+ * sum/avg over an integer column → number, anything else → string. */
166
+ interface AggregateField extends BaseField {
167
+ type: 'aggregate';
168
+ jsType: 'number' | 'string';
169
+ /** The aggregate expression producing this column. */
170
+ expr: ComputeExpr;
171
+ }
172
+
161
173
  export type Field =
162
174
  | StringField
163
175
  | TextField
@@ -172,7 +184,8 @@ export type Field =
172
184
  | EnumField
173
185
  | JsonField
174
186
  | ArrayField
175
- | ObjectField;
187
+ | ObjectField
188
+ | AggregateField;
176
189
 
177
190
  // Field builders: type and jsType are fixed, pass extra properties only.
178
191
  // The field name is written back from the map key later (see defineTable).
@@ -234,4 +247,12 @@ export function objectField(extra: FieldExtras<ObjectField>): ObjectField {
234
247
  export function enumField(extra: Omit<EnumField, 'name' | 'type' | 'jsType'>): EnumField {
235
248
  const jsType = extra.enum.valueType === 'integer' ? 'number' : 'string';
236
249
  return { name: '', type: 'enum', jsType, ...extra };
250
+ }
251
+
252
+ /** Aggregate result column field: count → number; sum/avg over an integer
253
+ * column → number, anything else (decimal/bigint/rate…) → string.
254
+ * Used in aggregate-query result entities (see AggregateSchema). */
255
+ export function aggField(name: string, expr: ComputeExpr): AggregateField {
256
+ const jsType = expr.field === undefined || expr.field.type === 'integer' ? 'number' : 'string';
257
+ return { name, type: 'aggregate', jsType, expr };
237
258
  }
package/src/dto.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase } from './dsl.js';
2
2
  import { TableSchema } from './db.js';
3
+ import type { EntitySchema } from './entity.js';
3
4
  import type { ImportBase } from './import-base.js';
4
5
  import type { ThirdMethodSchema } from './third-service.js';
5
6
  import { toCamelCase } from '@pylonts/core';
@@ -234,8 +235,9 @@ export function buildPk(name: string, fields: Record<string, DtoField>, descript
234
235
  return message;
235
236
  }
236
237
 
237
- /** Field-collection source a DTO can project from: a DB table or a third-party method message. */
238
- export type DtoFieldSource = TableSchema | ThirdMethodSchema;
238
+ /** Field-collection source a DTO can project from: a DB table, a third-party
239
+ * method message, or an entity (which may carry aggregate fields). */
240
+ export type DtoFieldSource = TableSchema | ThirdMethodSchema | EntitySchema;
239
241
 
240
242
  // Type guard instead of a plain discriminant check: TableSchema.type is declared
241
243
  // as a class property, so TS widens it to string and cannot narrow the union.
@@ -248,16 +250,17 @@ function ownsField(source: DtoFieldSource, field: Field): boolean {
248
250
  return Object.values(source.columns).includes(field);
249
251
  }
250
252
 
251
- /** Project fields from a field-collection source (table or third-party method
252
- * message) and wrap them as DTO fields (aligned with dto.from). */
253
+ /** Project fields from a field-collection source (table, third-party method
254
+ * message or entity) and wrap them as DTO fields (aligned with dto.from). */
253
255
  export function from(source: DtoFieldSource, fields: Field[]): Record<string, DtoField> {
254
256
  const out: Record<string, DtoField> = {};
255
257
  for (const field of fields) {
256
258
  if (!ownsField(source, field)) {
257
259
  throw new Error(`dto.from(${source.name}): field ${field.name} does not belong to this ${source.type}`);
258
260
  }
259
- // DB columns map to camelCase interface names (mer_id → merId); wire-format
260
- // names are protocol names themselves and stay untouched.
261
+ // DB columns map to camelCase interface names (mer_id → merId); aggregate
262
+ // field names are already camel and pass through; wire-format names are
263
+ // protocol names themselves and stay untouched.
261
264
  out[isThirdMethod(source) ? field.name : toCamelCase(field.name)] = dtoField(field);
262
265
  }
263
266
  return out;
package/src/entity.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { Field, SchemaBase } from './dsl.js';
2
+ import type { FrontAppSchema, ProjectApiSchema } from './project.js';
3
+
4
+ /** A row object: a set of database columns. Columns may come from one table
5
+ * (write args of insert/update/upsert) or span tables through the main
6
+ * table's foreign keys (read results of find/get) — where the columns come
7
+ * from is the responsibility of the consuming position, not of this schema.
8
+ * Storage is entity_schema/{api.name}/{app.name}/entity/{table}.entity.ts:
9
+ * one file per table (the main table), any number of entities per file.
10
+ * "Row" is only a naming convention for read-shaped entities
11
+ * (OrderListRow, OrderDetailRow) — they are all defineEntity declarations. */
12
+ export interface EntitySchema extends SchemaBase {
13
+ type: 'entity';
14
+ /** The backend api module this entity belongs to (shared instance from
15
+ * project.config.ts apis). Entities are always backend-side. */
16
+ api: ProjectApiSchema;
17
+ /** The frontend app this entity belongs to (shared instance from project.config). */
18
+ app: FrontAppSchema;
19
+ /** The columns of this row object: main-table columns, external reference
20
+ * columns, and — for aggregate result entities — aggField columns
21
+ * (count/sum/avg outputs). */
22
+ columns: Field[];
23
+ }
24
+
25
+ export function defineEntity(options: {
26
+ name: string;
27
+ api: ProjectApiSchema;
28
+ app: FrontAppSchema;
29
+ columns: Field[];
30
+ description?: string;
31
+ }): EntitySchema {
32
+ if (!options.api.apps.includes(options.app)) {
33
+ throw new Error(`entity ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
34
+ }
35
+ return {
36
+ type: 'entity',
37
+ name: options.name,
38
+ description: options.description,
39
+ api: options.api,
40
+ app: options.app,
41
+ columns: options.columns,
42
+ };
43
+ }
package/src/exception.ts CHANGED
@@ -8,21 +8,46 @@ import type { ImportBase } from './import-base.js';
8
8
  /** Describes an exception a method can throw. */
9
9
  export interface ExceptionSchema extends SchemaBase, Omit<ImportBase, 'type'> {
10
10
  type: 'exception';
11
- /** Whether the caller may safely retry after catching this exception. */
12
- retryable: boolean;
13
11
  }
14
12
 
15
13
  export function defineException(options: {
16
14
  name: string;
17
15
  from: string;
18
- retryable: boolean;
19
16
  description?: string;
20
17
  }): ExceptionSchema {
21
18
  return {
22
19
  type: 'exception',
23
20
  name: options.name,
24
21
  from: options.from,
25
- retryable: options.retryable,
26
22
  description: options.description,
27
23
  };
28
- }
24
+ }
25
+
26
+ /** Timeout or unrecoverable I/O failure */
27
+ export const IOException = defineException({
28
+ name: 'IOException',
29
+ from: '@pylonts/core',
30
+ description: 'Network timeout or unrecoverable error',
31
+ });
32
+
33
+ /** Business error code carried by the exception*/
34
+ export const CodeException = defineException({
35
+ name: 'CodeException',
36
+ from: '@pylonts/core',
37
+ description: 'Business error code',
38
+ });
39
+
40
+ /** Business rule violation (maps to 422) */
41
+ export const BusinessException = defineException({
42
+ name: 'BusinessException',
43
+ from: '@pylonts/core',
44
+ description: 'Business rule violation',
45
+ });
46
+
47
+ /** Anything else — unexpected, carries no business semantics. Rendered as a
48
+ * plain Error / system exception, no dedicated runtime class. */
49
+ export const UnexpectedException = defineException({
50
+ name: 'UnexpectedException',
51
+ from: '',
52
+ description: 'Unexpected error',
53
+ });
package/src/expr.ts ADDED
@@ -0,0 +1,65 @@
1
+ // Value expression model: the single declarative way to express computed
2
+ // values in dao methods — update SET expressions and criteria right sides.
3
+ // Structural AST (never SQL strings): column references are Field objects
4
+ // (definition-time ownership checks), values bind as parameters at render
5
+ // time. New node kinds can be added without touching existing declarations.
6
+
7
+ import type { Field } from './dsl.js';
8
+
9
+ /** Binary operator on numeric values. */
10
+ export type BinOp = 'add' | 'sub' | 'mul' | 'div';
11
+
12
+ /** A value expression: column reference / literal / method parameter / binary
13
+ * operation. Recursive — leaves are col/lit/param, bin combines them. */
14
+ export type ValueExpr =
15
+ | { kind: 'col'; field: Field }
16
+ | { kind: 'lit'; value: string | number }
17
+ | { kind: 'param'; name: string }
18
+ | { kind: 'bin'; op: BinOp; left: ValueExpr; right: ValueExpr };
19
+
20
+ /** An update SET assignment: `col = expr` (besides the direct-assignment
21
+ * columns carried by the args entity — a column must not appear in both). */
22
+ export interface SetExpr {
23
+ col: Field;
24
+ expr: ValueExpr;
25
+ }
26
+
27
+ export function col(field: Field): ValueExpr {
28
+ return { kind: 'col', field };
29
+ }
30
+
31
+ export function lit(value: string | number): ValueExpr {
32
+ return { kind: 'lit', value };
33
+ }
34
+
35
+ export function param(name: string): ValueExpr {
36
+ return { kind: 'param', name };
37
+ }
38
+
39
+ export function bin(op: BinOp, left: ValueExpr, right: ValueExpr): ValueExpr {
40
+ return { kind: 'bin', op, left, right };
41
+ }
42
+
43
+ /** Convenience builders for the common self-increment/decrement shapes. */
44
+ export const incr = (field: Field, by: ValueExpr): SetExpr => ({ col: field, expr: bin('add', col(field), by) });
45
+ export const decr = (field: Field, by: ValueExpr): SetExpr => ({ col: field, expr: bin('sub', col(field), by) });
46
+
47
+ /** Aggregate expression result of an aggregate query column. */
48
+ export interface ComputeExpr {
49
+ fn: 'sum' | 'avg' | 'count';
50
+ /** Column the function applies to; absent for count(*). */
51
+ field?: Field;
52
+ }
53
+
54
+ /** Aggregate expressions: Compute.sum(col) / Compute.avg(col) / Compute.count(). */
55
+ export const Compute = {
56
+ sum(field: Field): ComputeExpr {
57
+ return { fn: 'sum', field };
58
+ },
59
+ avg(field: Field): ComputeExpr {
60
+ return { fn: 'avg', field };
61
+ },
62
+ count(): ComputeExpr {
63
+ return { fn: 'count' };
64
+ },
65
+ };
package/src/filter.ts ADDED
@@ -0,0 +1,70 @@
1
+ import type { Field, Operator, SchemaBase } from './dsl.js';
2
+ import type { ValueExpr } from './expr.js';
3
+ import type { FrontAppSchema, ProjectApiSchema } from './project.js';
4
+
5
+ // Filter: the single declarative model for query conditions, shared by
6
+ // dao_schema methods and curd page lists. Conditions are AND-combined and
7
+ // may reference any table (cross-table filters render JOINs at the usage
8
+ // site, which owns the main table). Pagination is NOT part of a filter —
9
+ // it stays on the dao method (mode) and the curd list config.
10
+
11
+ /** One AND-combined criterion: a column plus its comparison operator. */
12
+ export interface FilterCondition {
13
+ /** Column to compare (any table — cross-table filters are allowed). */
14
+ field: Field;
15
+ /** Comparison operator; defaults to 'eq'. */
16
+ op?: Operator;
17
+ /** Right side of the comparison. Absent = the args parameter named after
18
+ * the column (existing semantics); present = an explicit value expression
19
+ * (column-vs-column, literal, computation). */
20
+ right?: ValueExpr;
21
+ /** Required (default) or optional criterion. A filter has one semantics:
22
+ * dao filters are required (missing criteria = error), page filters are
23
+ * optional (missing criteria = no WHERE). Declared by the filter author. */
24
+ optional?: boolean;
25
+ }
26
+
27
+ /** Query filter: AND-combined conditions plus an optional keyword search. */
28
+ export interface FilterSchema extends SchemaBase {
29
+ type: 'filter';
30
+ /** The backend api module this filter belongs to (shared instance from
31
+ * project.config.ts apis). Filters are backend-side, so storage is
32
+ * filter_schema/{api.name}/{app.name}/filter/. */
33
+ api: ProjectApiSchema;
34
+ /** The frontend app this filter belongs to (shared instance from project.config). */
35
+ app: FrontAppSchema;
36
+ /** AND-combined conditions (may be empty when keyword is present). */
37
+ conditions: FilterCondition[];
38
+ /** Fuzzy keyword search: one input value matched against multiple columns
39
+ * via OR-like. Presence drives the keyword query endpoint. */
40
+ keyword?: { columns: Field[] };
41
+ }
42
+
43
+ export function defineFilter(options: {
44
+ name: string;
45
+ api: ProjectApiSchema;
46
+ app: FrontAppSchema;
47
+ conditions?: FilterCondition[];
48
+ keyword?: { columns: Field[] };
49
+ description?: string;
50
+ }): FilterSchema {
51
+ if (!options.api.apps.includes(options.app)) {
52
+ throw new Error(`filter ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
53
+ }
54
+ const conditions = options.conditions ?? [];
55
+ if (conditions.length === 0 && options.keyword === undefined) {
56
+ throw new Error(`filter ${options.name}: conditions and keyword cannot both be empty`);
57
+ }
58
+ if (options.keyword !== undefined && options.keyword.columns.length === 0) {
59
+ throw new Error(`filter ${options.name}: keyword columns must be non-empty`);
60
+ }
61
+ return {
62
+ type: 'filter',
63
+ name: options.name,
64
+ description: options.description,
65
+ api: options.api,
66
+ app: options.app,
67
+ conditions,
68
+ keyword: options.keyword,
69
+ };
70
+ }