@avytrix/nestjs-drizzle-crud 0.0.0-stage → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT.md +513 -0
- package/AGENTS.template.md +31 -0
- package/CHANGELOG.md +565 -0
- package/CLAUDE.template.md +18 -0
- package/LICENSE +21 -0
- package/README.md +1307 -2
- package/dist/controller/crud-controller.factory.d.ts +28 -0
- package/dist/controller/crud-controller.factory.js +279 -0
- package/dist/controller/crud-controller.factory.js.map +1 -0
- package/dist/core/abstract/sql-base-crud.service.d.ts +230 -0
- package/dist/core/abstract/sql-base-crud.service.js +2899 -0
- package/dist/core/abstract/sql-base-crud.service.js.map +1 -0
- package/dist/core/context/crud-context.d.ts +6 -0
- package/dist/core/context/crud-context.js +27 -0
- package/dist/core/context/crud-context.js.map +1 -0
- package/dist/core/events/crud-event-bus.d.ts +7 -0
- package/dist/core/events/crud-event-bus.js +30 -0
- package/dist/core/events/crud-event-bus.js.map +1 -0
- package/dist/core/interfaces/crud-service.interface.d.ts +54 -0
- package/dist/core/interfaces/crud-service.interface.js +3 -0
- package/dist/core/interfaces/crud-service.interface.js.map +1 -0
- package/dist/core/interfaces/drizzle-crud-config.interface.d.ts +39 -0
- package/dist/core/interfaces/drizzle-crud-config.interface.js +3 -0
- package/dist/core/interfaces/drizzle-crud-config.interface.js.map +1 -0
- package/dist/core/interfaces/events.interface.d.ts +39 -0
- package/dist/core/interfaces/events.interface.js +3 -0
- package/dist/core/interfaces/events.interface.js.map +1 -0
- package/dist/core/interfaces/query.interface.d.ts +112 -0
- package/dist/core/interfaces/query.interface.js +3 -0
- package/dist/core/interfaces/query.interface.js.map +1 -0
- package/dist/core/interfaces/scope.interface.d.ts +3 -0
- package/dist/core/interfaces/scope.interface.js +3 -0
- package/dist/core/interfaces/scope.interface.js.map +1 -0
- package/dist/core/interfaces/sql-crud-config.interface.d.ts +87 -0
- package/dist/core/interfaces/sql-crud-config.interface.js +3 -0
- package/dist/core/interfaces/sql-crud-config.interface.js.map +1 -0
- package/dist/core/types/sql.types.d.ts +7 -0
- package/dist/core/types/sql.types.js +3 -0
- package/dist/core/types/sql.types.js.map +1 -0
- package/dist/core/utils/cursor.d.ts +2 -0
- package/dist/core/utils/cursor.js +44 -0
- package/dist/core/utils/cursor.js.map +1 -0
- package/dist/core/utils/merge-config.d.ts +2 -0
- package/dist/core/utils/merge-config.js +22 -0
- package/dist/core/utils/merge-config.js.map +1 -0
- package/dist/core/utils/query-parser.d.ts +4 -0
- package/dist/core/utils/query-parser.js +538 -0
- package/dist/core/utils/query-parser.js.map +1 -0
- package/dist/decorators/crud-service.decorator.d.ts +4 -0
- package/dist/decorators/crud-service.decorator.js +10 -0
- package/dist/decorators/crud-service.decorator.js.map +1 -0
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.js +6 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/exceptions/crud.exceptions.d.ts +46 -0
- package/dist/exceptions/crud.exceptions.js +93 -0
- package/dist/exceptions/crud.exceptions.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +236 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/modules/drizzle-connection.d.ts +12 -0
- package/dist/modules/drizzle-connection.js +73 -0
- package/dist/modules/drizzle-connection.js.map +1 -0
- package/dist/modules/drizzle-crud.module.d.ts +11 -0
- package/dist/modules/drizzle-crud.module.js +165 -0
- package/dist/modules/drizzle-crud.module.js.map +1 -0
- package/dist/openapi/api-crud.decorators.d.ts +10 -0
- package/dist/openapi/api-crud.decorators.js +216 -0
- package/dist/openapi/api-crud.decorators.js.map +1 -0
- package/dist/test-utils/base-crud.spec-helper.d.ts +64 -0
- package/dist/test-utils/base-crud.spec-helper.js +119 -0
- package/dist/test-utils/base-crud.spec-helper.js.map +1 -0
- package/dist/test-utils/index.d.ts +2 -0
- package/dist/test-utils/index.js +8 -0
- package/dist/test-utils/index.js.map +1 -0
- package/dist/test-utils/test-factory.d.ts +29 -0
- package/dist/test-utils/test-factory.js +81 -0
- package/dist/test-utils/test-factory.js.map +1 -0
- package/package.json +110 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@avytrix/nestjs-drizzle-crud` (published as `nestjs-drizzle-crud` up to 3.5.0) are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
Scope note: the 3.0.x line is PostgreSQL-first. MySQL-specific paths (non-`RETURNING`
|
|
9
|
+
restore, `ER_DUP_ENTRY` mapping, dialect locking) are tracked separately and unchanged here.
|
|
10
|
+
|
|
11
|
+
## [3.6.0] - 2026-11-02
|
|
12
|
+
|
|
13
|
+
Fixes from the second audit. The first one is a security fix: upgrade if you use `scope`.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- Related tables now follow the tenant scope. Loaded relations, the joins behind relation columns,
|
|
18
|
+
relation filters (`some`, `none`, `every` and nested columns) and `counts` only see related rows
|
|
19
|
+
of the caller's tenant when the related table has the scope columns. Before, a row pointing at
|
|
20
|
+
another tenant's row (a plain foreign key allows it) loaded that row, and a filter on it
|
|
21
|
+
answered questions about it.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `createCrudController` takes `validation`, the validator options for the bulk bodies. Pass the
|
|
26
|
+
same options as your global `ValidationPipe`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `PATCH /bulk` validates `data` against `dto.update`, the check a single `PATCH` already had.
|
|
31
|
+
- Route decorators that wrap the handler now run. Before, the factory dropped the wrapped handler.
|
|
32
|
+
- Bulk ids are parsed like `:id`, so a bad id is a 400 before any statement runs.
|
|
33
|
+
- A filter nested deeper than 32 objects is a 400. A thousand nested `not` groups used to
|
|
34
|
+
overflow the stack and answer with a 500.
|
|
35
|
+
- A batch past the bound-parameter limit (60,000 values: rows times columns for `massCreate` and
|
|
36
|
+
`massUpsert`, ids for the id based methods, values in an `IN` filter) is a 400 that names the
|
|
37
|
+
limit, instead of a driver protocol error and a 500.
|
|
38
|
+
- A missing required value (`23502`) is a 400 that names the column.
|
|
39
|
+
- MySQL `ER_DUP_ENTRY` is a 409, and foreign key errors are a 400 on a write and a 409 on a delete.
|
|
40
|
+
- `upsert` no longer updates a row in the trash: the conflict writes nothing and is a 404.
|
|
41
|
+
- With `useReturning: false`, a scoped `upsert` no longer reads back another tenant's row.
|
|
42
|
+
- `restore` on a live row is a 404 before any hook or event runs. It used to succeed and bump
|
|
43
|
+
`updated_at` and the version.
|
|
44
|
+
- `?relation[some]=` and `[none]=` with no column need the relation, or one of its columns, in
|
|
45
|
+
`filterableColumns`.
|
|
46
|
+
- A nested child row cannot set its own generated primary key.
|
|
47
|
+
|
|
48
|
+
## [3.5.2] - 2026-11-02
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- `softDelete`, `restore`, `massSoftDelete` and `massRestore` on an entity without soft delete,
|
|
53
|
+
and full-text search on MySQL (`fullTextSearch()` or `search.mode: "fullText"`), now throw a
|
|
54
|
+
`BadRequestException`. They were a plain `Error`, which reached the client as a 500.
|
|
55
|
+
- `options.counts` now applies the tenant scope to the related rows it counts, when the related
|
|
56
|
+
table has the scope column. Before, a parent's count included rows of other tenants.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- `sortOrder` now defaults to `"asc"` in `findAll` and `findByCursor`, so a bare
|
|
61
|
+
`sortBy: "name"` sorts the same way as `?sortBy=name` over HTTP. Pass `sortOrder: "desc"` to
|
|
62
|
+
keep the old order.
|
|
63
|
+
|
|
64
|
+
## [3.5.1] - 2026-11-02
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
- The package is now published as `@avytrix/nestjs-drizzle-crud`. The code is the same as 3.5.0.
|
|
69
|
+
Update the dependency and the imports:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pnpm remove nestjs-drizzle-crud && pnpm add @avytrix/nestjs-drizzle-crud
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { CrudService } from "@avytrix/nestjs-drizzle-crud";
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The MCP server runs with `npx -p @avytrix/nestjs-drizzle-crud nestjs-drizzle-crud-mcp`.
|
|
80
|
+
The unscoped `nestjs-drizzle-crud` package is deprecated and gets no new releases.
|
|
81
|
+
|
|
82
|
+
## [3.5.0] - 2026-11-02
|
|
83
|
+
|
|
84
|
+
The P2 features from the November 2026 audit (M10 to M18). Each one is covered by specs that run real
|
|
85
|
+
SQL against PGlite (`test/pglite.integration.spec.ts`), and the controller factory, scoping,
|
|
86
|
+
locking, OpenAPI document, counts and quantified filters also run over real HTTP in the example
|
|
87
|
+
app.
|
|
88
|
+
|
|
89
|
+
### Added
|
|
90
|
+
|
|
91
|
+
- **Relations on writes.** `create`, `update`, `upsert`, `restore` and every mass write take
|
|
92
|
+
`options.relations` and `options.counts` and return the rows with them loaded, through the same
|
|
93
|
+
batched loaders as a read: one query per relation for the whole batch. Ten rows from `massCreate`
|
|
94
|
+
with `relations: ['author']` cost one `INSERT` and one `SELECT`.
|
|
95
|
+
- **Nested create.** With a relation listed in the new `writableRelations` allowlist, a create body
|
|
96
|
+
can carry it: an array for a has-many (one multi-row `INSERT`), an object for a belongs-to
|
|
97
|
+
(inserted first, its key set on the row), and ids or objects for a many-to-many (new targets in
|
|
98
|
+
one `INSERT`, links in one more). Everything runs in one transaction and needs `RETURNING`.
|
|
99
|
+
- **`attach`, `detach` and `sync`** for many-to-many relations: `attach(id, 'tags', [1, 2])`. Set
|
|
100
|
+
based (a fixed number of statements whatever the number of ids), already-linked ids are skipped,
|
|
101
|
+
and an unknown or soft-deleted target is a 400 with a 404 entry per id. Each returns the row with
|
|
102
|
+
the relation loaded.
|
|
103
|
+
- **`createCrudController(Service, options)`**, a controller factory. It registers list (offset,
|
|
104
|
+
or keyset with `?cursor=`), trash, get, create, update, delete, soft delete, restore and four bulk
|
|
105
|
+
routes, all through `parseQuery`. The `:id` param is parsed from the primary key column
|
|
106
|
+
(ParseIntPipe for an integer, ParseUUIDPipe for a uuid) unless `idType` says otherwise. Routes can
|
|
107
|
+
be turned on or off one by one and take their own decorators, so guards and roles fit in. Bulk
|
|
108
|
+
routes are off by default. `service.parseId(raw)` and `service.idKind()` are public.
|
|
109
|
+
- **OpenAPI for list queries.** `@ApiCrudQuery(source)` documents `page`, `limit`, `sortBy`,
|
|
110
|
+
`sortOrder`, `cursor`, `count`, `withCount`, `search`, `fields`, `with`, `counts`, the trash flags
|
|
111
|
+
and one parameter per filterable column with its type. `@ApiCrudList(source)` adds the page
|
|
112
|
+
envelope with a row schema built from the table. `@nestjs/swagger` is an optional peer, loaded
|
|
113
|
+
only when one of these decorators runs.
|
|
114
|
+
- **Tenant and ownership scoping.** `scope: (ctx) => ({ tenant_id: ctx.tenantId })` (or a fixed
|
|
115
|
+
object) is ANDed into every read, count, cursor page, `countBy`, `fullTextSearch`, relation count,
|
|
116
|
+
write, bulk write, upsert, soft delete, restore, force delete and attach/detach/sync, and written
|
|
117
|
+
onto every row a create or upsert inserts (and onto nested child rows that have the column).
|
|
118
|
+
Another tenant's id is a 404 everywhere. The context comes from `options.context` or from
|
|
119
|
+
`CrudContext.run(...)` / `CrudContext.middleware(...)`, which keep it in `AsyncLocalStorage` for
|
|
120
|
+
the request. A missing context value fails closed with `MissingScopeException` (403); a resolver
|
|
121
|
+
that returns `null`, or `skipScope: true`, runs unscoped on purpose.
|
|
122
|
+
- **Module-level events.** `hooks.enableGlobalHooks: true` now does what it says: every service
|
|
123
|
+
raises a before and an after event on the injectable `CrudEventBus` for each write, bulk and upsert
|
|
124
|
+
included. Subscribe with `hooks.subscribers` in `forRoot` or by injecting the bus. A bulk call
|
|
125
|
+
raises one event with `bulk: true` and every id, so audit trails and cache busting stay set based.
|
|
126
|
+
A before listener that throws stops the write; an after listener that throws inside a transaction
|
|
127
|
+
rolls it back. `skipEvents: true` opts a call out.
|
|
128
|
+
- **Optimistic locking.** `versionColumn: 'version'` starts rows at 1 and adds one on every update,
|
|
129
|
+
soft delete, restore, upsert conflict and bulk write. An update, soft delete, restore or delete
|
|
130
|
+
that names a stale version (`options.expectedVersion`, or the version key in an update body) is a
|
|
131
|
+
409 `StaleVersionException`. Mass writes take `expectedVersions: { [id]: version }` and report a
|
|
132
|
+
409 entry per stale id.
|
|
133
|
+
- **Relation counts and grouped counts.** `options.counts: ['posts']` (or `{ posts: { status:
|
|
134
|
+
'published' } }`) puts `_count: { posts: 3 }` on every row of `find`, `findOne`, `findAll`,
|
|
135
|
+
`findByCursor` and the writes above, with one `GROUP BY` query per relation for the whole result.
|
|
136
|
+
Soft-deleted related rows are not counted. `countBy(column, filters)` returns `[{ value, count }]`,
|
|
137
|
+
largest group first, for a column of the table or of a top-level belongs-to.
|
|
138
|
+
- **`withCount: false`** on `findAll` (and `?withCount=false` in the parser) skips the `COUNT(*)`.
|
|
139
|
+
The envelope then has `total: null` and a `hasMore` flag, from fetching one row past the page.
|
|
140
|
+
- **Deeper relation filters.** A relation filter can go on through the related table's own
|
|
141
|
+
relations at any depth: `{ state: { country: { code: 'IN' } } }`, or `?state[country][code]=IN`.
|
|
142
|
+
Below the first joined belongs-to, each level is an `EXISTS` subquery. Collections take `some`,
|
|
143
|
+
`none` and `every` (`{ posts: { none: { status: 'draft' } } }`), compiled to `IN` / `NOT EXISTS`
|
|
144
|
+
subqueries that never multiply rows, inside `or` / `and` / `not` groups and in `count`.
|
|
145
|
+
- **New exports:** `createCrudController`, `CrudContext`, `CrudEventBus`, `ApiCrudQuery`,
|
|
146
|
+
`ApiCrudList`, `MissingScopeException`, `StaleVersionException`, and the types
|
|
147
|
+
`CrudControllerOptions`, `CrudControllerInstance`, `CrudRouteName`, `CrudRouteOptions`,
|
|
148
|
+
`CrudEvent`, `CrudEventPhase`, `CrudSubscriber`, `CrudWriteOperation`, `CrudScope`,
|
|
149
|
+
`CrudScopeValues`, `CrudContextValue`, `CollectionFilter`, `CollectionQuantifier`, `CountByRow`,
|
|
150
|
+
`CrudPage`, `UncountedCrudPage`, `RelationCounts`, `ApiCrudSource` and `ApiCrudQueryOptions`.
|
|
151
|
+
New config keys: `scope`, `versionColumn`, `events`, `writableRelations`. New options: `counts`,
|
|
152
|
+
`context`, `skipScope`, `skipEvents`, `expectedVersion`, `expectedVersions`, and
|
|
153
|
+
`PaginationOptions.withCount`.
|
|
154
|
+
|
|
155
|
+
### Changed
|
|
156
|
+
|
|
157
|
+
These change behaviour or types that existing code may rely on. Check each one before upgrading.
|
|
158
|
+
|
|
159
|
+
- **`findAll` is overloaded on `withCount`.** With a literal `withCount: false` it returns
|
|
160
|
+
`UncountedCrudPage<T>` (`total: null`), without one `CrudPage<T>` (`total: number`), and with a
|
|
161
|
+
`withCount` typed as `boolean` (which `ParsedCrudQuery.pagination` is) `total` is
|
|
162
|
+
`number | null`. Code that passes `q.pagination` from `parseQuery` straight to `findAll` and then
|
|
163
|
+
does arithmetic on `total` needs a `total !== null` check. The runtime result is unchanged unless
|
|
164
|
+
`withCount: false` is passed.
|
|
165
|
+
- **`withCount` and `counts` are reserved query parameters.** The parser no longer treats them as
|
|
166
|
+
filters. A column with one of those names can still be filtered from code.
|
|
167
|
+
- **Writes read `options.relations` and `options.counts`.** They used to be ignored on `create`,
|
|
168
|
+
`update`, `upsert` and the mass writes. They are now checked against the allowlists (with
|
|
169
|
+
`select` and `search`) before anything is written, and loaded onto the result.
|
|
170
|
+
- **A relation filter can name a relation of the related table**, and a collection filter whose
|
|
171
|
+
keys are all `some`, `none` or `every` is a quantifier. Both used to be a 400 ("Unknown filter
|
|
172
|
+
column"). A related table that really has a column called `some`, `none` or `every` keeps the old
|
|
173
|
+
meaning.
|
|
174
|
+
- **`DRIZZLE_CRUD_CONFIG.hooks`** carries only `enableGlobalHooks` (a boolean); `subscribers` stay
|
|
175
|
+
on the raw config.
|
|
176
|
+
|
|
177
|
+
### Fixed
|
|
178
|
+
|
|
179
|
+
- **`hooks.enableGlobalHooks` did nothing** (BUG-21). It now wires the module's `CrudEventBus` into
|
|
180
|
+
every service.
|
|
181
|
+
|
|
182
|
+
### Deprecated
|
|
183
|
+
|
|
184
|
+
- Nothing new. `hooks.enableGlobalHooks`, deprecated as a no-op in 3.3.1, is no longer deprecated.
|
|
185
|
+
|
|
186
|
+
## [3.4.0] - 2026-11-02
|
|
187
|
+
|
|
188
|
+
The P1 features from the November 2026 audit. Every one is covered by specs that run real SQL
|
|
189
|
+
against PGlite (`test/pglite.integration.spec.ts`) and by e2e specs over real HTTP in the example
|
|
190
|
+
app.
|
|
191
|
+
|
|
192
|
+
### Added
|
|
193
|
+
|
|
194
|
+
- **A query string parser.** `service.parseQuery(query)` and the standalone `parseCrudQuery(query,
|
|
195
|
+
table, options)` turn
|
|
196
|
+
`?age[gte]=18&status=active®ion[name]=Asia&or[0][code]=IN&sortBy=-name&page=2&limit=20&fields=id,name&with=region&search=jo`
|
|
197
|
+
into `{ filters, pagination, options, isCursor }`. Values are coerced by Drizzle column type:
|
|
198
|
+
integers and other numbers to `number`, `bigint` to `bigint`, booleans, dates to `Date` (an
|
|
199
|
+
invalid one is a 400), `numeric` checked and kept as an exact string, and text, uuid and enum left
|
|
200
|
+
as strings. An unknown column, relation, operator, sort column, `fields` entry or `with` path is a
|
|
201
|
+
400. It reads both Express 5's flat `age[gte]` keys and `qs` nested objects.
|
|
202
|
+
- **Per-entity allowlists.** `filterableColumns`, `sortableColumns`, `selectableColumns`,
|
|
203
|
+
`searchableColumns` and `loadableRelations` limit what a caller may filter, sort, select, search
|
|
204
|
+
and eager-load; `writableColumns` limits what create, update and upsert may write. Anything
|
|
205
|
+
outside a set list is a 400, in the service and in the parser. Unset lists allow everything.
|
|
206
|
+
`skipAllowlists: true` skips them for a trusted call, and `findOne` where keys are not checked.
|
|
207
|
+
- **Trash view and purge.** `withDeleted: true` and `onlyDeleted: true` work on every read (`find`,
|
|
208
|
+
`findOne`, `findAll`, `findByCursor`, `count`, `exists`, `fullTextSearch`). `forceDelete(id)` and
|
|
209
|
+
`massForceDelete(ids)` hard-delete rows whether they are soft-deleted or not.
|
|
210
|
+
- **Upsert.** `upsert(dto, { conflictTarget, update })` and `massUpsert(rows, ...)` insert or update
|
|
211
|
+
in one statement: `ON CONFLICT DO UPDATE` on Postgres, `ON DUPLICATE KEY UPDATE` on MySQL. A clash
|
|
212
|
+
on another unique key is a 409, bad input a 400, and the same key twice in one `massUpsert`
|
|
213
|
+
(`21000`) a 400.
|
|
214
|
+
- **`and`, `or` and `not` filter groups.** `{ or: [{ status: 'a' }, { author: { name: 'Bob' } }],
|
|
215
|
+
not: { tags: { name: 'x' } } }`. Groups nest, hold column, relation and collection filters, work
|
|
216
|
+
inside a relation filter, and apply to `count`.
|
|
217
|
+
- **New filter operators.** `notIn`, `between`, `startsWith`, `endsWith` and `contains` (escaped,
|
|
218
|
+
case-insensitive unless `sql.caseSensitive` is on), `iStartsWith`, `iEndsWith` and `iContains`
|
|
219
|
+
(always case-insensitive), and on PostgreSQL `arrayContains`, `arrayOverlaps` and `jsonContains`.
|
|
220
|
+
A pattern operator on a non-text column, an array operator on a non-array column, or a PostgreSQL
|
|
221
|
+
only operator on MySQL is a 400.
|
|
222
|
+
- **Keyset pagination.** `findByCursor(filters, { cursor, limit, sortBy, count }, options)` returns
|
|
223
|
+
`{ data, limit, nextCursor, total? }`. The cursor is base64url JSON of the last row's sort values
|
|
224
|
+
plus the primary key, so pages stay stable under inserts and deep pages cost the same as the
|
|
225
|
+
first. It works with every sort, filter, relation and `select`, keeps timestamp microseconds and
|
|
226
|
+
follows the database's NULL ordering. The count only runs with `count: true`. A cursor that is not
|
|
227
|
+
valid, or was made for another sort, is a 400.
|
|
228
|
+
- **Multi-column and relation sorts from the caller.** `sortBy` takes a list: `['region.name',
|
|
229
|
+
'-name']` or `[{ column, order }]`, and a comma separated string means the same. A dotted entry
|
|
230
|
+
sorts by a top-level belongs-to column, joined for the data query only. A collection column is a
|
|
231
|
+
400.
|
|
232
|
+
- **`bulkMode: 'perItem'`** keeps the 3.3 behaviour of one single-row call per item, in its own
|
|
233
|
+
savepoint.
|
|
234
|
+
- **New exports:** `parseCrudQuery`, and the types `CrudFilter`, `FilterGroup`, `FilterOperators`,
|
|
235
|
+
`FilterOperator`, `FilterScalar`, `ColumnFilter`, `SortInput`, `CursorPaginationOptions`,
|
|
236
|
+
`CursorPage`, `CrudAllowlists`, `UpsertOptions`, `BulkMode`, `ParseCrudQueryOptions` and
|
|
237
|
+
`ParsedCrudQuery`. The Drizzle helpers `between`, `not` and `notInArray` are re-exported.
|
|
238
|
+
|
|
239
|
+
### Changed
|
|
240
|
+
|
|
241
|
+
These change behaviour that existing code may rely on. Check each one before upgrading.
|
|
242
|
+
|
|
243
|
+
- **Mass writes are set-based.** `massUpdate`, `massSoftDelete`, `massRestore` and `massDelete` cost
|
|
244
|
+
one `SELECT` that checks every id plus one `UPDATE` or `DELETE ... WHERE id IN (...)`, and
|
|
245
|
+
`massCreate` one multi-row `INSERT`, instead of one or two queries per id. The
|
|
246
|
+
`BulkOperationException` body is unchanged: when the statement fails, the batch is replayed item
|
|
247
|
+
by item inside a savepoint that is rolled back, only to name the failing items. Hooks still run
|
|
248
|
+
once per item, but the `after*` hooks now run once the whole set is written instead of after each
|
|
249
|
+
item. Duplicate ids are written once. Pass `bulkMode: 'perItem'` for the old behaviour.
|
|
250
|
+
- **Create and update ignore the columns the package manages.** Without `writableColumns`, the soft
|
|
251
|
+
delete column is dropped from a create or update body, and the primary key and the `createdAt`
|
|
252
|
+
column from an update body. An update whose only key was one of these is now a 400 ("there are no
|
|
253
|
+
columns to update"). To write them on purpose, list them in `writableColumns`.
|
|
254
|
+
- **`PaginationOptions.sortBy` is `SortInput`** (`string | Array<string | SortColumn>`). Code that
|
|
255
|
+
passes a string is unaffected; code that reads `sortBy` back as a `string` needs a narrowing
|
|
256
|
+
check.
|
|
257
|
+
- **`between` is an operator now.** `{ col: { between: [a, b] } }` used to be a 400 ("Unknown filter
|
|
258
|
+
operator").
|
|
259
|
+
- `buildOrderBy` takes a `SortInput`, and `buildFilterConditions` and `buildCollectionFilter` take
|
|
260
|
+
an optional `options` argument, for subclasses that call them.
|
|
261
|
+
|
|
262
|
+
### Fixed
|
|
263
|
+
|
|
264
|
+
- **Mass operations cost a fixed number of queries** (BUG-16). `massUpdate` over 10 ids was 20
|
|
265
|
+
queries; it is now 2.
|
|
266
|
+
- **The soft delete guide's trash and purge recipes work** (BUG-17). They used raw Drizzle because
|
|
267
|
+
`findAll({ deleted_at: { isNotNull: true } })` always returned nothing; the guide now uses
|
|
268
|
+
`onlyDeleted` and `forceDelete`.
|
|
269
|
+
|
|
270
|
+
## [3.3.1] - 2026-11-02
|
|
271
|
+
|
|
272
|
+
Fixes from the November 2026 audit. Every fix below is proven by a spec that runs real SQL against
|
|
273
|
+
PGlite (`test/pglite.integration.spec.ts`) and failed before the change.
|
|
274
|
+
|
|
275
|
+
### Fixed
|
|
276
|
+
- **String filters on integer, uuid and enum columns no longer return a 500.** Query strings send
|
|
277
|
+
every value as text, and with the default `caseSensitive: false` a filter such as
|
|
278
|
+
`{ author_id: '1' }` became `lower(author_id)`, which Postgres rejects. `lower()` now only wraps
|
|
279
|
+
text, varchar and char columns; every other column compares with `=` and Postgres casts the value.
|
|
280
|
+
Relation and collection filters get the same fix.
|
|
281
|
+
- **Date strings work on date and timestamp columns.** `{ publishedAt: { gt: '2024-01-01' } }` used
|
|
282
|
+
to crash inside the driver (`value.toISOString is not a function`). The string is now turned into
|
|
283
|
+
a `Date`; a value that is not a date is a 400.
|
|
284
|
+
- **A soft-deleted belongs-to parent is no longer returned.** The `LEFT JOIN` for an eager load or a
|
|
285
|
+
relation filter now carries `deleted_at IS NULL` in its `ON` clause, so the parent comes back as
|
|
286
|
+
`null` and `{ author: { name: 'Bob' } }` does not match a deleted author.
|
|
287
|
+
- **`findOne` can no longer hand back an arbitrary row.** `findOne({ email: undefined })` used to run
|
|
288
|
+
`LIMIT 1` with no condition and return the first row in the table. It is now a 400, as is a key
|
|
289
|
+
that is not a column. `null` now means `IS NULL` instead of being skipped. An operator object works
|
|
290
|
+
as it does in `findAll`.
|
|
291
|
+
- **A soft-delete or timestamp column the table does not have is caught at startup.** With
|
|
292
|
+
camelCase keys (`deletedAt: timestamp('deleted_at')`) the default config used to break every read
|
|
293
|
+
with a SQL syntax error, and `updatedAt` silently never moved. The service now throws when it is
|
|
294
|
+
built, naming the key to use: `softDelete.column is "deleted_at", but the table has no column
|
|
295
|
+
under that key. The column "deleted_at" is declared under the key "deletedAt"; set
|
|
296
|
+
softDelete.column to "deletedAt".`
|
|
297
|
+
- **One bad item in a bulk call no longer fails every item after it.** Each item now runs in its own
|
|
298
|
+
savepoint, so `massCreate([ok, duplicate, ok, ok])` reports the duplicate alone instead of two more
|
|
299
|
+
`25P02` "current transaction is aborted" errors. The whole call still rolls back. The items still
|
|
300
|
+
run one query (or two) at a time; set-based bulk writes are planned separately.
|
|
301
|
+
- **Bulk errors reach the HTTP response.** `BulkOperationException` now answers with
|
|
302
|
+
`{ statusCode, error, message, errors: [{ index | id, status, message }] }`, one entry per failed
|
|
303
|
+
item with the status that item alone would have produced. An error that is not an HTTP exception
|
|
304
|
+
shows as `status: 500` with a generic message, so SQL never leaks.
|
|
305
|
+
- **`{ isNull: false }` means `IS NOT NULL`.** The value used to be ignored. `isNull` and `isNotNull`
|
|
306
|
+
also accept the strings `"true"` and `"false"`; anything else is a 400.
|
|
307
|
+
- **Filters the package does not understand are rejected instead of dropped.** A new `eq` operator
|
|
308
|
+
is supported, a `Date` value is an equality match instead of an empty operator object, and an
|
|
309
|
+
unknown column, relation column or operator is a 400. Each of these used to return every row.
|
|
310
|
+
- **Bad input is a 400 on reads too.** Postgres `22P02` (wrong type, unknown enum label), `22007` and
|
|
311
|
+
`22008` (bad dates) now map to `ValidationFailedException` on every path, including `find`,
|
|
312
|
+
`findAll`, `count` and `fullTextSearch`. `fullTextSearch` checks its column names (an unknown one
|
|
313
|
+
used to be a SQL syntax error), and `update` with nothing to set is a 400 instead of a raw
|
|
314
|
+
"No values to set".
|
|
315
|
+
- **`fullTextSearch` always paginates.** Without a pagination argument it returned every match while
|
|
316
|
+
the envelope said `limit: 20`. The resolved page and limit now always apply, and the data and count
|
|
317
|
+
queries run together.
|
|
318
|
+
- **A partial pagination override keeps the other half.** `pagination: { defaultLimit: 5 }` used to
|
|
319
|
+
replace the whole object, leaving `maxLimit` undefined and the query with no limit at all.
|
|
320
|
+
`softDelete`, `timestamps`, `pagination` and `sql` now merge one level deep at every config layer:
|
|
321
|
+
`forRoot` defaults, `@CrudService`, `forFeature` and the service constructor.
|
|
322
|
+
- **Search and the `ilike` operator emit `LIKE` on MySQL**, which has no `ILIKE`. MySQL's default
|
|
323
|
+
collations already compare case-insensitively.
|
|
324
|
+
- **Pages are stable.** The primary key is now always the last `ORDER BY` column, so rows that tie on
|
|
325
|
+
the sort columns, or a list with no sort at all, can no longer repeat or skip between pages.
|
|
326
|
+
- **A row lock works with an eager belongs-to.** Locks are now `FOR UPDATE OF <base table>` (and the
|
|
327
|
+
same for `share` and `no key update`). Postgres used to refuse with `FOR UPDATE cannot be applied
|
|
328
|
+
to the nullable side of an outer join`.
|
|
329
|
+
- **Nested collections key on their own table.** A nested has-many or many-to-many used the root
|
|
330
|
+
service's `primaryKey` as its default `parentKey`, so a root keyed on `uid` loaded
|
|
331
|
+
`author.posts` as `[]`. A nested level now defaults to its host table's own primary key column.
|
|
332
|
+
- **Types in public signatures are exported.** `PaginationOptions`, `OperationOptions`,
|
|
333
|
+
`SqlSearchOptions`, `SqlSearchMode` and `BulkOperationResult` come from the package root, along
|
|
334
|
+
with the new `CrudFeatureConfig` and `BulkOperationItemError`.
|
|
335
|
+
- `CrudFeature.config` is typed as `CrudFeatureConfig` instead of `Record<string, any>`, so a typo in
|
|
336
|
+
a key no longer compiles.
|
|
337
|
+
|
|
338
|
+
### Changed
|
|
339
|
+
These fixes change behaviour that existing code may rely on. Check each one before upgrading.
|
|
340
|
+
- **An unknown filter key is a 400.** `findAll` and `count` used to drop keys that were not columns,
|
|
341
|
+
so passing a whole query DTO through worked even with `page` or `sortBy` in it. Strip non-filter
|
|
342
|
+
fields first. The same goes for an unknown column inside a relation or collection filter, and for
|
|
343
|
+
an unknown operator. `null` and `undefined` values are still skipped.
|
|
344
|
+
- **`findOne` with no usable condition, or an unknown key, is a 400.** It used to return the first
|
|
345
|
+
row in the table.
|
|
346
|
+
- **`findOne({ col: null })` matches `IS NULL`.** It used to skip the key.
|
|
347
|
+
- **A configured soft-delete or timestamp key the table lacks stops the app at startup.** This
|
|
348
|
+
includes the project defaults: with `defaults.timestamps` on (the default), every table needs
|
|
349
|
+
`created_at` and `updated_at` keys, or the entity must set its own keys or turn timestamps off
|
|
350
|
+
with `timestamps: undefined`. Before, a missing timestamp key was silently ignored.
|
|
351
|
+
- **Every `findAll` and `fullTextSearch` ends with `ORDER BY <primary key> ASC`.** A list with no
|
|
352
|
+
sort configured used to come back in whatever order Postgres chose; it now comes back by primary
|
|
353
|
+
key.
|
|
354
|
+
- **`update(id, {})` is a 400**, also when timestamps are on. It used to bump `updated_at` alone.
|
|
355
|
+
- **`{ isNull: false }` returns the non-null rows** instead of the null ones.
|
|
356
|
+
- **`BulkOperationException` has a different response body.** `message` is unchanged; `errors` is
|
|
357
|
+
new. The `errors` property on the exception still holds the original errors.
|
|
358
|
+
- **Read errors from bad input are 400s.** `find('abc')` on an integer key used to be a 500.
|
|
359
|
+
- Row locks now carry `OF <table>`. A subclass that relied on locking joined rows must take that
|
|
360
|
+
lock itself.
|
|
361
|
+
|
|
362
|
+
### Deprecated
|
|
363
|
+
- `sql.jsonSupport`, `sql.enableFullTextSearch`, `hooks.enableGlobalHooks` and `primaryKeyType`.
|
|
364
|
+
Nothing reads them, so they have no effect. They are still accepted and will be removed in the
|
|
365
|
+
next major version. `primaryKeyType` is now optional. The docs no longer claim that `jsonSupport`
|
|
366
|
+
enables JSON handling or that `enableFullTextSearch` gates `fullTextSearch()`.
|
|
367
|
+
- `DatabaseConnectionException` and `TransactionException`. Nothing in the package throws them.
|
|
368
|
+
|
|
369
|
+
## [3.3.0] - 2026-08-10
|
|
370
|
+
|
|
371
|
+
### Added
|
|
372
|
+
- **Has-many, many-to-many and nested relations, all eager loaded without N+1.** Declare
|
|
373
|
+
`relations: { states: { table: states, type: 'hasMany', foreignKey: 'country_id' } }` and pass
|
|
374
|
+
`options.relations: ['states']` to `find`, `findOne` or `findAll`. Each has-many relation costs
|
|
375
|
+
exactly one extra query for the whole result set (`WHERE country_id IN (...)`), no matter how many
|
|
376
|
+
parent rows come back, and the relations run concurrently. A page of 50 countries with their
|
|
377
|
+
states is 3 queries: data, count, children. A parent with no children gets `[]`, never `undefined`.
|
|
378
|
+
- **Many-to-many through a join table.** Declare
|
|
379
|
+
`{ table: tags, type: 'manyToMany', through: countryTags, throughLocalKey: 'country_id', throughForeignKey: 'tag_id' }`
|
|
380
|
+
and the collection loads in one batched query per relation
|
|
381
|
+
(`SELECT ... FROM country_tags JOIN tags ... WHERE country_id IN (...)`). The join-table key is
|
|
382
|
+
used to group the rows and stripped before they are returned.
|
|
383
|
+
- **Nested relations, any depth, with a dotted path.** Put a `relations` map on a relation and
|
|
384
|
+
address it with `options.relations: ['states', 'states.cities']` or `['state.country']`. Each
|
|
385
|
+
nesting level costs one more batched query for the whole level, so a page of 50 countries with
|
|
386
|
+
their states and every state's cities is 4 queries. Asking only for `state.country` pulls in
|
|
387
|
+
`state` automatically.
|
|
388
|
+
- **`orderBy` on a collection relation.** `{ type: 'hasMany', foreignKey: 'country_id', orderBy: [{ column: 'name', order: 'asc' }] }`
|
|
389
|
+
orders the children in SQL. An unknown column is warned about and skipped, as with `defaultSort`.
|
|
390
|
+
- **Collections are never joined.** Filtering by a child column
|
|
391
|
+
(`findAll({ states: { name: 'Kerala' } })`, `findAll({ tags: { name: 'himalayan' } })`) compiles to
|
|
392
|
+
`countries.id IN (SELECT ... WHERE ...)`, so parent rows are never duplicated and `total` and
|
|
393
|
+
pagination stay correct. Belongs-to relations keep using a `LEFT JOIN`.
|
|
394
|
+
- **`options.select` now narrows relation columns with a dotted path, at any depth.**
|
|
395
|
+
`select: ['id', 'name', 'region.code', 'states.cities.name']` projects only those columns, for the
|
|
396
|
+
base table, a joined belongs-to relation and a batched collection alike. Listing only relation
|
|
397
|
+
columns leaves the parent row complete. The parent key, the child foreign key and any key a nested
|
|
398
|
+
level needs are added back automatically, because the grouping needs them.
|
|
399
|
+
- **An MCP server ships with the package.** `npx nestjs-drizzle-crud-mcp` serves the package docs
|
|
400
|
+
(`AGENT.md`, `README.md`, `CHANGELOG.md`) to any MCP client through three tools: `list_docs`,
|
|
401
|
+
`get_doc` and `search_docs`. It speaks JSON-RPC over stdio, still has zero dependencies, and works
|
|
402
|
+
with Claude Code, Codex, Cursor, Copilot, Gemini CLI, Windsurf, Zed and Cline.
|
|
403
|
+
- **`AGENTS.template.md`** for every agent that is not Claude Code. It carries the same pointer to
|
|
404
|
+
`AGENT.md` and lists the filename each tool reads. `CLAUDE.template.md` is unchanged.
|
|
405
|
+
- Exported the `RelationType` (`'belongsTo' | 'hasMany' | 'manyToMany'`), `RelationNode` and
|
|
406
|
+
`SelectTree` types.
|
|
407
|
+
|
|
408
|
+
### Changed
|
|
409
|
+
- A relation with a missing key column now throws at startup: belongs-to requires `localKey`,
|
|
410
|
+
has-many requires `foreignKey`, many-to-many requires `through`, `throughLocalKey` and
|
|
411
|
+
`throughForeignKey`. Nested relations are validated too, and the message carries the full path
|
|
412
|
+
(`Relation "states.cities" is belongsTo and requires a localKey`). It used to load nothing quietly,
|
|
413
|
+
which read as bad data rather than bad config.
|
|
414
|
+
- `RelationConfig.localKey` is optional, since has-many relations use `foreignKey` instead. Existing
|
|
415
|
+
belongs-to configs are unaffected.
|
|
416
|
+
|
|
417
|
+
## [3.2.2] - 2026-08-09
|
|
418
|
+
|
|
419
|
+
### Fixed
|
|
420
|
+
- **Restored the foreign-key mapping that 3.2.1 lost.** `ForeignKeyViolationException` and
|
|
421
|
+
`EntityInUseException` are exported again, and a Postgres `23503` is translated by direction:
|
|
422
|
+
**400** when `create`/`update` reference a parent row that does not exist (the offending column
|
|
423
|
+
is parsed out of the driver `detail`), **409** when a `delete` is blocked by rows that still
|
|
424
|
+
reference this one. Non-FK errors on the delete path still bubble unchanged. Anyone who upgraded
|
|
425
|
+
from 3.2.0 to 3.2.1 and started seeing 500s on bad references should move to this release.
|
|
426
|
+
- Covered by six specs in `test/bug-fixes.spec.ts`, including the wrapped `DrizzleQueryError`
|
|
427
|
+
shape and the missing-`detail` fallback.
|
|
428
|
+
|
|
429
|
+
## [3.2.1] - 2026-07-17
|
|
430
|
+
|
|
431
|
+
### Changed
|
|
432
|
+
- Packaging and documentation only: `homepage` now points at
|
|
433
|
+
[crud.viliha.com](https://crud.viliha.com), and the package and repository READMEs link the
|
|
434
|
+
docs site, npm and GitHub. No runtime code changed.
|
|
435
|
+
|
|
436
|
+
### Known issue
|
|
437
|
+
- **The foreign-key mapping added in 3.2.0 is missing from this build.** The move to the
|
|
438
|
+
Turborepo monorepo carried over a source tree from before 3.2.0, so `ForeignKeyViolationException`
|
|
439
|
+
and `EntityInUseException` are not exported and a `23503` bubbles as a 500 again. **Fixed in
|
|
440
|
+
3.2.2**, so upgrade rather than pinning here. The 3.1.1 and 3.2.0 entries below were lost in the
|
|
441
|
+
same move and are restored here from the published tarballs.
|
|
442
|
+
|
|
443
|
+
## [3.2.0] - 2026-07-09
|
|
444
|
+
|
|
445
|
+
### Added
|
|
446
|
+
- **Foreign-key violations (Postgres `23503`) now map to a typed HTTP error instead of a raw 500**,
|
|
447
|
+
and the direction decides the status:
|
|
448
|
+
- `create`/`update` referencing a parent row that does not exist → **400**
|
|
449
|
+
`ForeignKeyViolationException` (a client-supplied reference that points at nothing is bad input).
|
|
450
|
+
The offending column is parsed out of the driver `detail` (e.g. `language_id`).
|
|
451
|
+
- `delete` blocked because other rows still reference this one (`ON DELETE RESTRICT`/`NO ACTION`)
|
|
452
|
+
→ **409** `EntityInUseException`.
|
|
453
|
+
- Both exceptions are exported from the package root.
|
|
454
|
+
|
|
455
|
+
### Changed
|
|
456
|
+
- `delete` now wraps its statement execution so a `23503` on the delete side is translated (was
|
|
457
|
+
previously left to bubble as a 500). Non-FK errors still bubble unchanged.
|
|
458
|
+
- Reverses the prior deliberate choice to let FK violations bubble: a client-supplied FK is a
|
|
459
|
+
recoverable bad value, not a programming error. NOT-NULL violations are still left to bubble.
|
|
460
|
+
|
|
461
|
+
## [3.1.1] - 2026-07-03
|
|
462
|
+
|
|
463
|
+
### Changed
|
|
464
|
+
- Package metadata only, no code changes: repository and issues URLs now point to
|
|
465
|
+
[myviliha/viliha-drizzle-crud](https://github.com/myviliha/viliha-drizzle-crud), and the
|
|
466
|
+
homepage points to the docs site at [crud.viliha.com](https://crud.viliha.com/).
|
|
467
|
+
|
|
468
|
+
## [3.1.0] - 2026-06-29
|
|
469
|
+
|
|
470
|
+
### Added
|
|
471
|
+
- **`findAll`/`count` now support `options.search`.** Callers can pass a search term and a list of
|
|
472
|
+
table columns, e.g. `{ search: { term: "na", columns: ["name", "code"] } }`. The default mode is
|
|
473
|
+
cross-column `ILIKE '%term%'`; `mode: "fullText"` compiles a PostgreSQL
|
|
474
|
+
`to_tsvector(...) @@ plainto_tsquery(...)` condition. The search predicate is applied to both the
|
|
475
|
+
data query and the count query, so paginated `total` stays correct.
|
|
476
|
+
|
|
477
|
+
## [3.0.4] - 2026-06-21
|
|
478
|
+
|
|
479
|
+
Found while probing the package over real HTTP from the `drizzle-pkg-demo` consumer
|
|
480
|
+
(`scripts/probe.sh`). PostgreSQL-first. Verified by 56 jest specs + 56 HTTP e2e assertions.
|
|
481
|
+
|
|
482
|
+
### Fixed
|
|
483
|
+
- **Bad client input on `create`/`update` now maps to 400, not a raw 500.** A value that is too
|
|
484
|
+
long for its column (Postgres `22001`), fails a `CHECK` constraint (`23514`), or is numerically
|
|
485
|
+
out of range (`22003`) previously surfaced as an unhandled `DrizzleQueryError` → 500 Internal
|
|
486
|
+
Server Error. These are now translated to `ValidationFailedException` (400), mirroring the
|
|
487
|
+
existing unique-violation → 409 mapping. NOT-NULL/FK violations are intentionally left to bubble
|
|
488
|
+
(they usually indicate a programming/config error, not a recoverable bad value).
|
|
489
|
+
|
|
490
|
+
## [3.0.3] - 2026-06-20
|
|
491
|
+
|
|
492
|
+
Correctness fix found while validating the package from a fresh NestJS consumer
|
|
493
|
+
(see `drizzle-pkg-demo`). PostgreSQL-first. Verified by 53 jest specs + 47 HTTP e2e assertions.
|
|
494
|
+
|
|
495
|
+
### Fixed
|
|
496
|
+
- **Case-insensitive string filters are now EXACT, not wildcard patterns.** A plain string
|
|
497
|
+
value in a `findAll`/`count` filter (e.g. `{ name: "foo" }`) is documented as a
|
|
498
|
+
case-insensitive *exact* match, but compiled to `ilike(column, value)`. So a value containing
|
|
499
|
+
`%`, `_` or `\` was silently treated as a `LIKE` pattern and returned non-matching rows
|
|
500
|
+
(e.g. `{ title: "a_b" }` matched `"aXb"`; `{ title: "a%" }` matched everything). It now compiles
|
|
501
|
+
to `lower(column) = lower(value)`, so those characters are treated as literal data. The explicit
|
|
502
|
+
`{ like }` / `{ ilike }` operators are unchanged for intentional pattern matching.
|
|
503
|
+
|
|
504
|
+
## [3.0.2] - 2026-06-19
|
|
505
|
+
|
|
506
|
+
Residual-edge hardening found by an end-to-end HTTP probe of 3.0.1 (all LOW severity;
|
|
507
|
+
no behavior change to the happy path). Verified by 35 HTTP assertions on Neon + 49 jest specs.
|
|
508
|
+
|
|
509
|
+
### Changed
|
|
510
|
+
- **`fullTextSearch` now returns the same envelope as `findAll`**, `{ data, total, page, limit }`
|
|
511
|
+
instead of `{ data, total }`. `page`/`limit` are the resolved (clamped) values; the query is
|
|
512
|
+
still only constrained when the caller passes `pagination`. Callers paginating search results
|
|
513
|
+
can now read back the page/limit they got.
|
|
514
|
+
|
|
515
|
+
### Fixed
|
|
516
|
+
- **Invalid `sortOrder` now fails fast (400).** `?sortOrder=<anything but asc/desc>` previously
|
|
517
|
+
fell through to ascending silently; it now throws `BadRequestException`, mirroring the
|
|
518
|
+
unknown-`sortBy` check added in 3.0.1.
|
|
519
|
+
- **Non-finite numeric filter operands return 400, not 500.** A `{ gt: NaN }` / `{ lte: Infinity }`
|
|
520
|
+
operand on `gt`/`gte`/`lt`/`lte` (e.g. from an unguarded `Number('abc')` in a controller) reached
|
|
521
|
+
SQL and surfaced as an opaque `DatabaseException` (500). It is now rejected as
|
|
522
|
+
`BadRequestException`. Non-number operands (dates, strings) are unaffected.
|
|
523
|
+
|
|
524
|
+
## [3.0.1] - 2026-06-19
|
|
525
|
+
|
|
526
|
+
Correctness pass: typed exceptions now map to real HTTP status codes, plus several read-path fixes.
|
|
527
|
+
(PostgreSQL-first.)
|
|
528
|
+
|
|
529
|
+
### Fixed
|
|
530
|
+
- **Exceptions extend `HttpException`** so Nest maps them automatically:
|
|
531
|
+
`EntityNotFoundException` → 404, `DuplicateEntityException` → 409,
|
|
532
|
+
`ValidationFailedException` / `BulkOperationException` → 400,
|
|
533
|
+
`Database*` / `TransactionException` → 500. Previously every error surfaced as a generic 500.
|
|
534
|
+
- **Duplicate-key translation.** A Postgres unique violation (SQLSTATE `23505`, on either
|
|
535
|
+
`err.code` or the Drizzle-wrapped `err.cause.code`) is converted to `DuplicateEntityException`
|
|
536
|
+
(409), parsing the offending field/value from the driver `detail`.
|
|
537
|
+
- **`fullTextSearch` excludes soft-deleted rows** (added the `IS NULL` guard on the soft-delete
|
|
538
|
+
column to both the data and count queries, matching `findAll`/`count`).
|
|
539
|
+
- **Pagination clamping.** `page < 1`, `limit <= 0`, and `limit > maxLimit` are clamped via a shared
|
|
540
|
+
`resolvePagination` helper. No more negative `OFFSET` or `LIMIT 0`.
|
|
541
|
+
- **Unknown sort column fails fast.** A caller-supplied `sortBy` not on the table throws
|
|
542
|
+
`BadRequestException`; an unknown column in a configured `defaultSort` is warned and skipped.
|
|
543
|
+
- **`restore()` returns the restored row** on the Postgres `RETURNING` path (no longer re-`find()`s,
|
|
544
|
+
which would have excluded the just-restored row via the soft-delete filter).
|
|
545
|
+
|
|
546
|
+
### Added
|
|
547
|
+
- **Row-level lock options** on Postgres reads (`find`/`findOne`/`findAll`): `lock: 'update' | 'share'`
|
|
548
|
+
and `forNoKeyUpdate` apply `.for(...)`. No-op on MySQL.
|
|
549
|
+
|
|
550
|
+
## [3.0.0] - 2026-06-19
|
|
551
|
+
|
|
552
|
+
- First release under the `nestjs-drizzle-crud` name with the 3.x API surface
|
|
553
|
+
(`DrizzleCrudModule.forRoot`/`forRootAsync`/`forFeature`, `SqlBaseCrudService`).
|
|
554
|
+
|
|
555
|
+
## Earlier (≤ 2.2.0)
|
|
556
|
+
|
|
557
|
+
Published as `@quybquang/nestjs-drizzle-crud`. Notable: 2.2.0 added a configurable module-level
|
|
558
|
+
default sort; 2.1.2 documented the timestamps feature. See git history for details.
|
|
559
|
+
|
|
560
|
+
[3.1.0]: https://www.npmjs.com/package/nestjs-drizzle-crud/v/3.1.0
|
|
561
|
+
[3.0.4]: https://www.npmjs.com/package/nestjs-drizzle-crud/v/3.0.4
|
|
562
|
+
[3.0.3]: https://www.npmjs.com/package/nestjs-drizzle-crud/v/3.0.3
|
|
563
|
+
[3.0.2]: https://www.npmjs.com/package/nestjs-drizzle-crud/v/3.0.2
|
|
564
|
+
[3.0.1]: https://www.npmjs.com/package/nestjs-drizzle-crud/v/3.0.1
|
|
565
|
+
[3.0.0]: https://www.npmjs.com/package/nestjs-drizzle-crud/v/3.0.0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
nestjs-drizzle-crud — AI agent rules for apps that consume the package.
|
|
3
|
+
|
|
4
|
+
Copy this file to your PROJECT ROOT as CLAUDE.md:
|
|
5
|
+
|
|
6
|
+
cp node_modules/nestjs-drizzle-crud/CLAUDE.template.md ./CLAUDE.md
|
|
7
|
+
|
|
8
|
+
For any other agent (Codex, Cursor, Copilot, Gemini CLI, Windsurf, Zed,
|
|
9
|
+
Cline, Aider), use AGENTS.template.md instead. It lists the filename each
|
|
10
|
+
tool reads.
|
|
11
|
+
|
|
12
|
+
The line below loads the agent guide that ships inside the package, so
|
|
13
|
+
AGENT.md stays the single source of truth. You never copy the rules
|
|
14
|
+
themselves, only this pointer. If your agent tool doesn't resolve `@`
|
|
15
|
+
imports, open node_modules/nestjs-drizzle-crud/AGENT.md and paste it here.
|
|
16
|
+
-->
|
|
17
|
+
|
|
18
|
+
@node_modules/nestjs-drizzle-crud/AGENT.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Suman Bonakurthi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|