adorn-api 1.1.5 → 1.1.7

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
@@ -398,13 +398,10 @@ import {
398
398
  Body,
399
399
  Query,
400
400
  Returns,
401
- parseFilter,
402
- parsePagination,
403
- parseSort,
401
+ runPagedList,
404
402
  t,
405
403
  type RequestContext
406
404
  } from "adorn-api";
407
- import { applyFilter, toPagedResponse } from "metal-orm";
408
405
  import { createSession } from "./db";
409
406
  import { User } from "./user.entity";
410
407
  import {
@@ -428,24 +425,18 @@ export class UserController {
428
425
  @Query(UserQueryDto)
429
426
  @Returns(UserPagedResponseDto)
430
427
  async list(ctx: RequestContext<unknown, UserQueryDto>) {
431
- const query = (ctx.query ?? {}) as Record<string, unknown>;
432
- const { page, pageSize } = parsePagination(query);
433
- const filters = parseFilter(query, USER_FILTER_MAPPINGS);
434
- const sort = parseSort(query, USER_SORTABLE_COLUMNS, {
435
- defaultSortBy: "id"
436
- });
437
- const direction = sort?.sortDirection === "desc" ? "DESC" : "ASC";
438
428
  const session = createSession();
439
-
429
+
440
430
  try {
441
- const ormQuery = applyFilter(
442
- User.select().orderBy(User.id, direction),
443
- User,
444
- filters
445
- );
446
-
447
- const paged = await ormQuery.executePaged(session, { page, pageSize });
448
- return toPagedResponse(paged);
431
+ return await runPagedList({
432
+ query: (ctx.query ?? {}) as Record<string, unknown>,
433
+ target: User,
434
+ qb: () => User.select(),
435
+ session,
436
+ filterMappings: USER_FILTER_MAPPINGS,
437
+ sortableColumns: USER_SORTABLE_COLUMNS,
438
+ defaultSortBy: "id"
439
+ });
449
440
  } finally {
450
441
  await session.dispose();
451
442
  }
@@ -491,6 +482,80 @@ export class UserController {
491
482
 
492
483
  ### Migration Guide (Breaking)
493
484
 
485
+ ### CRUD Controller Factory (`createCrudController`)
486
+
487
+ When your controller only wires DTOs + service calls, you can generate the full CRUD controller and remove decorator boilerplate.
488
+
489
+ ```typescript
490
+ // user.controller.ts
491
+ import { createCrudController } from "adorn-api";
492
+ import { userCrudDtos } from "./user.dtos";
493
+ import { UserCrudService } from "./user.service";
494
+
495
+ export const UserController = createCrudController({
496
+ path: "/users",
497
+ service: UserCrudService, // class or instance
498
+ dtos: userCrudDtos, // result of createMetalCrudDtoClasses(...)
499
+ entityName: "User", // used by parseIdOrThrow messages
500
+ withOptionsRoute: true,
501
+ withReplace: true,
502
+ withPatch: true,
503
+ withDelete: true
504
+ });
505
+ ```
506
+
507
+ Generated routes:
508
+ - `GET /`
509
+ - `GET /options` (optional)
510
+ - `GET /:id`
511
+ - `POST /`
512
+ - `PUT /:id` (optional)
513
+ - `PATCH /:id` (optional)
514
+ - `DELETE /:id` (optional)
515
+
516
+ The factory applies the correct `@Query/@Body/@Params/@Returns` schemas and also propagates `dtos.errors` to all `/:id` routes.
517
+
518
+ Before (manual, repeated decorators/status/schema wiring):
519
+
520
+ ```typescript
521
+ @Controller("/users")
522
+ class UserController {
523
+ @Get("/")
524
+ @Query(UserQueryDto)
525
+ @Returns(UserPagedResponseDto)
526
+ async list(ctx: RequestContext<unknown, UserQueryDto>) { ... }
527
+
528
+ @Get("/:id")
529
+ @Params(UserParamsDto)
530
+ @Returns(UserDto)
531
+ @UserErrors
532
+ async getById(ctx: RequestContext<unknown, undefined, UserParamsDto>) { ... }
533
+
534
+ @Post("/")
535
+ @Body(CreateUserDto)
536
+ @Returns({ status: 201, schema: UserDto })
537
+ async create(ctx: RequestContext<CreateUserDto>) { ... }
538
+
539
+ // put/patch/delete/options...
540
+ }
541
+ ```
542
+
543
+ After (factory + service):
544
+
545
+ ```typescript
546
+ export const UserController = createCrudController({
547
+ path: "/users",
548
+ service: new UserCrudService(),
549
+ dtos: userCrudDtos,
550
+ entityName: "User"
551
+ });
552
+ ```
553
+
554
+ When to use factory vs manual controller:
555
+ - Use `createCrudController` when routes follow standard CRUD and behavior lives in a service.
556
+ - Use a manual controller when route contracts diverge (custom status/body shape, non-standard params, upload/stream/raw endpoints, or route-level auth/doc decorators not shared by all CRUD routes).
557
+ - For extra endpoints, keep the generated CRUD controller and add a second manual controller for custom routes on the same base path.
558
+
494
559
  Before (duplicated config):
495
560
 
496
561
  ```typescript
@@ -539,10 +604,9 @@ Breaking changes summary:
539
604
  // user.controller.ts — using listConfig directly
540
605
  import {
541
606
  Controller, Get, Query, Returns,
542
- parseFilter, parsePagination, parseSort,
607
+ runPagedList,
543
608
  type RequestContext
544
609
  } from "adorn-api";
545
- import { applyFilter, toPagedResponse } from "metal-orm";
546
610
  import { createSession } from "./db";
547
611
  import { User } from "./user.entity";
548
612
  import {
@@ -557,24 +621,15 @@ export class UserController {
557
621
  @Query(UserQueryDto)
558
622
  @Returns(UserPagedResponseDto)
559
623
  async list(ctx: RequestContext<unknown, UserQueryDto>) {
560
- const query = (ctx.query ?? {}) as Record<string, unknown>;
561
- const { page, pageSize } = parsePagination(query, USER_LIST_CONFIG);
562
- const filters = parseFilter(query, USER_LIST_CONFIG.filterMappings);
563
- const sort = parseSort(query, USER_LIST_CONFIG.sortableColumns, {
564
- defaultSortBy: USER_LIST_CONFIG.defaultSortBy,
565
- defaultSortDirection: USER_LIST_CONFIG.defaultSortDirection
566
- });
567
- const direction = sort?.sortDirection === "desc" ? "DESC" : "ASC";
568
-
569
624
  const session = createSession();
570
625
  try {
571
- const ormQuery = applyFilter(
572
- User.select().orderBy(User.id, direction),
573
- User,
574
- filters
575
- );
576
- const paged = await ormQuery.executePaged(session, { page, pageSize });
577
- return toPagedResponse(paged);
626
+ return await runPagedList({
627
+ query: (ctx.query ?? {}) as Record<string, unknown>,
628
+ target: User,
629
+ qb: () => User.select(),
630
+ session,
631
+ ...USER_LIST_CONFIG
632
+ });
578
633
  } finally {
579
634
  await session.dispose();
580
635
  }
@@ -584,6 +639,51 @@ export class UserController {
584
639
 
585
640
  The `listConfig` object contains: `filterMappings`, `sortableColumns`, `defaultSortBy`, `defaultSortDirection`, `defaultPageSize`, `maxPageSize`, `sortByKey`, and `sortDirectionKey`.
586
641
 
642
+ ### `BaseService.list` Before/After (Boilerplate Reduction)
643
+
644
+ Before:
645
+
646
+ ```typescript
647
+ async list(query: Record<string, unknown>) {
648
+ const { page, pageSize } = parsePagination(query, this.listConfig);
649
+ const filters = parseFilter(query, this.listConfig.filterMappings);
650
+ const sort = parseSort(query, this.listConfig.sortableColumns, {
651
+ defaultSortBy: this.listConfig.defaultSortBy,
652
+ defaultSortDirection: this.listConfig.defaultSortDirection,
653
+ sortByKey: this.listConfig.sortByKey,
654
+ sortDirectionKey: this.listConfig.sortDirectionKey
655
+ });
656
+ const direction = sort?.sortDirection === "desc" ? "DESC" : "ASC";
657
+
658
+ return withSession(this.createSession, async (session) => {
659
+ const qb = applyFilter(this.baseQuery().orderBy(this.ref.id, direction), this.entity, filters);
660
+ const paged = await qb.executePaged(session, { page, pageSize });
661
+ return toPagedResponse(paged);
662
+ });
663
+ }
664
+ ```
665
+
666
+ After:
667
+
668
+ ```typescript
669
+ async list(query: Record<string, unknown>) {
670
+ return withSession(this.createSession, async (session) =>
671
+ runPagedList({
672
+ query,
673
+ target: this.entity,
674
+ qb: () => this.baseQuery(),
675
+ session,
676
+ ...this.listConfig
677
+ })
678
+ );
679
+ }
680
+ ```
681
+
682
+ Migration note:
683
+ - Existing `parsePagination`, `parseFilter`, and `parseSort` remain unchanged and can still be used manually.
684
+ - `runPagedList`/`executeCrudList` is additive and optional; no breaking API changes.
685
+ - For sortable fields that are not direct columns of the base table, pass `allowedSortColumns` with explicit metal-orm sort terms.
686
+
587
687
  ### Sort Order Compatibility (`sortOrder` / `sortDirection`)
588
688
 
589
689
  `parseSort` now accepts both `sortDirection` (lowercase `asc`/`desc`) and `sortOrder` (uppercase `ASC`/`DESC`). This avoids the need for a custom helper when integrating with clients that send uppercase sort orders.
@@ -0,0 +1,16 @@
1
+ import type { DtoConstructor } from "../../core/types";
2
+ import type { RequestContext } from "../express/types";
3
+ import type { CreateCrudControllerOptions, MetalCrudDtoClasses } from "./types";
4
+ type DtoInstance<TDto extends DtoConstructor> = InstanceType<TDto>;
5
+ export declare function createCrudController<TDtos extends MetalCrudDtoClasses<any>>(options: CreateCrudControllerOptions<TDtos>): {
6
+ new (): {
7
+ list(ctx: RequestContext<unknown, DtoInstance<TDtos["queryDto"]>>): Promise<DtoInstance<TDtos["pagedResponseDto"]>>;
8
+ options(ctx: RequestContext<unknown, DtoInstance<TDtos["optionsQueryDto"]>>): Promise<DtoInstance<TDtos["optionsDto"]>>;
9
+ getById(ctx: RequestContext<unknown, undefined, DtoInstance<TDtos["params"]>>): Promise<DtoInstance<TDtos["response"]>>;
10
+ create(ctx: RequestContext<DtoInstance<TDtos["create"]>>): Promise<DtoInstance<TDtos["response"]>>;
11
+ replace(ctx: RequestContext<DtoInstance<TDtos["replace"]>, undefined, DtoInstance<TDtos["params"]>>): Promise<DtoInstance<TDtos["response"]>>;
12
+ update(ctx: RequestContext<DtoInstance<TDtos["update"]>, undefined, DtoInstance<TDtos["params"]>>): Promise<DtoInstance<TDtos["response"]>>;
13
+ delete(ctx: RequestContext<unknown, undefined, DtoInstance<TDtos["params"]>>): Promise<void>;
14
+ };
15
+ };
16
+ export {};
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
3
+ var useValue = arguments.length > 2;
4
+ for (var i = 0; i < initializers.length; i++) {
5
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
6
+ }
7
+ return useValue ? value : void 0;
8
+ };
9
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
10
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
11
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
12
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
13
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
14
+ var _, done = false;
15
+ for (var i = decorators.length - 1; i >= 0; i--) {
16
+ var context = {};
17
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
18
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
19
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
20
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
21
+ if (kind === "accessor") {
22
+ if (result === void 0) continue;
23
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
24
+ if (_ = accept(result.get)) descriptor.get = _;
25
+ if (_ = accept(result.set)) descriptor.set = _;
26
+ if (_ = accept(result.init)) initializers.unshift(_);
27
+ }
28
+ else if (_ = accept(result)) {
29
+ if (kind === "field") initializers.unshift(_);
30
+ else descriptor[key] = _;
31
+ }
32
+ }
33
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
34
+ done = true;
35
+ };
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.createCrudController = createCrudController;
38
+ const decorators_1 = require("../../core/decorators");
39
+ const utils_1 = require("./utils");
40
+ function createCrudController(options) {
41
+ const withOptionsRoute = options.withOptionsRoute ?? true;
42
+ const withReplace = options.withReplace ?? true;
43
+ const withPatch = options.withPatch ?? true;
44
+ const withDelete = options.withDelete ?? true;
45
+ const service = resolveService(options.service);
46
+ const routeErrorsDecorator = options.dtos.errors;
47
+ let GeneratedCrudController = (() => {
48
+ let _classDecorators = [(0, decorators_1.Controller)({ path: options.path, tags: options.tags })];
49
+ let _classDescriptor;
50
+ let _classExtraInitializers = [];
51
+ let _classThis;
52
+ let _instanceExtraInitializers = [];
53
+ let _list_decorators;
54
+ let _options_decorators;
55
+ let _getById_decorators;
56
+ let _create_decorators;
57
+ let _replace_decorators;
58
+ let _update_decorators;
59
+ let _delete_decorators;
60
+ var GeneratedCrudController = class {
61
+ static { _classThis = this; }
62
+ static {
63
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
64
+ _list_decorators = [(0, decorators_1.Get)("/"), (0, decorators_1.Query)(options.dtos.queryDto), (0, decorators_1.Returns)(options.dtos.pagedResponseDto)];
65
+ _options_decorators = [when(withOptionsRoute, (0, decorators_1.Get)("/options")), when(withOptionsRoute, (0, decorators_1.Query)(options.dtos.optionsQueryDto)), when(withOptionsRoute, (0, decorators_1.Returns)(options.dtos.optionsDto))];
66
+ _getById_decorators = [(0, decorators_1.Get)("/:id"), (0, decorators_1.Params)(options.dtos.params), (0, decorators_1.Returns)(options.dtos.response), applyRouteErrors(routeErrorsDecorator)];
67
+ _create_decorators = [(0, decorators_1.Post)("/"), (0, decorators_1.Body)(options.dtos.create), (0, decorators_1.Returns)({ status: 201, schema: options.dtos.response })];
68
+ _replace_decorators = [when(withReplace, (0, decorators_1.Put)("/:id")), when(withReplace, (0, decorators_1.Params)(options.dtos.params)), when(withReplace, (0, decorators_1.Body)(options.dtos.replace)), when(withReplace, (0, decorators_1.Returns)(options.dtos.response)), when(withReplace, applyRouteErrors(routeErrorsDecorator))];
69
+ _update_decorators = [when(withPatch, (0, decorators_1.Patch)("/:id")), when(withPatch, (0, decorators_1.Params)(options.dtos.params)), when(withPatch, (0, decorators_1.Body)(options.dtos.update)), when(withPatch, (0, decorators_1.Returns)(options.dtos.response)), when(withPatch, applyRouteErrors(routeErrorsDecorator))];
70
+ _delete_decorators = [when(withDelete, (0, decorators_1.Delete)("/:id")), when(withDelete, (0, decorators_1.Params)(options.dtos.params)), when(withDelete, (0, decorators_1.Returns)({ status: 204, description: "No Content" })), when(withDelete, applyRouteErrors(routeErrorsDecorator))];
71
+ __esDecorate(this, null, _list_decorators, { kind: "method", name: "list", static: false, private: false, access: { has: obj => "list" in obj, get: obj => obj.list }, metadata: _metadata }, null, _instanceExtraInitializers);
72
+ __esDecorate(this, null, _options_decorators, { kind: "method", name: "options", static: false, private: false, access: { has: obj => "options" in obj, get: obj => obj.options }, metadata: _metadata }, null, _instanceExtraInitializers);
73
+ __esDecorate(this, null, _getById_decorators, { kind: "method", name: "getById", static: false, private: false, access: { has: obj => "getById" in obj, get: obj => obj.getById }, metadata: _metadata }, null, _instanceExtraInitializers);
74
+ __esDecorate(this, null, _create_decorators, { kind: "method", name: "create", static: false, private: false, access: { has: obj => "create" in obj, get: obj => obj.create }, metadata: _metadata }, null, _instanceExtraInitializers);
75
+ __esDecorate(this, null, _replace_decorators, { kind: "method", name: "replace", static: false, private: false, access: { has: obj => "replace" in obj, get: obj => obj.replace }, metadata: _metadata }, null, _instanceExtraInitializers);
76
+ __esDecorate(this, null, _update_decorators, { kind: "method", name: "update", static: false, private: false, access: { has: obj => "update" in obj, get: obj => obj.update }, metadata: _metadata }, null, _instanceExtraInitializers);
77
+ __esDecorate(this, null, _delete_decorators, { kind: "method", name: "delete", static: false, private: false, access: { has: obj => "delete" in obj, get: obj => obj.delete }, metadata: _metadata }, null, _instanceExtraInitializers);
78
+ __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
79
+ GeneratedCrudController = _classThis = _classDescriptor.value;
80
+ if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
81
+ __runInitializers(_classThis, _classExtraInitializers);
82
+ }
83
+ async list(ctx) {
84
+ return await service.list(ctx);
85
+ }
86
+ async options(ctx) {
87
+ assertServiceMethod(service, "options");
88
+ return await service.options(ctx);
89
+ }
90
+ async getById(ctx) {
91
+ const id = parseContextId(ctx, options.entityName);
92
+ return await service.getById(id, ctx);
93
+ }
94
+ async create(ctx) {
95
+ return await service.create(ctx.body, ctx);
96
+ }
97
+ async replace(ctx) {
98
+ assertServiceMethod(service, "replace");
99
+ const id = parseContextId(ctx, options.entityName);
100
+ return await service.replace(id, ctx.body, ctx);
101
+ }
102
+ async update(ctx) {
103
+ assertServiceMethod(service, "update");
104
+ const id = parseContextId(ctx, options.entityName);
105
+ return await service.update(id, ctx.body, ctx);
106
+ }
107
+ async delete(ctx) {
108
+ assertServiceMethod(service, "delete");
109
+ const id = parseContextId(ctx, options.entityName);
110
+ await service.delete(id, ctx);
111
+ }
112
+ constructor() {
113
+ __runInitializers(this, _instanceExtraInitializers);
114
+ }
115
+ };
116
+ return GeneratedCrudController = _classThis;
117
+ })();
118
+ Object.defineProperty(GeneratedCrudController, "name", {
119
+ value: `${options.entityName}CrudController`,
120
+ configurable: true
121
+ });
122
+ return GeneratedCrudController;
123
+ }
124
+ function resolveService(input) {
125
+ if (typeof input === "function") {
126
+ return new input();
127
+ }
128
+ return input;
129
+ }
130
+ function when(enabled, decorator) {
131
+ return (value, context) => {
132
+ if (enabled) {
133
+ decorator(value, context);
134
+ }
135
+ };
136
+ }
137
+ function applyRouteErrors(decorator) {
138
+ return (value, context) => {
139
+ decorator?.(value, context);
140
+ };
141
+ }
142
+ function parseContextId(ctx, entityName) {
143
+ const params = (ctx.params ?? {});
144
+ return (0, utils_1.parseIdOrThrow)(toIdValue(params.id), entityName);
145
+ }
146
+ function toIdValue(value) {
147
+ if (typeof value === "string" || typeof value === "number") {
148
+ return value;
149
+ }
150
+ return "";
151
+ }
152
+ function assertServiceMethod(service, method) {
153
+ if (typeof service[method] !== "function") {
154
+ throw new Error(`CRUD service is missing "${method}" method required by enabled route.`);
155
+ }
156
+ }
@@ -2,11 +2,13 @@ export { MetalDto } from "./dto";
2
2
  export { parsePagination } from "./pagination";
3
3
  export { parseFilter, createFilterMappings } from "./filters";
4
4
  export { parseSort } from "./sort";
5
+ export { runPagedList, executeCrudList } from "./list";
5
6
  export { createPagedQueryDtoClass, createPagedResponseDtoClass, createPagedFilterQueryDtoClass } from "./paged-dtos";
6
7
  export { createMetalCrudDtos, createMetalCrudDtoClasses, createNestedCreateDtoClass } from "./crud-dtos";
8
+ export { createCrudController } from "./crud-controller";
7
9
  export { createMetalTreeDtoClasses } from "./tree-dtos";
8
10
  export { createMetalDtoOverrides, type CreateMetalDtoOverridesOptions } from "./convention-overrides";
9
11
  export { createErrorDtoClass, StandardErrorDto, SimpleErrorDto, BasicErrorDto } from "./error-dtos";
10
12
  export { withSession, parseIdOrThrow, compactUpdates, applyInput, getEntityOrThrow } from "./utils";
11
13
  export { validateEntityMetadata, hasValidEntityMetadata } from "./field-builder";
12
- export type { MetalDtoMode, MetalDtoOptions, MetalDtoTarget, PaginationConfig, PaginationOptions, ParsedPagination, Filter, FilterMapping, FilterFieldMapping, FilterFieldPath, FilterFieldPathArray, FilterFieldInput, RelationQuantifier, ParseFilterOptions, ParseSortOptions, ParsedSort, SortDirection, ListConfig, PagedQueryDtoOptions, PagedResponseDtoOptions, PagedFilterQueryDtoOptions, FilterFieldDef, MetalCrudQueryFilterDef, MetalCrudSortableColumns, MetalCrudOptionsQueryOptions, MetalCrudQueryOptions, MetalCrudStandardErrorsOptions, MetalCrudDtoOptions, MetalCrudDtoClassOptions, MetalCrudDtoDecorators, MetalCrudDtoClasses, MetalCrudDtoClassNameKey, MetalCrudDtoClassNames, RouteErrorsDecorator, NestedCreateDtoOptions, MetalTreeDtoClassOptions, MetalTreeDtoClasses, MetalTreeDtoClassNames, MetalTreeListEntryOptions, ErrorDtoOptions, CreateSessionFn } from "./types";
14
+ export type { MetalDtoMode, MetalDtoOptions, MetalDtoTarget, PaginationConfig, PaginationOptions, ParsedPagination, Filter, FilterMapping, FilterFieldMapping, FilterFieldPath, FilterFieldPathArray, FilterFieldInput, RelationQuantifier, ParseFilterOptions, ParseSortOptions, ParsedSort, SortDirection, CrudListSortTerm, RunPagedListOptions, ExecuteCrudListOptions, CrudPagedResponse, ListConfig, PagedQueryDtoOptions, PagedResponseDtoOptions, PagedFilterQueryDtoOptions, FilterFieldDef, MetalCrudQueryFilterDef, MetalCrudSortableColumns, MetalCrudOptionsQueryOptions, MetalCrudQueryOptions, MetalCrudStandardErrorsOptions, MetalCrudDtoOptions, MetalCrudDtoClassOptions, MetalCrudDtoDecorators, MetalCrudDtoClasses, MetalCrudDtoClassNameKey, MetalCrudDtoClassNames, CrudControllerService, CrudControllerServiceInput, CreateCrudControllerOptions, RouteErrorsDecorator, 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.createMetalTreeDtoClasses = exports.createNestedCreateDtoClass = exports.createMetalCrudDtoClasses = exports.createMetalCrudDtos = exports.createPagedFilterQueryDtoClass = exports.createPagedResponseDtoClass = exports.createPagedQueryDtoClass = exports.parseSort = 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.createCrudController = exports.createNestedCreateDtoClass = exports.createMetalCrudDtoClasses = exports.createMetalCrudDtos = exports.createPagedFilterQueryDtoClass = exports.createPagedResponseDtoClass = exports.createPagedQueryDtoClass = exports.executeCrudList = exports.runPagedList = exports.parseSort = 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) {
@@ -15,6 +15,9 @@ Object.defineProperty(exports, "parseFilter", { enumerable: true, get: function
15
15
  Object.defineProperty(exports, "createFilterMappings", { enumerable: true, get: function () { return filters_1.createFilterMappings; } });
16
16
  var sort_1 = require("./sort");
17
17
  Object.defineProperty(exports, "parseSort", { enumerable: true, get: function () { return sort_1.parseSort; } });
18
+ var list_1 = require("./list");
19
+ Object.defineProperty(exports, "runPagedList", { enumerable: true, get: function () { return list_1.runPagedList; } });
20
+ Object.defineProperty(exports, "executeCrudList", { enumerable: true, get: function () { return list_1.executeCrudList; } });
18
21
  var paged_dtos_1 = require("./paged-dtos");
19
22
  Object.defineProperty(exports, "createPagedQueryDtoClass", { enumerable: true, get: function () { return paged_dtos_1.createPagedQueryDtoClass; } });
20
23
  Object.defineProperty(exports, "createPagedResponseDtoClass", { enumerable: true, get: function () { return paged_dtos_1.createPagedResponseDtoClass; } });
@@ -23,6 +26,8 @@ var crud_dtos_1 = require("./crud-dtos");
23
26
  Object.defineProperty(exports, "createMetalCrudDtos", { enumerable: true, get: function () { return crud_dtos_1.createMetalCrudDtos; } });
24
27
  Object.defineProperty(exports, "createMetalCrudDtoClasses", { enumerable: true, get: function () { return crud_dtos_1.createMetalCrudDtoClasses; } });
25
28
  Object.defineProperty(exports, "createNestedCreateDtoClass", { enumerable: true, get: function () { return crud_dtos_1.createNestedCreateDtoClass; } });
29
+ var crud_controller_1 = require("./crud-controller");
30
+ Object.defineProperty(exports, "createCrudController", { enumerable: true, get: function () { return crud_controller_1.createCrudController; } });
26
31
  var tree_dtos_1 = require("./tree-dtos");
27
32
  Object.defineProperty(exports, "createMetalTreeDtoClasses", { enumerable: true, get: function () { return tree_dtos_1.createMetalTreeDtoClasses; } });
28
33
  var convention_overrides_1 = require("./convention-overrides");
@@ -0,0 +1,11 @@
1
+ import { type PagedResponse, type TableDef } from "metal-orm";
2
+ import type { ExecuteCrudListOptions, RunPagedListOptions } from "./types";
3
+ /**
4
+ * Runs a unified filtered/sorted/paginated list query for metal-orm.
5
+ */
6
+ export declare function runPagedList<TResult, TTable extends TableDef = TableDef, TTarget = unknown, TFilterTarget = Record<string, unknown>>(options: RunPagedListOptions<TResult, TTable, TTarget, TFilterTarget>): Promise<PagedResponse<TResult>>;
7
+ /**
8
+ * Alias for runPagedList.
9
+ */
10
+ export declare const executeCrudList: typeof runPagedList;
11
+ export type { ExecuteCrudListOptions, RunPagedListOptions };
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeCrudList = void 0;
4
+ exports.runPagedList = runPagedList;
5
+ const metal_orm_1 = require("metal-orm");
6
+ const filters_1 = require("./filters");
7
+ const pagination_1 = require("./pagination");
8
+ const sort_1 = require("./sort");
9
+ /**
10
+ * Runs a unified filtered/sorted/paginated list query for metal-orm.
11
+ */
12
+ async function runPagedList(options) {
13
+ const query = options.query ?? {};
14
+ const qb = resolveQueryBuilder(options.qb);
15
+ const { page, pageSize } = (0, pagination_1.parsePagination)(query, {
16
+ defaultPageSize: options.defaultPageSize,
17
+ maxPageSize: options.maxPageSize
18
+ });
19
+ const filters = (0, filters_1.parseFilter)(query, options.filterMappings);
20
+ const parsedSort = (0, sort_1.parseSort)(query, options.sortableColumns, {
21
+ defaultSortBy: options.defaultSortBy,
22
+ defaultSortDirection: options.defaultSortDirection,
23
+ sortByKey: options.sortByKey,
24
+ sortDirectionKey: options.sortDirectionKey,
25
+ sortOrderKey: options.sortOrderKey
26
+ });
27
+ const inferredSortColumns = inferAllowedSortColumns(qb.getTable(), options.sortableColumns);
28
+ const allowedSortColumns = {
29
+ ...inferredSortColumns,
30
+ ...(options.allowedSortColumns ?? {})
31
+ };
32
+ const sortBy = parsedSort?.sortBy && parsedSort.sortBy in allowedSortColumns
33
+ ? parsedSort.sortBy
34
+ : undefined;
35
+ const defaultSortBy = !sortBy && options.defaultSortBy && options.defaultSortBy in allowedSortColumns
36
+ ? options.defaultSortBy
37
+ : undefined;
38
+ return (0, metal_orm_1.executeFilteredPaged)({
39
+ qb,
40
+ tableOrEntity: options.target,
41
+ session: options.session,
42
+ page,
43
+ pageSize,
44
+ filters: filters,
45
+ sortBy,
46
+ sortDirection: toOrderDirection(parsedSort?.sortDirection),
47
+ allowedSortColumns: Object.keys(allowedSortColumns).length
48
+ ? allowedSortColumns
49
+ : undefined,
50
+ defaultSortBy,
51
+ defaultSortDirection: toOrderDirection(options.defaultSortDirection),
52
+ tieBreakerColumn: options.tieBreakerColumn
53
+ });
54
+ }
55
+ /**
56
+ * Alias for runPagedList.
57
+ */
58
+ exports.executeCrudList = runPagedList;
59
+ function resolveQueryBuilder(qbOrFactory) {
60
+ return typeof qbOrFactory === "function" ? qbOrFactory() : qbOrFactory;
61
+ }
62
+ function inferAllowedSortColumns(table, sortableColumns) {
63
+ const output = {};
64
+ for (const [queryKey, field] of Object.entries(sortableColumns)) {
65
+ const columnName = toSortableColumnName(field);
66
+ if (!columnName) {
67
+ continue;
68
+ }
69
+ const column = resolveTableColumn(table, columnName);
70
+ if (!column) {
71
+ continue;
72
+ }
73
+ output[queryKey] = column;
74
+ }
75
+ return output;
76
+ }
77
+ function toSortableColumnName(value) {
78
+ if (Array.isArray(value)) {
79
+ if (value.length !== 1) {
80
+ return undefined;
81
+ }
82
+ const segment = String(value[0]).trim();
83
+ return segment.length > 0 ? segment : undefined;
84
+ }
85
+ if (typeof value !== "string") {
86
+ return undefined;
87
+ }
88
+ const path = value
89
+ .split(".")
90
+ .map((segment) => segment.trim())
91
+ .filter((segment) => segment.length > 0);
92
+ if (path.length !== 1) {
93
+ return undefined;
94
+ }
95
+ return path[0];
96
+ }
97
+ function resolveTableColumn(table, name) {
98
+ const byKey = table.columns[name];
99
+ if (byKey) {
100
+ return byKey;
101
+ }
102
+ return Object.values(table.columns).find((column) => column.name === name);
103
+ }
104
+ function toOrderDirection(direction) {
105
+ if (direction === "desc") {
106
+ return "DESC";
107
+ }
108
+ if (direction === "asc") {
109
+ return "ASC";
110
+ }
111
+ return undefined;
112
+ }