| name | nestjs-crud-migration |
| description | Use when migrating `@nestjs-crud/*` v1.0.x → v2.x — the v2.0 strict allowlist break, deleted subclass internals, type tightening, write-path transactions; the v2.1 Prisma 7 driver-adapter switch (`schema.prisma` `datasource.url` removal, `prisma.config.ts` forwarding, dropped `--skip-generate`, `adapter-pg` `search_path` landmine, `adapter-mariadb` session-state landmine); the v2.1.1 swagger v3-gate cleanup; the v2.2.0 unified caching API; or v2.2.0 opt-in cursor pagination. Use when diagnosing v2 upgrade errors like `RequestQueryException`, `CrudCacheNotConfiguredError`, `setSearchCondition is not a function`, `count is not a function`, MikroORM stale-em, Prisma `Unknown argument 'where'` inside include, or cursor 400s (`single sort field` / `requires a limit` / `Invalid cursor`). |
@nestjs-crud Migration
Consumer playbook for v1 → v2 and within-v2 upgrades. Stay on v1.0.x: pin ^1.0.2 (see Stay-On Pin §). v2.0 = single coordinated breaking release; v2.1 narrows Prisma peer to v7; v2.1.1 = security/dead-code patch; v2.2.0 = unified caching API + opt-in cursor pagination (both additive).
Runtime detail (operator reference, @CrudAuth shape, Caching setup, MikroORM em-thunk) lives in nestjs-crud SKILL. This playbook covers the upgrade delta only.
Pre-Upgrade Audit (greps to run first)
grep -rE "sort=|filter=|search=|fields=" src/ test/
grep -rE "(setSearchCondition|setAndWhere|setOrWhere|setJoin|getRelationMetadata|checkSqlInjection|mapSort|getFieldWithAlias|getSort)\s*\(" src/
grep -rE "implements QueryTranslator<|extends TypeOrmQueryTranslator" src/
grep -rE "this\.em\s*=|private\s+(readonly)?\s*em:\s*EntityManager" src/
grep -rE "\.(sqlInjectionRegEx|entityRelationsHash)\s*=" src/
grep -rE "(doGetMany|createBuilder|prepareEntityBeforeSave|getSelect)\s*\(" src/
grep -rE "protected\s+db\s*:\s*any|extends\s+DrizzleCrudService" src/
grep -rE "extends\s+MikroOrmCrudService|override\s+(getMany|getOne|createOne|createMany|updateOne|replaceOne|deleteOne|recoverOne)" src/
grep -rE "from\s+['\"]@nestjs/swagger/dist/types/swagger-enum\.type['\"]|SwaggerEnumType|getSwaggerVersion|swaggerPkgJson" src/
grep -rE "@Override\(\)\s*(async\s+)?(updateOne|replaceOne|deleteOne)" src/
grep -rE "@CrudAuth|CrudAuth\s*\(\s*\{" src/
grep -rE "cache:\s*[0-9]+|cache:\s*true" src/
node --version
grep -rE "toMatchSnapshot.*swagger|toMatchSnapshot.*apioperation|Swagger\.operationsMap" src/ test/
| Hit | Action |
|---|
| #1 | Audit allowlist (§A) |
| #2-#4, #7-#8 | Subclass migration (§B-§C) |
| #5 | Runtime field-mutation — re-implement via new interfaces (§B) |
| #6 | Mid-tier override on a method whose shape changed — audit against v2 source (§B) |
| #9 | Drop deleted swagger imports (§C) |
| #10 | Audit transaction nesting (§D) |
| #11 | Audit persist keys against entity columns |
| #12 | Wire CacheStrategy (v2.2.0) or remove cache option |
| #13 fail | Upgrade Node 22+ or stay on v1.0.x |
| #14 | Swagger snapshot drift / operationsMap shape — re-record OR pin v1 wording via swagger.operations (§D) |
Quick Reference — What Breaks
| Area | Impact | Section |
|---|
| Strict allowlist — unknown sort/filter/search/fields → 400 | High (every consumer) | §A |
Deleted subclass internals (setSearchCondition, checkSqlInjection, etc) | Medium (subclass-override consumers) | §B |
QueryTranslator<Q,W> must implement count + findOneOrFail | Medium (custom translator consumers) | §B |
MikroORM getEm thunk contract — never cache em | Medium (MikroORM subclass consumers) | §B |
Drizzle protected db: DrizzleClient (was any) | Medium (subclass-override) | §C |
MikroORM signatures typed (15 any sites → typed generics) | Medium (subclass-override) | §C |
@CrudAuth persist runtime-validated — typos throw | Medium (auth-filter bypass closed) | §D |
| Write-path transactions READ COMMITTED on update/replace/deleteOne | Low normal; medium with consumer outer-tx | §D |
Cache fail-fast — @Crud cache without backend → throws | Medium (was silent in v1) | §D |
Swagger default text rewrite + Swagger.operationsMap shape change | Snapshot-test consumers only | §D |
engines.node: ">=22" | Install refuses on Node <22 | §E |
§A. Strict Column-Name Allowlist
Most consumers feel v2 here first. Field names in ?sort/?filter/?search/?fields/?join MUST be in the entity's per-adapter column allowlist (TypeORM entityColumnsHash, Drizzle columnsMap, MikroORM propertiesMap, Prisma entityColumns) OR a relation registered in @Crud({ query: { join: {...} } }). Otherwise 400.
| v1.x | v2.0 |
|---|
| Validation | Denylist regex | Allowlist from per-adapter column source |
| Unknown field | Silent 200 OK (typo → empty result) | 400 |
| Backing regex | sqlInjectionRegEx (per-adapter) | Deleted; InputSanitizer is sole path |
| Opt-out | — | None |
Common breakages: column typos; TypeORM @VirtualColumn/@Formula not in metadata; dotted paths (?sort=author.name) without explicit ?join=author AND @Crud({ query: { join: { author: {} } } }) registration.
§B. Deleted Service Internals
Subclass-override consumers only. Standard usage (extending for DI wiring) unaffected.
| v1 surface (deleted) | v2 replacement |
|---|
setSearchCondition / setAndWhere / setOrWhere | TypeOrmQueryTranslator.buildWhere |
setJoin / getRelationMetadata | TypeOrmJoinResolver.applyJoins |
checkSqlInjection | InputSanitizer.assert(field) |
mapSort / getFieldWithAlias / getSort | TypeOrmQueryComposer (@internal) |
prepareEntityBeforeSave / getSelect / findOneOrFail | Retained as 1-line delegators |
Deleted fields: sqlInjectionRegEx (no replacement); entityRelationsHash (moved to TypeOrmJoinResolver).
Custom QueryTranslator<Q, W> implementations must add 2 methods:
class MyTranslator implements QueryTranslator<SelectQueryBuilder<T>, ObjectLiteral> {
count(qb: SelectQueryBuilder<T>): Promise<number> { return qb.getCount(); }
findOneOrFail(qb: SelectQueryBuilder<T>, opts: { id: unknown }): Promise<T> {
return qb.where('id = :id', { id: opts.id }).getOneOrFail();
}
}
class MyTranslator<T extends ObjectLiteral> extends TypeOrmQueryTranslator<T> {
override buildWhere(search: SCondition): Brackets | undefined {
return super.buildWhere(search);
}
}
Common errors: TypeError: this.translator.count is not a function, TypeError: this.translator.findOneOrFail is not a function.
MikroORM getEm: () => EntityManager thunk contract. MikroOrmFetchHelper resolves em fresh per call. Subclasses MUST call this.getEm() inside every method; never cache em as field — re-introduces cross-request identity-map bug.
Internal piece interfaces (WhereBuilder<Q,W> / QueryComposer<Q> / FetchHelper<Q>) live in @nestjs-crud/core/query subpath, marked @internal. Public QueryTranslator<Q,W> contract unchanged.
§C. Type Tightening
- Drizzle:
protected db: DrizzleClient (was any). Subclasses re-declaring protected db: any conflict with base. Fix: delete the re-declaration, inherit.
- MikroORM: 15
any sites on public method signatures → typed generics (FilterQuery<T>, RequiredEntityData<T>, EntityMetadata<T>, QueryOrderMap<T>). Subclass callsites passing untyped values need explicit annotations.
- Core:
SwaggerEnumType inlined — no more @nestjs/swagger/dist/types/swagger-enum.type import. Replace with: string[] | number[] | (string | number)[] | Record<number, string>.
- v2.1.1 cleanup:
getSwaggerVersion and swaggerPkgJson removed from @nestjs-crud/core exports (internal v3-gate helpers). Delete imports if you had them; no replacement (safeRequire inside library handles missing swagger).
§D. Behavior Changes
Logger. v1 silent-no-op default → v2 new Logger(<ServiceName>) by default on all 4 adapters. If you relied on v1 silence: pass explicit no-op logger via constructor (Prisma: serviceConfig.logger). Never interpolate err.message in logs — DB drivers leak SQL parameter values; emit err.name + err.stack as 2nd arg.
@CrudAuth persist runtime validation. v1 silently ignored typos in persist (auth-filter bypass on writes). v2 validates each persist key against entity columns; throws RequestQueryException: Invalid persist key 'X' → 400.
Write-path transactions. updateOne / replaceOne / deleteOne wrap read-modify-write at READ COMMITTED on all 4 adapters. recoverOne excluded. Transaction nesting: if you @Override() and open an outer tx, adapter's inner tx becomes a savepoint inside yours; outer SERIALIZABLE NOT downgraded; rollbacks cascade. Decide: remove outer wrap, or accept savepoint nesting.
| Adapter | Primitive |
|---|
| TypeORM | QueryRunner + startTransaction('READ COMMITTED') |
| Drizzle | db.transaction(..., { isolationLevel: 'read committed' }) |
| MikroORM | em.transactional(..., { isolationLevel: READ_COMMITTED }) |
| Prisma | $transaction(..., { isolationLevel: 'ReadCommitted' }) |
Cache fail-fast. @Crud({ query: { cache } }) without a CacheStrategy (and no TypeORM DataSource.cache fallback) throws CrudCacheNotConfiguredError at the first cached read. Wire a strategy (v2.2.0 §) or remove cache from @Crud().
Swagger default text rewrite. v2 rewrites all 8 generated routes' summaries + descriptions. Runtime-additive — but snapshot-test consumers will drift. Re-record OR pin v1 wording via @Crud({ swagger: { operations: { getManyBase: { summary: 'Retrieve multiple Users' } } } }). Swagger.operationsMap shape changed string → { summary, description } tuples.
TypeORM split-query opt-in. @Crud({ query: { relationLoadStrategy: 'query' } }) opts into per-relation queries. Footgun: JoinOption.allow is ignored under 'query' (TypeORM's setFindOptions doesn't expose alias-level select). Don't opt in if you use allow to hide sensitive columns.
§E. Packaging
- All packages declare
"engines": { "node": ">=22.0.0" }. npm install refuses on Node <22.
peerDependencies declared on every adapter package. Install warns if peers missing.
@nestjs/common peer range: ^10.0.0 || ^11.0.0 (v2.0+).
- v2.1.1+:
@nestjs/swagger declared as optional peerDependency on @nestjs-crud/core (peerDependenciesMeta.optional: true); consumers without swagger get no install warning.
§F. @nestjs-crud/prisma (additive in v2.0)
Net-new package. Zero migration impact for existing consumers.
npm install @nestjs-crud/prisma @prisma/client
npm install -D prisma
Prisma-specific behaviors (consumer-visible):
- Default relation strategy = query decomposition (1 + N_depth queries), NOT SQL JOIN. Opt into
relationJoins preview on your PrismaClient for native joins.
where inside include rejected for to-one relations. Filter at parent where. Adapter handles SCondition dotted paths on to-one this way automatically.
createMany uses $transaction([create, ...]) for full-record return parity (slower; returns full records).
Service ctor: see nestjs-crud SKILL §Quickstart. CI matrix: 4 adapters × 2 DBs = 8 cells + parity + no-swagger sentinel.
§G. What Does NOT Change
@Crud() decorator signature, 8 endpoint paths, @Override/@ParsedRequest/@ParsedBody, @CrudAuth, RequestQueryBuilder + CondOperator, CrudValidationGroups, CrudConfigService.load, public QueryTranslator<Q,W> interface, getMany/getOne/etc return shapes. If you only use the public surface: audit column references in query params, then bump.
Cursor pagination is opt-in (v2.2.0+): offset-mode response shape ({ data, count, total, page, pageCount }) preserved bit-for-bit. Existing routes without pagination: 'cursor' see no behavior change.
v2.0 → v2.1 — Prisma 7 driver adapter
@nestjs-crud/prisma@2.1.0 narrows @prisma/client peer to ^7.0.0. Adapter runtime API unchanged — no code changes in subclasses. All migration is in schema files + CLI invocations + PrismaClient constructor. Other 6 packages republish at 2.1.0 with no behavior change.
Canonical walkthrough: v2.1 Migration wiki. This section is triage summary.
Pre-upgrade audit (Prisma consumers only):
grep -rnE 'url\s*=\s*env\("DATABASE_URL"\)' . --include='*.prisma'
grep -rn "skip-generate" . --include='*.json' --include='*.yml' --include='Dockerfile*'
grep -rnE "new PrismaClient\(\s*\)|datasourceUrl|datasources:\s*\{" src/
grep -rn '\?schema=' . --include='*.env*' --include='*.ts'
grep -rnE "FOREIGN_KEY_CHECKS|SET SESSION|SET @" src/ test/
| Symptom | Cause | Fix |
|---|
prisma generate: The property url on the datasource block is not allowed | v5-shaped schema on v7 | Drop url = env(...) from every datasource block (wiki §1) |
prisma db push: The datasource.url property is required in your Prisma config file | Missing prisma.config.ts forwarding DATABASE_URL | Add prisma.config.ts forwarding env into datasource.url (wiki §2) |
prisma db push: Unknown argument '--skip-generate' | v7 hard-removed the flag | Drop the flag — db push auto-generates now (wiki §3) |
new PrismaClient() throws datasourceUrl mismatch | v7 rejects datasourceUrl against v7 schemas; no implicit env read | Wire driver adapter: new PrismaClient({ adapter: new PrismaPg(...) }) for Postgres; new PrismaMariaDb(url) for MySQL (wiki §4) |
Postgres: tables in public despite ?schema=custom | @prisma/adapter-pg does NOT run SET search_path on connect (undocumented) | Pass libpq options=-c search_path=<schema> in pg PoolConfig AND schema as 2nd PrismaPg arg (wiki gotcha 1) |
MySQL: SET FOREIGN_KEY_CHECKS=0; TRUNCATE fails | @prisma/adapter-mariadb dispatches each call on fresh pool checkout — session SETs don't persist (undocumented) | Use dependency-ordered DELETE FROM + ALTER TABLE ... AUTO_INCREMENT = 1; replay SET SESSION via mariadb pool's initSql (wiki gotcha 2) |
v2.1.0 → v2.1.1
Security + dead-code patch. Near-zero consumer impact for standard usage.
- 22 dev-tree GHSAs closed via root
resolutions.
@nestjs/swagger declared as optional peerDependency on @nestjs-crud/core.
getSwaggerVersion / swaggerPkgJson removed (delete imports if you had them; no replacement).
- Dropped
swagger.ApiProperty || swagger.ApiModelProperty fallback (ApiModelProperty deprecated since 2018; v2.x peer floor ^10 makes it unreachable).
v2.1.1 → v2.2.0 — Caching API (additive)
Backward-compatible. No migration steps required. Existing @Crud({ query: { cache } }) consumers see no behavior change unless they wire a CacheStrategy.
What's new:
CacheStrategy interface in @nestjs-crud/core/cache honored by all 4 adapters (was TypeORM-only pre-2.2.0).
- Strategies (
TypeOrm | MikroOrm | Drizzle | PrismaRedis | PrismaAccelerate) accept redis (node-redis v5) or ioredis clients with lazy-once auto-connect — no explicit connect() required.
- Custom backends: implement
RedisLike (set / get / del / scanPrefix) from @nestjs-crud/core/cache.
- Layered override path:
CrudConfigService.load({ query: { cacheStrategy } }) global → per-CrudService ctor override → @Crud({ query: { cache } }) per-route → ?cache=0 per-request bypass.
- Auto-invalidate-on-write by entity prefix.
- New
cacheErrorPolicy: 'fail-fast' | 'fallback-to-source' knob.
- TypeORM-native
DataSource.cache pass-through tagged @deprecated since v2.2.0 — still works as fallback when CacheStrategy not wired.
Setup: Caching wiki.
v2.1.1 → v2.2.0 — Cursor pagination (additive, opt-in)
Backward-compatible. No migration steps required. Default pagination remains 'offset' (offset response shape preserved bit-for-bit). Consumers opt in per-controller:
@Crud({
model: { type: User },
query: { pagination: 'cursor', limit: 25 },
})
@Controller('users')
export class UsersController { constructor(public service: UsersService) {} }
getManyBase then returns { data, count, cursor: { next, prev } }. Forward: GET /users?sort=id,ASC → body.cursor.next. Back: GET /users?sort=id,ASC&cursor=<prev>. Per-route override via query.pagination; controller default via CrudOptions.pagination.
New errors consumers may hit AFTER opting in:
| Error | Cause | Fix |
|---|
Cursor pagination supports a single sort field → 400 | Multi-sort + cursor mode | Use one sort field |
Cursor pagination requires a limit → 400 | Cursor mode without query.limit or maxLimit | Set a limit |
Cursor sort field mismatch → 400 | Client sent cursor encoded against different sort field than current request | Match ?sort= to the field that issued the cursor |
Invalid cursor → 400 | Tampered, malformed, or oversized cursor (codec rejects payloads >1024 chars) | Re-fetch first page |
Cursor is opaque base64url JSON, NOT signed — keep authorization in @CrudAuth. Cursor mode bypasses the cache wrap (per-cursor cardinality unbounded); pair with @nestjs/throttler on hot endpoints. Setup + tradeoffs: Cursor Pagination wiki.
Common v2 Upgrade Errors
Migration-only — runtime issues live in nestjs-crud SKILL §Common Issues.
| Error / symptom | Cause | Fix |
|---|
BadRequestException: Field "X" is not allowed | Field not in entity column allowlist | Add @Column(), register virtual field, or remove from query param |
BadRequestException: Relation "Y" is not allowed | Dotted path with unknown relation | Add ?join=Y AND register in @Crud({ query: { join: { Y: {} } } }) |
TypeError: this.checkSqlInjection is not a function | Subclass called deleted private | this.sanitizer.assert(field) |
TypeError: this.setSearchCondition is not a function | Subclass called deleted protected | Override TypeOrmQueryTranslator.buildWhere |
TypeError: this.translator.count is not a function | Custom translator missing count() | count(qb): return qb.getCount() for TypeORM |
TypeError: this.translator.findOneOrFail is not a function | Custom translator missing method | Add findOneOrFail(qb, opts) per TypeOrmQueryTranslator reference |
Cannot read properties of undefined (reading 'entityRelationsHash') | Field moved | Now per-TypeOrmJoinResolver instance — access via this.joinResolver |
Class 'X' incorrectly implements interface 'QueryTranslator<Q, W>'. Missing: count, findOneOrFail | Pre-v2 custom translator | Add both methods |
TS: Type 'any' is not assignable to type 'DrizzleClient' | Drizzle subclass field re-declaration | Remove protected db: any; inherit |
TS: Argument of type 'any' is not assignable to parameter of type 'X' | MikroORM type tightening | Annotate with FilterQuery<T>/RequiredEntityData<T> |
Module '"@nestjs-crud/core"' has no exported member 'getSwaggerVersion' / swaggerPkgJson | Removed in v2.1.1 | Delete imports — no replacement |
RequestQueryException: Invalid persist key 'X' → 400 | Typo in @CrudAuth({ persist }) against entity column | Fix key |
CrudCacheNotConfiguredError on first cached read | @Crud cache set but no strategy/backend | Wire CacheStrategy, configure TypeORM DataSource.cache, or remove cache |
Property 'summary' does not exist on type 'string' on Swagger.operationsMap(...) | Internal API shape changed string → { summary, description } | Destructure, or switch to @Crud({ swagger: { operations: {...} } }) |
| Swagger snapshot tests fail | v2 rewrites default summaries + descriptions | Re-record OR pin v1 wording via swagger.operations |
| Unexpected SERIALIZABLE inside outer tx, or rollback cascade | Adapter inner tx is savepoint inside your outer | Remove outer wrap or accept savepoint nesting |
| Prisma service emits logs when it didn't before | v2 unified default — serviceConfig.logger omitted = new Logger(...) | Pass explicit no-op logger via serviceConfig.logger |
| npm install peer warnings | peerDeps declared in v2 | Install peers explicitly |
EBADENGINE: Unsupported engine | Node <22 | Upgrade Node 22+, or stay on v1.0.x |
Stay-On Pin
{
"dependencies": {
"@nestjs-crud/core": "^1.0.2",
"@nestjs-crud/typeorm": "^1.0.2"
}
}
npm update continues tracking the v1.0.x line. Bugfix patches continue. Per-version stay-on pins:
- v2.0:
"@nestjs-crud/prisma": "^2.0.0" + "@prisma/client": "^5 || ^6" — escapes Prisma 7 driver-adapter requirement
- v2.1.0: pin
"@nestjs-crud/core": "2.1.0" (or ~2.1.0); v2.1.0 line stops receiving patches once v2.2.0 ships