@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.
- 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 +17 -8
- package/dist/dto.js +75 -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 +86 -21
- 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/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,
|
|
239
|
-
*
|
|
240
|
-
|
|
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 (
|
|
250
|
-
|
|
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,
|
|
254
|
-
*
|
|
255
|
-
|
|
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;
|
|
327
|
+
// field names are already camel and pass through; DTO message fields are
|
|
263
328
|
// protocol names themselves and stay untouched.
|
|
264
|
-
out[
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
app
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
keyword
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
}
|