@pylonts/dsl 1.1.12 → 1.1.14

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 (48) hide show
  1. package/dist/convert.d.ts +6 -8
  2. package/dist/curd.js +1 -1
  3. package/dist/dao.d.ts +10 -7
  4. package/dist/dao.js +20 -7
  5. package/dist/dsl.d.ts +18 -1
  6. package/dist/dsl.js +40 -0
  7. package/dist/dto.d.ts +17 -8
  8. package/dist/dto.js +75 -13
  9. package/dist/entity.d.ts +4 -3
  10. package/dist/entity.js +1 -1
  11. package/dist/filter.d.ts +6 -4
  12. package/dist/filter.js +1 -1
  13. package/dist/flow-script.js +8 -2
  14. package/dist/flow.d.ts +10 -2
  15. package/dist/flow.js +44 -4
  16. package/dist/mermaid-driver.js +2 -2
  17. package/dist/service.d.ts +13 -8
  18. package/dist/service.js +1 -1
  19. package/dist/third-service.d.ts +10 -53
  20. package/dist/third-service.js +3 -78
  21. package/dist/typebox-driver.d.ts +0 -6
  22. package/dist/typebox-driver.js +8 -36
  23. package/dist/utils.d.ts +2 -2
  24. package/docs/curd.md +146 -146
  25. package/docs/dao-generation.md +477 -477
  26. package/docs/project.md +31 -31
  27. package/docs/token.md +326 -326
  28. package/package.json +1 -1
  29. package/src/action.ts +51 -51
  30. package/src/controller.ts +53 -53
  31. package/src/convert.ts +76 -78
  32. package/src/curd.ts +104 -104
  33. package/src/dao.ts +504 -485
  34. package/src/dsl.ts +296 -257
  35. package/src/dto.ts +86 -21
  36. package/src/entity.ts +43 -42
  37. package/src/expr.ts +64 -64
  38. package/src/filter.ts +71 -69
  39. package/src/flow-script.ts +702 -695
  40. package/src/flow.ts +1272 -1226
  41. package/src/index.ts +46 -46
  42. package/src/mermaid-driver.ts +339 -339
  43. package/src/mysql-driver.ts +108 -108
  44. package/src/project.ts +138 -138
  45. package/src/service.ts +112 -107
  46. package/src/third-service.ts +68 -191
  47. package/src/typebox-driver.ts +234 -268
  48. 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, 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;
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 SchemaBase {
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 app '${curd.list.filter.app.name}' but the curd belongs to app '${curd.app.name}'`);
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 exactly one frontend app. */
8
- export interface DaoSchema extends SchemaBase {
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
- app: FrontAppSchema;
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: FrontAppSchema;
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 tenant = dao.app.tenant;
8
- if (!tenant)
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 '${dao.app.name}' tenant table '${tenant.name}' must declare a single-column primaryKey and a phrase (tenant column name = {phrase}_{pk})`);
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.type === 'aggregate') {
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) && (ref.api !== options.api || ref.app !== options.app)) {
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). */
@@ -48,6 +47,8 @@ export declare class DtoField implements SchemaBase {
48
47
  operator?: Operator;
49
48
  /** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
50
49
  default?: unknown;
50
+ /** Optional reference to another DtoField — this field reuses the referenced field's type/constraints */
51
+ ref?: DtoField;
51
52
  constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef);
52
53
  setPattern(value: string): this;
53
54
  setDescription(value: string): this;
