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 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
- field: K;
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>>(entity: T, fields: Array<{
16
+ export declare function createFilterMappings<T extends Record<string, unknown>>(_entity: T, fields: Array<{
19
17
  queryKey: string;
20
- field: keyof T;
21
- operator?: "equals" | "contains" | "startsWith" | "endsWith";
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
- * Parses filter parameters from query parameters.
7
- * @param query - Query parameters
8
- * @param mappings - Filter field mappings
9
- * @returns Parsed filter or undefined
10
- */
11
- function parseFilter(query, mappings) {
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
- for (const [queryKey, value] of Object.entries(query)) {
17
- const mapping = mappings[queryKey];
18
- if (!mapping || value === undefined || value === null || value === "") {
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 { field, operator } = mapping;
22
- if (!filter[field]) {
23
- filter[field] = {};
24
+ const operator = mapping.operator ?? "equals";
25
+ const fieldPath = normalizePath(mapping.field);
26
+ if (!fieldPath.length) {
27
+ continue;
24
28
  }
25
- filter[field][operator] = value;
29
+ setFilterValue(filter, fieldPath, operator, value);
30
+ hasValues = true;
26
31
  }
27
- return Object.keys(filter).length ? filter : undefined;
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(entity, fields) {
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,2 @@
1
+ import type { MetalTreeDtoClassOptions, MetalTreeDtoClasses } from "./types";
2
+ export declare function createMetalTreeDtoClasses(target: any, options?: MetalTreeDtoClassOptions): MetalTreeDtoClasses;
@@ -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: "equals" | "contains" | "startsWith" | "endsWith" | "gt" | "gte" | "lt" | "lte";
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