Skip to content

feat(rest): Created rest package#432

Open
TriPSs wants to merge 52 commits intomasterfrom
feature/rest
Open

feat(rest): Created rest package#432
TriPSs wants to merge 52 commits intomasterfrom
feature/rest

Conversation

@TriPSs
Copy link
Copy Markdown
Owner

@TriPSs TriPSs commented Feb 12, 2026

Summary by CodeRabbit

  • New Features

    • New REST query package for automatic CRUD controllers, export/CSV, hooks, and authorization filters.
    • Added offset and none (array) pagination strategies and generated OpenAPI spec for the example REST API.
    • Example Todo/Sub-task/Tag REST app with E2E fixtures and tests.
  • Chores

    • Project/config + build/test setup for the new package; updated dependency versions.
    • CI: Dependabot and stale-issue automation added; updated ignore rules.

TriPSs added 30 commits November 9, 2023 13:38
# Conflicts:
#	examples/project.json
#	package.json
#	yarn.lock
# Conflicts:
#	package.json
#	yarn.lock
…to use it

- Added `FindOneArgsType` for standardizing ID parameter handling.
- Updated delete, update, and read resolvers to use `FindOneArgsType`.
- Introduced new `id-field.decorator` for marking ID fields.
- Removed redundant `BadRequestException` in read resolver.
TriPSs added 14 commits October 10, 2024 14:41
- Correct `FindOneArgsType` parameter in `read.resolver.ts`
- Enhance `field.decorator.ts` to properly retrieve metadata
- Add `IsNumber` and `IsString` validators for specific types
- Improve handling of array options and validation
# Conflicts:
#	examples/project.json
#	package.json
#	yarn.lock
- Added `.npmrc` to the `.gitignore` file for excluding configuration from version control.
- Replaced `resolver.provider` with `controller.provider` implementation.
- Updated import paths and typings throughout the module.
- Added `ParamArgsType` and `QueryParamArgsType` in `param-args.type`.
- Refactored `OffsetPaging` to use the updated `Field` decorator.
- Removed unused decorators and methods related to resolvers.
- Updated dependencies and added `csv-stringify`.
- Bump versions of `@nestjs` packages and other dependencies in `package.json`.
- Update `tslib` in `packages/query-rest`.
- Upgrade `@mikro-orm` dependencies in `packages/query-mikro-orm`.
…leFieldOptionsToField`

- Enhanced `filterableFieldOptionsToField` to accept default empty object for `advancedOptions`.
…y decorator

- Corrected type annotations in `ManyToMany` decorator to resolve type issues in `tag.entity`.
@TriPSs
Copy link
Copy Markdown
Owner Author

TriPSs commented Feb 12, 2026

@coderabbitai review

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Feb 12, 2026

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit b8a2d39

Command Status Duration Result
nx run-many --target=e2e --all ❌ Failed 2m 3s View ↗
nx run-many --target=lint --all ❌ Failed 9s View ↗
nx run-many --target=test --all ✅ Succeeded 1s View ↗
nx run-many --target=build --all ✅ Succeeded 3s View ↗
nx run workspace:version ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-12 16:58:01 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Feb 12, 2026

Open in StackBlitz

@ptc-org/nestjs-query-core

npm i https://pkg.pr.new/TriPSs/nestjs-query/@ptc-org/nestjs-query-core@432

@ptc-org/nestjs-query-graphql

npm i https://pkg.pr.new/TriPSs/nestjs-query/@ptc-org/nestjs-query-graphql@432

@ptc-org/nestjs-query-mikro-orm

npm i https://pkg.pr.new/TriPSs/nestjs-query/@ptc-org/nestjs-query-mikro-orm@432

@ptc-org/nestjs-query-mongoose

npm i https://pkg.pr.new/TriPSs/nestjs-query/@ptc-org/nestjs-query-mongoose@432

@ptc-org/nestjs-query-rest

npm i https://pkg.pr.new/TriPSs/nestjs-query/@ptc-org/nestjs-query-rest@432

@ptc-org/nestjs-query-sequelize

npm i https://pkg.pr.new/TriPSs/nestjs-query/@ptc-org/nestjs-query-sequelize@432

@ptc-org/nestjs-query-typegoose

npm i https://pkg.pr.new/TriPSs/nestjs-query/@ptc-org/nestjs-query-typegoose@432

@ptc-org/nestjs-query-typeorm

npm i https://pkg.pr.new/TriPSs/nestjs-query/@ptc-org/nestjs-query-typeorm@432

commit: b8a2d39

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Adds a new package @ptc-org/nestjs-query-rest with a full REST CRUD framework (controllers, decorators, auth, hooks, paging, connections), example Basic REST app (entities, DTOs, E2E tests, OpenAPI), package/config files for the monorepo, and Dependabot/stale GitHub workflows.

Changes

Cohort / File(s) Summary
Package config & workspace
packages/query-rest/package.json, packages/query-rest/project.json, packages/query-rest/.babelrc, packages/query-rest/.eslintrc.json, packages/query-rest/jest.config.ts, packages/query-rest/tsconfig*.json, package.json, tsconfig.json, jest.preset.js
Added new package manifest, build/test/lint configs, TypeScript project files, workspace path alias and updated root dependencies and jest moduleNameMapper.
Module bootstrap & providers
packages/query-rest/src/module.ts, packages/query-rest/src/providers/*, packages/query-rest/src/index.ts
New Nest module builder and provider factories to register services, authorizers, hooks, and to create dynamic controllers/endpoints.
Controllers (mixins & factories)
packages/query-rest/src/controllers/*
New composable controller factories: Create, Read, Update, Delete, Export and composite CRUD controller; typed options and wiring for endpoints, paging, tags, and Swagger metadata.
Authorization surface
packages/query-rest/src/auth/*, packages/query-rest/src/decorators/authorizer*.ts, packages/query-rest/src/decorators/authorize-filter.decorator.ts, packages/query-rest/src/interceptors/authorizer.interceptor.ts, packages/query-rest/src/auth/tokens.ts
Authorizer interfaces, OperationGroup enum, default-authorizer factory, tokens, decorator to attach custom authorizers, injection helper, and an interceptor to attach authorizer to request.
Hooks system
packages/query-rest/src/hooks/*, packages/query-rest/src/decorators/hook*.ts, packages/query-rest/src/interceptors/hook.interceptor.ts, packages/query-rest/src/providers/hook.provider.ts
Hook types, hook tokens, default hook factory, lifecycle decorators (BeforeCreateOne/BeforeUpdateOne/BeforeQueryMany), hook arg transformers, hook interceptor, and hook provider wiring.
Connection & paging
packages/query-rest/src/connection/*, packages/query-rest/src/connection/offset/*, packages/query-rest/src/types/query/paging/*
Connection interfaces (Array/Offset), offset pager implementation, offset page info type, and paging strategy types (OFFSET, NONE) with factories.
Query argument & filter types
packages/query-rest/src/types/query/*, packages/query-rest/src/types/query/query-args/*, packages/query-rest/src/types/query/filter.type.ts
Factories for QueryArgs (offset/none/export), FilterType generation, BuildableQuery interfaces, and related query-args utilities and constants.
DTO/input utilities
packages/query-rest/src/types/*-input.type.ts, packages/query-rest/src/types/param-args.type.ts, packages/query-rest/src/types/mutation-args.type.ts, packages/query-rest/src/types/update-one-input.type.ts
CreateOne/UpdateOne input factories, mutation/param arg types, ID-field-aware mutation arg generation.
Decorators & metadata
packages/query-rest/src/decorators/*
Many new decorators: Field, FilterableField, IDField, ApiSchema, Authorize, AuthorizerFilter, InjectAuthorizer, controller-method wrappers (Get/Post/Put/Delete) with Swagger/serialization, resolver/query options, hook-args, param-args, skip-if and decorator utilities.
Utilities & common
packages/query-rest/src/common/*, packages/query-rest/src/decorators/constants.ts, packages/query-rest/src/decorators/decorator.utils.ts
DTO name helpers, removeUndefinedValues, resolver options merging, metadata key constants and decorator composition utilities.
Interceptors & validators
packages/query-rest/src/interceptors/*, packages/query-rest/src/types/validators/*
Authorizer and Hook interceptors, hook-context types, and an IsUndefined validator helper.
Barrels & exports
packages/query-rest/src/*/index.ts, packages/query-rest/src/index.ts, packages/query-rest/README.md
Added index barrels re-exporting auth, connection, decorators, hooks, interceptors, controllers, providers, types; README for new package.
Example Basic REST app
examples/basic-rest/src/app.module.ts, examples/basic-rest/src/*/dto/*, examples/basic-rest/src/*/*.entity.ts, examples/basic-rest/src/*/*.module.ts, examples/basic-rest/open-api.json
New example app wiring TypeORM, DTOs (Tag, TodoItem, SubTask), entities, modules and generated OpenAPI JSON.
Example E2E tests & helpers
examples/basic-rest/e2e/fixtures.ts, examples/basic-rest/e2e/tag.endpoint.spec.ts, examples/helpers/generate-openapi-spec.ts
E2E fixtures for DB seeding/truncate, Tag endpoint test, and helper to generate/write OpenAPI spec.
Repo tooling/config
.github/dependabot.yml, .github/stale.yml, .gitignore
Dependabot configured for GitHub Actions updates, stale workflow configuration, and .gitignore updated for .nx/workspace-data and .npmrc.

Sequence Diagram

sequenceDiagram
    participant Client
    participant NestApp as NestJS App
    participant AuthInt as Authorizer Interceptor
    participant HookInt as Hook Interceptor
    participant Controller
    participant Authorizer
    participant Hooks
    participant Service
    participant DB

    Client->>NestApp: HTTP Request (route)
    NestApp->>AuthInt: run AuthorizerInterceptor
    AuthInt->>Authorizer: resolve/inject authorizer
    Authorizer->>AuthInt: authorizer instance
    AuthInt->>NestApp: attach authorizer to request

    NestApp->>HookInt: run HookInterceptor
    HookInt->>Hooks: retrieve hooks for DTO/operation
    HookInt->>NestApp: attach hooks to request

    NestApp->>Controller: invoke endpoint handler
    Controller->>Hooks: execute before-hooks (transform input/query)
    Hooks->>Controller: return transformed input/query
    Controller->>Service: call service (create/read/update/delete/export) with authorize filter
    Service->>Authorizer: ask for authorize filter
    Authorizer->>Service: return filter
    Service->>DB: execute query
    DB->>Service: return results
    Service->>Controller: return DTO(s)
    Controller->>NestApp: send HTTP response
    NestApp->>Client: HTTP Response (JSON/CSV)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

"I hopped through code with eager paws,
New controllers, hooks, and auth in my claws.
DTOs and pages neatly arrayed,
Seeds and tests were planted and laid.
A joyful rabbit cheers this PR's cause! 🐇"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(rest): Created rest package' clearly and concisely describes the main change: introducing a new REST package with complete feature implementation including controllers, decorators, and supporting infrastructure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/rest

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Replaced direct `id` reference with `params.getId()` for consistency.
- Ensures proper handling of parameter extraction in delete controller.
- Replaced direct `id` reference with `params.getId()` for consistency.
- Improves parameter extraction handling in read controller.
…corator`