@@ -57,6 +58,8 @@ export declare class DtoField implements SchemaBase {
57
58
  setOptional(value: boolean): this;
58
59
  /** Set a default value — emitted as a TypeBox schema default annotation */
59
60
  setDefault(value: unknown): this;
61
+ /** Reference another DtoField — this field reuses the referenced field's type/constraints */
62
+ setRef(value: DtoField): this;
60
63
  /** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
61
64
  setOperator(value: Operator): this;
62
65
  /** optional 优先于 field.optional */
@@ -90,7 +93,11 @@ export declare class DtoMessage implements CollectionSchemaBase {
90
93
  /** 引用已存在的 TypeBox base schema,例如 include({ from: '@pylonts/core', name: 'PageRequest' }) */
91
94
  include(...refs: ImportRef[]): this;
92
95
  }
93
- export declare function dtoField(field: Field): DtoField;
96
+ export declare function dtoField(field: Field | DtoArrayFieldDef | DtoObjectFieldDef): DtoField;
97
+ /** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
98
+ export declare function isDtoMessage(v: unknown): v is DtoMessage;
99
+ /** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
100
+ export declare function isDtoField(v: unknown): v is DtoField;
94
101
  export declare function dtoArrayField(def: {
95
102
  items: DtoField | DtoMessage;
96
103
  } & Omit<BaseField, 'name'>): DtoArrayField;
@@ -101,9 +108,11 @@ export declare function buildInput(name: string, fields: Record<string, DtoField
101
108
  export declare function buildOutput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
102
109
  export declare function buildQuery(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage;
103
110
  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, a third-party
105
- * method message, or an entity (which may carry aggregate fields). */
106
- export type DtoFieldSource = TableSchema | ThirdMethodSchema | EntitySchema;
107
- /** Project fields from a field-collection source (table, third-party method
108
- * message or entity) and wrap them as DTO fields (aligned with dto.from). */
109
- export declare function from(source: DtoFieldSource, fields: Field[]): Record<string, DtoField>;
111
+ /** Field-collection source a DTO can project from: a DB table, another DTO
112
+ * message (protocol fields keep their names), or an entity (which may carry
113
+ * aggregate fields). */
114
+ export type DtoFieldSource = TableSchema | DtoMessage | EntitySchema;
115
+ /** Project fields from a field-collection source (table, DTO message or
116
+ * entity) and wrap them as DTO fields (aligned with dto.from). Shared Field
117
+ * instances keep their original identity — the projection references them. */
118
+ 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(数据库列名)含义不同。
@@ -13,6 +14,8 @@ export class DtoField {
13
14
  operator;
14
15
  /** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
15
16
  default;
17
+ /** Optional reference to another DtoField — this field reuses the referenced field's type/constraints */
18
+ ref;
16
19
  constructor(field) {
17
20
  this.name = '';
18
21
  this.field = field;
@@ -41,6 +44,11 @@ export class DtoField {
41
44
  this.default = value;
42
45
  return this;
43
46
  }
47
+ /** Reference another DtoField — this field reuses the referenced field's type/constraints */
48
+ setRef(value) {
49
+ this.ref = value;
50
+ return this;
51
+ }
44
52
  /** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
45
53
  setOperator(value) {
46
54
  this.operator = value;
@@ -95,6 +103,18 @@ export class DtoMessage {
95
103
  export function dtoField(field) {
96
104
  return new DtoField(field);
97
105
  }
106
+ /** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
107
+ export function isDtoMessage(v) {
108
+ if (typeof v !== 'object' || v === null)
109
+ return false;
110
+ return v.type === 'dto';
111
+ }
112
+ /** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
113
+ export function isDtoField(v) {
114
+ if (typeof v !== 'object' || v === null)
115
+ return false;
116
+ return 'field' in v && !('type' in v);
117
+ }
98
118
  export function dtoArrayField(def) {
99
119
  // Items stay as-is: an inline DtoField is rendered inline, a DtoMessage is
100
120
  // referenced by name (the driver renders Type.Array(<DtoName>)).
@@ -123,9 +143,54 @@ function buildMessage(name, direction, fields, description) {
123
143
  df.field.name = key;
124
144
  df.field.schema = message;
125
145
  }
146
+ writeBackNested(df, message);
126
147
  }
127
148
  return message;
128
149
  }
150
+ /** Write back name/schema on nested DTO fields (array items, object
151
+ * properties) — both plain-Field containers (objectField/arrayField) and
152
+ * DtoField containers (dtoObjectField/dtoArrayField). DtoMessage item
153
+ * references are skipped — they carry their own identity. */
154
+ function writeBackNested(df, message) {
155
+ const f = df.field;
156
+ if (f.type === 'array') {
157
+ const items = f.items;
158
+ if (isDtoMessage(items))
159
+ return;
160
+ if (isDtoField(items)) {
161
+ writeBackNested(items, message);
162
+ return;
163
+ }
164
+ walkContainer(items, writeBackLeaf(message));
165
+ return;
166
+ }
167
+ if (f.type === 'object') {
168
+ for (const [key, child] of Object.entries(f.properties)) {
169
+ if (isDtoField(child)) {
170
+ child.name = key;
171
+ child.schema = message;
172
+ if (child.field.schema === undefined) {
173
+ child.field.name = key;
174
+ child.field.schema = message;
175
+ }
176
+ writeBackNested(child, message);
177
+ }
178
+ else {
179
+ writeBackLeaf(message)(child, key);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ /** Name/schema write-back for a plain Field (own fields only — shared
185
+ * instances keep their original identity). */
186
+ function writeBackLeaf(message) {
187
+ return (f, key) => {
188
+ if (key !== undefined && f.schema === undefined) {
189
+ f.name = key;
190
+ f.schema = message;
191
+ }
192
+ };
193
+ }
129
194
  export function buildInput(name, fields, description) {
130
195
  const message = buildMessage(name, DtoDirection.Input, fields, description);
131
196
  // Rule A — set optionality from the DB column rule (skips fields the author
@@ -164,28 +229,25 @@ export function buildPk(name, fields, description) {
164
229
  }
165
230
  return message;
166
231
  }
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
232
  function ownsField(source, field) {
173
- if (isThirdMethod(source))
174
- return Object.values(source.fields).includes(field);
175
- return Object.values(source.columns).includes(field);
233
+ if (isDtoMessage(source)) {
234
+ return Object.values(source.fields).some((df) => df.field === field);
235
+ }
236
+ return Object.values(source.columns).some((c) => c === field);
176
237
  }
177
- /** Project fields from a field-collection source (table, third-party method
178
- * message or entity) and wrap them as DTO fields (aligned with dto.from). */
238
+ /** Project fields from a field-collection source (table, DTO message or
239
+ * entity) and wrap them as DTO fields (aligned with dto.from). Shared Field
240
+ * instances keep their original identity — the projection references them. */
179
241
  export function from(source, fields) {
180
242
  const out = {};
181
243
  for (const field of fields) {
182
244
  if (!ownsField(source, field)) {
183
- throw new Error(`dto.from(${source.name}): field ${field.name} does not belong to this ${source.type}`);
245
+ throw new Error(`dto.from(${source.name}): field ${field.name} does not belong to this ${isDtoMessage(source) ? 'dto' : source.type}`);
184
246
  }
185
247
  // DB columns map to camelCase interface names (mer_id → merId); aggregate
186
- // field names are already camel and pass through; wire-format names are
248
+ // field names are already camel and pass through; DTO message fields are
187
249
  // protocol names themselves and stay untouched.
188
- out[isThirdMethod(source) ? field.name : toCamelCase(field.name)] = dtoField(field);
250
+ out[isDtoMessage(source) ? field.name : toCamelCase(field.name)] = dtoField(field);
189
251
  }
190
252
  return out;
191
253
  }
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
- app: FrontAppSchema;
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: FrontAppSchema;
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
- app: FrontAppSchema;
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: FrontAppSchema;
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 ?? [];
@@ -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: SlotFieldRef;
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;