@pylonts/dsl 1.1.12 → 1.1.13
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/dist/convert.d.ts +6 -8
- package/dist/curd.js +1 -1
- package/dist/dao.d.ts +10 -7
- package/dist/dao.js +20 -7
- package/dist/dsl.d.ts +18 -1
- package/dist/dsl.js +40 -0
- package/dist/dto.d.ts +13 -8
- package/dist/dto.js +68 -13
- package/dist/entity.d.ts +4 -3
- package/dist/entity.js +1 -1
- package/dist/filter.d.ts +6 -4
- package/dist/filter.js +1 -1
- package/dist/flow-script.js +8 -2
- package/dist/flow.d.ts +10 -2
- package/dist/flow.js +44 -4
- package/dist/mermaid-driver.js +2 -2
- package/dist/service.d.ts +13 -8
- package/dist/service.js +1 -1
- package/dist/third-service.d.ts +10 -53
- package/dist/third-service.js +3 -78
- package/dist/typebox-driver.d.ts +0 -6
- package/dist/typebox-driver.js +8 -36
- package/dist/utils.d.ts +2 -2
- package/docs/curd.md +146 -146
- package/docs/dao-generation.md +477 -477
- package/docs/project.md +31 -31
- package/docs/token.md +326 -326
- package/package.json +1 -1
- package/src/action.ts +51 -51
- package/src/controller.ts +53 -53
- package/src/convert.ts +76 -78
- package/src/curd.ts +104 -104
- package/src/dao.ts +504 -485
- package/src/dsl.ts +296 -257
- package/src/dto.ts +323 -266
- package/src/entity.ts +43 -42
- package/src/expr.ts +64 -64
- package/src/filter.ts +71 -69
- package/src/flow-script.ts +702 -695
- package/src/flow.ts +1272 -1226
- package/src/index.ts +46 -46
- package/src/mermaid-driver.ts +339 -339
- package/src/mysql-driver.ts +108 -108
- package/src/project.ts +138 -138
- package/src/service.ts +112 -107
- package/src/third-service.ts +68 -191
- package/src/typebox-driver.ts +234 -268
- package/src/utils.ts +74 -74
package/dist/convert.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import type { SchemaBase } from './dsl.js';
|
|
1
|
+
import type { CollectionSchemaBase, SchemaBase } from './dsl.js';
|
|
2
2
|
import type { DtoMessage } from './dto.js';
|
|
3
3
|
import type { TableSchema } from './db.js';
|
|
4
4
|
import type { EntitySchema } from './entity.js';
|
|
5
|
-
import type { ThirdMethodSchema } from './third-service.js';
|
|
6
5
|
import type { FrontAppSchema, ProjectApiSchema } from './project.js';
|
|
7
|
-
/** A source/target collection of a convert — dto, entity
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
export type ConvertSourceSchema = DtoMessage | TableSchema | ThirdMethodSchema | EntitySchema;
|
|
6
|
+
/** A source/target collection of a convert — dto, entity or table. Entity
|
|
7
|
+
* sources may carry aggregate fields (aggField), e.g. an aggregate result
|
|
8
|
+
* entity projected into a wire message. */
|
|
9
|
+
export type ConvertSourceSchema = DtoMessage | TableSchema | EntitySchema;
|
|
12
10
|
/** Method input for defineConvert: type/schema/name are set by the builder. */
|
|
13
11
|
export type ConvertMethodDef = Omit<ConvertMethodSchema, 'type' | 'schema' | 'name'>;
|
|
14
12
|
/** One conversion: multiple source collections → single target collection. */
|
|
@@ -22,7 +20,7 @@ export interface ConvertMethodSchema extends SchemaBase {
|
|
|
22
20
|
target: ConvertSourceSchema;
|
|
23
21
|
}
|
|
24
22
|
/** Declares multi-source → single-target schema integrations grouped by source identity. */
|
|
25
|
-
export interface ConvertSchema extends
|
|
23
|
+
export interface ConvertSchema extends CollectionSchemaBase {
|
|
26
24
|
type: 'convert';
|
|
27
25
|
/** The backend api module this convert belongs to (shared instance from
|
|
28
26
|
* project.config.ts apis). Storage is convert_schema/{api.name}/{app.name}/
|
package/dist/curd.js
CHANGED
|
@@ -33,7 +33,7 @@ export function defineCurd(name, schema) {
|
|
|
33
33
|
assertColumns(curd, `actionPages.${pageName}`, page.columns);
|
|
34
34
|
}
|
|
35
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
|
|
36
|
+
throw new Error(`curd ${name}: list.filter '${curd.list.filter.name}' is bound to ${curd.list.filter.app?.name ?? 'common'} but the curd belongs to app '${curd.app.name}'`);
|
|
37
37
|
}
|
|
38
38
|
assertFieldsOwnTable(curd, 'orderBy', [curd.list.orderBy.column]);
|
|
39
39
|
return curd;
|
package/dist/dao.d.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
|
-
import { SchemaBase, Field } from './dsl.js';
|
|
1
|
+
import { CollectionSchemaBase, SchemaBase, Field } from './dsl.js';
|
|
2
2
|
import type { FrontAppSchema, ProjectApiSchema } from './project.js';
|
|
3
3
|
import { TableSchema } from './db.js';
|
|
4
4
|
import type { EntitySchema } from './entity.js';
|
|
5
5
|
import type { FilterSchema } from './filter.js';
|
|
6
6
|
import type { SetExpr } from './expr.js';
|
|
7
|
-
/** A data-access layer bound to
|
|
8
|
-
|
|
7
|
+
/** A data-access layer bound to one frontend app, or to the api-level common
|
|
8
|
+
* domain layer (app unset — shared across modules). */
|
|
9
|
+
export interface DaoSchema extends CollectionSchemaBase {
|
|
9
10
|
type: 'dao';
|
|
10
11
|
/** The backend api module this DAO belongs to (shared instance from
|
|
11
12
|
* project.config.ts apis). DAOs are always backend-side, so storage is
|
|
12
|
-
* dao_schema/{api.name}/{app.name}/dao
|
|
13
|
+
* dao_schema/{api.name}/{app.name}/dao/ — app unset = the api-level common
|
|
14
|
+
* domain layer, stored at dao_schema/{api.name}/common/dao/. */
|
|
13
15
|
api: ProjectApiSchema;
|
|
14
|
-
/** The frontend app this DAO belongs to (shared instance from project.config).
|
|
15
|
-
|
|
16
|
+
/** The frontend app this DAO belongs to (shared instance from project.config).
|
|
17
|
+
* Unset = api-level common domain DAO shared by all modules of the api. */
|
|
18
|
+
app?: FrontAppSchema;
|
|
16
19
|
/** The table this DAO operates on (single-table atomicity). */
|
|
17
20
|
table: TableSchema;
|
|
18
21
|
/** Methods keyed by name — the map key is written back as the method name. */
|
|
@@ -29,7 +32,7 @@ export declare function tenantFkOf(dao: DaoSchema): Field | undefined;
|
|
|
29
32
|
export declare function defineDao(options: {
|
|
30
33
|
name: string;
|
|
31
34
|
api: ProjectApiSchema;
|
|
32
|
-
app
|
|
35
|
+
app?: FrontAppSchema;
|
|
33
36
|
table: TableSchema;
|
|
34
37
|
methods: Record<string, DaoMethodDef>;
|
|
35
38
|
description?: string;
|
package/dist/dao.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import { isAggregate } from './dsl.js';
|
|
1
2
|
/** The tenant column of the dao table when the app declares a tenant.
|
|
2
3
|
* Deterministic name derivation: `{tenant.phrase}_{tenant.pk}` (e.g. shop
|
|
3
4
|
* with pk id → `shop_id`) — checked directly against the table columns,
|
|
4
5
|
* no FK traversal. Tables without that column are global tables (valid:
|
|
5
6
|
* system config tables carry no tenant id). Exported for generator/linter. */
|
|
6
7
|
export function tenantFkOf(dao) {
|
|
7
|
-
const
|
|
8
|
-
|
|
8
|
+
const app = dao.app;
|
|
9
|
+
const tenant = app?.tenant;
|
|
10
|
+
if (!tenant || !app)
|
|
9
11
|
return undefined;
|
|
10
12
|
const pk = tenant.primaryKey;
|
|
11
13
|
const phrase = tenant.phrase;
|
|
12
14
|
if (!pk || Array.isArray(pk) || !phrase) {
|
|
13
|
-
throw new Error(`app '${
|
|
15
|
+
throw new Error(`app '${app.name}' tenant table '${tenant.name}' must declare a single-column primaryKey and a phrase (tenant column name = {phrase}_{pk})`);
|
|
14
16
|
}
|
|
15
17
|
return dao.table.columns[`${phrase.name}_${pk.name}`];
|
|
16
18
|
}
|
|
@@ -51,6 +53,10 @@ function validateValueExpr(dao, expr, params, methodName) {
|
|
|
51
53
|
function validateUpdateSet(dao, methodName, m) {
|
|
52
54
|
const argCols = new Set(m.args.columns.map((c) => c.name));
|
|
53
55
|
const version = versionOf(dao);
|
|
56
|
+
// Where criteria columns are locators carried by args, not direct assignments —
|
|
57
|
+
// expression-setting the same column is the conditional-update idiom
|
|
58
|
+
// (WHERE state = row.state, SET state = ?).
|
|
59
|
+
const whereCols = new Set((m.where?.conditions ?? []).map((c) => c.field.name));
|
|
54
60
|
for (const setExpr of m.set ?? []) {
|
|
55
61
|
if (version && setExpr.col === version) {
|
|
56
62
|
throw new Error(`dao ${dao.name}.${methodName}: set must not touch version column '${version.name}' — the optimistic lock manages it`);
|
|
@@ -58,7 +64,7 @@ function validateUpdateSet(dao, methodName, m) {
|
|
|
58
64
|
if (!Object.values(dao.table.columns).includes(setExpr.col)) {
|
|
59
65
|
throw new Error(`dao ${dao.name}.${methodName}: set column '${setExpr.col.name}' is not a column of table ${dao.table.name}`);
|
|
60
66
|
}
|
|
61
|
-
if (argCols.has(setExpr.col.name)) {
|
|
67
|
+
if (argCols.has(setExpr.col.name) && !whereCols.has(setExpr.col.name)) {
|
|
62
68
|
throw new Error(`dao ${dao.name}.${methodName}: set column '${setExpr.col.name}' also appears in args '${m.args.name}' — a column is either directly assigned or expression-set, never both`);
|
|
63
69
|
}
|
|
64
70
|
validateValueExpr(dao, setExpr.expr, new Set(), methodName);
|
|
@@ -152,7 +158,7 @@ function validateAggregateResults(dao, methodName, m) {
|
|
|
152
158
|
throw new Error(`dao ${dao.name}.${methodName}: aggregate has no results`);
|
|
153
159
|
}
|
|
154
160
|
for (const c of columns) {
|
|
155
|
-
if (c
|
|
161
|
+
if (isAggregate(c)) {
|
|
156
162
|
if (c.expr.field)
|
|
157
163
|
assertColumnReachable(dao, methodName, c.expr.field);
|
|
158
164
|
}
|
|
@@ -250,7 +256,7 @@ function validateRowCarriedColumns(dao, methodName, method) {
|
|
|
250
256
|
}
|
|
251
257
|
}
|
|
252
258
|
export function defineDao(options) {
|
|
253
|
-
if (!options.api.apps.includes(options.app)) {
|
|
259
|
+
if (options.app && !options.api.apps.includes(options.app)) {
|
|
254
260
|
throw new Error(`dao ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
|
|
255
261
|
}
|
|
256
262
|
const schema = {
|
|
@@ -265,7 +271,14 @@ export function defineDao(options) {
|
|
|
265
271
|
for (const key of Object.keys(options.methods)) {
|
|
266
272
|
const method = options.methods[key];
|
|
267
273
|
for (const ref of [method.args, 'where' in method ? method.where : undefined]) {
|
|
268
|
-
if (isFilterSchema(ref) &&
|
|
274
|
+
if (isFilterSchema(ref) && ref.api !== options.api) {
|
|
275
|
+
throw new Error(`dao ${options.name}: method '${key}' references filter '${ref.name}' bound to ${ref.api.name}/${ref.app?.name ?? 'common'} but the dao is bound to ${options.api.name}/${options.app?.name ?? 'common'}`);
|
|
276
|
+
}
|
|
277
|
+
// app-bound dao may use common or same-app filters; common dao may only use common filters.
|
|
278
|
+
if (isFilterSchema(ref) && ref.api === options.api && !options.app && ref.app) {
|
|
279
|
+
throw new Error(`dao ${options.name}: method '${key}' references app-bound filter '${ref.name}' (${ref.app.name}) but the dao is api-level common`);
|
|
280
|
+
}
|
|
281
|
+
if (isFilterSchema(ref) && ref.api === options.api && options.app && ref.app && ref.app !== options.app) {
|
|
269
282
|
throw new Error(`dao ${options.name}: method '${key}' references filter '${ref.name}' bound to ${ref.api.name}/${ref.app.name} but the dao is bound to ${options.api.name}/${options.app.name}`);
|
|
270
283
|
}
|
|
271
284
|
}
|
package/dist/dsl.d.ts
CHANGED
|
@@ -118,13 +118,30 @@ interface ObjectField extends BaseField {
|
|
|
118
118
|
* so that aggregate result entities can carry it like any other column.
|
|
119
119
|
* jsType follows the aggregate precision rule: count → number;
|
|
120
120
|
* sum/avg over an integer column → number, anything else → string. */
|
|
121
|
-
interface AggregateField extends BaseField {
|
|
121
|
+
export interface AggregateField extends BaseField {
|
|
122
122
|
type: 'aggregate';
|
|
123
123
|
jsType: 'number' | 'string';
|
|
124
124
|
/** The aggregate expression producing this column. */
|
|
125
125
|
expr: ComputeExpr;
|
|
126
126
|
}
|
|
127
|
+
/** Whether the field is an aggregate result column — narrows to AggregateField. */
|
|
128
|
+
export declare function isAggregate(field: Field): field is AggregateField;
|
|
127
129
|
export type Field = StringField | TextField | IntField | BigintField | DecimalField | RateField | BooleanField | DateField | TimeField | DateTimeField | EnumField | JsonField | ArrayField | ObjectField | AggregateField;
|
|
130
|
+
/** TS type of a field in generated code (row interfaces, dao params, filter
|
|
131
|
+
* args): enum → its JS name, date/datetime → string (transported as ISO
|
|
132
|
+
* strings), everything else → jsType. Aggregate fields carry their precision
|
|
133
|
+
* rule in jsType already. The single source for this mapping — renderers
|
|
134
|
+
* must not branch on field.type for a TS type string. */
|
|
135
|
+
export declare function fieldJsType(field: Field): string;
|
|
136
|
+
/** Enum JS names referenced by a field, recursing into array/object
|
|
137
|
+
* containers (wire-format nesting). First-occurrence order — callers that
|
|
138
|
+
* need uniqueness collect into a Set. */
|
|
139
|
+
export declare function collectEnumRefs(field: Field, out?: string[]): string[];
|
|
140
|
+
/** Depth-first walk over a field's container structure: visit is called for
|
|
141
|
+
* every field including containers (top-level, array items, object
|
|
142
|
+
* properties). `key` is the object property name the field sits under
|
|
143
|
+
* (undefined for the top-level field and for array items). */
|
|
144
|
+
export declare function walkContainer(field: Field, visit: (f: Field, key?: string) => void, key?: string): void;
|
|
128
145
|
type FieldExtras<T extends Field> = Omit<T, 'name' | 'type' | 'jsType'>;
|
|
129
146
|
export declare function stringField(extra?: FieldExtras<StringField>): StringField;
|
|
130
147
|
export declare function textField(extra?: FieldExtras<TextField>): TextField;
|
package/dist/dsl.js
CHANGED
|
@@ -10,6 +10,46 @@ export function rateScale(unit) {
|
|
|
10
10
|
export function defineEnum(jsName, valueType, values) {
|
|
11
11
|
return { jsName, valueType, values };
|
|
12
12
|
}
|
|
13
|
+
/** Whether the field is an aggregate result column — narrows to AggregateField. */
|
|
14
|
+
export function isAggregate(field) {
|
|
15
|
+
return field.type === 'aggregate';
|
|
16
|
+
}
|
|
17
|
+
/** TS type of a field in generated code (row interfaces, dao params, filter
|
|
18
|
+
* args): enum → its JS name, date/datetime → string (transported as ISO
|
|
19
|
+
* strings), everything else → jsType. Aggregate fields carry their precision
|
|
20
|
+
* rule in jsType already. The single source for this mapping — renderers
|
|
21
|
+
* must not branch on field.type for a TS type string. */
|
|
22
|
+
export function fieldJsType(field) {
|
|
23
|
+
if (field.type === 'enum')
|
|
24
|
+
return field.enum.jsName;
|
|
25
|
+
if (field.type === 'date' || field.type === 'datetime')
|
|
26
|
+
return 'string';
|
|
27
|
+
return field.jsType;
|
|
28
|
+
}
|
|
29
|
+
/** Enum JS names referenced by a field, recursing into array/object
|
|
30
|
+
* containers (wire-format nesting). First-occurrence order — callers that
|
|
31
|
+
* need uniqueness collect into a Set. */
|
|
32
|
+
export function collectEnumRefs(field, out = []) {
|
|
33
|
+
walkContainer(field, (leaf) => {
|
|
34
|
+
if (leaf.type === 'enum')
|
|
35
|
+
out.push(leaf.enum.jsName);
|
|
36
|
+
});
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** Depth-first walk over a field's container structure: visit is called for
|
|
40
|
+
* every field including containers (top-level, array items, object
|
|
41
|
+
* properties). `key` is the object property name the field sits under
|
|
42
|
+
* (undefined for the top-level field and for array items). */
|
|
43
|
+
export function walkContainer(field, visit, key) {
|
|
44
|
+
visit(field, key);
|
|
45
|
+
if (field.type === 'array') {
|
|
46
|
+
walkContainer(field.items, visit);
|
|
47
|
+
}
|
|
48
|
+
else if (field.type === 'object') {
|
|
49
|
+
for (const [k, child] of Object.entries(field.properties))
|
|
50
|
+
walkContainer(child, visit, k);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
13
53
|
export function stringField(extra = {}) {
|
|
14
54
|
return { name: '', type: 'string', jsType: 'string', ...extra };
|
|
15
55
|
}
|
package/dist/dto.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase } from './
|
|
|
2
2
|
import { TableSchema } from './db.js';
|
|
3
3
|
import type { EntitySchema } from './entity.js';
|
|
4
4
|
import type { ImportBase } from './import-base.js';
|
|
5
|
-
import type { ThirdMethodSchema } from './third-service.js';
|
|
6
5
|
/** Re-export — Operator lives on the DSL level (see dsl.ts). */
|
|
7
6
|
export type { Operator } from './dsl.js';
|
|
8
7
|
/** Re-export — ImportBase lives on its own module (see import-base.ts). */
|
|
@@ -90,7 +89,11 @@ export declare class DtoMessage implements CollectionSchemaBase {
|
|
|
90
89
|
/** 引用已存在的 TypeBox base schema,例如 include({ from: '@pylonts/core', name: 'PageRequest' }) */
|
|
91
90
|
include(...refs: ImportRef[]): this;
|
|
92
91
|
}
|
|
93
|
-
export declare function dtoField(field: Field): DtoField;
|
|
92
|
+
export declare function dtoField(field: Field | DtoArrayFieldDef | DtoObjectFieldDef): DtoField;
|
|
93
|
+
/** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
|
|
94
|
+
export declare function isDtoMessage(v: unknown): v is DtoMessage;
|
|
95
|
+
/** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
|
|
96
|
+
export declare function isDtoField(v: unknown): v is DtoField;
|
|
94
97
|
export declare function dtoArrayField(def: {
|
|
95
98
|
items: DtoField | DtoMessage;
|
|
96
99
|
} & Omit<BaseField, 'name'>): DtoArrayField;
|
|
@@ -101,9 +104,11 @@ export declare function buildInput(name: string, fields: Record<string, DtoField
|
|
|
101
104
|
export declare function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
|
|
102
105
|
export declare function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
|
|
103
106
|
export declare function buildPk(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
|
|
104
|
-
/** Field-collection source a DTO can project from: a DB table,
|
|
105
|
-
*
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
/** Field-collection source a DTO can project from: a DB table, another DTO
|
|
108
|
+
* message (protocol fields keep their names), or an entity (which may carry
|
|
109
|
+
* aggregate fields). */
|
|
110
|
+
export type DtoFieldSource = TableSchema | DtoMessage | EntitySchema;
|
|
111
|
+
/** Project fields from a field-collection source (table, DTO message or
|
|
112
|
+
* entity) and wrap them as DTO fields (aligned with dto.from). Shared Field
|
|
113
|
+
* instances keep their original identity — the projection references them. */
|
|
114
|
+
export declare function from(source: DtoFieldSource, fields: (Field | DtoArrayFieldDef | DtoObjectFieldDef)[]): Record<string, DtoField>;
|
package/dist/dto.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { walkContainer } from './dsl.js';
|
|
1
2
|
import { toCamelCase } from '@pylonts/core';
|
|
2
3
|
export class DtoField {
|
|
3
4
|
/** DTO 语义字段名(接口字段名),与 field.name(数据库列名)含义不同。
|
|
@@ -95,6 +96,18 @@ export class DtoMessage {
|
|
|
95
96
|
export function dtoField(field) {
|
|
96
97
|
return new DtoField(field);
|
|
97
98
|
}
|
|
99
|
+
/** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
|
|
100
|
+
export function isDtoMessage(v) {
|
|
101
|
+
if (typeof v !== 'object' || v === null)
|
|
102
|
+
return false;
|
|
103
|
+
return v.type === 'dto';
|
|
104
|
+
}
|
|
105
|
+
/** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
|
|
106
|
+
export function isDtoField(v) {
|
|
107
|
+
if (typeof v !== 'object' || v === null)
|
|
108
|
+
return false;
|
|
109
|
+
return 'field' in v && !('type' in v);
|
|
110
|
+
}
|
|
98
111
|
export function dtoArrayField(def) {
|
|
99
112
|
// Items stay as-is: an inline DtoField is rendered inline, a DtoMessage is
|
|
100
113
|
// referenced by name (the driver renders Type.Array(<DtoName>)).
|
|
@@ -123,9 +136,54 @@ function buildMessage(name, direction, fields, description) {
|
|
|
123
136
|
df.field.name = key;
|
|
124
137
|
df.field.schema = message;
|
|
125
138
|
}
|
|
139
|
+
writeBackNested(df, message);
|
|
126
140
|
}
|
|
127
141
|
return message;
|
|
128
142
|
}
|
|
143
|
+
/** Write back name/schema on nested DTO fields (array items, object
|
|
144
|
+
* properties) — both plain-Field containers (objectField/arrayField) and
|
|
145
|
+
* DtoField containers (dtoObjectField/dtoArrayField). DtoMessage item
|
|
146
|
+
* references are skipped — they carry their own identity. */
|
|
147
|
+
function writeBackNested(df, message) {
|
|
148
|
+
const f = df.field;
|
|
149
|
+
if (f.type === 'array') {
|
|
150
|
+
const items = f.items;
|
|
151
|
+
if (isDtoMessage(items))
|
|
152
|
+
return;
|
|
153
|
+
if (isDtoField(items)) {
|
|
154
|
+
writeBackNested(items, message);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
walkContainer(items, writeBackLeaf(message));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (f.type === 'object') {
|
|
161
|
+
for (const [key, child] of Object.entries(f.properties)) {
|
|
162
|
+
if (isDtoField(child)) {
|
|
163
|
+
child.name = key;
|
|
164
|
+
child.schema = message;
|
|
165
|
+
if (child.field.schema === undefined) {
|
|
166
|
+
child.field.name = key;
|
|
167
|
+
child.field.schema = message;
|
|
168
|
+
}
|
|
169
|
+
writeBackNested(child, message);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
writeBackLeaf(message)(child, key);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Name/schema write-back for a plain Field (own fields only — shared
|
|
178
|
+
* instances keep their original identity). */
|
|
179
|
+
function writeBackLeaf(message) {
|
|
180
|
+
return (f, key) => {
|
|
181
|
+
if (key !== undefined && f.schema === undefined) {
|
|
182
|
+
f.name = key;
|
|
183
|
+
f.schema = message;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
129
187
|
export function buildInput(name, fields, description) {
|
|
130
188
|
const message = buildMessage(name, DtoDirection.Input, fields, description);
|
|
131
189
|
// Rule A — set optionality from the DB column rule (skips fields the author
|
|
@@ -164,28 +222,25 @@ export function buildPk(name, fields, description) {
|
|
|
164
222
|
}
|
|
165
223
|
return message;
|
|
166
224
|
}
|
|
167
|
-
// Type guard instead of a plain discriminant check: TableSchema.type is declared
|
|
168
|
-
// as a class property, so TS widens it to string and cannot narrow the union.
|
|
169
|
-
function isThirdMethod(source) {
|
|
170
|
-
return source.type === 'thirdMethod';
|
|
171
|
-
}
|
|
172
225
|
function ownsField(source, field) {
|
|
173
|
-
if (
|
|
174
|
-
return Object.values(source.fields).
|
|
175
|
-
|
|
226
|
+
if (isDtoMessage(source)) {
|
|
227
|
+
return Object.values(source.fields).some((df) => df.field === field);
|
|
228
|
+
}
|
|
229
|
+
return Object.values(source.columns).some((c) => c === field);
|
|
176
230
|
}
|
|
177
|
-
/** Project fields from a field-collection source (table,
|
|
178
|
-
*
|
|
231
|
+
/** Project fields from a field-collection source (table, DTO message or
|
|
232
|
+
* entity) and wrap them as DTO fields (aligned with dto.from). Shared Field
|
|
233
|
+
* instances keep their original identity — the projection references them. */
|
|
179
234
|
export function from(source, fields) {
|
|
180
235
|
const out = {};
|
|
181
236
|
for (const field of fields) {
|
|
182
237
|
if (!ownsField(source, field)) {
|
|
183
|
-
throw new Error(`dto.from(${source.name}): field ${field.name} does not belong to this ${source.type}`);
|
|
238
|
+
throw new Error(`dto.from(${source.name}): field ${field.name} does not belong to this ${isDtoMessage(source) ? 'dto' : source.type}`);
|
|
184
239
|
}
|
|
185
240
|
// DB columns map to camelCase interface names (mer_id → merId); aggregate
|
|
186
|
-
// field names are already camel and pass through;
|
|
241
|
+
// field names are already camel and pass through; DTO message fields are
|
|
187
242
|
// protocol names themselves and stay untouched.
|
|
188
|
-
out[
|
|
243
|
+
out[isDtoMessage(source) ? field.name : toCamelCase(field.name)] = dtoField(field);
|
|
189
244
|
}
|
|
190
245
|
return out;
|
|
191
246
|
}
|
package/dist/entity.d.ts
CHANGED
|
@@ -13,8 +13,9 @@ export interface EntitySchema extends SchemaBase {
|
|
|
13
13
|
/** The backend api module this entity belongs to (shared instance from
|
|
14
14
|
* project.config.ts apis). Entities are always backend-side. */
|
|
15
15
|
api: ProjectApiSchema;
|
|
16
|
-
/** The frontend app this entity belongs to (shared instance from project.config).
|
|
17
|
-
|
|
16
|
+
/** The frontend app this entity belongs to (shared instance from project.config).
|
|
17
|
+
* Unset = api-level common domain entity shared by all modules of the api. */
|
|
18
|
+
app?: FrontAppSchema;
|
|
18
19
|
/** The columns of this row object: main-table columns, external reference
|
|
19
20
|
* columns, and — for aggregate result entities — aggField columns
|
|
20
21
|
* (count/sum/avg outputs). */
|
|
@@ -23,7 +24,7 @@ export interface EntitySchema extends SchemaBase {
|
|
|
23
24
|
export declare function defineEntity(options: {
|
|
24
25
|
name: string;
|
|
25
26
|
api: ProjectApiSchema;
|
|
26
|
-
app
|
|
27
|
+
app?: FrontAppSchema;
|
|
27
28
|
columns: Field[];
|
|
28
29
|
description?: string;
|
|
29
30
|
}): EntitySchema;
|
package/dist/entity.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function defineEntity(options) {
|
|
2
|
-
if (!options.api.apps.includes(options.app)) {
|
|
2
|
+
if (options.app && !options.api.apps.includes(options.app)) {
|
|
3
3
|
throw new Error(`entity ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
|
|
4
4
|
}
|
|
5
5
|
return {
|
package/dist/filter.d.ts
CHANGED
|
@@ -21,10 +21,12 @@ export interface FilterSchema extends SchemaBase {
|
|
|
21
21
|
type: 'filter';
|
|
22
22
|
/** The backend api module this filter belongs to (shared instance from
|
|
23
23
|
* project.config.ts apis). Filters are backend-side, so storage is
|
|
24
|
-
* filter_schema/{api.name}/{app.name}/filter
|
|
24
|
+
* filter_schema/{api.name}/{app.name}/filter/ — app unset = the api-level
|
|
25
|
+
* common domain layer, stored at filter_schema/{api.name}/common/filter/. */
|
|
25
26
|
api: ProjectApiSchema;
|
|
26
|
-
/** The frontend app this filter belongs to (shared instance from project.config).
|
|
27
|
-
|
|
27
|
+
/** The frontend app this filter belongs to (shared instance from project.config).
|
|
28
|
+
* Unset = api-level common domain filter shared by all modules of the api. */
|
|
29
|
+
app?: FrontAppSchema;
|
|
28
30
|
/** AND-combined conditions (may be empty when keyword is present). */
|
|
29
31
|
conditions: FilterCondition[];
|
|
30
32
|
/** Fuzzy keyword search: one input value matched against multiple columns
|
|
@@ -36,7 +38,7 @@ export interface FilterSchema extends SchemaBase {
|
|
|
36
38
|
export declare function defineFilter(options: {
|
|
37
39
|
name: string;
|
|
38
40
|
api: ProjectApiSchema;
|
|
39
|
-
app
|
|
41
|
+
app?: FrontAppSchema;
|
|
40
42
|
conditions?: FilterCondition[];
|
|
41
43
|
keyword?: {
|
|
42
44
|
columns: Field[];
|
package/dist/filter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function defineFilter(options) {
|
|
2
|
-
if (!options.api.apps.includes(options.app)) {
|
|
2
|
+
if (options.app && !options.api.apps.includes(options.app)) {
|
|
3
3
|
throw new Error(`filter ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
|
|
4
4
|
}
|
|
5
5
|
const conditions = options.conditions ?? [];
|
package/dist/flow-script.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// The IR stays the single executable model (mermaid, must-analysis, throws
|
|
10
10
|
// coverage, service contracts all consume it); this layer is a lowering, not
|
|
11
11
|
// a parallel model.
|
|
12
|
-
import { defineFlow, defineSlots, edge, guard, ifNode, isCall, isEnd, isFlowNode, isGuard, isIfNode, methodOf, node, tryNode, } from './flow.js';
|
|
12
|
+
import { defineFlow, defineSlots, edge, guard, ifNode, isCall, isEnd, isFlowNode, isFlowSlot, isGuard, isIfNode, methodOf, node, tryNode, } from './flow.js';
|
|
13
13
|
/** Call a method as a statement or (in IF position) as a utils predicate. */
|
|
14
14
|
export function invoke(method, args, result) {
|
|
15
15
|
return { kind: 'invoke', method, args, result };
|
|
@@ -133,7 +133,7 @@ function addUsed(ctx, slot) {
|
|
|
133
133
|
}
|
|
134
134
|
function addConditionUsed(ctx, c) {
|
|
135
135
|
if (!isCall(c)) {
|
|
136
|
-
addUsed(ctx, c.field.slot);
|
|
136
|
+
addUsed(ctx, isFlowSlot(c.field) ? c.field : c.field.slot);
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
for (const t of c.args ?? [])
|
|
@@ -175,6 +175,9 @@ function methodThrows(m) {
|
|
|
175
175
|
* label when THROW carries no message). */
|
|
176
176
|
function renderCondition(c) {
|
|
177
177
|
if (!isCall(c)) {
|
|
178
|
+
if (isFlowSlot(c.field)) {
|
|
179
|
+
return c.op === 'isNull' ? `${c.field.name} is null` : `${c.field.name} is not null`;
|
|
180
|
+
}
|
|
178
181
|
const field = c.field.field;
|
|
179
182
|
const ref = `${c.field.slot.name}.${field.name}`;
|
|
180
183
|
switch (c.op) {
|
|
@@ -473,6 +476,9 @@ function rewriteSlots(nodes, edges, slots, entrySlots) {
|
|
|
473
476
|
};
|
|
474
477
|
const cond = (c) => {
|
|
475
478
|
if (!isCall(c)) {
|
|
479
|
+
if (isFlowSlot(c.field)) {
|
|
480
|
+
return { kind: 'comparison', op: c.op, field: map(c.field), value: c.value };
|
|
481
|
+
}
|
|
476
482
|
return { kind: 'comparison', op: c.op, field: { slot: map(c.field.slot), field: c.field.field }, value: c.value };
|
|
477
483
|
}
|
|
478
484
|
return { method: c.method, args: c.args?.map(map), result: c.result ? map(c.result) : undefined };
|
package/dist/flow.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ import type { ServiceMethodSchema } from './service.js';
|
|
|
9
9
|
import type { ThirdServiceMethodSchema } from './third-service.js';
|
|
10
10
|
import type { UtilsMethodSchema } from './utils.js';
|
|
11
11
|
/** A method a flow step can invoke: contract references, or a pure descriptor
|
|
12
|
-
* (MethodSchema) for the period before the contract file exists.
|
|
12
|
+
* (MethodSchema) for the period before the contract file exists.
|
|
13
|
+
* Third-party methods are their own contract (ThirdServiceMethodSchema) —
|
|
14
|
+
* they can never bind a flow. */
|
|
13
15
|
export type FlowMethodRef = MethodSchema | ConvertMethodSchema | DaoMethodSchema | ServiceMethodSchema | ThirdServiceMethodSchema | UtilsMethodSchema;
|
|
14
16
|
/** One named data slot of a flow — a register holding a whole object (like a
|
|
15
17
|
* compiler's register file). The built-in args slot carries the flow input
|
|
@@ -85,7 +87,9 @@ export type CompareOp = 'lt' | 'le' | 'gt' | 'ge' | 'eq' | 'ne' | 'isNull' | 'is
|
|
|
85
87
|
export interface Comparison {
|
|
86
88
|
kind: 'comparison';
|
|
87
89
|
op: CompareOp;
|
|
88
|
-
field
|
|
90
|
+
/** Field-level comparisons bind a slot field; slot-level null checks
|
|
91
|
+
* (isNull/isNotNull on the slot itself) bind the slot directly. */
|
|
92
|
+
field: SlotFieldRef | FlowSlot;
|
|
89
93
|
/** Compared-against value; null checks take none. */
|
|
90
94
|
value?: string | number | EnumValue;
|
|
91
95
|
}
|
|
@@ -93,6 +97,10 @@ export interface Comparison {
|
|
|
93
97
|
* (optional): a utils predicate call (its boolean result decides) or a field
|
|
94
98
|
* comparison. */
|
|
95
99
|
export type GuardCondition = FlowCall | Comparison;
|
|
100
|
+
/** True when the condition operand is the slot itself (slot-level null check),
|
|
101
|
+
* not a field access on it. Slot metadata (name) lives on the proxy target and
|
|
102
|
+
* reads without field interception; a field access resolves to { slot, field }. */
|
|
103
|
+
export declare function isFlowSlot(v: unknown): v is FlowSlot;
|
|
96
104
|
export declare function lt(field: unknown, value: string | number): Comparison;
|
|
97
105
|
export declare function le(field: unknown, value: string | number): Comparison;
|
|
98
106
|
export declare function gt(field: unknown, value: string | number): Comparison;
|
package/dist/flow.js
CHANGED
|
@@ -20,7 +20,8 @@ export function defineSlots(slots) {
|
|
|
20
20
|
prop in target) {
|
|
21
21
|
return Reflect.get(target, prop, receiver);
|
|
22
22
|
}
|
|
23
|
-
const
|
|
23
|
+
const typeObj = target.type;
|
|
24
|
+
const fields = slotFields(typeObj);
|
|
24
25
|
if (fields !== undefined && Object.prototype.hasOwnProperty.call(fields, prop)) {
|
|
25
26
|
return { slot: proxy, field: fields[prop] };
|
|
26
27
|
}
|
|
@@ -48,7 +49,7 @@ export function defineSlots(slots) {
|
|
|
48
49
|
// unreachable via dot access (slots.args.type reads the message, not the
|
|
49
50
|
// field) — warn instead of failing: the field may never be needed.
|
|
50
51
|
function warnShadowedFields(slot) {
|
|
51
|
-
const fields = slot.type
|
|
52
|
+
const fields = slotFields(slot.type);
|
|
52
53
|
if (fields === undefined)
|
|
53
54
|
return;
|
|
54
55
|
const shadowed = ['type', 'name', 'description'].filter((k) => k in fields);
|
|
@@ -56,6 +57,19 @@ function warnShadowedFields(slot) {
|
|
|
56
57
|
console.warn(`slot "${slot.name}": message fields ${shadowed.join(', ')} are shadowed by slot metadata and unreachable via field access`);
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
/** Field map of a slot's declared type: dto/third messages carry `fields`,
|
|
61
|
+
* entity rows carry `columns` (named Fields). */
|
|
62
|
+
function slotFields(type) {
|
|
63
|
+
if (typeof type !== 'object' || type === null)
|
|
64
|
+
return undefined;
|
|
65
|
+
const t = type;
|
|
66
|
+
if (t.fields !== undefined)
|
|
67
|
+
return t.fields;
|
|
68
|
+
if (t.columns !== undefined) {
|
|
69
|
+
return Object.fromEntries(t.columns.map((c) => [c.name, c]));
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
59
73
|
export function invoke(method, options = {}) {
|
|
60
74
|
return { method, args: options.args, result: options.result };
|
|
61
75
|
}
|
|
@@ -66,7 +80,22 @@ export function isCall(m) {
|
|
|
66
80
|
export function methodOf(m) {
|
|
67
81
|
return isCall(m) ? m.method : m;
|
|
68
82
|
}
|
|
83
|
+
/** True when the condition operand is the slot itself (slot-level null check),
|
|
84
|
+
* not a field access on it. Slot metadata (name) lives on the proxy target and
|
|
85
|
+
* reads without field interception; a field access resolves to { slot, field }. */
|
|
86
|
+
export function isFlowSlot(v) {
|
|
87
|
+
return typeof v === 'object' && v !== null && typeof v.name === 'string';
|
|
88
|
+
}
|
|
69
89
|
function comparison(op, field, value) {
|
|
90
|
+
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`);
|
|
93
|
+
}
|
|
94
|
+
if (value !== undefined) {
|
|
95
|
+
throw new Error(`${op}: takes no value`);
|
|
96
|
+
}
|
|
97
|
+
return { kind: 'comparison', op, field, value };
|
|
98
|
+
}
|
|
70
99
|
const ref = field;
|
|
71
100
|
if (typeof ref !== 'object' || ref === null || typeof ref.slot !== 'object' || typeof ref.field !== 'object') {
|
|
72
101
|
throw new Error(`${op}: field must be a slot field access like slots.args.amt`);
|
|
@@ -471,6 +500,16 @@ function validateGuardChecks(schema) {
|
|
|
471
500
|
const COMPARE_OPS = ['lt', 'le', 'gt', 'ge', 'eq', 'ne', 'isNull', 'isNotNull'];
|
|
472
501
|
function validateCondition(where, c) {
|
|
473
502
|
if (!isCall(c)) {
|
|
503
|
+
if (isFlowSlot(c.field)) {
|
|
504
|
+
const nullOp = c.op === 'isNull' || c.op === 'isNotNull';
|
|
505
|
+
if (!nullOp) {
|
|
506
|
+
throw new Error(`${where}: a slot-level check only supports isNull/isNotNull`);
|
|
507
|
+
}
|
|
508
|
+
if (c.value !== undefined) {
|
|
509
|
+
throw new Error(`${where}: ${c.op} takes no value`);
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
474
513
|
const f = c.field;
|
|
475
514
|
if (typeof f !== 'object' || f === null || typeof f.slot !== 'object' || typeof f.field !== 'object') {
|
|
476
515
|
throw new Error(`${where}: ${c.op} field must be a slot field access like slots.args.amt`);
|
|
@@ -502,10 +541,11 @@ function validateCondition(where, c) {
|
|
|
502
541
|
throw new Error(`${where}: a predicate call cannot bind a result slot`);
|
|
503
542
|
}
|
|
504
543
|
}
|
|
505
|
-
/** Slots a condition reads: a comparison's field slot or
|
|
544
|
+
/** 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. */
|
|
506
546
|
function conditionSlots(c) {
|
|
507
547
|
if (!isCall(c))
|
|
508
|
-
return [c.field.slot];
|
|
548
|
+
return [isFlowSlot(c.field) ? c.field : c.field.slot];
|
|
509
549
|
return c.args ?? [];
|
|
510
550
|
}
|
|
511
551
|
// ifNode rules: at least one case, every condition well-formed, and targets
|
package/dist/mermaid-driver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isCall, methodOf } from './flow.js';
|
|
1
|
+
import { isCall, 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
|
|
@@ -215,7 +215,7 @@ function renderDataLine(n, outgoing) {
|
|
|
215
215
|
if (c === undefined)
|
|
216
216
|
return;
|
|
217
217
|
if (!isCall(c)) {
|
|
218
|
-
reads.add(c.field.slot.name);
|
|
218
|
+
reads.add(isFlowSlot(c.field) ? c.field.name : c.field.slot.name);
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
221
221
|
for (const t of c.args ?? [])
|