@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.
- package/README.md +4 -0
- package/dist/action.d.ts +32 -0
- package/dist/action.js +14 -0
- package/dist/aggregate.d.ts +38 -0
- package/dist/aggregate.js +46 -0
- package/dist/business-flow.d.ts +9 -0
- package/dist/business-flow.js +72 -0
- package/dist/controller.d.ts +17 -9
- package/dist/controller.js +8 -2
- package/dist/convert.d.ts +28 -10
- package/dist/convert.js +16 -5
- package/dist/curd.d.ts +7 -10
- package/dist/curd.js +3 -1
- package/dist/dao.d.ts +81 -53
- package/dist/dao.js +291 -12
- package/dist/db.d.ts +6 -0
- package/dist/db.js +10 -0
- package/dist/domain-event.d.ts +48 -0
- package/dist/domain-event.js +24 -0
- package/dist/dsl.d.ts +17 -2
- package/dist/dsl.js +7 -0
- package/dist/dto.d.ts +6 -4
- package/dist/dto.js +5 -4
- package/dist/entity.d.ts +29 -0
- package/dist/entity.js +13 -0
- package/dist/exception.d.ts +9 -3
- package/dist/exception.js +25 -1
- package/dist/expr.d.ts +45 -0
- package/dist/expr.js +32 -0
- package/dist/filter.d.ts +45 -0
- package/dist/filter.js +21 -0
- package/dist/flow-script.d.ts +108 -0
- package/dist/flow-script.js +505 -0
- package/dist/flow.d.ts +294 -17
- package/dist/flow.js +803 -18
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/mermaid-driver.js +264 -24
- package/dist/mysql-driver.js +3 -0
- package/dist/project.d.ts +5 -4
- package/dist/project.js +14 -2
- package/dist/repository.d.ts +26 -0
- package/dist/repository.js +8 -0
- package/dist/service.d.ts +14 -2
- package/dist/service.js +49 -0
- package/dist/third-service.d.ts +5 -0
- package/dist/third-service.js +1 -0
- package/dist/typebox-driver.js +4 -0
- package/dist/utils.d.ts +9 -2
- package/dist/utils.js +4 -0
- package/docs/aggregate.md +110 -0
- package/docs/dao-generation.md +478 -0
- package/docs/ddd-principles.md +75 -0
- package/docs/domain-event.md +137 -0
- package/docs/keyword-matcher.md +182 -0
- package/docs/token.md +327 -0
- package/docs/trans-reentrant.md +85 -0
- package/package.json +25 -6
- package/src/action.ts +51 -10
- package/src/aggregate.ts +104 -0
- package/src/business-flow.ts +80 -0
- package/src/controller.ts +25 -11
- package/src/convert.ts +51 -15
- package/src/curd.ts +12 -6
- package/src/dao.ts +377 -63
- package/src/db.ts +13 -0
- package/src/domain-event.ts +74 -0
- package/src/dsl.ts +23 -2
- package/src/dto.ts +9 -6
- package/src/entity.ts +43 -0
- package/src/exception.ts +30 -5
- package/src/expr.ts +65 -0
- package/src/filter.ts +70 -0
- package/src/flow-script.ts +696 -0
- package/src/flow.ts +1129 -46
- package/src/index.ts +6 -2
- package/src/mermaid-driver.ts +256 -29
- package/src/mysql-driver.ts +3 -0
- package/src/project.ts +114 -97
- package/src/repository.ts +35 -0
- package/src/service.ts +68 -3
- package/src/third-service.ts +6 -0
- package/src/typebox-driver.ts +4 -0
- package/src/utils.ts +13 -2
- package/src/endpoint.ts +0 -18
- 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
|
|
238
|
-
|
|
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
|
|
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);
|
|
260
|
-
// names are
|
|
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
|
+
}
|