Changelog
Release-by-release behavior changes for nestjs-drizzle-crud. Mirrors CHANGELOG.md shipped in the npm tarball.
This page mirrors the CHANGELOG.md shipped in the npm tarball. Feature pages describe current behavior; this page is the historical record. If something here disagrees with a feature page, the feature page wins (and the changelog is the bug).
[3.3.0] - 2026-08-10
Added
- Has-many relations, eager loaded without N+1. Declare
relations: { states: { table: states, type: 'hasMany', foreignKey: 'country_id' } }and passoptions.relations: ['states']tofind,findOneorfindAll. Each has-many relation costs exactly one extra query for the whole result set (WHERE country_id IN (...)) no matter how many parent rows come back, and the relations run concurrently. A page of 50 countries with their states is 3 queries: data, count, children. A parent with no children gets[], neverundefined. See Relations. - Many-to-many through a join table. Declare
{ table: tags, type: 'manyToMany', through: countryTags, throughLocalKey: 'country_id', throughForeignKey: 'tag_id' }and the collection loads in one batched query per relation (SELECT ... FROM country_tags JOIN tags ... WHERE country_id IN (...)). The join-table key is used to group the rows and stripped before they are returned. - Nested relations, any depth, with a dotted path. Put a
relationsmap on a relation and address it withoptions.relations: ['states', 'states.cities']or['state.country']. Each nesting level costs one more batched query for the whole level, so a page of 50 countries with their states and every state's cities is 4 queries. Asking only forstate.countrypulls instateautomatically. orderByon a collection relation.{ type: 'hasMany', foreignKey: 'country_id', orderBy: [{ column: 'name', order: 'asc' }] }orders the children in SQL. An unknown column is warned about and skipped, as withdefaultSort.- Collections are never joined. Filtering by a child column (
findAll({ states: { name: 'Kerala' } }),findAll({ tags: { name: 'himalayan' } })) compiles tocountries.id IN (SELECT ... WHERE ...), so parent rows are never duplicated andtotaland pagination stay correct. Belongs-to relations keep using aLEFT JOIN. options.selectnow narrows relation columns with a dotted path, at any depth.select: ['id', 'name', 'region.code', 'states.cities.name']projects only those columns, for the base table, a joined belongs-to relation and a batched collection alike. Listing only relation columns leaves the parent row complete. The parent key, the child foreign key and any key a nested level needs are added back automatically, because the grouping needs them. See Column selection.- An MCP server ships with the package.
npx nestjs-drizzle-crud-mcpserves the package docs to any MCP client through three tools:list_docs,get_docandsearch_docs. It speaks JSON-RPC over stdio, still has zero dependencies, and works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, Windsurf, Zed and Cline. See AI agents and MCP. AGENTS.template.mdfor every agent that is not Claude Code. It carries the same pointer toAGENT.mdand lists the filename each tool reads.CLAUDE.template.mdis unchanged.- Exported the
RelationType('belongsTo' | 'hasMany' | 'manyToMany'),RelationNodeandSelectTreetypes.
Changed
- A relation with a missing key column now throws at startup: belongs-to requires
localKey, has-many requiresforeignKey, many-to-many requiresthrough,throughLocalKeyandthroughForeignKey. Nested relations are validated too, and the message carries the full path (Relation "states.cities" is belongsTo and requires a localKey). It used to load nothing quietly, which read as bad data rather than bad config. RelationConfig.localKeyis optional, since collection relations use their own keys instead. Existing belongs-to configs are unaffected.
[3.2.2] - 2026-08-09
Fixed
- Restored the foreign-key mapping that 3.2.1 lost.
ForeignKeyViolationExceptionandEntityInUseExceptionare exported again, and a Postgres23503is translated by direction: 400 whencreateorupdatereference a parent row that does not exist (the offending column is parsed out of the driverdetail), 409 when adeleteis blocked by rows that still reference this one. Non-FK errors on the delete path still bubble unchanged. If you upgraded from 3.2.0 to 3.2.1 and started seeing 500s on bad references, move to this release.
[3.2.1] - 2026-07-17
Changed
- Packaging and documentation only:
homepagenow points at crud.viliha.com, and the package and repository READMEs link the docs site, npm and GitHub. No runtime code changed.
Known issue
- The foreign-key mapping added in 3.2.0 is missing from this build. The move to the Turborepo monorepo carried over a source tree from before 3.2.0, so
ForeignKeyViolationExceptionandEntityInUseExceptionare not exported and a23503bubbles as a 500 again. Fixed in 3.2.2, so upgrade rather than pinning here. The 3.1.1 and 3.2.0 entries below were lost in the same move and are restored here from the published tarballs.
[3.2.0] - 2026-07-09
Added
- Foreign-key violations (Postgres
23503) map to a typed HTTP error instead of a raw 500, and the direction decides the status. Acreateorupdatethat references a parent row which does not exist gives 400ForeignKeyViolationException, with the offending column parsed out of the driverdetail. Adeleteblocked because other rows still reference this one (ON DELETE RESTRICT/NO ACTION) gives 409EntityInUseException. Both exceptions are exported from the package root.
Changed
deletewraps its statement execution so a23503on the delete side is translated; it previously bubbled as a 500. Non-FK errors still bubble unchanged.- This reverses the earlier choice to let FK violations bubble. A client-supplied foreign key is a recoverable bad value, not a programming error. NOT-NULL violations are still left to bubble.
[3.1.1] - 2026-07-03
Changed
- Package metadata only, no code changes: repository and issues URLs point to myviliha/viliha-drizzle-crud, and the homepage points to crud.viliha.com.
[3.1.0] - 2026-06-29
Added
findAll/countnow supportoptions.search. Callers can pass a search term and a list of table columns, e.g.{ search: { term: "na", columns: ["name", "code"] } }. The default mode is cross-columnILIKE '%term%';mode: "fullText"compiles a PostgreSQLto_tsvector(...) @@ plainto_tsquery(...)condition. The search predicate is applied to both the data query and the count query, so paginatedtotalstays correct.
[3.0.4] - 2026-06-21
Found while probing the package over real HTTP from the drizzle-pkg-demo consumer. PostgreSQL-first.
Fixed
- Bad client input on
create/updatenow maps to 400, not a raw 500. A value that is too long for its column (Postgres22001), fails aCHECKconstraint (23514), or is numerically out of range (22003) previously surfaced as an unhandledDrizzleQueryError→ 500 Internal Server Error. These are now translated toValidationFailedException(400), mirroring the existing unique-violation → 409 mapping.
[3.0.3] - 2026-06-20
Correctness fix found while validating the package from a fresh NestJS consumer. PostgreSQL-first.
Fixed
- Case-insensitive string filters are now EXACT, not wildcard patterns. A plain string value in a
findAll/countfilter (e.g.{ name: "foo" }) is documented as a case-insensitive exact match, but compiled toilike(column, value). ILIKE interprets%,_and\in the value as wildcards / escapes, so an equality filter silently became aLIKEpattern and returned non-matching rows (e.g.{ title: "a_b" }matched"aXb";{ title: "a%" }matched everything). It now compiles tolower(column) = lower(value), so those characters are treated as literal data. The explicit{ like }/{ ilike }operators are unchanged for intentional pattern matching.
[3.0.2] - 2026-06-19
Residual-edge hardening found by an end-to-end HTTP probe of 3.0.1 (all LOW severity; no behavior change to the happy path).
Changed
fullTextSearchnow returns the same envelope asfindAll,{ data, total, page, limit }instead of{ data, total }.page/limitare the resolved (clamped) values; the query is still only constrained when the caller passespagination. Callers paginating search results can now read back the page / limit they got.
Fixed
- Invalid
sortOrdernow fails fast (400).?sortOrder=<anything but asc/desc>previously fell through to ascending silently; it now throwsBadRequestException, mirroring the unknown-sortBycheck added in 3.0.1. - Non-finite numeric filter operands return 400, not 500. A
{ gt: NaN }/{ lte: Infinity }operand ongt/gte/lt/lte(e.g. from an unguardedNumber('abc')in a controller) reached SQL and surfaced as an opaqueDatabaseException(500). It is now rejected asBadRequestException. Non-number operands (dates, strings) are unaffected.
[3.0.1] - 2026-06-19
Correctness pass: typed exceptions now map to real HTTP status codes, plus several read-path fixes. PostgreSQL-first.
Fixed
- Exceptions extend
HttpExceptionso Nest maps them automatically:EntityNotFoundException→ 404,DuplicateEntityException→ 409,ValidationFailedException/BulkOperationException→ 400,Database*/TransactionException→ 500. Previously every error surfaced as a generic 500. - Duplicate-key translation. A Postgres unique violation (SQLSTATE
23505, on eithererr.codeor the Drizzle-wrappederr.cause.code) is converted toDuplicateEntityException(409), parsing the offending field / value from the driverdetail. fullTextSearchexcludes soft-deleted rows (added theIS NULLguard on the soft-delete column to both the data and count queries, matchingfindAll/count).- Pagination clamping.
page < 1,limit <= 0, andlimit > maxLimitare clamped via a sharedresolvePaginationhelper. No more negativeOFFSETorLIMIT 0. - Unknown sort column fails fast. A caller-supplied
sortBynot on the table throwsBadRequestException; an unknown column in a configureddefaultSortis warned and skipped. restore()returns the restored row on the PostgresRETURNINGpath (no longer re-find()s, which would have excluded the just-restored row via the soft-delete filter).
Added
- Row-level lock options on Postgres reads (
find/findOne/findAll):lock: 'update' | 'share'andforNoKeyUpdateapply.for(...). No-op on MySQL. See Row-level locks.
[3.0.0] - 2026-06-19
- First release under the
nestjs-drizzle-crudname with the 3.x API surface (DrizzleCrudModule.forRoot/forRootAsync/forFeature,SqlBaseCrudService).
Earlier (≤ 2.2.0)
Published as @quybquang/nestjs-drizzle-crud. Notable: 2.2.0 added a configurable module-level default sort; 2.1.2 documented the timestamps feature. See git history for details.