adorn-api 1.0.42 → 1.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -4
- package/dist/adapter/metal-orm/filters.d.ts +7 -12
- package/dist/adapter/metal-orm/filters.js +107 -17
- package/dist/adapter/metal-orm/index.d.ts +2 -1
- package/dist/adapter/metal-orm/index.js +3 -1
- package/dist/adapter/metal-orm/tree-dtos.d.ts +2 -0
- package/dist/adapter/metal-orm/tree-dtos.js +194 -0
- package/dist/adapter/metal-orm/types.d.ts +72 -2
- package/examples/metal-orm-deep-filters/alpha.controller.ts +74 -0
- package/examples/metal-orm-deep-filters/alpha.dtos.ts +93 -0
- package/examples/metal-orm-deep-filters/alpha.entity.ts +15 -0
- package/examples/metal-orm-deep-filters/app.ts +26 -0
- package/examples/metal-orm-deep-filters/bravo.entity.ts +22 -0
- package/examples/metal-orm-deep-filters/charlie.entity.ts +25 -0
- package/examples/metal-orm-deep-filters/db.ts +136 -0
- package/examples/metal-orm-deep-filters/delta.entity.ts +10 -0
- package/examples/metal-orm-deep-filters/index.ts +6 -0
- package/package.json +2 -2
- package/src/adapter/metal-orm/filters.ts +165 -45
- package/src/adapter/metal-orm/types.ts +50 -28
- package/tests/metal-orm-integration/deep-filters.test.ts +104 -0
- package/tests/unit/metal-orm.test.ts +45 -17
package/README.md
CHANGED
|
@@ -309,9 +309,9 @@ export const {
|
|
|
309
309
|
```
|
|
310
310
|
|
|
311
311
|
### 3. Create a CRUD Controller
|
|
312
|
-
|
|
313
|
-
```typescript
|
|
314
|
-
// user.controller.ts
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// user.controller.ts
|
|
315
315
|
import {
|
|
316
316
|
Controller,
|
|
317
317
|
Get,
|
|
@@ -375,10 +375,115 @@ export class UserController {
|
|
|
375
375
|
}
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
// Other CRUD operations...
|
|
378
|
+
// Other CRUD operations...
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Deep Relation Filters
|
|
383
|
+
|
|
384
|
+
`parseFilter` now accepts nested relation paths, so you can filter deep chains like Alpha → Bravo → Charlie → Delta.
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
// alpha.entity.ts
|
|
388
|
+
import { BelongsTo, Column, Entity, HasMany, PrimaryKey, col } from "metal-orm";
|
|
389
|
+
import type { BelongsToReference, HasManyCollection } from "metal-orm";
|
|
390
|
+
|
|
391
|
+
@Entity({ tableName: "alphas" })
|
|
392
|
+
export class Alpha {
|
|
393
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
394
|
+
id!: number;
|
|
395
|
+
|
|
396
|
+
@Column(col.notNull(col.text()))
|
|
397
|
+
name!: string;
|
|
398
|
+
|
|
399
|
+
@HasMany({ target: () => Bravo, foreignKey: "alphaId" })
|
|
400
|
+
bravos!: HasManyCollection<Bravo>;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@Entity({ tableName: "bravos" })
|
|
404
|
+
export class Bravo {
|
|
405
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
406
|
+
id!: number;
|
|
407
|
+
|
|
408
|
+
@Column(col.notNull(col.text()))
|
|
409
|
+
code!: string;
|
|
410
|
+
|
|
411
|
+
@Column(col.notNull(col.int()))
|
|
412
|
+
alphaId!: number;
|
|
413
|
+
|
|
414
|
+
@BelongsTo({ target: () => Alpha, foreignKey: "alphaId" })
|
|
415
|
+
alpha!: BelongsToReference<Alpha>;
|
|
416
|
+
|
|
417
|
+
@HasMany({ target: () => Charlie, foreignKey: "bravoId" })
|
|
418
|
+
charlies!: HasManyCollection<Charlie>;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
@Entity({ tableName: "charlies" })
|
|
422
|
+
export class Charlie {
|
|
423
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
424
|
+
id!: number;
|
|
425
|
+
|
|
426
|
+
@Column(col.notNull(col.int()))
|
|
427
|
+
score!: number;
|
|
428
|
+
|
|
429
|
+
@Column(col.notNull(col.int()))
|
|
430
|
+
bravoId!: number;
|
|
431
|
+
|
|
432
|
+
@Column(col.int())
|
|
433
|
+
deltaId?: number | null;
|
|
434
|
+
|
|
435
|
+
@BelongsTo({ target: () => Bravo, foreignKey: "bravoId" })
|
|
436
|
+
bravo!: BelongsToReference<Bravo>;
|
|
437
|
+
|
|
438
|
+
@BelongsTo({ target: () => Delta, foreignKey: "deltaId" })
|
|
439
|
+
delta?: BelongsToReference<Delta>;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
@Entity({ tableName: "deltas" })
|
|
443
|
+
export class Delta {
|
|
444
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
445
|
+
id!: number;
|
|
446
|
+
|
|
447
|
+
@Column(col.notNull(col.text()))
|
|
448
|
+
name!: string;
|
|
379
449
|
}
|
|
380
450
|
```
|
|
381
451
|
|
|
452
|
+
```typescript
|
|
453
|
+
// alpha.controller.ts (filtering)
|
|
454
|
+
import { parseFilter } from "adorn-api";
|
|
455
|
+
import { applyFilter, selectFromEntity, type WhereInput } from "metal-orm";
|
|
456
|
+
import { Alpha } from "./alpha.entity";
|
|
457
|
+
|
|
458
|
+
const ALPHA_FILTERS = {
|
|
459
|
+
deltaNameContains: {
|
|
460
|
+
field: "bravos.some.charlies.some.delta.name",
|
|
461
|
+
operator: "contains"
|
|
462
|
+
},
|
|
463
|
+
deltaIsMissing: {
|
|
464
|
+
field: "bravos.some.charlies.some.delta",
|
|
465
|
+
operator: "isEmpty"
|
|
466
|
+
},
|
|
467
|
+
charlieScoreGte: {
|
|
468
|
+
field: "bravos.some.charlies.some.score",
|
|
469
|
+
operator: "gte"
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const filters = parseFilter(
|
|
474
|
+
(ctx.query ?? {}) as Record<string, unknown>,
|
|
475
|
+
ALPHA_FILTERS
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
const query = applyFilter(
|
|
479
|
+
selectFromEntity(Alpha),
|
|
480
|
+
Alpha,
|
|
481
|
+
filters as WhereInput<typeof Alpha>
|
|
482
|
+
);
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Example query string: `?deltaNameContains=core&charlieScoreGte=90`
|
|
486
|
+
|
|
382
487
|
### Tree DTOs (Nested Set / MPTT)
|
|
383
488
|
|
|
384
489
|
Metal ORM's tree helpers map cleanly into Adorn. Use `createMetalTreeDtoClasses` to generate DTOs for tree nodes,
|
|
@@ -769,6 +874,7 @@ Check out the `examples/` directory for more comprehensive examples:
|
|
|
769
874
|
- `restful/` - RESTful API with complete CRUD operations
|
|
770
875
|
- `metal-orm-sqlite/` - Metal ORM integration with SQLite
|
|
771
876
|
- `metal-orm-tree/` - Metal ORM tree (nested set) DTO + OpenAPI integration
|
|
877
|
+
- `metal-orm-deep-filters/` - Deep relation filtering example (Alpha → Bravo → Charlie → Delta)
|
|
772
878
|
- `metal-orm-sqlite-music/` - Complex relations with Metal ORM
|
|
773
879
|
- `streaming/` - SSE and streaming responses
|
|
774
880
|
- `openapi/` - OpenAPI documentation customization
|
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
import type { Filter } from "./types";
|
|
1
|
+
import type { Filter, FilterMapping, FilterOperator, ParseFilterOptions } from "./types";
|
|
2
2
|
/**
|
|
3
3
|
* Parses filter parameters from query parameters.
|
|
4
4
|
* @param query - Query parameters
|
|
5
5
|
* @param mappings - Filter field mappings
|
|
6
6
|
* @returns Parsed filter or undefined
|
|
7
7
|
*/
|
|
8
|
-
export declare function parseFilter<T, K extends keyof T>(query: Record<string, unknown> | undefined, mappings: Record<string,
|
|
9
|
-
|
|
10
|
-
operator: "equals" | "contains" | "startsWith" | "endsWith" | "gt" | "gte" | "lt" | "lte";
|
|
11
|
-
}>): Filter<T, K> | undefined;
|
|
8
|
+
export declare function parseFilter<T, K extends keyof T>(query: Record<string, unknown> | undefined, mappings: Record<string, FilterMapping>): Filter<T, K> | undefined;
|
|
9
|
+
export declare function parseFilter<T, K extends keyof T>(options: ParseFilterOptions): Filter<T, K> | undefined;
|
|
12
10
|
/**
|
|
13
11
|
* Creates filter mappings for an entity.
|
|
14
12
|
* @param entity - Entity type
|
|
15
13
|
* @param fields - Array of field definitions
|
|
16
14
|
* @returns Filter mappings
|
|
17
15
|
*/
|
|
18
|
-
export declare function createFilterMappings<T extends Record<string, unknown>>(
|
|
16
|
+
export declare function createFilterMappings<T extends Record<string, unknown>>(_entity: T, fields: Array<{
|
|
19
17
|
queryKey: string;
|
|
20
|
-
field: keyof T;
|
|
21
|
-
operator?:
|
|
22
|
-
}>): Record<string,
|
|
23
|
-
field: keyof T;
|
|
24
|
-
operator: "equals" | "contains" | "startsWith" | "endsWith";
|
|
25
|
-
}>;
|
|
18
|
+
field: (keyof T & string) | string[];
|
|
19
|
+
operator?: FilterOperator;
|
|
20
|
+
}>): Record<string, FilterMapping>;
|
|
@@ -2,29 +2,34 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseFilter = parseFilter;
|
|
4
4
|
exports.createFilterMappings = createFilterMappings;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (!query) {
|
|
5
|
+
function parseFilter(queryOrOptions, mappings) {
|
|
6
|
+
const options = mappings ? undefined : queryOrOptions;
|
|
7
|
+
const query = mappings
|
|
8
|
+
? queryOrOptions
|
|
9
|
+
: options?.query;
|
|
10
|
+
const fieldMappings = mappings ?? options?.fieldMappings;
|
|
11
|
+
if (!query || !fieldMappings) {
|
|
13
12
|
return undefined;
|
|
14
13
|
}
|
|
15
14
|
const filter = {};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!mapping
|
|
15
|
+
let hasValues = false;
|
|
16
|
+
for (const [queryKey, mapping] of Object.entries(fieldMappings)) {
|
|
17
|
+
if (!mapping) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const value = getQueryValue(query, queryKey);
|
|
21
|
+
if (isSkippableValue(value)) {
|
|
19
22
|
continue;
|
|
20
23
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
const operator = mapping.operator ?? "equals";
|
|
25
|
+
const fieldPath = normalizePath(mapping.field);
|
|
26
|
+
if (!fieldPath.length) {
|
|
27
|
+
continue;
|
|
24
28
|
}
|
|
25
|
-
filter
|
|
29
|
+
setFilterValue(filter, fieldPath, operator, value);
|
|
30
|
+
hasValues = true;
|
|
26
31
|
}
|
|
27
|
-
return
|
|
32
|
+
return hasValues ? filter : undefined;
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
35
|
* Creates filter mappings for an entity.
|
|
@@ -32,10 +37,95 @@ function parseFilter(query, mappings) {
|
|
|
32
37
|
* @param fields - Array of field definitions
|
|
33
38
|
* @returns Filter mappings
|
|
34
39
|
*/
|
|
35
|
-
function createFilterMappings(
|
|
40
|
+
function createFilterMappings(_entity, fields) {
|
|
36
41
|
const mappings = {};
|
|
37
42
|
for (const { queryKey, field, operator = "equals" } of fields) {
|
|
38
43
|
mappings[queryKey] = { field, operator };
|
|
39
44
|
}
|
|
40
45
|
return mappings;
|
|
41
46
|
}
|
|
47
|
+
const RELATION_OPERATORS = new Set(["isEmpty", "isNotEmpty"]);
|
|
48
|
+
function normalizePath(path) {
|
|
49
|
+
if (Array.isArray(path)) {
|
|
50
|
+
return path.map(String).filter((segment) => segment.length > 0);
|
|
51
|
+
}
|
|
52
|
+
return splitPath(path);
|
|
53
|
+
}
|
|
54
|
+
function splitPath(path) {
|
|
55
|
+
if (!path) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const normalized = path.replace(/\[(.*?)\]/g, ".$1");
|
|
59
|
+
return normalized
|
|
60
|
+
.split(".")
|
|
61
|
+
.map((segment) => segment.trim())
|
|
62
|
+
.filter((segment) => segment.length > 0);
|
|
63
|
+
}
|
|
64
|
+
function getQueryValue(query, queryKey) {
|
|
65
|
+
if (queryKey in query) {
|
|
66
|
+
return query[queryKey];
|
|
67
|
+
}
|
|
68
|
+
const path = splitPath(queryKey);
|
|
69
|
+
if (!path.length) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
return getValueAtPath(query, path);
|
|
73
|
+
}
|
|
74
|
+
function getValueAtPath(value, path) {
|
|
75
|
+
let current = value;
|
|
76
|
+
for (const segment of path) {
|
|
77
|
+
if (current === null || current === undefined) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (Array.isArray(current)) {
|
|
81
|
+
const index = Number(segment);
|
|
82
|
+
if (!Number.isInteger(index)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
current = current[index];
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (typeof current !== "object") {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
current = current[segment];
|
|
92
|
+
}
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
function isSkippableValue(value) {
|
|
96
|
+
if (value === undefined || value === null || value === "") {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
return value.length === 0;
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
function setFilterValue(filter, fieldPath, operator, value) {
|
|
105
|
+
if (!fieldPath.length) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let cursor = filter;
|
|
109
|
+
if (RELATION_OPERATORS.has(operator)) {
|
|
110
|
+
for (const segment of fieldPath) {
|
|
111
|
+
cursor = ensureObject(cursor, segment);
|
|
112
|
+
}
|
|
113
|
+
cursor[operator] = value;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (let index = 0; index < fieldPath.length - 1; index += 1) {
|
|
117
|
+
cursor = ensureObject(cursor, fieldPath[index]);
|
|
118
|
+
}
|
|
119
|
+
const fieldKey = fieldPath[fieldPath.length - 1];
|
|
120
|
+
const fieldTarget = ensureObject(cursor, fieldKey);
|
|
121
|
+
fieldTarget[operator] = value;
|
|
122
|
+
}
|
|
123
|
+
function ensureObject(container, key) {
|
|
124
|
+
const existing = container[key];
|
|
125
|
+
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
|
126
|
+
return existing;
|
|
127
|
+
}
|
|
128
|
+
const next = {};
|
|
129
|
+
container[key] = next;
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
@@ -3,8 +3,9 @@ export { parsePagination } from "./pagination";
|
|
|
3
3
|
export { parseFilter, createFilterMappings } from "./filters";
|
|
4
4
|
export { createPagedQueryDtoClass, createPagedResponseDtoClass, createPagedFilterQueryDtoClass } from "./paged-dtos";
|
|
5
5
|
export { createMetalCrudDtos, createMetalCrudDtoClasses, createNestedCreateDtoClass } from "./crud-dtos";
|
|
6
|
+
export { createMetalTreeDtoClasses } from "./tree-dtos";
|
|
6
7
|
export { createMetalDtoOverrides, type CreateMetalDtoOverridesOptions } from "./convention-overrides";
|
|
7
8
|
export { createErrorDtoClass, StandardErrorDto, SimpleErrorDto, BasicErrorDto } from "./error-dtos";
|
|
8
9
|
export { withSession, parseIdOrThrow, compactUpdates, applyInput, getEntityOrThrow } from "./utils";
|
|
9
10
|
export { validateEntityMetadata, hasValidEntityMetadata } from "./field-builder";
|
|
10
|
-
export type { MetalDtoMode, MetalDtoOptions, MetalDtoTarget, PaginationConfig, PaginationOptions, ParsedPagination, Filter, FilterMapping, FilterFieldMapping, ParseFilterOptions, PagedQueryDtoOptions, PagedResponseDtoOptions, PagedFilterQueryDtoOptions, FilterFieldDef, MetalCrudDtoOptions, MetalCrudDtoClassOptions, MetalCrudDtoDecorators, MetalCrudDtoClasses, MetalCrudDtoClassNames, NestedCreateDtoOptions, ErrorDtoOptions, CreateSessionFn } from "./types";
|
|
11
|
+
export type { MetalDtoMode, MetalDtoOptions, MetalDtoTarget, PaginationConfig, PaginationOptions, ParsedPagination, Filter, FilterMapping, FilterFieldMapping, ParseFilterOptions, PagedQueryDtoOptions, PagedResponseDtoOptions, PagedFilterQueryDtoOptions, FilterFieldDef, MetalCrudDtoOptions, MetalCrudDtoClassOptions, MetalCrudDtoDecorators, MetalCrudDtoClasses, MetalCrudDtoClassNames, NestedCreateDtoOptions, MetalTreeDtoClassOptions, MetalTreeDtoClasses, MetalTreeDtoClassNames, MetalTreeListEntryOptions, ErrorDtoOptions, CreateSessionFn } from "./types";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.hasValidEntityMetadata = exports.validateEntityMetadata = exports.getEntityOrThrow = exports.applyInput = exports.compactUpdates = exports.parseIdOrThrow = exports.withSession = exports.BasicErrorDto = exports.SimpleErrorDto = exports.StandardErrorDto = exports.createErrorDtoClass = exports.createMetalDtoOverrides = exports.createNestedCreateDtoClass = exports.createMetalCrudDtoClasses = exports.createMetalCrudDtos = exports.createPagedFilterQueryDtoClass = exports.createPagedResponseDtoClass = exports.createPagedQueryDtoClass = exports.createFilterMappings = exports.parseFilter = exports.parsePagination = exports.MetalDto = void 0;
|
|
3
|
+
exports.hasValidEntityMetadata = exports.validateEntityMetadata = exports.getEntityOrThrow = exports.applyInput = exports.compactUpdates = exports.parseIdOrThrow = exports.withSession = exports.BasicErrorDto = exports.SimpleErrorDto = exports.StandardErrorDto = exports.createErrorDtoClass = exports.createMetalDtoOverrides = exports.createMetalTreeDtoClasses = exports.createNestedCreateDtoClass = exports.createMetalCrudDtoClasses = exports.createMetalCrudDtos = exports.createPagedFilterQueryDtoClass = exports.createPagedResponseDtoClass = exports.createPagedQueryDtoClass = exports.createFilterMappings = exports.parseFilter = exports.parsePagination = exports.MetalDto = void 0;
|
|
4
4
|
// Ensure standard decorator metadata is available for metal-orm transformers.
|
|
5
5
|
const symbolMetadata = Symbol.metadata;
|
|
6
6
|
if (!symbolMetadata) {
|
|
@@ -21,6 +21,8 @@ var crud_dtos_1 = require("./crud-dtos");
|
|
|
21
21
|
Object.defineProperty(exports, "createMetalCrudDtos", { enumerable: true, get: function () { return crud_dtos_1.createMetalCrudDtos; } });
|
|
22
22
|
Object.defineProperty(exports, "createMetalCrudDtoClasses", { enumerable: true, get: function () { return crud_dtos_1.createMetalCrudDtoClasses; } });
|
|
23
23
|
Object.defineProperty(exports, "createNestedCreateDtoClass", { enumerable: true, get: function () { return crud_dtos_1.createNestedCreateDtoClass; } });
|
|
24
|
+
var tree_dtos_1 = require("./tree-dtos");
|
|
25
|
+
Object.defineProperty(exports, "createMetalTreeDtoClasses", { enumerable: true, get: function () { return tree_dtos_1.createMetalTreeDtoClasses; } });
|
|
24
26
|
var convention_overrides_1 = require("./convention-overrides");
|
|
25
27
|
Object.defineProperty(exports, "createMetalDtoOverrides", { enumerable: true, get: function () { return convention_overrides_1.createMetalDtoOverrides; } });
|
|
26
28
|
var error_dtos_1 = require("./error-dtos");
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMetalTreeDtoClasses = createMetalTreeDtoClasses;
|
|
4
|
+
const metal_orm_1 = require("metal-orm");
|
|
5
|
+
const schema_1 = require("../../core/schema");
|
|
6
|
+
const metadata_1 = require("../../core/metadata");
|
|
7
|
+
const dto_1 = require("./dto");
|
|
8
|
+
const field_builder_1 = require("./field-builder");
|
|
9
|
+
function createMetalTreeDtoClasses(target, options = {}) {
|
|
10
|
+
const baseName = options.baseName ?? getTargetName(target);
|
|
11
|
+
const names = resolveNames(baseName, options.names);
|
|
12
|
+
const includeTreeMetadata = options.includeTreeMetadata ?? true;
|
|
13
|
+
const entityDto = options.entityDto ?? createEntityDtoClass(target, {
|
|
14
|
+
...(options.entity ?? {}),
|
|
15
|
+
name: names.entity
|
|
16
|
+
});
|
|
17
|
+
const nodeDto = createTreeNodeDtoClass(entityDto, {
|
|
18
|
+
name: names.node,
|
|
19
|
+
includeTreeMetadata
|
|
20
|
+
});
|
|
21
|
+
const parentKey = resolveParentKey(target, options.parentKey);
|
|
22
|
+
const parentSchema = resolveParentSchema(target, parentKey, options.entity?.overrides, options.entity?.strict);
|
|
23
|
+
const nodeResultDto = createTreeNodeResultDtoClass(entityDto, {
|
|
24
|
+
name: names.nodeResult,
|
|
25
|
+
includeTreeMetadata,
|
|
26
|
+
parentSchema
|
|
27
|
+
});
|
|
28
|
+
const threadedNodeDto = createThreadedNodeDtoClass(entityDto, {
|
|
29
|
+
name: names.threadedNode
|
|
30
|
+
});
|
|
31
|
+
const keySchema = options.treeListEntry?.keySchema
|
|
32
|
+
?? resolvePrimaryKeySchema(target, options.entity?.overrides, options.entity?.strict)
|
|
33
|
+
?? schema_1.t.integer();
|
|
34
|
+
const valueSchema = options.treeListEntry?.valueSchema ?? schema_1.t.string();
|
|
35
|
+
const treeListEntryDto = createTreeListEntryDtoClass({
|
|
36
|
+
name: names.treeListEntry,
|
|
37
|
+
keySchema,
|
|
38
|
+
valueSchema
|
|
39
|
+
});
|
|
40
|
+
const treeListSchema = schema_1.t.array(schema_1.t.ref(treeListEntryDto), {
|
|
41
|
+
description: `Flat list of ${baseName} tree entries for dropdown/select`
|
|
42
|
+
});
|
|
43
|
+
const threadedTreeSchema = schema_1.t.array(schema_1.t.ref(threadedNodeDto), {
|
|
44
|
+
description: `Threaded tree structure of ${baseName} nodes`
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
entity: entityDto,
|
|
48
|
+
node: nodeDto,
|
|
49
|
+
nodeResult: nodeResultDto,
|
|
50
|
+
threadedNode: threadedNodeDto,
|
|
51
|
+
treeListEntry: treeListEntryDto,
|
|
52
|
+
treeListSchema,
|
|
53
|
+
threadedTreeSchema
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function createEntityDtoClass(target, options) {
|
|
57
|
+
const DtoClass = class {
|
|
58
|
+
};
|
|
59
|
+
Object.defineProperty(DtoClass, "name", { value: options.name, configurable: true });
|
|
60
|
+
(0, dto_1.MetalDto)(target, options)(DtoClass);
|
|
61
|
+
return DtoClass;
|
|
62
|
+
}
|
|
63
|
+
function createTreeNodeDtoClass(entityDto, options) {
|
|
64
|
+
const TreeNodeDto = class {
|
|
65
|
+
};
|
|
66
|
+
Object.defineProperty(TreeNodeDto, "name", { value: options.name, configurable: true });
|
|
67
|
+
const fields = {
|
|
68
|
+
entity: { schema: schema_1.t.ref(entityDto) },
|
|
69
|
+
lft: { schema: schema_1.t.integer(), description: "Left boundary value (nested set)" },
|
|
70
|
+
rght: { schema: schema_1.t.integer(), description: "Right boundary value (nested set)" },
|
|
71
|
+
isLeaf: { schema: schema_1.t.boolean(), description: "Whether this node has no children" },
|
|
72
|
+
isRoot: { schema: schema_1.t.boolean(), description: "Whether this node has no parent" },
|
|
73
|
+
childCount: { schema: schema_1.t.integer({ minimum: 0 }), description: "Number of descendants" }
|
|
74
|
+
};
|
|
75
|
+
if (options.includeTreeMetadata) {
|
|
76
|
+
fields.depth = {
|
|
77
|
+
schema: schema_1.t.integer({ minimum: 0 }),
|
|
78
|
+
optional: true,
|
|
79
|
+
description: "Depth level (0 = root)"
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
(0, metadata_1.registerDto)(TreeNodeDto, {
|
|
83
|
+
name: options.name,
|
|
84
|
+
description: "A tree node with nested set boundaries and metadata",
|
|
85
|
+
fields
|
|
86
|
+
});
|
|
87
|
+
return TreeNodeDto;
|
|
88
|
+
}
|
|
89
|
+
function createTreeNodeResultDtoClass(entityDto, options) {
|
|
90
|
+
const TreeNodeResultDto = class {
|
|
91
|
+
};
|
|
92
|
+
Object.defineProperty(TreeNodeResultDto, "name", { value: options.name, configurable: true });
|
|
93
|
+
const fields = {
|
|
94
|
+
data: { schema: schema_1.t.ref(entityDto) },
|
|
95
|
+
lft: { schema: schema_1.t.integer(), description: "Left boundary value (nested set)" },
|
|
96
|
+
rght: { schema: schema_1.t.integer(), description: "Right boundary value (nested set)" },
|
|
97
|
+
parentId: { schema: options.parentSchema, description: "Parent identifier (null for roots)" },
|
|
98
|
+
isLeaf: { schema: schema_1.t.boolean(), description: "Whether this node has no children" },
|
|
99
|
+
isRoot: { schema: schema_1.t.boolean(), description: "Whether this node has no parent" }
|
|
100
|
+
};
|
|
101
|
+
if (options.includeTreeMetadata) {
|
|
102
|
+
fields.depth = {
|
|
103
|
+
schema: schema_1.t.integer({ minimum: 0 }),
|
|
104
|
+
optional: true,
|
|
105
|
+
description: "Depth level (0 = root)"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
(0, metadata_1.registerDto)(TreeNodeResultDto, {
|
|
109
|
+
name: options.name,
|
|
110
|
+
description: "A tree node result with nested set boundaries and metadata",
|
|
111
|
+
fields
|
|
112
|
+
});
|
|
113
|
+
return TreeNodeResultDto;
|
|
114
|
+
}
|
|
115
|
+
function createThreadedNodeDtoClass(entityDto, options) {
|
|
116
|
+
const ThreadedNodeDto = class {
|
|
117
|
+
};
|
|
118
|
+
Object.defineProperty(ThreadedNodeDto, "name", { value: options.name, configurable: true });
|
|
119
|
+
(0, metadata_1.registerDto)(ThreadedNodeDto, {
|
|
120
|
+
name: options.name,
|
|
121
|
+
description: "A node in a threaded tree structure with nested children",
|
|
122
|
+
fields: {
|
|
123
|
+
node: { schema: schema_1.t.ref(entityDto) },
|
|
124
|
+
children: {
|
|
125
|
+
schema: schema_1.t.array(schema_1.t.ref(ThreadedNodeDto)),
|
|
126
|
+
description: "Child nodes in the tree hierarchy"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return ThreadedNodeDto;
|
|
131
|
+
}
|
|
132
|
+
function createTreeListEntryDtoClass(options) {
|
|
133
|
+
const TreeListEntryDto = class {
|
|
134
|
+
};
|
|
135
|
+
Object.defineProperty(TreeListEntryDto, "name", { value: options.name, configurable: true });
|
|
136
|
+
(0, metadata_1.registerDto)(TreeListEntryDto, {
|
|
137
|
+
name: options.name,
|
|
138
|
+
description: "A tree list entry for dropdown/select rendering",
|
|
139
|
+
fields: {
|
|
140
|
+
key: { schema: options.keySchema, description: "The key (usually primary key)" },
|
|
141
|
+
value: { schema: options.valueSchema, description: "The display value with depth prefix" },
|
|
142
|
+
depth: { schema: schema_1.t.integer({ minimum: 0 }), description: "The depth level" }
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return TreeListEntryDto;
|
|
146
|
+
}
|
|
147
|
+
function resolveParentKey(target, parentKey) {
|
|
148
|
+
if (parentKey) {
|
|
149
|
+
return parentKey;
|
|
150
|
+
}
|
|
151
|
+
if (typeof target === "function") {
|
|
152
|
+
const config = (0, metal_orm_1.getTreeConfig)(target);
|
|
153
|
+
if (config?.parentKey) {
|
|
154
|
+
return config.parentKey;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return "parentId";
|
|
158
|
+
}
|
|
159
|
+
function resolveParentSchema(target, parentKey, overrides, strict) {
|
|
160
|
+
const fields = (0, field_builder_1.buildFields)(target, {
|
|
161
|
+
include: [parentKey],
|
|
162
|
+
overrides,
|
|
163
|
+
strict
|
|
164
|
+
});
|
|
165
|
+
return fields[parentKey]?.schema ?? schema_1.t.nullable(schema_1.t.string());
|
|
166
|
+
}
|
|
167
|
+
function resolvePrimaryKeySchema(target, overrides, strict) {
|
|
168
|
+
const columns = (0, metal_orm_1.getColumnMap)(target);
|
|
169
|
+
const primaryKey = Object.keys(columns).find((key) => columns[key]?.primary);
|
|
170
|
+
if (!primaryKey) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const fields = (0, field_builder_1.buildFields)(target, {
|
|
174
|
+
include: [primaryKey],
|
|
175
|
+
overrides,
|
|
176
|
+
strict
|
|
177
|
+
});
|
|
178
|
+
return fields[primaryKey]?.schema;
|
|
179
|
+
}
|
|
180
|
+
function resolveNames(baseName, names) {
|
|
181
|
+
return {
|
|
182
|
+
entity: names?.entity ?? `${baseName}Dto`,
|
|
183
|
+
node: names?.node ?? `${baseName}NodeDto`,
|
|
184
|
+
nodeResult: names?.nodeResult ?? `${baseName}NodeResultDto`,
|
|
185
|
+
threadedNode: names?.threadedNode ?? `${baseName}ThreadedNodeDto`,
|
|
186
|
+
treeListEntry: names?.treeListEntry ?? `${baseName}TreeListEntryDto`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function getTargetName(target) {
|
|
190
|
+
if (typeof target === "function" && target.name) {
|
|
191
|
+
return target.name;
|
|
192
|
+
}
|
|
193
|
+
return "Entity";
|
|
194
|
+
}
|
|
@@ -66,9 +66,9 @@ export interface FilterFieldMapping {
|
|
|
66
66
|
*/
|
|
67
67
|
export interface FilterMapping {
|
|
68
68
|
/** Field name */
|
|
69
|
-
field: string;
|
|
69
|
+
field: string | string[];
|
|
70
70
|
/** Filter operator */
|
|
71
|
-
operator:
|
|
71
|
+
operator: FilterOperator;
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
74
|
* Options for parsing filters.
|
|
@@ -79,12 +79,19 @@ export interface ParseFilterOptions {
|
|
|
79
79
|
/** Field mappings */
|
|
80
80
|
fieldMappings?: Record<string, FilterMapping>;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Filter operator.
|
|
84
|
+
*/
|
|
85
|
+
export type FilterOperator = "equals" | "not" | "in" | "notIn" | "contains" | "startsWith" | "endsWith" | "gt" | "gte" | "lt" | "lte" | "isEmpty" | "isNotEmpty";
|
|
82
86
|
/**
|
|
83
87
|
* Filter type for querying.
|
|
84
88
|
*/
|
|
85
89
|
export type Filter<T, K extends keyof T> = {
|
|
86
90
|
[P in K]?: {
|
|
87
91
|
equals?: T[P];
|
|
92
|
+
not?: T[P];
|
|
93
|
+
in?: Array<T[P]>;
|
|
94
|
+
notIn?: Array<T[P]>;
|
|
88
95
|
contains?: T[P];
|
|
89
96
|
startsWith?: T[P];
|
|
90
97
|
endsWith?: T[P];
|
|
@@ -92,6 +99,7 @@ export type Filter<T, K extends keyof T> = {
|
|
|
92
99
|
gte?: T[P];
|
|
93
100
|
lt?: T[P];
|
|
94
101
|
lte?: T[P];
|
|
102
|
+
mode?: "default" | "insensitive";
|
|
95
103
|
};
|
|
96
104
|
};
|
|
97
105
|
/**
|
|
@@ -206,6 +214,68 @@ export interface MetalCrudDtoClasses {
|
|
|
206
214
|
/** Params DTO class */
|
|
207
215
|
params: DtoConstructor;
|
|
208
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Metal Tree DTO class names.
|
|
219
|
+
*/
|
|
220
|
+
export interface MetalTreeDtoClassNames {
|
|
221
|
+
/** Entity DTO class name */
|
|
222
|
+
entity?: string;
|
|
223
|
+
/** Tree node DTO class name */
|
|
224
|
+
node?: string;
|
|
225
|
+
/** Tree node result DTO class name */
|
|
226
|
+
nodeResult?: string;
|
|
227
|
+
/** Threaded tree node DTO class name */
|
|
228
|
+
threadedNode?: string;
|
|
229
|
+
/** Tree list entry DTO class name */
|
|
230
|
+
treeListEntry?: string;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Options for tree list entry DTOs.
|
|
234
|
+
*/
|
|
235
|
+
export interface MetalTreeListEntryOptions {
|
|
236
|
+
/** Schema for list entry key */
|
|
237
|
+
keySchema?: SchemaNode;
|
|
238
|
+
/** Schema for list entry value */
|
|
239
|
+
valueSchema?: SchemaNode;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Options for Metal Tree DTO classes.
|
|
243
|
+
*/
|
|
244
|
+
export interface MetalTreeDtoClassOptions {
|
|
245
|
+
/** Base name for DTO classes */
|
|
246
|
+
baseName?: string;
|
|
247
|
+
/** Custom class names */
|
|
248
|
+
names?: MetalTreeDtoClassNames;
|
|
249
|
+
/** Reuse an existing entity DTO class */
|
|
250
|
+
entityDto?: DtoConstructor;
|
|
251
|
+
/** Options for generated entity DTO when entityDto is not provided */
|
|
252
|
+
entity?: MetalDtoOptions;
|
|
253
|
+
/** Whether to include depth metadata (default: true) */
|
|
254
|
+
includeTreeMetadata?: boolean;
|
|
255
|
+
/** Override the parent key column name */
|
|
256
|
+
parentKey?: string;
|
|
257
|
+
/** Tree list entry options */
|
|
258
|
+
treeListEntry?: MetalTreeListEntryOptions;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Metal Tree DTO classes.
|
|
262
|
+
*/
|
|
263
|
+
export interface MetalTreeDtoClasses {
|
|
264
|
+
/** Entity DTO class */
|
|
265
|
+
entity: DtoConstructor;
|
|
266
|
+
/** Tree node DTO class */
|
|
267
|
+
node: DtoConstructor;
|
|
268
|
+
/** Tree node result DTO class */
|
|
269
|
+
nodeResult: DtoConstructor;
|
|
270
|
+
/** Threaded tree node DTO class */
|
|
271
|
+
threadedNode: DtoConstructor;
|
|
272
|
+
/** Tree list entry DTO class */
|
|
273
|
+
treeListEntry: DtoConstructor;
|
|
274
|
+
/** Tree list schema */
|
|
275
|
+
treeListSchema: SchemaNode;
|
|
276
|
+
/** Threaded tree schema */
|
|
277
|
+
threadedTreeSchema: SchemaNode;
|
|
278
|
+
}
|
|
209
279
|
/**
|
|
210
280
|
* Options for nested create DTOs.
|
|
211
281
|
* @extends MetalDtoOptions
|