{"id":51565841,"url":"https://github.com/davidecarvalho/nestjs-filter","last_synced_at":"2026-07-10T15:00:46.121Z","repository":{"id":359827802,"uuid":"1247646739","full_name":"DavideCarvalho/nestjs-filter","owner":"DavideCarvalho","description":"NestJS filter library with ORM adapters (MikroORM, TypeORM). Inspired by adonis-lucid-filter.","archived":false,"fork":false,"pushed_at":"2026-06-26T13:43:44.000Z","size":1377,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-08T17:25:03.605Z","etag":null,"topics":["declarative","filter","mikro-orm","nestjs","orm","query-builder","query-filter","rest-api","typeorm","typescript"],"latest_commit_sha":null,"homepage":"https://davidecarvalho.github.io/aviary/docs/filter","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DavideCarvalho.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-23T15:40:39.000Z","updated_at":"2026-06-26T13:41:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/DavideCarvalho/nestjs-filter","commit_stats":null,"previous_names":["davidecarvalho/nestjs-filter"],"tags_count":61,"template":false,"template_full_name":null,"purl":"pkg:github/DavideCarvalho/nestjs-filter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavideCarvalho%2Fnestjs-filter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavideCarvalho%2Fnestjs-filter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavideCarvalho%2Fnestjs-filter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavideCarvalho%2Fnestjs-filter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DavideCarvalho","download_url":"https://codeload.github.com/DavideCarvalho/nestjs-filter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavideCarvalho%2Fnestjs-filter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35334719,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-10T02:00:06.465Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["declarative","filter","mikro-orm","nestjs","orm","query-builder","query-filter","rest-api","typeorm","typescript"],"created_at":"2026-07-10T15:00:20.883Z","updated_at":"2026-07-10T15:00:46.107Z","avatar_url":"https://github.com/DavideCarvalho.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://davidecarvalho.github.io/aviary/docs/filter\"\u003e\n    \u003cimg src=\"./.github/banner.svg\" alt=\"@dudousxd/nestjs-filter — an Aviary library. Call sign: Sandpiper.\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cb\u003e\u003ca href=\"https://davidecarvalho.github.io/aviary/docs/filter\"\u003e📖 Read the documentation\u003c/a\u003e\u003c/b\u003e\n  \u0026nbsp;·\u0026nbsp; part of the \u003ca href=\"https://davidecarvalho.github.io/aviary/\"\u003e\u003cb\u003eAviary\u003c/b\u003e\u003c/a\u003e ecosystem for NestJS\n\u003c/p\u003e\n\n---\n\n# nestjs-filter\n\nDeclarative, ORM-agnostic filter classes for NestJS. Inspired by `adonis-lucid-filter` and `eloquent-filter`, redesigned for NestJS idioms.\n\n[![CI](https://github.com/DavideCarvalho/nestjs-filter/actions/workflows/ci.yml/badge.svg)](https://github.com/DavideCarvalho/nestjs-filter/actions/workflows/ci.yml)\n[![Integration Tests](https://github.com/DavideCarvalho/nestjs-filter/actions/workflows/integration.yml/badge.svg)](https://github.com/DavideCarvalho/nestjs-filter/actions/workflows/integration.yml)\n[![npm version](https://img.shields.io/npm/v/@dudousxd/nestjs-filter.svg)](https://www.npmjs.com/package/@dudousxd/nestjs-filter)\n[![npm downloads](https://img.shields.io/npm/dm/@dudousxd/nestjs-filter.svg)](https://www.npmjs.com/package/@dudousxd/nestjs-filter)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/DavideCarvalho/nestjs-filter/blob/main/LICENSE)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.4+-blue.svg)](https://www.typescriptlang.org/)\n[![NestJS](https://img.shields.io/badge/NestJS-10%2B-e0234e.svg)](https://nestjs.com/)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/DavideCarvalho/nestjs-filter/blob/main/CONTRIBUTING.md)\n\n## Docs\n\nFull documentation: **[davidecarvalho.github.io/nestjs-filter](https://davidecarvalho.github.io/nestjs-filter/)**\n\n## Packages\n\n| Package | Description |\n|---|---|\n| [`@dudousxd/nestjs-filter`](packages/core/README.md) | Core: BaseFilter, FilterRunner, decorators, FilterModule |\n| [`@dudousxd/nestjs-filter-mikro-orm`](packages/mikro-orm/README.md) | MikroORM 7 adapter |\n| [`@dudousxd/nestjs-filter-typeorm`](packages/typeorm/README.md) | TypeORM adapter |\n\n## Examples\n\n| Example | Stack | Run |\n|---------|-------|-----|\n| [`examples/mikro-orm-app/`](examples/mikro-orm-app/) | MikroORM + SQLite | `pnpm --filter @example/mikro-orm-app test` |\n| [`examples/typeorm-app/`](examples/typeorm-app/) | TypeORM + better-sqlite3 | `pnpm --filter @example/typeorm-app test` |\n\n## Features\n\n- **Declarative filter classes** with `@FilterFor()` method decorators\n- **Full NestJS DI** inside filters (inject any service)\n- **Optional class-validator integration** -- your filter class doubles as the DTO\n- **`@ApplyFilter()` param decorator** for controllers (method-aware: GET=query, POST=body+query)\n- **`resolve` option** on `@ApplyFilter` for dynamic filter class selection per request\n- **AsyncLocalStorage-based state isolation** -- singleton filters with zero cross-request contamination\n- **ORM-agnostic core** with native QueryBuilder access per adapter\n- **Structured input** -- `filter` + `sort` + `paginate` + `search` + `include` + `distinct` in one request\n- **`distinct`** -- `SELECT DISTINCT` projection for filter dropdowns (filters/search/sort still apply)\n- **`runner.describe(entity)`** -- metadata-derived field + one-hop relation map (memoized) for dynamic UIs / generic endpoints\n- **`runner.findAndCount(entity, input)`** -- runs + executes a dynamic query with **pagination-safe** to-many relation loading\n- **`FilterRunner.apply()`** for programmatic use in services\n- **`FilterTestingModule` + `makeMockQueryBuilder`** for isolated unit tests\n- **`FilterExceptionFilter`** maps validation errors to 400 responses\n- **Input normalization** (camelCase, snake_case, or custom)\n- **Unknown key policies** (ignore, warn, throw)\n\n## Install\n\n```bash\n# MikroORM\npnpm add @dudousxd/nestjs-filter @dudousxd/nestjs-filter-mikro-orm\n\n# TypeORM\npnpm add @dudousxd/nestjs-filter @dudousxd/nestjs-filter-typeorm\n```\n\n## Quick Start\n\n### 1. Define a filter\n\n```typescript\nimport { Injectable } from '@nestjs/common';\nimport { Filterable, FilterFor } from '@dudousxd/nestjs-filter';\nimport { MikroOrmFilter } from '@dudousxd/nestjs-filter-mikro-orm';\nimport { IsOptional, IsString, IsNumber } from 'class-validator';\nimport { Type } from 'class-transformer';\nimport { User } from './user.entity';\n\n@Injectable()\n@Filterable({ entity: User })\nexport class UserFilter extends MikroOrmFilter\u003cUser\u003e {\n  @IsOptional() @IsString()\n  name?: string;\n\n  @IsOptional() @IsNumber()\n  @Type(() =\u003e Number)\n  minAge?: number;\n\n  @FilterFor('name')\n  applyName(value: string) {\n    this.$query.andWhere({ name: { $like: `%${value}%` } });\n  }\n\n  @FilterFor('minAge')\n  applyMinAge(value: number) {\n    this.$query.andWhere({ age: { $gte: value } });\n  }\n}\n```\n\n### 2. Register the module\n\n```typescript\nimport { Module } from '@nestjs/common';\nimport { FilterModule } from '@dudousxd/nestjs-filter';\nimport { MikroOrmFilterModule } from '@dudousxd/nestjs-filter-mikro-orm';\nimport { UserFilter } from './user.filter';\n\n@Module({\n  imports: [\n    FilterModule.forRoot({ inputNormalizer: 'camelCase' }),\n    MikroOrmFilterModule.forRoot(),\n    FilterModule.forFeature([UserFilter]),\n  ],\n})\nexport class AppModule {}\n```\n\n### 3. Use in a controller\n\n```typescript\nimport { Controller, Get, Post, Body } from '@nestjs/common';\nimport { ApplyFilter } from '@dudousxd/nestjs-filter';\nimport type { QueryBuilder } from '@mikro-orm/sql';\nimport { User } from './user.entity';\nimport { UserFilter } from './user.filter';\n\n@Controller('users')\nexport class UsersController {\n  @Get()\n  list(@ApplyFilter(UserFilter) qb: QueryBuilder\u003cUser\u003e) {\n    return qb.getResultList();\n  }\n\n  @Post('search')\n  search(@ApplyFilter(UserFilter) qb: QueryBuilder\u003cUser\u003e, @Body() _body: unknown) {\n    return qb.getResultList();\n  }\n}\n```\n\n### 4. Use via `FilterRunner` (programmatic)\n\n```typescript\nimport { Injectable } from '@nestjs/common';\nimport { FilterRunner } from '@dudousxd/nestjs-filter';\nimport type { SqlEntityManager } from '@mikro-orm/sql';\nimport { User } from './user.entity';\nimport { UserFilter } from './user.filter';\n\n@Injectable()\nexport class UsersService {\n  constructor(\n    private readonly runner: FilterRunner,\n    private readonly em: SqlEntityManager,\n  ) {}\n\n  async search(input: Record\u003cstring, unknown\u003e) {\n    const qb = this.em.createQueryBuilder(User);\n    await this.runner.apply(UserFilter, input, qb);\n    return qb.getResultList();\n  }\n}\n```\n\n## API Reference\n\n### `@Filterable({ entity, allowed?, blocked? })`\n\nClass decorator. Marks a filter class and associates it with an entity.\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `entity` | `Type\u003cunknown\u003e` | **Required.** The entity class this filter targets. |\n| `allowed` | `readonly string[]` | Whitelist of input keys. Only these keys are dispatched. |\n| `blocked` | `readonly string[]` | Blacklist of input keys. These keys are never dispatched. |\n\n### `@FilterFor(inputKey?)`\n\nMethod decorator. Maps an input key to the decorated method. If `inputKey` is omitted, the method name is used as the key.\n\n```typescript\n@FilterFor('name')\napplyName(value: string) { ... }\n\n// Equivalent -- method name = key\n@FilterFor()\nname(value: string) { ... }\n```\n\n### `@ApplyFilter(FilterClass, options?)`\n\nParameter decorator for controller methods. Resolves input from the request, runs the filter, and injects the resulting QueryBuilder into the parameter.\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `source` | `InputSource` | `'auto'` | Where to read input from. |\n| `dto` | `Type\u003cunknown\u003e` | — | Override the DTO class used for validation. |\n| `resolve` | `(req) =\u003e Type` | — | Dynamically select a filter class per request. |\n\n**Source defaults (`'auto'`):**\n\n| HTTP method | Source |\n|-------------|--------|\n| `GET`, `HEAD` | `req.query` |\n| `POST`, `PUT`, `PATCH`, `DELETE` | `{ ...req.query, ...req.body }` (body wins) |\n\n### `FilterRunner.apply(FilterClass, input, qb, context?)`\n\nProgrammatic entry point. Resolves the filter from the DI container, normalizes input, validates (if enabled), runs `setup()`, then dispatches each key to the matching `@FilterFor` method.\n\n```typescript\nconst qb = await runner.apply(UserFilter, { name: 'Al' }, queryBuilder, { req });\n```\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `FilterClass` | `Type\u003cF\u003e` | The filter class to resolve and run. |\n| `input` | `unknown` | Raw input (object, null, or undefined). |\n| `qb` | `Q` | The ORM query builder instance. Returned as-is after mutation. |\n| `context` | `FilterContext` | Optional context (`{ req?, user?, raw? }`). |\n\n### `FilterModule.forRoot(options?)`\n\nRegisters the core filter infrastructure globally.\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `inputNormalizer` | `'camelCase' \\| 'snakeCase' \\| (key: string) =\u003e string` | `'camelCase'` | How to normalize input keys before dispatch. |\n| `inputFormat` | `'native' \\| 'spatie'` | `'native'` | Input query format. `'native'` is the library's structured model. `'spatie'` opts into spatie-laravel-query-builder / JSON:API style query strings (`filter[field]=…`, `filter[field][op]=…`, `sort=-a,b`, `include=a,b`, `fields[resource]=a,b`, `page[number]`/`page[after]`). Parsed into the native model first, so allowlist / operator-allowlist / `throwOnInvalid` safety still applies. |\n| `dropId` | `boolean` | _set explicitly_ | Strip trailing `Id` / `_id` from keys (e.g. `companyId` -\u003e `company`). The effective default is currently inconsistent in the code — when run through `FilterRunner`/`@ApplyFilter` the suffix is stripped unless you pass `dropId: false`. Set this explicitly to be safe. |\n| `onUnknownKey` | `'ignore' \\| 'warn' \\| 'throw'` | `'ignore'` | What to do when an input key has no matching `@FilterFor`. |\n| `validation` | `'auto' \\| 'off'` | `'auto'` | `'auto'` uses class-validator if installed. `'off'` skips validation. |\n| `throwOnInvalid` | `boolean` | `false` | When `true`, invalid sorts, distinct fields, and unknown `where` columns raise a `BadRequestException` instead of being silently dropped. Overridable per `@Filterable`. |\n| `defaultSort` | `string \\| SortItem[]` | – | Sort applied when the client provides no `sort` (stable ordering). Accepts a JSON:API string (`'-createdAt'`) or `SortItem[]`. Overridable per `@Filterable`. |\n\nBoth `throwOnInvalid` and `defaultSort` can also be set per filter class via\n`@Filterable({ entity, throwOnInvalid: true, defaultSort: '-createdAt' })`, which\ntakes precedence over the module-level option.\n\n\u003e **Full-text search (Postgres).** The TypeORM adapter's vector search uses\n\u003e `websearch_to_tsquery`, so arbitrary multi-word input (e.g. `\"foo bar\"`) is\n\u003e parsed safely instead of throwing a syntax error. Opt into relevance ordering\n\u003e with `search = { vector: 'search_vector', rank: true }` on the filter class\n\u003e (adds `ORDER BY ts_rank(...) DESC`).\n\n### `FilterModule.forFeature(filters)`\n\nRegisters filter classes with the DI container. Call once per feature module.\n\n```typescript\nFilterModule.forFeature([UserFilter, OrderFilter])\n```\n\n### `FilterTestingModule` (from `@dudousxd/nestjs-filter/testing`)\n\nStripped-down module for unit tests. Defaults to `validation: 'off'`.\n\n```typescript\nimport { FilterTestingModule, makeMockQueryBuilder } from '@dudousxd/nestjs-filter/testing';\n\nconst mod = await Test.createTestingModule({\n  imports: [FilterTestingModule.forRoot(), FilterTestingModule.forFeature([UserFilter])],\n}).compile();\n\nconst runner = mod.get(FilterRunner);\nconst qb = makeMockQueryBuilder();\nawait runner.apply(UserFilter, { name: 'Al' }, qb);\nexpect(qb.calls).toEqual([['andWhere', { name: { $like: '%Al%' } }]]);\n```\n\n### `makeMockQueryBuilder\u003cE\u003e()`\n\nCreates a Proxy-based mock query builder that records all method calls. Every method returns `this` for chaining. Access recorded calls via `qb.calls`.\n\n### `FilterInput\u003cF\u003e`\n\nType helper that extracts the input shape from a filter class. Picks all non-function, non-`$`-prefixed, non-`setup` properties.\n\n```typescript\ntype Input = FilterInput\u003cUserFilter\u003e;\n// { name?: string; minAge?: number }\n```\n\n### `FilterExceptionFilter`\n\nNestJS exception filter that catches `FilterValidationException` and returns a 400 JSON response.\n\n```typescript\nimport { FilterExceptionFilter } from '@dudousxd/nestjs-filter';\n\napp.useGlobalFilters(new FilterExceptionFilter());\n```\n\nResponse shape:\n\n```json\n{\n  \"statusCode\": 400,\n  \"message\": \"Filter input validation failed.\",\n  \"errors\": [...]\n}\n```\n\n## Configuration\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `inputNormalizer` | `'camelCase' \\| 'snakeCase' \\| (key: string) =\u003e string` | `'camelCase'` | Normalize input keys before dispatch. |\n| `dropId` | `boolean` | _set explicitly_ | Strip trailing `Id` / `_id` from keys. Effective default is currently inconsistent in the code — when run through `FilterRunner`/`@ApplyFilter` the suffix is stripped unless you pass `dropId: false`. |\n| `onUnknownKey` | `'ignore' \\| 'warn' \\| 'throw'` | `'ignore'` | Policy for unrecognized input keys. |\n| `validation` | `'auto' \\| 'off'` | `'auto'` | `'auto'` validates with class-validator if installed; `'off'` skips. |\n\n## Edge Cases\n\n| Scenario | Behavior |\n|----------|----------|\n| `null` / `undefined` input | Treated as empty object. Only `setup()` runs. |\n| `undefined` value for a key | Key is skipped (not dispatched). |\n| `__proto__` / `constructor` / `prototype` keys | Silently dropped (prototype pollution guard). |\n| `allowed` + `blocked` both set | `allowed` whitelist is checked first, then `blocked` blacklist. |\n| `setup()` throws | Wrapped in `FilterMethodException` with `key === 'setup'`. |\n| Filter method throws | Wrapped in `FilterMethodException` with the input key and value. |\n| No adapter registered | `@ApplyFilter` interceptor throws a descriptive error. |\n| Filter class not in DI | `FilterNotRegisteredException` is thrown. |\n| class-validator not installed | Validation is silently skipped (auto mode). |\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions, TDD workflow, and commit conventions.\n\n## License\n\nMIT -- Copyright (c) 2026 Davi Carvalho\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidecarvalho%2Fnestjs-filter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidecarvalho%2Fnestjs-filter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidecarvalho%2Fnestjs-filter/lists"}