nnestjs-drizzle-crud
Reference

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 pass options.relations: ['states'] to find, findOne or findAll. 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 [], never undefined. 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 relations map on a relation and address it with options.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 for state.country pulls in state automatically.
  • orderBy on 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 with defaultSort.
  • Collections are never joined. Filtering by a child column (findAll({ states: { name: 'Kerala' } }), findAll({ tags: { name: 'himalayan' } })) compiles to countries.id IN (SELECT ... WHERE ...), so parent rows are never duplicated and total and pagination stay correct. Belongs-to relations keep using a LEFT JOIN.
  • options.select now 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-mcp serves the package docs to any MCP client through three tools: list_docs, get_doc and search_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.md for every agent that is not Claude Code. It carries the same pointer to AGENT.md and lists the filename each tool reads. CLAUDE.template.md is unchanged.
  • Exported the RelationType ('belongsTo' | 'hasMany' | 'manyToMany'), RelationNode and SelectTree types.

Changed

  • A relation with a missing key column now throws at startup: belongs-to requires localKey, has-many requires foreignKey, many-to-many requires through, throughLocalKey and throughForeignKey. 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.localKey is 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. ForeignKeyViolationException and EntityInUseException are exported again, and a Postgres 23503 is translated by direction: 400 when create or update reference a parent row that does not exist (the offending column is parsed out of the driver detail), 409 when a delete is 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: homepage now 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 ForeignKeyViolationException and EntityInUseException are not exported and a 23503 bubbles 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. A create or update that references a parent row which does not exist gives 400 ForeignKeyViolationException, with the offending column parsed out of the driver detail. A delete blocked because other rows still reference this one (ON DELETE RESTRICT / NO ACTION) gives 409 EntityInUseException. Both exceptions are exported from the package root.

Changed

  • delete wraps its statement execution so a 23503 on 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

[3.1.0] - 2026-06-29

Added

  • findAll / count now support options.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-column ILIKE '%term%'; mode: "fullText" compiles a PostgreSQL to_tsvector(...) @@ plainto_tsquery(...) condition. The search predicate is applied to both the data query and the count query, so paginated total stays 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 / update now maps to 400, not a raw 500. A value that is too long for its column (Postgres 22001), fails a CHECK constraint (23514), or is numerically out of range (22003) previously surfaced as an unhandled DrizzleQueryError → 500 Internal Server Error. These are now translated to ValidationFailedException (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 / count filter (e.g. { name: "foo" }) is documented as a case-insensitive exact match, but compiled to ilike(column, value). ILIKE interprets %, _ and \ in the value as wildcards / escapes, so an equality filter silently became a LIKE pattern and returned non-matching rows (e.g. { title: "a_b" } matched "aXb"; { title: "a%" } matched everything). It now compiles to lower(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

  • fullTextSearch now returns the same envelope as findAll, { data, total, page, limit } instead of { data, total }. page / limit are the resolved (clamped) values; the query is still only constrained when the caller passes pagination. Callers paginating search results can now read back the page / limit they got.

Fixed

  • Invalid sortOrder now fails fast (400). ?sortOrder=<anything but asc/desc> previously fell through to ascending silently; it now throws BadRequestException, mirroring the unknown-sortBy check added in 3.0.1.
  • Non-finite numeric filter operands return 400, not 500. A { gt: NaN } / { lte: Infinity } operand on gt / gte / lt / lte (e.g. from an unguarded Number('abc') in a controller) reached SQL and surfaced as an opaque DatabaseException (500). It is now rejected as BadRequestException. 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 HttpException so 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 either err.code or the Drizzle-wrapped err.cause.code) is converted to DuplicateEntityException (409), parsing the offending field / value from the driver detail.
  • fullTextSearch excludes soft-deleted rows (added the IS NULL guard on the soft-delete column to both the data and count queries, matching findAll / count).
  • Pagination clamping. page < 1, limit <= 0, and limit > maxLimit are clamped via a shared resolvePagination helper. No more negative OFFSET or LIMIT 0.
  • Unknown sort column fails fast. A caller-supplied sortBy not on the table throws BadRequestException; an unknown column in a configured defaultSort is warned and skipped.
  • restore() returns the restored row on the Postgres RETURNING path (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' and forNoKeyUpdate apply .for(...). No-op on MySQL. See Row-level locks.

[3.0.0] - 2026-06-19

  • First release under the nestjs-drizzle-crud name 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.


Next

On this page