- Updated `IsEnum` decorator to include `{ each: isArray }` for array handling support.
- Ensures proper validation of enum types for array fields.
…and connections

- Adjusted spacing in `IsEnum` decorator to enhance code formatting.
- Renamed variable `isArray` to `isReturnTypeArray` for clarity in `controller-methods.decorator`.
- Reorganized imports for better structure and readability in offset connection files.
…crud.controller`

- Updated `extractDeleteResolverOpts` to ensure `delete` argument defaults to an empty object.
- Prevents potential runtime errors when `delete` is undefined.
- Updated `@mikro-orm/better-sqlite` and `@mikro-orm/core` to version 6.6.6 in `package.json`.
- Refreshed `yarn.lock` to reflect dependency changes.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@examples/basic-rest/e2e/fixtures.ts`:
- Around line 22-23: SubTaskEntity.todoItemId is declared as string but must
match TodoItemEntity.id (number); change the type of SubTaskEntity.todoItemId to
number and update its `@Column` type accordingly, then update all DTOs
(SubTaskDTO, SubTaskInputDTO, SubTaskUpdateDTO) and any places that construct
sub-task objects (e.g., fixtures, tests) to use number for todoItemId so TypeORM
types align and no runtime coercion occurs.

In `@examples/basic-rest/open-api.json`:
- Around line 393-401: The PUT operation for /tag-dtos/{id} incorrectly uses
CreateTagDTO for the requestBody (CreateTagDTO) which forces full-create fields
on updates; change the schema to a proper partial update DTO (e.g.,
TagUpdateDTO) or add TagUpdateDTO to components that allows optional/partial
fields, then reference that new schema in the PUT requestBody instead of
CreateTagDTO; follow the existing pattern used by SubTaskUpdateDTO and
TodoItemUpdateDTO for naming and optional property semantics so PUT
/tag-dtos/{id} uses TagUpdateDTO.

In `@examples/basic-rest/src/sub-task/dto/subtask-update.dto.ts`:
- Around line 5-9: The `@Field` decorator on the title property is missing
nullable: true which causes OpenAPI to mark title as required; update the title
property’s GraphQL decorator to `@Field`({ nullable: true }) while keeping the
existing `@IsOptional`(), `@IsNotEmpty`(), and `@IsString`() validators so the
optional title?: string in the DTO (e.g., in SubtaskUpdateDto or the title
property) is correctly represented as nullable/optional in the generated schema.

In `@examples/basic-rest/src/sub-task/sub-task.entity.ts`:
- Around line 28-29: The todoItemId property is typed as string but must match
TodoItemEntity.id (which is a number via `@PrimaryGeneratedColumn`); change the
declaration of todoItemId from string to number (todoItemId!: number) and update
the Column metadata if needed (e.g., Column({ type: 'int', nullable: false,
name: 'todo_item_id' })) so the TypeScript type and DB column type match the
referenced primary key.

In `@examples/basic-rest/src/todo-item/dto/todo-item.dto.ts`:
- Around line 13-16: The FilterableField decorator usage on the completed
property is intended to map the schema name to "isCompleted" but currently
schemaName metadata is recorded as "completed"; fix the bug in
filterable-field.decorator.ts so the decorator writes the provided name option
into the metadata key (schemaName) rather than defaulting to the property name.
Update the decorator implementation (the function that defines metadata for
FilterableField) to check for an explicit name option and set
Reflect.defineMetadata('schemaName', options.name, target, propertyKey) (or
equivalent metadata key used) when provided, so the mapping for the completed
property to "isCompleted" works as intended.

In `@packages/query-rest/package.json`:
- Around line 40-48: Remove the unused peerDependencies entries for
"@nestjs/graphql" and "ts-morph" from the peerDependencies block in
package.json; locate the peerDependencies object (the keys "@nestjs/common",
"@nestjs/core", "@nestjs/graphql", "@nestjs/swagger", "class-transformer",
"class-validator", "ts-morph") and delete the "@nestjs/graphql" and "ts-morph"
entries so only actually used peers remain (e.g., "@nestjs/common",
"@nestjs/core", "@nestjs/swagger", "class-transformer", "class-validator").

In `@packages/query-rest/src/controllers/crud.controller.ts`:
- Around line 45-56: The CRUDController interface currently extends
CreateController, ReadControllerFromOpts, and UpdateController but omits
DeleteController and ExportController, causing methods like deleteOne and
exportMany to be present at runtime (via Deletable/Exportable composition) but
missing from the type; update the CRUDController declaration to also extend
DeleteController<DTO, QS> and ExportController<DTO, QS> (keeping the existing
generics R and QS) so the type includes delete and export operations and aligns
with the runtime composition.

In `@packages/query-rest/src/controllers/delete.controller.ts`:
- Around line 25-27: The Deletable mixin ignores opts.disabled so delete
endpoints are always registered; update the Deletable function to mirror
Creatable/Readable by returning BaseClass as never when opts.disabled is truthy
(check opts.disabled at the top of the returned function), i.e. inside the
Deletable closure before defining the subclass check if (opts.disabled) { return
BaseClass as never } so DeleteController/Deletable honors
DeleteResolverOpts.disabled and avoids registering the endpoint.

In `@packages/query-rest/src/controllers/export.controller.ts`:
- Around line 110-115: Remove the unsupported defaultEncoding option from the
stringifyCsv call in export.controller; locate the stringifyCsv invocation that
wraps plainToInstance(ExportDTOClass, items, { excludeExtraneousValues: true })
and delete the defaultEncoding: 'utf8' entry so the options object only contains
valid csv-stringify settings (header, delimiter, quoted_string, etc.).

