@zucker-framework/crud 1.0.1 → 1.0.2

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 ADDED
@@ -0,0 +1,11 @@
1
+ # CRUD
2
+
3
+ ## Services with custom dependency injection
4
+
5
+ Extend `CrudService<T>` when your Nest service has a custom constructor. Inject the configured `DATABASE_ADAPTER` explicitly and pass it, `DynamicQueryService`, and the model name to `super`. `CrudService` has no inherited Nest parameter tokens, so product dependencies cannot accidentally resolve as a database adapter or event bus. `CrudEntity` requires an ID and permits optional timestamps; the host chooses the ordering field. Existing `BaseService<T>` retains its standard injected constructor and delegates to the same implementation.
6
+
7
+ `BaseController(Entity, options)` accepts `only` (enabled methods), `routes` (method/path overrides and additional decorators), and `queryDto` (the runtime query DTO used by Nest validation). Defaults preserve the existing Framework HTTP surface; single-record `saveOrUpdate` must be explicitly enabled. Pass application policy as a configuration object instead of mutating generated prototypes in consumers. Controller factories are isolated, so configuring one resource does not change another.
8
+
9
+ ## Validated HTTP query DTO
10
+
11
+ `QueryParamDto` supplies transformation and validation for filters, orderBy, page (default 1), pageSize (default 10, maximum 100), and comma-separated includes/excludes. Non-string projection inputs remain subject to validation instead of throwing inside the transform. `QueryParamEntity` remains the query-builder API; it is not replaced by the transport DTO.
package/dist/index.d.mts CHANGED
@@ -479,10 +479,17 @@ declare class QueryParamEntity implements DynamicQueryParam {
479
479
  private cloneValue;
480
480
  }
481
481
 
482
+ /** Minimum CRUD record shape; timestamp names/presence belong to the host model. */
483
+ interface CrudEntity {
484
+ id: string;
485
+ createdAt?: Date;
486
+ updatedAt?: Date;
487
+ }
488
+
482
489
  /**
483
490
  * 链式查询构建器接口
484
491
  */
