@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/src/dto.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase } from './dsl.js';
1
+ import { BaseField, CollectionSchemaBase, Field, Operator, SchemaBase, walkContainer } from './dsl.js';
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
  import { toCamelCase } from '@pylonts/core';
7
6
 
8
7
  // Interface (DTO) field definitions.
@@ -60,6 +59,8 @@ export class DtoField implements SchemaBase {
60
59
  operator?: Operator;
61
60
  /** TypeBox default annotation (API contract level); falls back to field.default (DB default) */
62
61
  default?: unknown;
62
+ /** Optional reference to another DtoField — this field reuses the referenced field's type/constraints */
63
+ ref?: DtoField;
63
64
 
64
65
  constructor(field: Field | DtoArrayFieldDef | DtoObjectFieldDef) {
65
66
  this.name = '';
@@ -96,6 +97,12 @@ export class DtoField implements SchemaBase {
96
97
  return this;
97
98
  }
98
99
 
100
+ /** Reference another DtoField — this field reuses the referenced field's type/constraints */
101
+ setRef(value: DtoField): this {
102
+ this.ref = value;
103
+ return this;
104
+ }
105
+
99
106
  /** 查询比较操作符(query 方向字段)。Rule B: 查询字段恒为可选 */
100
107
  setOperator(value: Operator): this {
101
108
  this.operator = value;
@@ -157,10 +164,22 @@ export class DtoMessage implements CollectionSchemaBase {
157
164
  }
158
165
  }
159
166
 
160
- export function dtoField(field: Field): DtoField {
167
+ export function dtoField(field: Field | DtoArrayFieldDef | DtoObjectFieldDef): DtoField {
161
168
  return new DtoField(field);
162
169
  }
163
170
 
171
+ /** Structural check — DtoMessage instances may come from a different module copy, so instanceof is unreliable. */
172
+ export function isDtoMessage(v: unknown): v is DtoMessage {
173
+ if (typeof v !== 'object' || v === null) return false;
174
+ return (v as Record<string, unknown>).type === 'dto';
175
+ }
176
+
177
+ /** Structural check — a DtoField wraps a Field in a .field property and has no .type of its own. */
178
+ export function isDtoField(v: unknown): v is DtoField {
179
+ if (typeof v !== 'object' || v === null) return false;
180
+ return 'field' in v && !('type' in v);
181
+ }
182
+
164
183
  export function dtoArrayField(def: { items: DtoField | DtoMessage } & Omit<BaseField, 'name'>): DtoArrayField {
165
184
  // Items stay as-is: an inline DtoField is rendered inline, a DtoMessage is
166
185
  // referenced by name (the driver renders Type.Array(<DtoName>)).
@@ -193,10 +212,55 @@ function buildMessage(name: string, direction: DtoDirection, fields: Record<stri
193
212
  df.field.name = key;
194
213
  df.field.schema = message;
195
214
  }
215
+ writeBackNested(df, message);
196
216
  }
197
217
  return message;
198
218
  }
199
219
 
220
+ /** Write back name/schema on nested DTO fields (array items, object
221
+ * properties) — both plain-Field containers (objectField/arrayField) and
222
+ * DtoField containers (dtoObjectField/dtoArrayField). DtoMessage item
223
+ * references are skipped — they carry their own identity. */
224
+ function writeBackNested(df: DtoField, message: DtoMessage): void {
225
+ const f = df.field;
226
+ if (f.type === 'array') {
227
+ const items = f.items;
228
+ if (isDtoMessage(items)) return;
229
+ if (isDtoField(items)) {
230
+ writeBackNested(items, message);
231
+ return;
232
+ }
233
+ walkContainer(items, writeBackLeaf(message));
234
+ return;
235
+ }
236
+ if (f.type === 'object') {
237
+ for (const [key, child] of Object.entries(f.properties)) {
238
+ if (isDtoField(child)) {
239
+ child.name = key;
240
+ child.schema = message;
241
+ if (child.field.schema === undefined) {
242
+ child.field.name = key;
243
+ child.field.schema = message;
244
+ }
245
+ writeBackNested(child, message);
246
+ } else {
247
+ writeBackLeaf(message)(child, key);
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ /** Name/schema write-back for a plain Field (own fields only — shared
254
+ * instances keep their original identity). */
255
+ function writeBackLeaf(message: DtoMessage): (f: Field, key?: string) => void {
256
+ return (f, key) => {
257
+ if (key !== undefined && f.schema === undefined) {
258
+ f.name = key;
259
+ f.schema = message;
260
+ }
261
+ };
262
+ }
263
+
200
264
  export function buildInput(name: string, fields: Record<string, DtoField>, description?: string): DtoMessage {
201
265
  const message = buildMessage(name, DtoDirection.Input, fields, description);
202
266
  // Rule A — set optionality from the DB column rule (skips fields the author
@@ -235,33 +299,34 @@ export function buildPk(name: string, fields: Record<string, DtoField>, descript
235
299
  return message;
236
300
  }
237
301
 
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;
241
-
242
- // Type guard instead of a plain discriminant check: TableSchema.type is declared
243
- // as a class property, so TS widens it to string and cannot narrow the union.
244
- function isThirdMethod(source: DtoFieldSource): source is ThirdMethodSchema {
245
- return source.type === 'thirdMethod';
246
- }
302
+ /** Field-collection source a DTO can project from: a DB table, another DTO
303
+ * message (protocol fields keep their names), or an entity (which may carry
304
+ * aggregate fields). */
305
+ export type DtoFieldSource = TableSchema | DtoMessage | EntitySchema;
247
306
 
248
- function ownsField(source: DtoFieldSource, field: Field): boolean {
249
- if (isThirdMethod(source)) return Object.values(source.fields).includes(field);
250
- return Object.values(source.columns).includes(field);
307
+ function ownsField(source: DtoFieldSource, field: Field | DtoArrayFieldDef | DtoObjectFieldDef): boolean {
308
+ if (isDtoMessage(source)) {
309
+ return Object.values(source.fields).some((df) => df.field === field);
310
+ }
311
+ return Object.values(source.columns).some((c) => c === field);
251
312
  }
252
313
 
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). */
255
- export function from(source: DtoFieldSource, fields: Field[]): Record<string, DtoField> {
314
+ /** Project fields from a field-collection source (table, DTO message or
315
+ * entity) and wrap them as DTO fields (aligned with dto.from). Shared Field
316
+ * instances keep their original identity the projection references them. */
317
+ export function from(
318
+ source: DtoFieldSource,
319
+ fields: (Field | DtoArrayFieldDef | DtoObjectFieldDef)[],
320
+ ): Record<string, DtoField> {
256
321
  const out: Record<string, DtoField> = {};
257
322
  for (const field of fields) {
258
323
  if (!ownsField(source, field)) {
259
- throw new Error(`dto.from(${source.name}): field ${field.name} does not belong to this ${source.type}`);
324
+ throw new Error(`dto.from(${source.name}): field ${field.name} does not belong to this ${isDtoMessage(source) ? 'dto' : source.type}`);
260
325
  }
261
326
  // DB columns map to camelCase interface names (mer_id → merId); aggregate
262
- // field names are already camel and pass through; wire-format names are
327
+ // field names are already camel and pass through; DTO message fields are
263
328
  // protocol names themselves and stay untouched.
264
- out[isThirdMethod(source) ? field.name : toCamelCase(field.name)] = dtoField(field);
329
+ out[isDtoMessage(source) ? field.name : toCamelCase(field.name)] = dtoField(field);
265
330
  }
266
331
  return out;
267
332
  }
package/src/entity.ts CHANGED
@@ -1,43 +1,44 @@
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
- };
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
+ * Unset = api-level common domain entity shared by all modules of the api. */
19
+ app?: FrontAppSchema;
20
+ /** The columns of this row object: main-table columns, external reference
21
+ * columns, and — for aggregate result entities — aggField columns
22
+ * (count/sum/avg outputs). */
23
+ columns: Field[];
24
+ }
25
+
26
+ export function defineEntity(options: {
27
+ name: string;
28
+ api: ProjectApiSchema;
29
+ app?: FrontAppSchema;
30
+ columns: Field[];
31
+ description?: string;
32
+ }): EntitySchema {
33
+ if (options.app && !options.api.apps.includes(options.app)) {
34
+ throw new Error(`entity ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
35
+ }
36
+ return {
37
+ type: 'entity',
38
+ name: options.name,
39
+ description: options.description,
40
+ api: options.api,
41
+ app: options.app,
42
+ columns: options.columns,
43
+ };
43
44
  }
package/src/expr.ts CHANGED
@@ -1,65 +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
- },
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
65
  };
package/src/filter.ts CHANGED
@@ -1,70 +1,72 @@
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
- };
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/ — app unset = the api-level
33
+ * common domain layer, stored at filter_schema/{api.name}/common/filter/. */
34
+ api: ProjectApiSchema;
35
+ /** The frontend app this filter belongs to (shared instance from project.config).
36
+ * Unset = api-level common domain filter shared by all modules of the api. */
37
+ app?: FrontAppSchema;
38
+ /** AND-combined conditions (may be empty when keyword is present). */
39
+ conditions: FilterCondition[];
40
+ /** Fuzzy keyword search: one input value matched against multiple columns
41
+ * via OR-like. Presence drives the keyword query endpoint. */
42
+ keyword?: { columns: Field[] };
43
+ }
44
+
45
+ export function defineFilter(options: {
46
+ name: string;
47
+ api: ProjectApiSchema;
48
+ app?: FrontAppSchema;
49
+ conditions?: FilterCondition[];
50
+ keyword?: { columns: Field[] };
51
+ description?: string;
52
+ }): FilterSchema {
53
+ if (options.app && !options.api.apps.includes(options.app)) {
54
+ throw new Error(`filter ${options.name}: api '${options.api.name}' does not serve app '${options.app.name}'`);
55
+ }
56
+ const conditions = options.conditions ?? [];
57
+ if (conditions.length === 0 && options.keyword === undefined) {
58
+ throw new Error(`filter ${options.name}: conditions and keyword cannot both be empty`);
59
+ }
60
+ if (options.keyword !== undefined && options.keyword.columns.length === 0) {
61
+ throw new Error(`filter ${options.name}: keyword columns must be non-empty`);
62
+ }
63
+ return {
64
+ type: 'filter',
65
+ name: options.name,
66
+ description: options.description,
67
+ api: options.api,
68
+ app: options.app,
69
+ conditions,
70
+ keyword: options.keyword,
71
+ };
70
72
  }