In `@packages/query-rest/src/controllers/read.controller.ts`:
- Around line 139-141: The method queryMany incorrectly uses the
`@ParamHookArgs`() decorator for the params argument; replace `@ParamHookArgs`()
with `@QueryHookArgs`() so queryMany reads filtering parameters from the query
string (not route params). Update the parameter decorator on queryMany(params:
QOP) to use `@QueryHookArgs`() consistent with exportMany and the
QueryParamArgsType semantics, ensuring NestQuery is used to extract query
parameters.

In `@packages/query-rest/src/decorators/filterable-field.decorator.ts`:
- Around line 87-93: The metadata currently sets schemaName to
propertyName.toString() inside reflector.append, ignoring advancedOptions.name;
update the call in filterable-field.decorator (reflector.append) to set
schemaName to advancedOptions?.name || propertyName.toString() while keeping
propertyName as propertyName.toString(), so downstream consumers like
getFilterableFields will see the provided API/schema name when
advancedOptions.name is set.
- Line 99: The call passes a function that returns returnTypeFunc
(double-wrapping) causing Field to receive a function-returning-function; change
the call to pass returnTypeFunc directly when it's defined and only wrap it in a
zero-arg function when it's not — i.e., call Field with either returnTypeFunc
(if truthy) or (() => returnTypeFunc) appropriately; update the expression
around applyDecorators(Field(...),
filterableFieldOptionsToField(advancedOptions)) so Field receives a function
that returns a type (or undefined) rather than a function that returns a
function; adjust the code where applyDecorators, Field, returnTypeFunc,
filterableFieldOptionsToField, advancedOptions, target, propertyName, descriptor
are used to implement this conditional pass-through.

In `@packages/query-rest/src/decorators/hook-args.decorator.ts`:
- Around line 11-12: HooksTransformer is missing the `@Injectable`() decorator so
`@Inject`(REQUEST) is ignored when Nest instantiates the pipe outside DI; add
`@Injectable()` (imported from `@nestjs/common`) to the HooksTransformer class
declaration so Nest will resolve and inject REQUEST correctly (so this.request
is available for runMutationHooks and runQueryHooks when the pipe is used via
NestParam / NestQuery / NestBody).

In `@packages/query-rest/src/index.ts`:
- Around line 1-7: The barrel file is missing exports for the REST controllers,
preventing consumers from importing core symbols like CRUDController and
ReadController from the package root; update the index barrel (export list in
packages/query-rest/src/index.ts) to export the controllers by adding an export
for './controllers' so that CRUDController, ReadController and any other
controller types are re-exported from the package entrypoint.

In `@packages/query-rest/src/interceptors/hook.interceptor.ts`:
- Around line 28-33: The interceptor's intercept method sets request.hooks but
the downstream HooksTransformer pipe (used by hook-args.decorator.ts) can't get
REQUEST via DI when the pipe is instantiated as a class reference; fix this by
changing the decorator to instantiate the pipe with the hooks from the current
request instead of relying on DI (e.g., in hook-args.decorator.ts, read
context.switchToHttp().getRequest().hooks and pass them into new
HooksTransformer(...)), and keep request.hooks assignment in intercept
(intercept method / HooksTransformer are the symbols to locate).

In `@packages/query-rest/src/types/param-args.type.ts`:
- Around line 19-38: The getId method in the generated FindOneArgs class (inside
ParamArgsType) uses `this.id || this[properties[0]]`, which treats numeric 0 as
falsy; update the fallback to use nullish coalescing so only null/undefined fall
through: inside the getId method of class FindOneArgs (returned by
ParamArgsType), replace the `||` fallback with `??` to return `this.id` when it
is 0 and only use `this[properties[0]]` when `this.id` is null or undefined.

In `@packages/query-rest/src/types/query/filter.type.ts`:
- Around line 46-60: The getter filter builds filters by iterating fields but
uses a truthiness check (if (this[field.schemaName])) which drops valid falsy
values like 0, false, or "". Change the condition in the Filter<T> builder
inside the public get filter() method to explicitly test for presence (e.g.,
Object.prototype.hasOwnProperty.call(this, field.schemaName) or
this[field.schemaName] !== undefined) so falsy-but-valid values are included,
leaving the rest of the logic (populate filter[field.schemaName] and fallback to
super.filter) unchanged.
- Around line 30-85: The circular-reference guard is inserted too late: move the
early cache population so recursive calls can short-circuit. Specifically, after
creating QueryFilter (and before the fields.forEach decoration loop), ensure
TClassCache = TClassCache ?? new Map(); TClassCache.set(typeName, QueryFilter);
internalCache.set(TClass, TClassCache) so that internalCache contains the
partially-constructed QueryFilter for (TClass, typeName) prior to running the
field decoration code; keep the existing reflector.memoize usage and leave the
rest of the filter logic unchanged.
🟡 Minor comments (35)
packages/query-rest/src/types/query/filter.type.ts-59-59 (1)

59-59: ⚠️ Potential issue | 🟡 Minor

super.filter may be undefined if BaseClass lacks a filter getter.

When no fields match, the getter falls through to super.filter. If the supplied BaseClass doesn't define a filter property, this returns undefined, which will surprise callers expecting a Filter<T> (or at minimum {}). Consider falling back to an empty object.

Proposed fix
-        return super.filter
+        return super.filter ?? ({} as Filter<T>)
packages/query-rest/README.md-21-22 (1)

21-22: ⚠️ Potential issue | 🟡 Minor

Typo: "The get started" → "To get started".

📝 Proposed fix
-The get started with the `@ptc-org/nestjs-query-rest` package checkout
+To get started with the `@ptc-org/nestjs-query-rest` package checkout
packages/query-rest/README.md-12-13 (1)

12-13: ⚠️ Potential issue | 🟡 Minor

Fix typos in the description.

  • "code first" → "code-first" (hyphenated compound modifier)
  • "built on top of of" → "built on top of" (duplicated "of")