485
- interface IQueryBuilder<T extends GenericEntity> {
492
+ interface IQueryBuilder<T extends CrudEntity> {
486
493
  where<K extends string & keyof T>(field: K, op: string, value: unknown): IQueryBuilder<T>;
487
494
  whereEq<K extends string & keyof T>(field: K, value: T[K]): IQueryBuilder<T>;
488
495
  in<K extends string & keyof T>(field: K, values: unknown[]): IQueryBuilder<T>;
@@ -531,14 +538,14 @@ interface GroupByQueryArgs {
531
538
  _max?: Record<string, boolean>;
532
539
  }
533
540
  /** JOIN 子句接口 */
534
- interface IJoinClause<T extends GenericEntity> {
541
+ interface IJoinClause<T extends CrudEntity> {
535
542
  on(localField: string, foreignField: string): IQueryBuilder<T>;
536
543
  }
537
544
  /**
538
545
  * 链式查询构建器实现
539
546
  * 通过 DatabaseAdapter 执行查询,将链式 API 转换为 Prisma 兼容的查询参数
540
547
  */
541
- declare class QueryBuilder<T extends GenericEntity> implements IQueryBuilder<T> {
548
+ declare class QueryBuilder<T extends CrudEntity> implements IQueryBuilder<T> {
542
549
  private readonly db;
543
550
  private readonly modelName;
544
551
  private _conditions;
@@ -612,9 +619,9 @@ declare class QueryBuilder<T extends GenericEntity> implements IQueryBuilder<T>
612
619
  *
613
620
  * Subclasses should provide the model name and optionally customize the default order field.
614
621
  *
615
- * @template T - The entity type, must extend GenericEntity
622
+ * @template T - The entity type, must extend CrudEntity
616
623
  */
617
- declare abstract class BaseService<T extends GenericEntity> {
624
+ declare abstract class CrudService<T extends CrudEntity> {
618
625
  protected readonly db: DatabaseAdapter;
619
626
  protected readonly dynamicQuery: DynamicQueryService;
620
627
  protected readonly eventBus?: TopicEventBus | undefined;
@@ -791,9 +798,16 @@ declare abstract class BaseService<T extends GenericEntity> {
791
798
  */
792
799
  protected emitEventWithCancelCheck(type: EntityEventType, entity: unknown): Promise<void>;
793
800
  }
801
+ /** Nest-injected base retained for consumers whose constructor follows this signature.
802
+ * Custom constructor layouts should extend CrudService and inject their own dependencies. */
803
+ declare abstract class BaseService<T extends CrudEntity> extends CrudService<T> {
804
+ constructor(db: DatabaseAdapter, dynamicQuery: DynamicQueryService, modelName: string, eventBus?: TopicEventBus, options?: {
805
+ defaultOrderField?: string;
806
+ });
807
+ }
794
808
 
795
- declare class BaseControllerHost<T extends GenericEntity> {
796
- protected readonly service: BaseService<T>;
809
+ declare class BaseControllerHost<T extends CrudEntity> {
810
+ protected readonly service: CrudService<T>;
797
811
  create(..._args: unknown[]): Promise<T>;
798
812
  findOne(..._args: unknown[]): Promise<T>;
799
813
  findAll(..._args: unknown[]): Promise<unknown>;
@@ -831,7 +845,20 @@ declare class BaseControllerHost<T extends GenericEntity> {
831
845
  exists: boolean;
832
846
  }>;
833
847
  }
834
- declare function BaseController<T extends GenericEntity>(EntityClass: Type<T>): Type<BaseControllerHost<T>>;
848
+ type BaseControllerMethod = Exclude<keyof BaseControllerHost<CrudEntity>, 'service'>;
849
+ interface BaseControllerRouteOptions {
850
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
851
+ path?: string;
852
+ decorators?: MethodDecorator[];
853
+ }
854
+ interface BaseControllerOptions {
855
+ /** Restrict the generated HTTP surface to these operations. Defaults to existing standard routes. */
856
+ only?: readonly BaseControllerMethod[];
857
+ routes?: Partial<Record<BaseControllerMethod, BaseControllerRouteOptions | false>>;
858
+ /** Runtime query DTO for the host's validation/defaults policy. */
859
+ queryDto?: Type<DynamicQueryParam>;
860
+ }
861
+ declare function BaseController<T extends CrudEntity>(EntityClass: Type<T>, options?: BaseControllerOptions): Type<BaseControllerHost<T>>;
835
862
 
836
863
  interface CacheEnabledServiceOptions {
837
864
  /** 缓存 TTL(毫秒) */
@@ -1331,4 +1358,13 @@ interface CrudOptions {
1331
1358
  */
1332
1359
  declare function CrudControllerFactory<T>(serviceToken: new (...args: unknown[]) => T, options?: CrudOptions): new (...args: unknown[]) => object;
1333
1360
 
1334
- export { ALL_OPERATORS, type AggregateQueryArgs, BaseController, BaseControllerHost, BaseService, BetweenOperator, CRUD_METADATA, CURRENT_USER_PROVIDER, CacheEnabledService, type CacheEnabledServiceOptions, Cacheable, ContainsOperator, CreatorEventListener, CrudControllerFactory, type CrudModuleAsyncOptions, type CrudOptions, type CrudRouteConfig, type CurrentUserProvider, type DynamicQueryParam, DynamicQueryParamService, DynamicQueryService, ENTITY_VALIDATION_REGISTRY, EmptyOperator, EndsWithOperator, type EntityValidationRegistry, EqualsOperator, FILTER_OPERATORS, FilterOperatorFactory, type FilterOperatorType, type GroupByQueryArgs, GtOperator, GteOperator, HasOperator, HasSomeOperator, type IFilterOperator, type IJoinClause, type IQueryBuilder, InMemoryAdapter, InOperator, IsNotNullOperator, IsNullOperator, LikeOperator, LtOperator, LteOperator, NotEmptyOperator, NotEqualsOperator, NotInOperator, NotLikeOperator, type OptimizedQuery, type PaginatedResult, PrismaAdapter, type PrismaAdapterOptions, PrismaDatabaseAdapterModule, type PrismaDatabaseAdapterModuleOptions, QueryAnalyzer, QueryBuilder, QueryParamEntity as QueryParam, QueryParamEntity, type SaveResult, SimpleEntityValidationRegistry, type SoftDeletableEntity, type SoftDeleteQueryOptions, SoftDeleteService, StartsWithOperator, TermExpressionParser, TreeController, TreeControllerHost, TreeService, ValidateEventListener, ZuckerCrudModule, calculateTotalPages, emptyPagerResult, isPaged, mergeSaveResult, noPaging, parseExpression, parseFilterMap, rePaging, toPaginatedResult };
1361
+ declare class QueryParamDto {
1362
+ filters?: string;
1363
+ orderBy?: string;
1364
+ page: number;
1365
+ pageSize: number;
1366
+ includes?: string[];
1367
+ excludes?: string[];
1368
+ }
1369
+
1370
+ export { ALL_OPERATORS, type AggregateQueryArgs, BaseController, BaseControllerHost, type BaseControllerMethod, type BaseControllerOptions, type BaseControllerRouteOptions, BaseService, BetweenOperator, CRUD_METADATA, CURRENT_USER_PROVIDER, CacheEnabledService, type CacheEnabledServiceOptions, Cacheable, ContainsOperator, CreatorEventListener, CrudControllerFactory, type CrudEntity, type CrudModuleAsyncOptions, type CrudOptions, type CrudRouteConfig, CrudService, type CurrentUserProvider, type DynamicQueryParam, DynamicQueryParamService, DynamicQueryService, ENTITY_VALIDATION_REGISTRY, EmptyOperator, EndsWithOperator, type EntityValidationRegistry, EqualsOperator, FILTER_OPERATORS, FilterOperatorFactory, type FilterOperatorType, type GroupByQueryArgs, GtOperator, GteOperator, HasOperator, HasSomeOperator, type IFilterOperator, type IJoinClause, type IQueryBuilder, InMemoryAdapter, InOperator, IsNotNullOperator, IsNullOperator, LikeOperator, LtOperator, LteOperator, NotEmptyOperator, NotEqualsOperator, NotInOperator, NotLikeOperator, type OptimizedQuery, type PaginatedResult, PrismaAdapter, type PrismaAdapterOptions, PrismaDatabaseAdapterModule, type PrismaDatabaseAdapterModuleOptions, QueryAnalyzer, QueryBuilder, QueryParamEntity as QueryParam, QueryParamDto, QueryParamEntity, type SaveResult, SimpleEntityValidationRegistry, type SoftDeletableEntity, type SoftDeleteQueryOptions, SoftDeleteService, StartsWithOperator, TermExpressionParser, TreeController, TreeControllerHost, TreeService, ValidateEventListener, ZuckerCrudModule, calculateTotalPages, emptyPagerResult, isPaged, mergeSaveResult, noPaging, parseExpression, parseFilterMap, rePaging, toPaginatedResult };
package/dist/index.d.ts CHANGED
@@ -479,10 +479,17 @@ declare class QueryParamEntity implements DynamicQueryParam {
479
479
  private cloneValue;
480
480
  }
481
481
 
482
+ /** Minimum CRUD record shape; timestamp names/presence belong to the host model. */
483
+ interface CrudEntity {
484
+ id: string;
485
+ createdAt?: Date;
486
+ updatedAt?: Date;
487
+ }
488
+
482
489
  /**
483
490
  * 链式查询构建器接口
484
491
  */
485
- interface IQueryBuilder<T extends GenericEntity> {
492
+ interface IQueryBuilder<T extends CrudEntity> {
486
493
  where<K extends string & keyof T>(field: K, op: string, value: unknown): IQueryBuilder<T>;
487
494
  whereEq<K extends string & keyof T>(field: K, value: T[K]): IQueryBuilder<T>;
488
495
  in<K extends string & keyof T>(field: K, values: unknown[]): IQueryBuilder<T>;
@@ -531,14 +538,14 @@ interface GroupByQueryArgs {
531
538
  _max?: Record<string, boolean>;
532
539
  }
533
540
  /** JOIN 子句接口 */
534
- interface IJoinClause<T extends GenericEntity> {
541
+ interface IJoinClause<T extends CrudEntity> {
535
542
  on(localField: string, foreignField: string): IQueryBuilder<T>;
536
543
  }
537
544
  /**
538
545
  * 链式查询构建器实现
539
546
  * 通过 DatabaseAdapter 执行查询,将链式 API 转换为 Prisma 兼容的查询参数
540
547
  */
541
- declare class QueryBuilder<T extends GenericEntity> implements IQueryBuilder<T> {
548
+ declare class QueryBuilder<T extends CrudEntity> implements IQueryBuilder<T> {
542
549
  private readonly db;
543
550
  private readonly modelName;
544
551
  private _conditions;
@@ -612,9 +619,9 @@ declare class QueryBuilder<T extends GenericEntity> implements IQueryBuilder<T>
612
619
  *
613
620
  * Subclasses should provide the model name and optionally customize the default order field.
614
621
  *
615
- * @template T - The entity type, must extend GenericEntity
622
+ * @template T - The entity type, must extend CrudEntity
616
623
  */
617
- declare abstract class BaseService<T extends GenericEntity> {
624
+ declare abstract class CrudService<T extends CrudEntity> {
618
625
  protected readonly db: DatabaseAdapter;
619
626
  protected readonly dynamicQuery: DynamicQueryService;
620
627
  protected readonly eventBus?: TopicEventBus | undefined;
@@ -791,9 +798,16 @@ declare abstract class BaseService<T extends GenericEntity> {
791
798
  */
792
799
  protected emitEventWithCancelCheck(type: EntityEventType, entity: unknown): Promise<void>;
793
800
  }
801
+ /** Nest-injected base retained for consumers whose constructor follows this signature.
802
+ * Custom constructor layouts should extend CrudService and inject their own dependencies. */
803
+ declare abstract class BaseService<T extends CrudEntity> extends CrudService<T> {
804
+ constructor(db: DatabaseAdapter, dynamicQuery: DynamicQueryService, modelName: string, eventBus?: TopicEventBus, options?: {
805
+ defaultOrderField?: string;
806
+ });
807
+ }
794
808
 
795
- declare class BaseControllerHost<T extends GenericEntity> {
796
- protected readonly service: BaseService<T>;
809
+ declare class BaseControllerHost<T extends CrudEntity> {
810
+ protected readonly service: CrudService<T>;
797
811
  create(..._args: unknown[]): Promise<T>;
798
812
  findOne(..._args: unknown[]): Promise<T>;
799
813
  findAll(..._args: unknown[]): Promise<unknown>;
@@ -831,7 +845,20 @@ declare class BaseControllerHost<T extends GenericEntity> {
831
845
  exists: boolean;
832
846
  }>;
833
847
  }
834
- declare function BaseController<T extends GenericEntity>(EntityClass: Type<T>): Type<BaseControllerHost<T>>;
848
+ type BaseControllerMethod = Exclude<keyof BaseControllerHost<CrudEntity>, 'service'>;
849
+ interface BaseControllerRouteOptions {
850
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
851
+ path?: string;
852
+ decorators?: MethodDecorator[];
853
+ }
854
+ interface BaseControllerOptions {
855
+ /** Restrict the generated HTTP surface to these operations. Defaults to existing standard routes. */
856
+ only?: readonly BaseControllerMethod[];
857
+ routes?: Partial<Record<BaseControllerMethod, BaseControllerRouteOptions | false>>;
858
+ /** Runtime query DTO for the host's validation/defaults policy. */
859
+ queryDto?: Type<DynamicQueryParam>;
860
+ }
861
+ declare function BaseController<T extends CrudEntity>(EntityClass: Type<T>, options?: BaseControllerOptions): Type<BaseControllerHost<T>>;
835
862
 
836
863
  interface CacheEnabledServiceOptions {
837
864
  /** 缓存 TTL(毫秒) */
@@ -1331,4 +1358,13 @@ interface CrudOptions {
1331
1358
  */
1332
1359
  declare function CrudControllerFactory<T>(serviceToken: new (...args: unknown[]) => T, options?: CrudOptions): new (...args: unknown[]) => object;
1333
1360
 
1334
- export { ALL_OPERATORS, type AggregateQueryArgs, BaseController, BaseControllerHost, BaseService, BetweenOperator, CRUD_METADATA, CURRENT_USER_PROVIDER, CacheEnabledService, type CacheEnabledServiceOptions, Cacheable, ContainsOperator, CreatorEventListener, CrudControllerFactory, type CrudModuleAsyncOptions, type CrudOptions, type CrudRouteConfig, type CurrentUserProvider, type DynamicQueryParam, DynamicQueryParamService, DynamicQueryService, ENTITY_VALIDATION_REGISTRY, EmptyOperator, EndsWithOperator, type EntityValidationRegistry, EqualsOperator, FILTER_OPERATORS, FilterOperatorFactory, type FilterOperatorType, type GroupByQueryArgs, GtOperator, GteOperator, HasOperator, HasSomeOperator, type IFilterOperator, type IJoinClause, type IQueryBuilder, InMemoryAdapter, InOperator, IsNotNullOperator, IsNullOperator, LikeOperator, LtOperator, LteOperator, NotEmptyOperator, NotEqualsOperator, NotInOperator, NotLikeOperator, type OptimizedQuery, type PaginatedResult, PrismaAdapter, type PrismaAdapterOptions, PrismaDatabaseAdapterModule, type PrismaDatabaseAdapterModuleOptions, QueryAnalyzer, QueryBuilder, QueryParamEntity as QueryParam, QueryParamEntity, type SaveResult, SimpleEntityValidationRegistry, type SoftDeletableEntity, type SoftDeleteQueryOptions, SoftDeleteService, StartsWithOperator, TermExpressionParser, TreeController, TreeControllerHost, TreeService, ValidateEventListener, ZuckerCrudModule, calculateTotalPages, emptyPagerResult, isPaged, mergeSaveResult, noPaging, parseExpression, parseFilterMap, rePaging, toPaginatedResult };
1361
+ declare class QueryParamDto {
1362
+ filters?: string;
1363
+ orderBy?: string;
1364
+ page: number;
1365
+ pageSize: number;
1366
+ includes?: string[];
1367
+ excludes?: string[];
1368
+ }
1369
+
1370
+ export { ALL_OPERATORS, type AggregateQueryArgs, BaseController, BaseControllerHost, type BaseControllerMethod, type BaseControllerOptions, type BaseControllerRouteOptions, BaseService, BetweenOperator, CRUD_METADATA, CURRENT_USER_PROVIDER, CacheEnabledService, type CacheEnabledServiceOptions, Cacheable, ContainsOperator, CreatorEventListener, CrudControllerFactory, type CrudEntity, type CrudModuleAsyncOptions, type CrudOptions, type CrudRouteConfig, CrudService, type CurrentUserProvider, type DynamicQueryParam, DynamicQueryParamService, DynamicQueryService, ENTITY_VALIDATION_REGISTRY, EmptyOperator, EndsWithOperator, type EntityValidationRegistry, EqualsOperator, FILTER_OPERATORS, FilterOperatorFactory, type FilterOperatorType, type GroupByQueryArgs, GtOperator, GteOperator, HasOperator, HasSomeOperator, type IFilterOperator, type IJoinClause, type IQueryBuilder, InMemoryAdapter, InOperator, IsNotNullOperator, IsNullOperator, LikeOperator, LtOperator, LteOperator, NotEmptyOperator, NotEqualsOperator, NotInOperator, NotLikeOperator, type OptimizedQuery, type PaginatedResult, PrismaAdapter, type PrismaAdapterOptions, PrismaDatabaseAdapterModule, type PrismaDatabaseAdapterModuleOptions, QueryAnalyzer, QueryBuilder, QueryParamEntity as QueryParam, QueryParamDto, QueryParamEntity, type SaveResult, SimpleEntityValidationRegistry, type SoftDeletableEntity, type SoftDeleteQueryOptions, SoftDeleteService, StartsWithOperator, TermExpressionParser, TreeController, TreeControllerHost, TreeService, ValidateEventListener, ZuckerCrudModule, calculateTotalPages, emptyPagerResult, isPaged, mergeSaveResult, noPaging, parseExpression, parseFilterMap, rePaging, toPaginatedResult };
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  ContainsOperator: () => ContainsOperator,
34
34
  CreatorEventListener: () => CreatorEventListener,
35
35
  CrudControllerFactory: () => CrudControllerFactory,
36
+ CrudService: () => CrudService,
36
37
  DATABASE_ADAPTER: () => import_core2.DATABASE_ADAPTER,
37
38
  DynamicQueryParamService: () => DynamicQueryParamService,
38
39
  DynamicQueryService: () => DynamicQueryService,
@@ -62,6 +63,7 @@ __export(index_exports, {
62
63
  QueryAnalyzer: () => QueryAnalyzer,
63
64
  QueryBuilder: () => QueryBuilder,
64
65
  QueryParam: () => QueryParamEntity,
66
+ QueryParamDto: () => QueryParamDto,
65
67
  QueryParamEntity: () => QueryParamEntity,
66
68
  SimpleEntityValidationRegistry: () => SimpleEntityValidationRegistry,
67
69
  SoftDeleteService: () => SoftDeleteService,
@@ -2563,9 +2565,9 @@ function hasErrorCode(error, code) {
2563
2565
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
2564
2566
  }
2565
2567
  __name(hasErrorCode, "hasErrorCode");
2566
- var BaseService = class {
2568
+ var CrudService = class {
2567
2569
  static {
2568
- __name(this, "BaseService");
2570
+ __name(this, "CrudService");
2569
2571
  }
2570
2572
  db;
2571
2573
  dynamicQuery;
@@ -3109,6 +3111,14 @@ var BaseService = class {
3109
3111
  }
3110
3112
  }
3111
3113
  };
3114
+ var BaseService = class extends CrudService {
3115
+ static {
3116
+ __name(this, "BaseService");
3117
+ }
3118
+ constructor(db, dynamicQuery, modelName, eventBus, options) {
3119
+ super(db, dynamicQuery, modelName, eventBus, options);
3120
+ }
3121
+ };
3112
3122
  BaseService = _ts_decorate4([
3113
3123
  (0, import_common4.Injectable)(),
3114
3124
  _ts_param3(0, (0, import_common4.Inject)(import_core2.DATABASE_ADAPTER)),
@@ -3212,7 +3222,7 @@ var BaseControllerHost = class {
3212
3222
  throw new Error("Not implemented");
3213
3223
  }
3214
3224
  };
3215
- function BaseController(EntityClass) {
3225
+ function BaseController(EntityClass, options = {}) {
3216
3226
  let BaseControllerHostImpl = class BaseControllerHostImpl {
3217
3227
  static {
3218
3228
  __name(this, "BaseControllerHostImpl");
@@ -3304,6 +3314,10 @@ function BaseController(EntityClass) {
3304
3314
  async findAll(query) {
3305
3315
  return this.service.findAll(query);
3306
3316
  }
3317
+ /** Explicitly opt into single-record upsert through routes/only. */
3318
+ async saveOrUpdate(data) {
3319
+ return this.service.saveOrUpdate(data);
3320
+ }
3307
3321
  // ===================== Save Routes =====================
3308
3322
  /**
3309
3323
  * 新增单个数据,并返回新增后的数据
@@ -3515,6 +3529,15 @@ function BaseController(EntityClass) {
3515
3529
  ]),
3516
3530
  _ts_metadata4("design:returntype", Promise)
3517
3531
  ], BaseControllerHostImpl.prototype, "findAll", null);
3532
+ _ts_decorate5([
3533
+ (0, import_common5.Patch)("save"),
3534
+ _ts_param4(0, (0, import_common5.Body)()),
3535
+ _ts_metadata4("design:type", Function),
3536
+ _ts_metadata4("design:paramtypes", [
3537
+ typeof Partial === "undefined" ? Object : Partial
3538
+ ]),
3539
+ _ts_metadata4("design:returntype", Promise)
3540
+ ], BaseControllerHostImpl.prototype, "saveOrUpdate", null);
3518
3541
  _ts_decorate5([
3519
3542
  (0, import_common5.Post)(),
3520
3543
  (0, import_swagger.ApiOperation)({
@@ -3628,6 +3651,35 @@ function BaseController(EntityClass) {
3628
3651
  ]),
3629
3652
  _ts_metadata4("design:returntype", Promise)
3630
3653
  ], BaseControllerHostImpl.prototype, "deleteMany", null);
3654
+ const prototype = BaseControllerHostImpl.prototype;
3655
+ const verbs = {
3656
+ GET: import_common5.Get,
3657
+ POST: import_common5.Post,
3658
+ PUT: import_common5.Put,
3659
+ PATCH: import_common5.Patch,
3660
+ DELETE: import_common5.Delete
3661
+ };
3662
+ for (const name of Object.getOwnPropertyNames(prototype)) {
3663
+ if (name === "constructor") continue;
3664
+ const key = name;
3665
+ const route = options.routes?.[key];
3666
+ const enabled = options.only ? options.only.includes(key) : key !== "saveOrUpdate" || route !== void 0;
3667
+ if (!enabled || route === false) {
3668
+ Reflect.deleteProperty(prototype, key);
3669
+ continue;
3670
+ }
3671
+ if (route) {
3672
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
3673
+ if (route.method) verbs[route.method](route.path ?? "")(prototype, key, descriptor);
3674
+ else if (route.path !== void 0) throw new Error("A route path override requires an HTTP method");
3675
+ for (const decorator of route.decorators ?? []) decorator(prototype, key, descriptor);
3676
+ }
3677
+ }
3678
+ if (options.queryDto && Object.hasOwn(prototype, "findAll")) {
3679
+ Reflect.defineMetadata("design:paramtypes", [
3680
+ options.queryDto
3681
+ ], prototype, "findAll");
3682
+ }
3631
3683
  return BaseControllerHostImpl;
3632
3684
  }
3633
3685
  __name(BaseController, "BaseController");
@@ -5880,6 +5932,122 @@ function CrudControllerFactory(serviceToken, options = {}) {
5880
5932
  return CrudController;
5881
5933
  }
5882
5934
  __name(CrudControllerFactory, "CrudControllerFactory");
5935
+
5936
+ // src/query/query-param.dto.ts
5937
+ var import_class_validator = require("class-validator");
5938
+ var import_class_transformer = require("class-transformer");
5939
+ var import_swagger3 = require("@nestjs/swagger");
5940
+ function _ts_decorate13(decorators, target, key, desc) {
5941
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5942
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
5943
+ r = Reflect.decorate(decorators, target, key, desc);
5944
+ } else {
5945
+ for (var i = decorators.length - 1; i >= 0; i--) {
5946
+ if (d = decorators[i]) {
5947
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5948
+ }
5949
+ }
5950
+ }
5951
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
5952
+ }
5953
+ __name(_ts_decorate13, "_ts_decorate");
5954
+ function _ts_metadata10(metadataKey, metadataValue) {
5955
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
5956
+ return Reflect.metadata(metadataKey, metadataValue);
5957
+ }
5958
+ }
5959
+ __name(_ts_metadata10, "_ts_metadata");
5960
+ var QueryParamDto = class {
5961
+ static {
5962
+ __name(this, "QueryParamDto");
5963
+ }
5964
+ filters;
5965
+ orderBy;
5966
+ page = 1;
5967
+ pageSize = 10;
5968
+ includes;
5969
+ excludes;
5970
+ };
5971
+ _ts_decorate13([
5972
+ (0, import_swagger3.ApiProperty)({
5973
+ required: false,
5974
+ description: 'JSON\u683C\u5F0F\u7684\u67E5\u8BE2\u6761\u4EF6\uFF0C\u4F8B\u5982: {"name$like":"example","age$gte":18}'
5975
+ }),
5976
+ (0, import_class_validator.IsOptional)(),
5977
+ (0, import_class_validator.IsString)(),
5978
+ _ts_metadata10("design:type", String)
5979
+ ], QueryParamDto.prototype, "filters", void 0);
5980
+ _ts_decorate13([
5981
+ (0, import_swagger3.ApiProperty)({
5982
+ required: false,
5983
+ description: '\u6392\u5E8F\u5B57\u6BB5\uFF0C\u4F8B\u5982: "age desc, createdAt asc"'
5984
+ }),
5985
+ (0, import_class_validator.IsOptional)(),
5986
+ (0, import_class_validator.IsString)(),
5987
+ _ts_metadata10("design:type", String)
5988
+ ], QueryParamDto.prototype, "orderBy", void 0);
5989
+ _ts_decorate13([
5990
+ (0, import_swagger3.ApiProperty)({
5991
+ required: false,
5992
+ default: 1,
5993
+ minimum: 1,
5994
+ description: "\u9875\u7801"
5995
+ }),
5996
+ (0, import_class_validator.IsOptional)(),
5997
+ (0, import_class_transformer.Type)(() => Number),
5998
+ (0, import_class_validator.IsNumber)(),
5999
+ (0, import_class_validator.Min)(1),
6000
+ _ts_metadata10("design:type", Number)
6001
+ ], QueryParamDto.prototype, "page", void 0);
6002
+ _ts_decorate13([
6003
+ (0, import_swagger3.ApiProperty)({
6004
+ required: false,
6005
+ default: 10,
6006
+ minimum: 1,
6007
+ maximum: 100,
6008
+ description: "\u6BCF\u9875\u6761\u6570"
6009
+ }),
6010
+ (0, import_class_validator.IsOptional)(),
6011
+ (0, import_class_transformer.Type)(() => Number),
6012
+ (0, import_class_validator.IsNumber)(),
6013
+ (0, import_class_validator.Min)(1),
6014
+ (0, import_class_validator.Max)(100),
6015
+ _ts_metadata10("design:type", Number)
6016
+ ], QueryParamDto.prototype, "pageSize", void 0);
6017
+ _ts_decorate13([
6018
+ (0, import_swagger3.ApiProperty)({
6019
+ required: false,
6020
+ type: [
6021
+ String
6022
+ ],
6023
+ description: "\u6307\u5B9A\u8FD4\u56DE\u7684\u5B57\u6BB5\uFF0C\u591A\u4E2A\u5B57\u6BB5\u7528\u9017\u53F7\u5206\u9694"
6024
+ }),
6025
+ (0, import_class_validator.IsOptional)(),
6026
+ (0, import_class_transformer.Transform)(({ value }) => {
6027
+ if (!value) return void 0;
6028
+ if (Array.isArray(value) || typeof value !== "string") return value;
6029
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
6030
+ }),
6031
+ (0, import_class_validator.IsArray)(),
6032
+ _ts_metadata10("design:type", Array)
6033
+ ], QueryParamDto.prototype, "includes", void 0);
6034
+ _ts_decorate13([
6035
+ (0, import_swagger3.ApiProperty)({
6036
+ required: false,
6037
+ type: [
6038
+ String
6039
+ ],
6040
+ description: "\u6307\u5B9A\u6392\u9664\u7684\u5B57\u6BB5\uFF0C\u591A\u4E2A\u5B57\u6BB5\u7528\u9017\u53F7\u5206\u9694"
6041
+ }),
6042
+ (0, import_class_validator.IsOptional)(),
6043
+ (0, import_class_transformer.Transform)(({ value }) => {
6044
+ if (!value) return void 0;
6045
+ if (Array.isArray(value) || typeof value !== "string") return value;
6046
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
6047
+ }),
6048
+ (0, import_class_validator.IsArray)(),
6049
+ _ts_metadata10("design:type", Array)
6050
+ ], QueryParamDto.prototype, "excludes", void 0);
5883
6051
  // Annotate the CommonJS export names for ESM import in node:
5884
6052
  0 && (module.exports = {
5885
6053
  ALL_OPERATORS,
@@ -5894,6 +6062,7 @@ __name(CrudControllerFactory, "CrudControllerFactory");
5894
6062
  ContainsOperator,
5895
6063
  CreatorEventListener,
5896
6064
  CrudControllerFactory,
6065
+ CrudService,
5897
6066
  DATABASE_ADAPTER,
5898
6067
  DynamicQueryParamService,
5899
6068
  DynamicQueryService,
@@ -5923,6 +6092,7 @@ __name(CrudControllerFactory, "CrudControllerFactory");
5923
6092
  QueryAnalyzer,
5924
6093
  QueryBuilder,
5925
6094
  QueryParam,
6095
+ QueryParamDto,
5926
6096
  QueryParamEntity,
5927
6097
  SimpleEntityValidationRegistry,
5928
6098
  SoftDeleteService,
package/dist/index.mjs CHANGED
@@ -2480,9 +2480,9 @@ function hasErrorCode(error, code) {
2480
2480
  return typeof error === "object" && error !== null && "code" in error && error.code === code;
2481
2481
  }
2482
2482
  __name(hasErrorCode, "hasErrorCode");
2483
- var BaseService = class {
2483
+ var CrudService = class {
2484
2484
  static {
2485
- __name(this, "BaseService");
2485
+ __name(this, "CrudService");
2486
2486
  }
2487
2487
  db;
2488
2488
  dynamicQuery;
@@ -3026,6 +3026,14 @@ var BaseService = class {
3026
3026
  }
3027
3027
  }
3028
3028
  };
3029
+ var BaseService = class extends CrudService {
3030
+ static {
3031
+ __name(this, "BaseService");
3032
+ }
3033
+ constructor(db, dynamicQuery, modelName, eventBus, options) {
3034
+ super(db, dynamicQuery, modelName, eventBus, options);
3035
+ }
3036
+ };
3029
3037
  BaseService = _ts_decorate4([
3030
3038
  Injectable3(),
3031
3039
  _ts_param3(0, Inject3(DATABASE_ADAPTER2)),
@@ -3129,7 +3137,7 @@ var BaseControllerHost = class {
3129
3137
  throw new Error("Not implemented");
3130
3138
  }
3131
3139
  };
3132
- function BaseController(EntityClass) {
3140
+ function BaseController(EntityClass, options = {}) {
3133
3141
  let BaseControllerHostImpl = class BaseControllerHostImpl {
3134
3142
  static {
3135
3143
  __name(this, "BaseControllerHostImpl");
@@ -3221,6 +3229,10 @@ function BaseController(EntityClass) {
3221
3229
  async findAll(query) {
3222
3230
  return this.service.findAll(query);
3223
3231
  }
3232
+ /** Explicitly opt into single-record upsert through routes/only. */
3233
+ async saveOrUpdate(data) {
3234
+ return this.service.saveOrUpdate(data);
3235
+ }
3224
3236
  // ===================== Save Routes =====================
3225
3237
  /**
3226
3238
  * 新增单个数据,并返回新增后的数据
@@ -3432,6 +3444,15 @@ function BaseController(EntityClass) {
3432
3444
  ]),
3433
3445
  _ts_metadata4("design:returntype", Promise)
3434
3446
  ], BaseControllerHostImpl.prototype, "findAll", null);
3447
+ _ts_decorate5([
3448
+ Patch("save"),
3449
+ _ts_param4(0, Body()),
3450
+ _ts_metadata4("design:type", Function),
3451
+ _ts_metadata4("design:paramtypes", [
3452
+ typeof Partial === "undefined" ? Object : Partial
3453
+ ]),
3454
+ _ts_metadata4("design:returntype", Promise)
3455
+ ], BaseControllerHostImpl.prototype, "saveOrUpdate", null);
3435
3456
  _ts_decorate5([
3436
3457
  Post(),
3437
3458
  ApiOperation({
@@ -3545,6 +3566,35 @@ function BaseController(EntityClass) {
3545
3566
  ]),
3546
3567
  _ts_metadata4("design:returntype", Promise)
3547
3568
  ], BaseControllerHostImpl.prototype, "deleteMany", null);
3569
+ const prototype = BaseControllerHostImpl.prototype;
3570
+ const verbs = {
3571
+ GET: Get,
3572
+ POST: Post,
3573
+ PUT: Put,
3574
+ PATCH: Patch,
3575
+ DELETE: Delete
3576
+ };
3577
+ for (const name of Object.getOwnPropertyNames(prototype)) {
3578
+ if (name === "constructor") continue;
3579
+ const key = name;
3580
+ const route = options.routes?.[key];
3581
+ const enabled = options.only ? options.only.includes(key) : key !== "saveOrUpdate" || route !== void 0;
3582
+ if (!enabled || route === false) {
3583
+ Reflect.deleteProperty(prototype, key);
3584
+ continue;
3585
+ }
3586
+ if (route) {
3587
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
3588
+ if (route.method) verbs[route.method](route.path ?? "")(prototype, key, descriptor);
3589
+ else if (route.path !== void 0) throw new Error("A route path override requires an HTTP method");
3590
+ for (const decorator of route.decorators ?? []) decorator(prototype, key, descriptor);
3591
+ }
3592
+ }
3593
+ if (options.queryDto && Object.hasOwn(prototype, "findAll")) {
3594
+ Reflect.defineMetadata("design:paramtypes", [
3595
+ options.queryDto
3596
+ ], prototype, "findAll");
3597
+ }
3548
3598
  return BaseControllerHostImpl;
3549
3599
  }
3550
3600
  __name(BaseController, "BaseController");
@@ -5797,6 +5847,122 @@ function CrudControllerFactory(serviceToken, options = {}) {
5797
5847
  return CrudController;
5798
5848
  }
5799
5849
  __name(CrudControllerFactory, "CrudControllerFactory");
5850
+
5851
+ // src/query/query-param.dto.ts
5852
+ import { IsOptional, IsString, IsNumber, Min, Max, IsArray } from "class-validator";
5853
+ import { Transform, Type } from "class-transformer";
5854
+ import { ApiProperty } from "@nestjs/swagger";
5855
+ function _ts_decorate13(decorators, target, key, desc) {
5856
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5857
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
5858
+ r = Reflect.decorate(decorators, target, key, desc);
5859
+ } else {
5860
+ for (var i = decorators.length - 1; i >= 0; i--) {
5861
+ if (d = decorators[i]) {
5862
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5863
+ }
5864
+ }
5865
+ }
5866
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
5867
+ }
5868
+ __name(_ts_decorate13, "_ts_decorate");
5869
+ function _ts_metadata10(metadataKey, metadataValue) {
5870
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
5871
+ return Reflect.metadata(metadataKey, metadataValue);
5872
+ }
5873
+ }
5874
+ __name(_ts_metadata10, "_ts_metadata");
5875
+ var QueryParamDto = class {
5876
+ static {
5877
+ __name(this, "QueryParamDto");
5878
+ }
5879
+ filters;
5880
+ orderBy;
5881
+ page = 1;
5882
+ pageSize = 10;
5883
+ includes;
5884
+ excludes;
5885
+ };
5886
+ _ts_decorate13([
5887
+ ApiProperty({
5888
+ required: false,
5889
+ description: 'JSON\u683C\u5F0F\u7684\u67E5\u8BE2\u6761\u4EF6\uFF0C\u4F8B\u5982: {"name$like":"example","age$gte":18}'
5890
+ }),
5891
+ IsOptional(),
5892
+ IsString(),
5893
+ _ts_metadata10("design:type", String)
5894
+ ], QueryParamDto.prototype, "filters", void 0);
5895
+ _ts_decorate13([
5896
+ ApiProperty({
5897
+ required: false,
5898
+ description: '\u6392\u5E8F\u5B57\u6BB5\uFF0C\u4F8B\u5982: "age desc, createdAt asc"'
5899
+ }),
5900
+ IsOptional(),
5901
+ IsString(),
5902
+ _ts_metadata10("design:type", String)
5903
+ ], QueryParamDto.prototype, "orderBy", void 0);
5904
+ _ts_decorate13([
5905
+ ApiProperty({
5906
+ required: false,
5907
+ default: 1,
5908
+ minimum: 1,
5909
+ description: "\u9875\u7801"
5910
+ }),
5911
+ IsOptional(),
5912
+ Type(() => Number),
5913
+ IsNumber(),
5914
+ Min(1),
5915
+ _ts_metadata10("design:type", Number)
5916
+ ], QueryParamDto.prototype, "page", void 0);
5917
+ _ts_decorate13([
5918
+ ApiProperty({
5919
+ required: false,
5920
+ default: 10,
5921
+ minimum: 1,
5922
+ maximum: 100,
5923
+ description: "\u6BCF\u9875\u6761\u6570"
5924
+ }),
5925
+ IsOptional(),
5926
+ Type(() => Number),
5927
+ IsNumber(),
5928
+ Min(1),
5929
+ Max(100),
5930
+ _ts_metadata10("design:type", Number)
5931
+ ], QueryParamDto.prototype, "pageSize", void 0);
5932
+ _ts_decorate13([
5933
+ ApiProperty({
5934
+ required: false,
5935
+ type: [
5936
+ String
5937
+ ],
5938
+ description: "\u6307\u5B9A\u8FD4\u56DE\u7684\u5B57\u6BB5\uFF0C\u591A\u4E2A\u5B57\u6BB5\u7528\u9017\u53F7\u5206\u9694"
5939
+ }),
5940
+ IsOptional(),
5941
+ Transform(({ value }) => {
5942
+ if (!value) return void 0;
5943
+ if (Array.isArray(value) || typeof value !== "string") return value;
5944
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
5945
+ }),
5946
+ IsArray(),
5947
+ _ts_metadata10("design:type", Array)
5948
+ ], QueryParamDto.prototype, "includes", void 0);
5949
+ _ts_decorate13([
5950
+ ApiProperty({
5951
+ required: false,
5952
+ type: [
5953
+ String
5954
+ ],
5955
+ description: "\u6307\u5B9A\u6392\u9664\u7684\u5B57\u6BB5\uFF0C\u591A\u4E2A\u5B57\u6BB5\u7528\u9017\u53F7\u5206\u9694"
5956
+ }),
5957
+ IsOptional(),
5958
+ Transform(({ value }) => {
5959
+ if (!value) return void 0;
5960
+ if (Array.isArray(value) || typeof value !== "string") return value;
5961
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
5962
+ }),
5963
+ IsArray(),
5964
+ _ts_metadata10("design:type", Array)
5965
+ ], QueryParamDto.prototype, "excludes", void 0);
5800
5966
  export {
5801
5967
  ALL_OPERATORS,
5802
5968
  BaseController,
@@ -5810,6 +5976,7 @@ export {
5810
5976
  ContainsOperator,
5811
5977
  CreatorEventListener,
5812
5978
  CrudControllerFactory,
5979
+ CrudService,
5813
5980
  DATABASE_ADAPTER2 as DATABASE_ADAPTER,
5814
5981
  DynamicQueryParamService,
5815
5982
  DynamicQueryService,
@@ -5839,6 +6006,7 @@ export {
5839
6006
  QueryAnalyzer,
5840
6007
  QueryBuilder,
5841
6008
  QueryParamEntity as QueryParam,
6009
+ QueryParamDto,
5842
6010
  QueryParamEntity,
5843
6011
  SimpleEntityValidationRegistry,
5844
6012
  SoftDeleteService,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zucker-framework/crud",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -23,20 +23,24 @@
23
23
  "THIRD_PARTY_NOTICES"
24
24
  ],
25
25
  "dependencies": {
26
- "@zucker-framework/core": "1.0.0"
26
+ "@zucker-framework/core": "1.0.2"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "@nestjs/common": "^12.0.0",
30
30
  "@nestjs/core": "^12.0.0",
31
31
  "@nestjs/swagger": "^12.0.0",
32
- "@zucker-framework/cache": "1.0.0",
33
- "@zucker-framework/event": "1.0.0"
32
+ "@zucker-framework/cache": "1.0.2",
33
+ "@zucker-framework/event": "1.0.2",
34
+ "class-transformer": "^0.5.0",
35
+ "class-validator": "^0.14.0 || ^0.15.0"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@nestjs/swagger": "^12.0.1",
37
- "@zucker-framework/cache": "1.0.0",
38
- "@zucker-framework/event": "1.0.0",
39
- "@types/node": "^20.19.43"
39
+ "@zucker-framework/cache": "1.0.2",
40
+ "@zucker-framework/event": "1.0.2",
41
+ "@types/node": "^20.19.43",
42
+ "class-transformer": "^0.5.1",
43
+ "class-validator": "^0.15.1"
40
44
  },
41
45
  "license": "Apache-2.0",
42
46
  "author": "Lind",
@@ -50,7 +54,7 @@
50
54
  "registry": "https://registry.npmjs.org/"
51
55
  },
52
56
  "scripts": {
53
- "build": "tsup src/index.ts --format cjs,esm --dts --external @nestjs/common --external @nestjs/core --external @nestjs/swagger --external @zucker-framework/core --external @zucker-framework/event --external @zucker-framework/cache --external eventemitter2",
57
+ "build": "tsup src/index.ts --format cjs,esm --dts --external @nestjs/common --external @nestjs/core --external @nestjs/swagger --external @zucker-framework/core --external @zucker-framework/event --external @zucker-framework/cache --external eventemitter2 --external class-transformer --external class-validator",
54
58
  "clean": "rm -rf dist",
55
59
  "lint": "eslint src/"
56
60
  }