📝 Proposed fix
-This package provides a code first implementation of rest CRUD endpoints. It is built on top of
-of [nestjs](https://nestjs.com/).
+This package provides a code-first implementation of REST CRUD endpoints. It is built on top
+of [nestjs](https://nestjs.com/).
.github/stale.yml-1-15 (1)

1-15: ⚠️ Potential issue | 🟡 Minor

Misleading staleLabel value.

The label "auto closed" is applied when an issue is marked as stale, not when it is actually closed. An issue marked stale may receive activity and avoid closure, yet it would still carry the "auto closed" label. Consider renaming it to something like "stale" to accurately reflect the state.

packages/query-rest/src/types/update-one-input.type.ts-7-12 (1)

7-12: ⚠️ Potential issue | 🟡 Minor

Stale JSDoc — references "create one" and "graphql schema" instead of "update one" and REST.

This was likely copied from CreateOneInputType. Update the doc to accurately describe this type.

Proposed fix
 /**
- * The abstract input type for create one operations.
+ * The abstract input type for update one operations.
  *
- * `@param` fieldName - The name of the field to be exposed in the graphql schema
  * `@param` UpdateClass - The InputType to be used.
  */
packages/query-rest/src/decorators/authorizer.decorator.ts-12-19 (1)

12-19: ⚠️ Potential issue | 🟡 Minor

Fix return from a void-typed function (Biome lint error).

The arrow function on line 12 declares a void return type, but lines 16 and 18 return the result of reflector.set(...). While this works at runtime, it violates the type contract and is flagged by Biome (noVoidTypeReturn). Separate the call from the return:

Proposed fix
   return (DTOClass: Class<DTO>): void => {
     if (!('authorize' in optsOrAuthorizerOrClass)) {
       // If the user provided a class, provide the custom authorizer and create a default authorizer that injects the custom authorizer
       customAuthorizerReflector.set(DTOClass, optsOrAuthorizerOrClass)
-      return reflector.set(DTOClass, createDefaultAuthorizer(DTOClass))
+      reflector.set(DTOClass, createDefaultAuthorizer(DTOClass))
+      return
     }
-    return reflector.set(DTOClass, createDefaultAuthorizer(DTOClass, optsOrAuthorizerOrClass))
+    reflector.set(DTOClass, createDefaultAuthorizer(DTOClass, optsOrAuthorizerOrClass))
   }
packages/query-rest/src/decorators/id-field.decorator.ts-17-31 (1)

17-31: ⚠️ Potential issue | 🟡 Minor

JSDoc is copy-pasted from FilterableField and doesn't describe IDField.

The doc says "Fields that should be filterable through a FilterType" and shows a FilterableField/GraphQL example, but this decorator marks ID fields. Update the doc to describe IDField semantics and the idOnly option.

packages/query-rest/src/types/param-args.type.ts-19-19 (1)

19-19: ⚠️ Potential issue | 🟡 Minor

Missing eslint-disable for @typescript-eslint/no-redeclare.

Static analysis flags both ParamArgsType (line 19) and QueryParamArgsType (line 40) as redeclared. Other files using the same dual-declaration pattern (e.g., mutation-args.type.ts line 11) include // eslint-disable-next-line @typescript-eslint/no-redeclare -- intentional. Add the same suppression here for consistency and to silence the linter.

Suggested fix
+// eslint-disable-next-line `@typescript-eslint/no-redeclare` -- intentional
 export function ParamArgsType(DTOClass: Class<unknown>): Class<ParamArgsType> {
+// eslint-disable-next-line `@typescript-eslint/no-redeclare` -- intentional
 export function QueryParamArgsType(DTOClass: Class<unknown>): Class<QueryParamArgsType> {

Also applies to: 40-40

packages/query-rest/src/decorators/field.decorator.ts-32-61 (1)

32-61: ⚠️ Potential issue | 🟡 Minor

JSDoc references GraphQL imports instead of REST.

The example in the doc comment references @nestjs/graphql types (ObjectType, ID, GraphQLISODateTime, FilterableField from @ptc-org/nestjs-query-graphql). Since this is the REST package, the documentation should show REST-specific usage with @ptc-org/nestjs-query-rest imports and Swagger/REST decorators.

packages/query-rest/src/types/param-args.type.ts-20-21 (1)

20-21: ⚠️ Potential issue | 🟡 Minor

Missing null guard on getIDFields result before calling .map().

getIDFields has return type MetaValue<IDFieldDescriptor[]> (i.e., IDFieldDescriptor[] | undefined). If it ever returns undefined, line 21 will throw at runtime. Add a ?? [] fallback, consistent with how mutation-args.type.ts (line 13) handles it.

Suggested fix
-  const dtoIDFields = getIDFields(DTOClass)
+  const dtoIDFields = getIDFields(DTOClass) ?? []

Same issue applies to line 41 in QueryParamArgsType.

packages/query-rest/src/types/create-one-input.type.ts-7-12 (1)

7-12: ⚠️ Potential issue | 🟡 Minor

JSDoc references a non-existent fieldName param and "graphql schema".

This appears to be a copy-paste artifact from the GraphQL package. The @param fieldName doesn't correspond to any parameter, and "graphql schema" doesn't apply to the REST package.

Suggested fix
 /**
- * The abstract input type for create one operations.
- *
- * `@param` fieldName - The name of the field to be exposed in the graphql schema
- * `@param` InputClass - The InputType to be used.
+ * Factory that produces an input type class for create-one operations.
+ *
+ * `@param` InputClass - The InputType to be used.
  */
packages/query-rest/src/providers/hook.provider.ts-14-32 (1)

14-32: ⚠️ Potential issue | 🟡 Minor

Return type Provider[] | undefined is misleading — reduce always returns an array.

The reduce is initialized with [], so it can never return undefined. However, getHookProviders spreads the result directly (...createHookProvider(...)), which would throw at runtime if undefined were ever returned. The type annotation creates a false sense that undefined is a valid return value.

Suggested fix
-function createHookProvider(hookType: HookTypes, ...DTOClass: Class<unknown>[]): Provider[] | undefined {
+function createHookProvider(hookType: HookTypes, ...DTOClass: Class<unknown>[]): Provider[] {
packages/query-rest/src/decorators/id-field.decorator.ts-44-44 (1)

44-44: ⚠️ Potential issue | 🟡 Minor

delete options?.idOnly mutates the caller's options object.

If a consumer passes a reusable/shared options object, this silently strips idOnly from it. Create a shallow copy before deleting, or destructure idOnly out before forwarding.

Suggested fix
+    const { idOnly: _idOnly, ...fieldOptions } = options ?? {}
+
     if (descriptor) {
-      return Field(options)(target, propertyName, descriptor)
+      return Field(fieldOptions)(target, propertyName, descriptor)
     }
-    return Field(options)(target, propertyName)
+    return Field(fieldOptions)(target, propertyName)

And remove line 44 (delete options?.idOnly).

packages/query-rest/src/decorators/filterable-field.decorator.ts-9-14 (1)

9-14: ⚠️ Potential issue | 🟡 Minor

allowedComparisons type is a fixed tuple ['=', '!='] instead of a flexible array.

This restricts the value to exactly the two-element tuple ['=', '!=']. It should likely be an array of allowed comparison operators, e.g. ('=' | '!=' | '>' | '<' | '>=' | '<=' | 'like' | ...)[] or at least a string[] to be useful.

packages/query-rest/src/decorators/filterable-field.decorator.ts-31-59 (1)

31-59: ⚠️ Potential issue | 🟡 Minor

JSDoc example references GraphQL package, not the REST package.

The doc comment imports from @ptc-org/nestjs-query-graphql and uses @nestjs/graphql decorators (ObjectType, ID, GraphQLISODateTime). Since this is the REST package, the example should reference @ptc-org/nestjs-query-rest and use REST-appropriate decorators.

packages/query-rest/src/controllers/update.controller.ts-40-43 (1)

40-43: ⚠️ Potential issue | 🟡 Minor

JSDoc says "graphql endpoints" — should be "REST endpoints".

Copy-paste artifact from the GraphQL package.

📝 Proposed fix
 /**
  * `@internal`
- * Mixin to add `update` graphql endpoints.
+ * Mixin to add `update` REST endpoints.
  */
packages/query-rest/src/decorators/resolver-method.decorator.ts-68-75 (1)

68-75: ⚠️ Potential issue | 🟡 Minor

JSDoc says "disabled" but the function checks enabled.

The doc comment states "Returns true if any of the ResolverRelationMethodOpts are disabled" but the function isEnabled checks o.enabled.

📝 Proposed fix
 /**
  * `@internal`
- * Returns true if any of the [[ResolverRelationMethodOpts]] are disabled.
+ * Returns true if any of the [[ResolverRelationMethodOpts]] are enabled.
  * `@param` opts - The array of [[ResolverRelationMethodOpts]] to check.
  */
packages/query-rest/src/controllers/create.controller.ts-45-48 (1)

45-48: ⚠️ Potential issue | 🟡 Minor

JSDoc comments reference GraphQL — should reference REST.

Line 47 says "Mixin to add create graphql endpoints" and Lines 110-113 mention "graphql query endpoints", "createOneTodoItem and createManyTodoItems". These should reference REST endpoints, and the createMany mention is inaccurate since only createOne is implemented.

Also applies to: 107-121

packages/query-rest/src/decorators/hook-args.decorator.ts-48-61 (1)

48-61: ⚠️ Potential issue | 🟡 Minor

runQueryHooks doesn't guard against missing buildQuery before calling it.

At Line 50, data.buildQuery() is called unconditionally. If the transformed value doesn't implement BuildableQueryType (e.g., transformValue returns a plain object without the method), this will throw at runtime. Consider adding a guard or an informative error message.

packages/query-rest/src/controllers/read.controller.ts-58-61 (1)

58-61: ⚠️ Potential issue | 🟡 Minor

JSDoc says "graphql endpoints" — should say "REST endpoints".

The comment at Line 60 reads Mixin to add 'read' graphql endpoints, but this is the REST package.

Proposed fix
 /**
  * `@internal`
- * Mixin to add `read` graphql endpoints.
+ * Mixin to add `read` REST endpoints.
  */
packages/query-rest/src/controllers/delete.controller.ts-21-24 (1)

21-24: ⚠️ Potential issue | 🟡 Minor

JSDoc says "graphql endpoints" — should say "REST endpoints".

Same pattern as the other controller files.

Proposed fix
 /**
  * `@internal`
- * Mixin to add `delete` graphql endpoints.
+ * Mixin to add `delete` REST endpoints.
  */
packages/query-rest/src/controllers/controller.interface.ts-8-25 (1)

8-25: ⚠️ Potential issue | 🟡 Minor

JSDoc references "graphql" throughout — should reference REST.

Multiple comments refer to "graphql query or mutation" and "graphql endpoints" (Lines 9, 11, 18, 22). This is the REST package — these should say "REST endpoint" or simply "endpoint".

Proposed fix
 export type NamedEndpoint = {
-  /** Specify to override the name of the graphql query or mutation * */
+  /** Specify to override the endpoint path * */
   path?: string
-  /** Specify a description for the graphql query or mutation* */
+  /** Specify a description for the endpoint * */
   description?: string
   operationOptions?: ApiOperationOptions
 }
 
 export interface ControllerOpts extends QueryResolverMethodOpts, DTONamesOpts {
   /**
-   * Options for single record graphql endpoints
+   * Options for single record endpoints
    */
   one?: QueryResolverMethodOpts & NamedEndpoint
   /**
-   * Options for multiple record graphql endpoints
+   * Options for multiple record endpoints
    */
   many?: QueryResolverMethodOpts & NamedEndpoint
 }
packages/query-rest/src/controllers/read.controller.ts-110-122 (1)

110-122: ⚠️ Potential issue | 🟡 Minor

Return type mismatch between interface and implementation.

The ReadController interface (Line 55) declares findById as returning Promise<DTO | undefined>, but the implementation at Line 117 returns Promise<DTO>. Typically QueryService.getById throws NotFoundException when the record is missing rather than returning undefined, so the implementation is correct — but the interface should match.

Proposed fix in the interface
-  findById(params: ParamArgsType, authorizeFilter?: Filter<DTO>): Promise<DTO | undefined>
+  findById(params: ParamArgsType, authorizeFilter?: Filter<DTO>): Promise<DTO>
packages/query-rest/src/decorators/hook.decorator.ts-38-46 (1)

38-46: ⚠️ Potential issue | 🟡 Minor

Remove explicit return from void-typed function.

The arrow function's signature implies void, but return Reflect.defineMetadata(...) explicitly returns a value. While Reflect.defineMetadata itself returns void, the explicit return is unnecessary and triggers the Biome lint error (noVoidTypeReturn).

Proposed fix
   return (...data: HookDecoratorArg<H>[]) =>
     // eslint-disable-next-line `@typescript-eslint/ban-types`
     (target: Function): void => {
-      return Reflect.defineMetadata(
+      Reflect.defineMetadata(
         key,
         data.map((d) => getHook(d)),
         target
       )
     }
packages/query-rest/src/decorators/param-args.decorator.ts-7-19 (1)

7-19: ⚠️ Potential issue | 🟡 Minor

Incorrect return type and duplicated transformValue logic.

Two issues:

  1. Misleading return type (Line 8): transform declares Promise<MutationArgsType<T> | Query<T>> but actually returns T (the plain transformed value). The Query import (Line 2) and MutationArgsType import (Line 5) appear unused in the implementation—they're only referenced in this inaccurate type signature. This likely was copy-pasted from HooksTransformer.

  2. Duplicated code: transformValue is identical to the one in hook-args.decorator.ts. Consider extracting it into a shared utility.

,

Proposed fix for the return type
-import { Class, Query } from '@ptc-org/nestjs-query-core'
+import { Class } from '@ptc-org/nestjs-query-core'
 import { plainToInstance } from 'class-transformer'
 
-import { MutationArgsType, ParamArgsType } from '../types'
+import { ParamArgsType } from '../types'
 
 class ParamTransformer<T> implements PipeTransform {
-  public async transform(value: T, metadata: ArgumentMetadata): Promise<MutationArgsType<T> | Query<T>> {
+  public async transform(value: T, metadata: ArgumentMetadata): Promise<T> {
     return this.transformValue(value, metadata.metatype)
   }
packages/query-rest/src/decorators/controller-methods.decorator.ts-37-42 (1)

37-42: ⚠️ Potential issue | 🟡 Minor

Type guard doesn't distinguish null from object options.

typeof null === 'object' in JavaScript. If a caller passes null as the first argument (e.g., Get(null, { path: '/foo' })), line 38 would assign options = null, then line 48 would throw when accessing null.path. While the TypeScript overloads should prevent this at compile-time, a defensive check would be safer:

🛡️ Suggested defensive fix
-    if (typeof returnTypeFuncOrOptions === 'object') {
+    if (returnTypeFuncOrOptions !== null && typeof returnTypeFuncOrOptions === 'object') {
examples/basic-rest/e2e/tag.endpoint.spec.ts-34-34 (1)

34-34: ⚠️ Potential issue | 🟡 Minor

Consolidate duplicate afterAll hooks.

There are two afterAll hooks at the same describe level (Lines 34 and 56). Only the last one is guaranteed to run in some test runners, but regardless this should be a single hook. The static analysis tool (Biome) also flags this.

Proposed fix

Remove line 34 and merge the refresh call into the existing afterAll:

-  afterAll(() => refresh(app.get(Connection)))
-
   const tags = [
   afterAll(async () => {
+    await refresh(app.get(Connection))
     await app.close()
   })

Also applies to: 56-58

examples/basic-rest/e2e/tag.endpoint.spec.ts-12-12 (1)

12-12: ⚠️ Potential issue | 🟡 Minor

Test description says "TagResolver" — should be "TagController" or similar.

This is a REST endpoint test, not a GraphQL resolver test. The describe string appears to be a copy-paste artifact.

Proposed fix
-describe('TagResolver (basic rest - e2e)', () => {
+describe('TagEndpoint (basic rest - e2e)', () => {
examples/basic-rest/src/sub-task/dto/sub-task.dto.ts-22-23 (1)

22-23: ⚠️ Potential issue | 🟡 Minor

todoItemId should be number to match the referenced entity's primary key type.

TodoItemEntity.id is number (via @PrimaryGeneratedColumn()), so this field should also be number for type consistency. This was also noted in the entity definition.

Proposed fix
   `@FilterableField`()
-  todoItemId!: string
+  todoItemId!: number
packages/query-rest/src/types/query/query-args/none-paging-query-args.type.ts-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor

Inconsistent import pattern — align with relative imports used elsewhere in the package.

While RestQuery is properly exported from the @ptc-org/nestjs-query-rest package entry point, the codebase shows mixed import patterns: buildable-query.type.ts uses a relative import (import { RestQuery } from '../rest-query.type'), while multiple query-args files use self-referential imports from @ptc-org/nestjs-query-rest. For consistency and to avoid potential issues during development when the package hasn't been compiled, use relative imports throughout.

Proposed fix
-import { RestQuery } from '@ptc-org/nestjs-query-rest'
+import { RestQuery } from '../../rest-query.type'
examples/basic-rest/open-api.json-150-161 (1)

150-161: ⚠️ Potential issue | 🟡 Minor

POST operations return 200 instead of 201.

REST convention is to return 201 Created for successful resource creation. All three POST endpoints (/sub-task-dtos, /todo-item-dtos, /tag-dtos) return 200. Consider updating the controller to produce 201 status codes.

Also applies to: 310-322, 470-482

packages/query-rest/src/connection/offset/offset-connection.type.ts-44-47 (1)

44-47: ⚠️ Potential issue | 🟡 Minor

totalCount is optional but @Field lacks nullable: true.

The property is declared as totalCount?: number meaning it can be undefined, but the @Field decorator doesn't indicate nullability. This may cause the OpenAPI spec to mark totalCount as required (which the generated open-api.json indeed does — see the required arrays in connection schemas). If a count query isn't provided, this will be undefined and may confuse API consumers expecting a value.

Proposed fix
      `@Field`({
-       description: 'Total amount of records.'
+       description: 'Total amount of records.',
+       nullable: true
      })
      public totalCount?: number
packages/query-rest/src/types/query/query-args/interfaces.ts-8-9 (1)

8-9: ⚠️ Potential issue | 🟡 Minor

Minor typo in JSDoc comment.

"an filter" → "a filter".

📝 Suggested fix
-   * to build an filter
+   * to build a filter
packages/query-rest/src/controllers/export.controller.ts-23-26 (1)

23-26: ⚠️ Potential issue | 🟡 Minor

Incorrect JSDoc: "finding one record" should reference export.

This comment was copied from the read controller. The ExportDTOClass is used for exporting many records, not finding one.

📝 Suggested fix
   /**
-   * DTO to return with finding one record
+   * DTO class to use when exporting records
    */
   ExportDTOClass?: Class<DTO>
packages/query-rest/src/controllers/crud.controller.ts-107-123 (1)

107-123: ⚠️ Potential issue | 🟡 Minor

JSDoc references GraphQL instead of REST.

The docblock still mentions @ptc-org/nestjs-query-graphql, @Resolver(), and CRUDResolver — all from the GraphQL package. This should be updated to reflect the REST package.

📝 Suggested fix
 /**
- * Factory to create a resolver that includes all CRUD methods from [[CreateResolver]], [[ReadResolver]],
- * [[UpdateResolver]], and [[DeleteResolver]].
+ * Factory to create a controller that includes all CRUD methods from [[CreateController]], [[ReadController]],
+ * [[UpdateController]], [[DeleteController]], and [[ExportController]].
  *
  * ```ts
- * import { CRUDResolver } from '@ptc-org/nestjs-query-graphql';
- * import { Resolver } from '@nestjs/graphql';
+ * import { CRUDController } from '@ptc-org/nestjs-query-rest';
+ * import { Controller } from '@nestjs/common';
  * import { TodoItemDTO } from './dto/todo-item.dto';
  * import { TodoItemService } from './todo-item.service';
  *
- * `@Resolver`()
- * export class TodoItemResolver extends CRUDResolver(TodoItemDTO) {
+ * `@Controller`('todo-items')
+ * export class TodoItemController extends CRUDController(TodoItemDTO) {
  *   constructor(readonly service: TodoItemService) {
  *     super(service);
  *   }
  * }
  * ```
- * `@param` DTOClass - The DTO Class that the resolver is for. All methods will use types derived from this class.
- * `@param` opts - Options to customize the resolver.
+ * `@param` DTOClass - The DTO Class that the controller is for. All methods will use types derived from this class.
+ * `@param` opts - Options to customize the controller.
  */

Comment on lines +22 to +23
const blockedTag = await tagsRepo.save({ name: 'Blocked' })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "todoItemId" examples/basic-rest/ --type=ts -B2 -A2

Repository: TriPSs/nestjs-query

Length of output: 1378


🏁 Script executed:

rg -n "class TodoItemEntity" examples/basic-rest/ --type=ts -A 20

Repository: TriPSs/nestjs-query

Length of output: 1625


Fix the type mismatch between SubTaskEntity.todoItemId and TodoItemEntity.id.

SubTaskEntity.todoItemId is typed as string (sub-task.entity.ts line 29), but TodoItemEntity.id is typed as number (@PrimaryGeneratedColumn() at todo-item.entity.ts line 18). The DTOs (SubTaskDTO, SubTaskInputDTO, SubTaskUpdateDTO) also declare todoItemId as string. This type mismatch will cause TypeORM to coerce types at runtime, leading to potential runtime errors or silent failures. Align SubTaskEntity.todoItemId to number to match the primary key type.

🤖 Prompt for AI Agents
In `@examples/basic-rest/e2e/fixtures.ts` around lines 22 - 23,
SubTaskEntity.todoItemId is declared as string but must match TodoItemEntity.id
(number); change the type of SubTaskEntity.todoItemId to number and update its
`@Column` type accordingly, then update all DTOs (SubTaskDTO, SubTaskInputDTO,
SubTaskUpdateDTO) and any places that construct sub-task objects (e.g.,
fixtures, tests) to use number for todoItemId so TypeORM types align and no
runtime coercion occurs.

Comment on lines +393 to +401
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateTagDTO"
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

PUT /tag-dtos/{id} references CreateTagDTO instead of an update DTO.

The update operation for tags uses CreateTagDTO as its request body schema (Line 398), whereas SubTask and TodoItem have dedicated update DTOs (SubTaskUpdateDTO, TodoItemUpdateDTO). This means an update requires the same fields as creation, which is typically undesirable (updates should allow partial payloads). Either introduce a TagUpdateDTO or confirm this is intentional.

🤖 Prompt for AI Agents
In `@examples/basic-rest/open-api.json` around lines 393 - 401, The PUT operation
for /tag-dtos/{id} incorrectly uses CreateTagDTO for the requestBody
(CreateTagDTO) which forces full-create fields on updates; change the schema to
a proper partial update DTO (e.g., TagUpdateDTO) or add TagUpdateDTO to
components that allows optional/partial fields, then reference that new schema
in the PUT requestBody instead of CreateTagDTO; follow the existing pattern used
by SubTaskUpdateDTO and TodoItemUpdateDTO for naming and optional property
semantics so PUT /tag-dtos/{id} uses TagUpdateDTO.

Comment on lines +5 to +9
@Field()
@IsOptional()
@IsNotEmpty()
@IsString()
title?: string
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

title field missing nullable: true — OpenAPI will incorrectly mark it as required.

All other fields in this update DTO use @Field({ nullable: true }), but title uses @Field() without options. Since the Field decorator defaults to required: true when nullable is not set, the generated OpenAPI spec will mark title as a required field, contradicting the ? optional type annotation and the @IsOptional() decorator.

Proposed fix
-  `@Field`()
+  `@Field`({ nullable: true })
   `@IsOptional`()
   `@IsNotEmpty`()
   `@IsString`()
   title?: string
🤖 Prompt for AI Agents
In `@examples/basic-rest/src/sub-task/dto/subtask-update.dto.ts` around lines 5 -
9, The `@Field` decorator on the title property is missing nullable: true which
causes OpenAPI to mark title as required; update the title property’s GraphQL
decorator to `@Field`({ nullable: true }) while keeping the existing
`@IsOptional`(), `@IsNotEmpty`(), and `@IsString`() validators so the optional title?:
string in the DTO (e.g., in SubtaskUpdateDto or the title property) is correctly
represented as nullable/optional in the generated schema.

Comment on lines +28 to +29
@Column({ nullable: false, name: 'todo_item_id' })
todoItemId!: string
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Type mismatch: todoItemId should be number, not string.

TodoItemEntity.id is declared as number (via @PrimaryGeneratedColumn() in todo-item.entity.ts), so the foreign key column todoItemId must match that type. Using string here will cause type-safety issues and potentially runtime query mismatches.

🐛 Proposed fix
   `@Column`({ nullable: false, name: 'todo_item_id' })
-  todoItemId!: string
+  todoItemId!: number
🤖 Prompt for AI Agents
In `@examples/basic-rest/src/sub-task/sub-task.entity.ts` around lines 28 - 29,
The todoItemId property is typed as string but must match TodoItemEntity.id
(which is a number via `@PrimaryGeneratedColumn`); change the declaration of
todoItemId from string to number (todoItemId!: number) and update the Column
metadata if needed (e.g., Column({ type: 'int', nullable: false, name:
'todo_item_id' })) so the TypeScript type and DB column type match the
referenced primary key.

Comment on lines +13 to +16
@FilterableField({
name: 'isCompleted'
})
completed!: boolean
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Note: The name: 'isCompleted' mapping depends on the schemaName fix in filterable-field.decorator.ts.

The schemaName metadata for completed will be recorded as "completed" instead of "isCompleted" due to the bug flagged in the decorator. Once that is fixed, this mapping should work as intended.

🤖 Prompt for AI Agents
In `@examples/basic-rest/src/todo-item/dto/todo-item.dto.ts` around lines 13 - 16,
The FilterableField decorator usage on the completed property is intended to map
the schema name to "isCompleted" but currently schemaName metadata is recorded
as "completed"; fix the bug in filterable-field.decorator.ts so the decorator
writes the provided name option into the metadata key (schemaName) rather than
defaulting to the property name. Update the decorator implementation (the
function that defines metadata for FilterableField) to check for an explicit
name option and set Reflect.defineMetadata('schemaName', options.name, target,
propertyKey) (or equivalent metadata key used) when provided, so the mapping for
the completed property to "isCompleted" works as intended.

Comment on lines +1 to +7
export { AuthorizationContext, Authorizer, AuthorizerOptions, CustomAuthorizer, OperationGroup } from './auth'
export * from './connection'
export * from './decorators'
export * from './hooks'
export * from './interceptors'
export { NestjsQueryRestModule } from './module'
export * from './types'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing ./controllers export — likely an oversight.

The package barrel exports ./connection, ./decorators, ./hooks, ./interceptors, and ./types, but omits ./controllers. Since this package's core value is providing auto-generated REST controllers (CRUDController, ReadController, etc.), consumers can't access them from the package root.

Proposed fix
 export { AuthorizationContext, Authorizer, AuthorizerOptions, CustomAuthorizer, OperationGroup } from './auth'
 export * from './connection'
+export * from './controllers'
 export * from './decorators'
 export * from './hooks'
 export * from './interceptors'
 export { NestjsQueryRestModule } from './module'
 export * from './types'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export { AuthorizationContext, Authorizer, AuthorizerOptions, CustomAuthorizer, OperationGroup } from './auth'
export * from './connection'
export * from './decorators'
export * from './hooks'
export * from './interceptors'
export { NestjsQueryRestModule } from './module'
export * from './types'
export { AuthorizationContext, Authorizer, AuthorizerOptions, CustomAuthorizer, OperationGroup } from './auth'
export * from './connection'
export * from './controllers'
export * from './decorators'
export * from './hooks'
export * from './interceptors'
export { NestjsQueryRestModule } from './module'
export * from './types'
🤖 Prompt for AI Agents
In `@packages/query-rest/src/index.ts` around lines 1 - 7, The barrel file is
missing exports for the REST controllers, preventing consumers from importing
core symbols like CRUDController and ReadController from the package root;
update the index barrel (export list in packages/query-rest/src/index.ts) to
export the controllers by adding an export for './controllers' so that
CRUDController, ReadController and any other controller types are re-exported
from the package entrypoint.

Comment on lines +28 to +33
public intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest()
request.hooks = this.hooks

return next.handle()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Hooks are attached to the request, but the downstream pipe cannot access them.

This interceptor correctly sets request.hooks = this.hooks (Line 30), but the HooksTransformer pipe in hook-args.decorator.ts tries to read them via @Inject(REQUEST), which won't resolve in a pipe instantiated as a class reference. See the related comment on hook-args.decorator.ts — the interceptor-to-pipe handoff for hook context is broken.

🤖 Prompt for AI Agents
In `@packages/query-rest/src/interceptors/hook.interceptor.ts` around lines 28 -
33, The interceptor's intercept method sets request.hooks but the downstream
HooksTransformer pipe (used by hook-args.decorator.ts) can't get REQUEST via DI
when the pipe is instantiated as a class reference; fix this by changing the
decorator to instantiate the pipe with the hooks from the current request
instead of relying on DI (e.g., in hook-args.decorator.ts, read
context.switchToHttp().getRequest().hooks and pass them into new
HooksTransformer(...)), and keep request.hooks assignment in intercept
(intercept method / HooksTransformer are the symbols to locate).

Comment on lines +19 to +38
export function ParamArgsType(DTOClass: Class<unknown>): Class<ParamArgsType> {
const dtoIDFields = getIDFields(DTOClass)
const properties = dtoIDFields.map(({ propertyName }) => propertyName)

if (properties.length === 0) {
// @ts-expect-error we want to return empty class
return class FindOneArgsType {}
}

class FindOneArgs extends PickType(DTOClass, properties as never) implements ParamArgsType {
id: string | number

getId() {
// Return the default "id", unless it's not set then return the first ID field
return this.id || this[properties[0]]
}
}

return FindOneArgs
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

this.id || this[properties[0]] on Line 33 is falsy for id = 0.

Using || means a numeric ID of 0 would be treated as falsy, falling through to this[properties[0]]. Since id is typed string | number, use nullish coalescing (??) to only fall through on null/undefined.

Suggested fix
     getId() {
       // Return the default "id", unless it's not set then return the first ID field
-      return this.id || this[properties[0]]
+      return this.id ?? this[properties[0]]
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function ParamArgsType(DTOClass: Class<unknown>): Class<ParamArgsType> {
const dtoIDFields = getIDFields(DTOClass)
const properties = dtoIDFields.map(({ propertyName }) => propertyName)
if (properties.length === 0) {
// @ts-expect-error we want to return empty class
return class FindOneArgsType {}
}
class FindOneArgs extends PickType(DTOClass, properties as never) implements ParamArgsType {
id: string | number
getId() {
// Return the default "id", unless it's not set then return the first ID field
return this.id || this[properties[0]]
}
}
return FindOneArgs
}
export function ParamArgsType(DTOClass: Class<unknown>): Class<ParamArgsType> {
const dtoIDFields = getIDFields(DTOClass)
const properties = dtoIDFields.map(({ propertyName }) => propertyName)
if (properties.length === 0) {
// `@ts-expect-error` we want to return empty class
return class FindOneArgsType {}
}
class FindOneArgs extends PickType(DTOClass, properties as never) implements ParamArgsType {
id: string | number
getId() {
// Return the default "id", unless it's not set then return the first ID field
return this.id ?? this[properties[0]]
}
}
return FindOneArgs
}
🧰 Tools
🪛 ESLint

[error] 19-19: 'ParamArgsType' is already defined.

(@typescript-eslint/no-redeclare)

🤖 Prompt for AI Agents
In `@packages/query-rest/src/types/param-args.type.ts` around lines 19 - 38, The
getId method in the generated FindOneArgs class (inside ParamArgsType) uses
`this.id || this[properties[0]]`, which treats numeric 0 as falsy; update the
fallback to use nullish coalescing so only null/undefined fall through: inside
the getId method of class FindOneArgs (returned by ParamArgsType), replace the
`||` fallback with `??` to return `this.id` when it is 0 and only use
`this[properties[0]]` when `this.id` is null or undefined.

Comment on lines +30 to +85
return reflector.memoize(TClass, typeName, () => {
const fields = getFilterableFields(TClass)

// if the filter is already in the cache, exist early and return it
// otherwise add it to the cache early so we don't get into an infinite loop
let TClassCache = internalCache.get(TClass)

if (TClassCache && TClassCache.has(typeName)) {
return TClassCache.get(typeName) as FilterConstructor<T>
}

const hasRequiredFilters = fields.some((f) => f.advancedOptions?.filterRequired === true)

class QueryFilter extends BaseClass {
static hasRequiredFilters: boolean = hasRequiredFilters

public get filter(): Filter<T> {
const filters = fields.reduce((filter, field) => {
if (this[field.schemaName]) {
filter[field.schemaName] = { eq: this[field.schemaName] }
}

return filter
}, {} as Filter<T>)

if (Object.keys(filters).length > 0) {
return filters
}

return super.filter
}
}

fields.forEach(({ schemaName, advancedOptions }) => {
applyDecorators(
Field(
filterableFieldOptionsToField({
...advancedOptions,
nullable:
typeof advancedOptions.filterRequired !== 'undefined' ? !advancedOptions.filterRequired : advancedOptions.nullable,
required: Boolean(
typeof advancedOptions.filterRequired !== 'undefined' ? advancedOptions.filterRequired : advancedOptions.required
)
} as FilterableFieldOptions)
),
...(advancedOptions.filterDecorators || [])
)(QueryFilter.prototype, schemaName)
})

TClassCache = TClassCache ?? new Map()

TClassCache.set(typeName, QueryFilter)
internalCache.set(TClass, TClassCache)

return QueryFilter as never as FilterConstructor<T>
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Circular-reference guard is populated too late — infinite recursion is still possible.

The internalCache entry is written at lines 79-82, after the field-decoration loop (lines 63-77). If decorating a field triggers a recursive getOrCreateFilterType call for the same TClass/typeName (e.g. User → Post → User), neither reflector.memoize (value not yet stored) nor internalCache (not yet populated) will short-circuit the call, causing unbounded recursion.

Move the cache insertion to before the field-processing loop so re-entrant calls can exit early with the partially-constructed class:

Proposed fix
     const hasRequiredFilters = fields.some((f) => f.advancedOptions?.filterRequired === true)
 
     class QueryFilter extends BaseClass {
       static hasRequiredFilters: boolean = hasRequiredFilters
 
       public get filter(): Filter<T> {
         const filters = fields.reduce((filter, field) => {
           if (this[field.schemaName]) {
             filter[field.schemaName] = { eq: this[field.schemaName] }
           }
 
           return filter
         }, {} as Filter<T>)
 
         if (Object.keys(filters).length > 0) {
           return filters
         }
 
         return super.filter
       }
     }
 
+    // Register in cache *before* decorating fields so that
+    // circular references can resolve to this (incomplete) class.
+    TClassCache = TClassCache ?? new Map()
+    TClassCache.set(typeName, QueryFilter)
+    internalCache.set(TClass, TClassCache)
+
     fields.forEach(({ schemaName, advancedOptions }) => {
       applyDecorators(
         Field(
           filterableFieldOptionsToField({
             ...advancedOptions,
             nullable:
               typeof advancedOptions.filterRequired !== 'undefined' ? !advancedOptions.filterRequired : advancedOptions.nullable,
             required: Boolean(
               typeof advancedOptions.filterRequired !== 'undefined' ? advancedOptions.filterRequired : advancedOptions.required
             )
           } as FilterableFieldOptions)
         ),
         ...(advancedOptions.filterDecorators || [])
       )(QueryFilter.prototype, schemaName)
     })
 
-    TClassCache = TClassCache ?? new Map()
-
-    TClassCache.set(typeName, QueryFilter)
-    internalCache.set(TClass, TClassCache)
-
     return QueryFilter as never as FilterConstructor<T>
🤖 Prompt for AI Agents
In `@packages/query-rest/src/types/query/filter.type.ts` around lines 30 - 85, The
circular-reference guard is inserted too late: move the early cache population
so recursive calls can short-circuit. Specifically, after creating QueryFilter
(and before the fields.forEach decoration loop), ensure TClassCache =
TClassCache ?? new Map(); TClassCache.set(typeName, QueryFilter);
internalCache.set(TClass, TClassCache) so that internalCache contains the
partially-constructed QueryFilter for (TClass, typeName) prior to running the
field decoration code; keep the existing reflector.memoize usage and leave the
rest of the filter logic unchanged.

Comment on lines +46 to +60
public get filter(): Filter<T> {
const filters = fields.reduce((filter, field) => {
if (this[field.schemaName]) {
filter[field.schemaName] = { eq: this[field.schemaName] }
}

return filter
}, {} as Filter<T>)

if (Object.keys(filters).length > 0) {
return filters
}

return super.filter
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Truthiness check drops valid falsy filter values (0, false, "").

if (this[field.schemaName]) on line 48 silently ignores legitimate filter values such as 0, false, and empty string. For a filter builder this is a correctness problem — e.g. filtering records where isActive eq false or count eq 0 would never work.

Proposed fix — check for explicit presence instead
       public get filter(): Filter<T> {
         const filters = fields.reduce((filter, field) => {
-          if (this[field.schemaName]) {
+          if (this[field.schemaName] !== undefined && this[field.schemaName] !== null) {
             filter[field.schemaName] = { eq: this[field.schemaName] }
           }
 
           return filter
         }, {} as Filter<T>)
🤖 Prompt for AI Agents
In `@packages/query-rest/src/types/query/filter.type.ts` around lines 46 - 60, The
getter filter builds filters by iterating fields but uses a truthiness check (if
(this[field.schemaName])) which drops valid falsy values like 0, false, or "".
Change the condition in the Filter<T> builder inside the public get filter()
method to explicitly test for presence (e.g.,
Object.prototype.hasOwnProperty.call(this, field.schemaName) or
this[field.schemaName] !== undefined) so falsy-but-valid values are included,
leaving the rest of the logic (populate filter[field.schemaName] and fallback to
super.filter) unchanged.

…OffsetPaging`

- Applied `@IsInt()` decorator to `limit` and `offset` fields for integer validation.
- Ensures stricter input validation for offset paging parameters.
- Moved `IsNotEmpty` and `IsOptional` decorators for cleaner, logical grouping in `field.decorator`.
- Prevents duplicate logic by restructuring `skipRequired` condition block.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/query-rest/src/decorators/field.decorator.ts`:
- Around line 32-61: The JSDoc example is copied from the GraphQL package and
should be replaced with a REST-appropriate example: update the docblock for the
FilterableField decorator (referenced as FilterableField and TodoItemDTO in this
file) to remove GraphQL imports and types (`@ptc-org/nestjs-query-graphql`,
`@nestjs/graphql`, `ObjectType`, `ID`, `GraphQLISODateTime`) and instead show a
simple REST DTO usage (e.g., import of FilterableField, a plain class
TodoItemDTO with typed properties decorated by FilterableField and standard
class-transformer/validator or plain TypeScript types) so the example
demonstrates how to mark DTO fields as filterable for the REST controller.
- Around line 117-119: The forceArray Transform currently wraps null/undefined
into [null]/[undefined]; update the Transform pushed into decorators (the one
using Transform in field.decorator.ts tied to advancedOptions?.forceArray) so it
first checks for null or undefined and returns the original value in that case,
otherwise returns the existing array or wraps the value in an array; locate the
Transform call added to the decorators array and change its mapping logic to
skip wrapping when value == null.
🧹 Nitpick comments (2)
packages/query-rest/src/types/query/offset-paging.type.ts (1)

18-26: Consider adding a default value for offset.

limit defaults to 25, but offset has no default. If a client omits both, offset will be undefined rather than 0. Depending on how downstream query logic handles this, it could lead to unexpected behavior. Adding default: 0 would make the contract consistent.

   `@Field`({
     type: Number,
     description: 'The offset to start returning results from.',
     nullable: true,
     required: false,
-    minimum: 0
+    minimum: 0,
+    default: 0
   })
packages/query-rest/src/decorators/field.decorator.ts (1)

129-135: Inconsistent nullish/falsy checks for constraint options.

minLength and maxLength use truthiness checks, so a value of 0 would be silently skipped. In contrast, minimum and maximum (lines 137-143) correctly use !== undefined. Align these for consistency.

Proposed fix
-    if (options.minLength) {
+    if (options.minLength !== undefined) {
       decorators.push(MinLength(options.minLength))
     }

-    if (options.maxLength) {
+    if (options.maxLength !== undefined) {
       decorators.push(MaxLength(options.maxLength))
     }

Comment on lines +32 to +61
/**
* Decorator for Fields that should be filterable through a [[FilterType]]
*
* @example
*
* In the following DTO `id`, `title` and `completed` are filterable.
*
* ```ts
* import { FilterableField } from '@ptc-org/nestjs-query-graphql';
* import { ObjectType, ID, GraphQLISODateTime, Field } from '@nestjs/graphql';
*
* @ObjectType('TodoItem')
* export class TodoItemDTO {
* @FilterableField(() => ID)
* id!: string;
*
* @FilterableField()
* title!: string;
*
* @FilterableField()
* completed!: boolean;
*
* @Field(() => GraphQLISODateTime)
* created!: Date;
*
* @Field(() => GraphQLISODateTime)
* updated!: Date;
* }
* ```
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

JSDoc is copy-pasted from the GraphQL package.

The docblock references @ptc-org/nestjs-query-graphql, @nestjs/graphql, ObjectType, ID, and GraphQLISODateTime — none of which apply to this REST package. Update the example to show REST-appropriate usage (e.g., using @Field with a DTO class for a REST controller).

🤖 Prompt for AI Agents
In `@packages/query-rest/src/decorators/field.decorator.ts` around lines 32 - 61,
The JSDoc example is copied from the GraphQL package and should be replaced with
a REST-appropriate example: update the docblock for the FilterableField
decorator (referenced as FilterableField and TodoItemDTO in this file) to remove
GraphQL imports and types (`@ptc-org/nestjs-query-graphql`, `@nestjs/graphql`,
`ObjectType`, `ID`, `GraphQLISODateTime`) and instead show a simple REST DTO
usage (e.g., import of FilterableField, a plain class TodoItemDTO with typed
properties decorated by FilterableField and standard class-transformer/validator
or plain TypeScript types) so the example demonstrates how to mark DTO fields as
filterable for the REST controller.

Comment on lines +117 to +119
if (isArray && advancedOptions?.forceArray) {
decorators.push(Transform(({ value }) => (Array.isArray(value) ? value : [value])))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

forceArray Transform wraps null/undefined into a single-element array.

When the field is optional and the value is null or undefined, Transform will produce [null] or [undefined], which is likely unintended and would break downstream validation or persistence.

Proposed fix
-      decorators.push(Transform(({ value }) => (Array.isArray(value) ? value : [value])))
+      decorators.push(Transform(({ value }) => (Array.isArray(value) ? value : value != null ? [value] : value)))
🤖 Prompt for AI Agents
In `@packages/query-rest/src/decorators/field.decorator.ts` around lines 117 -
119, The forceArray Transform currently wraps null/undefined into
[null]/[undefined]; update the Transform pushed into decorators (the one using
Transform in field.decorator.ts tied to advancedOptions?.forceArray) so it first
checks for null or undefined and returns the original value in that case,
otherwise returns the existing array or wraps the value in an array; locate the
Transform call added to the decorators array and change its mapping logic to
skip wrapping when value == null.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant