@avytrix/nestjs-drizzle-crud 3.5.1 → 3.7.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 CHANGED
@@ -11,7 +11,7 @@
11
11
  > # Gemini CLI reads ./GEMINI.md, Aider reads ./CONVENTIONS.md
12
12
  > ```
13
13
  >
14
- > Or let the agent read these docs on demand: `npx nestjs-drizzle-crud-mcp` is an
14
+ > Or let the agent read these docs on demand: `npx -p @avytrix/nestjs-drizzle-crud nestjs-drizzle-crud-mcp` is an
15
15
  > MCP server shipped with the package. See
16
16
  > [crud.avytrix.dev/docs/ai-agents](https://crud.avytrix.dev/docs/ai-agents).
17
17
  >
@@ -56,15 +56,20 @@ service, stop and check the API reference first.
56
56
  # Installation
57
57
 
58
58
  ```bash
59
- pnpm add nestjs-drizzle-crud
59
+ pnpm add @avytrix/nestjs-drizzle-crud
60
60
  pnpm add @nestjs/common @nestjs/core drizzle-orm reflect-metadata # peers, always
61
61
  pnpm add postgres # PostgreSQL
62
62
  pnpm add mysql2 # MySQL
63
+ pnpm add @libsql/client # SQLite / libSQL
63
64
  ```
64
65
 
65
- `postgres` is an optional peer. You only need it when you let the module build
66
- the connection from a `connectionString` on PostgreSQL. If you pass your own
67
- `db`, no driver peer is required.
66
+ `postgres` and `mysql2` are optional peers. You only need them when you let the
67
+ module build the connection from a `connectionString` (PostgreSQL or MySQL;
68
+ `connection: { poolSize, idleTimeout, ssl }` tunes it). If you pass your own
69
+ `db`, no driver peer is required. SQLite (`dialect: 'sqlite'`, also libSQL and
70
+ Cloudflare D1) always takes your own `db`. On D1 and synchronous SQLite drivers
71
+ the mass writes, nested creates and attach / detach / sync throw a
72
+ `TransactionException`; use libSQL when you need them.
68
73
 
69
74
  Requires Node >= 20 and `emitDecoratorMetadata` / `experimentalDecorators` in
70
75
  `tsconfig.json`, which the Nest CLI already sets.
@@ -87,7 +92,8 @@ DrizzleCrudModule.forRootAsync({
87
92
  ```
88
93
 
89
94
  Use `forRoot` for a static config and `forRootAsync` when the value comes from
90
- `ConfigService` or the environment. Provide exactly one of `connectionString`
95
+ `ConfigService` or the environment (`useFactory`, or `useClass` / `useExisting`
96
+ with a class that implements `DrizzleCrudOptionsFactory`). Provide exactly one of `connectionString`
91
97
  or `db`. The module is global, so feature modules do not import it again for
92
98
  the connection.
93
99
 
@@ -138,7 +144,7 @@ Those are legacy manual patterns and they break registration.
138
144
  | OpenAPI | `@ApiCrudList(source)`, `@ApiCrudQuery(source)` (needs `@nestjs/swagger`) |
139
145
  | Bulk | `massCreate`, `massUpdate`, `massUpsert`, `massSoftDelete`, `massRestore`, `massDelete`, `massForceDelete` |
140
146
  | Query string | `parseQuery(query, options)`, or the standalone `parseCrudQuery(query, table, options)` |
141
- | Search | `fullTextSearch(term, columns, pagination)`, PostgreSQL only |
147
+ | Search | `fullTextSearch(term, columns, pagination, { filters, relations, select, language, queryMode })`, PostgreSQL only |
142
148
  | Transaction | `executeSqlTransaction(cb)` |
143
149
 
144
150
  `findAll` returns `{ data, total, page, limit }`, not a bare array; with
@@ -246,7 +252,7 @@ ignores the primary key and `createdAt`.
246
252
 
247
253
  # Sorting and cursor pagination
248
254
 
249
- `sortBy` takes one column (with `sortOrder`, default `'desc'`) or a list:
255
+ `sortBy` takes one column (with `sortOrder`, default `'asc'`) or a list:
250
256
  `['team.name', '-created_at']` or `[{ column, order }]`. A dotted entry sorts by a
251
257
  top-level belongs-to column. The primary key is always the last sort column.
252
258
 
@@ -427,7 +433,8 @@ Driver error codes are translated at the service boundary, so bad client input
427
433
  never reaches the caller as a 500: a unique violation is a 409, a value that is
428
434
  too long, fails a check, has the wrong type or is not a valid date is a 400 (on
429
435
  reads too), a foreign key pointing at a row that does not exist is a 400, and a
430
- delete blocked by dependent rows is a 409.
436
+ delete blocked by dependent rows is a 409. The same holds on MySQL (strict mode
437
+ errors included) and SQLite (constraint errors).
431
438
 
432
439
  ---
433
440
 
@@ -452,9 +459,10 @@ To insert or update by a unique key, use
452
459
  than a find followed by a create.
453
460
 
454
461
  Take a row lock with `options.lock: 'update' | 'share'` only inside a
455
- transaction, and prefer `forNoKeyUpdate` when the key is not changing. The lock
456
- covers the service's own table only (`FOR UPDATE OF <table>`), so it works with
457
- an eager belongs-to.
462
+ transaction, and prefer `forNoKeyUpdate` when the key is not changing. On
463
+ PostgreSQL the lock covers the service's own table only (`FOR UPDATE OF
464
+ <table>`), so it works with an eager belongs-to. MySQL gets `FOR UPDATE` or
465
+ `FOR SHARE`; on SQLite a lock is a 400.
458
466
 
459
467
  ---
460
468
 
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to `@avytrix/nestjs-drizzle-crud` (published as `nestjs-drizzle-crud` up to 3.5.0) are documented here.
3
+ All notable changes to `@avytrix/nestjs-drizzle-crud` (published as `nestjs-drizzle-crud` up to 3.3.0) are documented here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
@@ -8,6 +8,105 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  Scope note: the 3.0.x line is PostgreSQL-first. MySQL-specific paths (non-`RETURNING`
9
9
  restore, `ER_DUP_ENTRY` mapping, dialect locking) are tracked separately and unchanged here.
10
10
 
11
+ 3.3.1, 3.4.0, 3.5.0 and 3.5.2 were tagged but never published to npm. Their changes reached npm
12
+ in 3.5.1 (3.3.1 to 3.5.0) and 3.6.0 (3.5.2). The unscoped `nestjs-drizzle-crud` stops at 3.3.0.
13
+
14
+ ## [3.7.0] - 2026-11-02
15
+
16
+ SQLite support, MySQL connections built for you, and full-text search that works like `findAll`.
17
+
18
+ ### Added
19
+
20
+ - `dialect: "sqlite"` for SQLite, libSQL (Turso) and Cloudflare D1. Pass the Drizzle instance as
21
+ `db`. Writes use `RETURNING`, `upsert` uses `ON CONFLICT DO UPDATE` with the tenant guard, and
22
+ keyset pages follow SQLite's NULL order. `LIKE` gets `ESCAPE '\'`, so `%`, `_` and `\` in a
23
+ search term match literally. Constraint errors map like the other dialects: `UNIQUE` 409,
24
+ `FOREIGN KEY` 400 on a write and 409 on a delete, `NOT NULL` and `CHECK` 400. Full-text search,
25
+ row locks and the array and JSON operators are a 400.
26
+ - On Cloudflare D1 and synchronous SQLite drivers (better-sqlite3, bun:sqlite, sql.js), the calls
27
+ that need an interactive transaction (mass writes, nested creates, `attach`, `detach`, `sync`)
28
+ throw a `TransactionException` that says why, before any statement runs. `TransactionException`
29
+ is no longer deprecated.
30
+ - `connectionString` works with `dialect: "mysql"`: the module builds a `mysql2` pool, wraps it
31
+ with `drizzle-orm/mysql2` and ends it on shutdown. `mysql2` is a new optional peer, loaded only
32
+ on that path.
33
+ - `connection: { poolSize, idleTimeout, ssl }` for the connection the module builds from
34
+ `connectionString`, on PostgreSQL (postgres-js) and MySQL (mysql2).
35
+ - `forRootAsync` takes `useClass` and `useExisting`, with a class that implements
36
+ `DrizzleCrudOptionsFactory` (`createDrizzleCrudOptions()`), next to `useFactory`.
37
+ - `fullTextSearch()` takes `options.filters`, and honours `options.relations`, `options.select`
38
+ and `options.counts`, the same as `findAll`. Before, they were ignored.
39
+ - `language` (the text search configuration, default `"english"`) and `queryMode` (`"plain"`, the
40
+ default, or `"websearch"` for `websearch_to_tsquery`) on `fullTextSearch()` options and on
41
+ `search` with `mode: "fullText"`. A language that is not a plain identifier is a 400.
42
+ - New types: `DrizzleConnectionOptions`, `DrizzleCrudAsyncOptions`, `DrizzleCrudOptionsFactory`,
43
+ `FullTextOptions`, `FullTextQueryMode` and `FullTextSearchOptions`.
44
+
45
+ ### Fixed
46
+
47
+ - MySQL: `lock` and `forNoKeyUpdate` add `FOR UPDATE` or `FOR SHARE` (`forNoKeyUpdate` becomes
48
+ `FOR UPDATE`). They used to be accepted and silently ignored.
49
+ - MySQL strict mode data errors (a missing required value, a value too long, out of range, of
50
+ the wrong type, a bad date, a failed check) are a 400 that names the column, instead of a 500.
51
+
52
+ ### Changed
53
+
54
+ - The default `sql.useReturning` is `true` for every dialect except MySQL, so SQLite gets it.
55
+ PostgreSQL and MySQL keep the values they had.
56
+
57
+ ## [3.6.0] - 2026-11-02
58
+
59
+ Fixes from the second audit. The first one is a security fix: upgrade if you use `scope`.
60
+
61
+ ### Security
62
+
63
+ - Related tables now follow the tenant scope. Loaded relations, the joins behind relation columns,
64
+ relation filters (`some`, `none`, `every` and nested columns) and `counts` only see related rows
65
+ of the caller's tenant when the related table has the scope columns. Before, a row pointing at
66
+ another tenant's row (a plain foreign key allows it) loaded that row, and a filter on it
67
+ answered questions about it.
68
+
69
+ ### Added
70
+
71
+ - `createCrudController` takes `validation`, the validator options for the bulk bodies. Pass the
72
+ same options as your global `ValidationPipe`.
73
+
74
+ ### Fixed
75
+
76
+ - `PATCH /bulk` validates `data` against `dto.update`, the check a single `PATCH` already had.
77
+ - Route decorators that wrap the handler now run. Before, the factory dropped the wrapped handler.
78
+ - Bulk ids are parsed like `:id`, so a bad id is a 400 before any statement runs.
79
+ - A filter nested deeper than 32 objects is a 400. A thousand nested `not` groups used to
80
+ overflow the stack and answer with a 500.
81
+ - A batch past the bound-parameter limit (60,000 values: rows times columns for `massCreate` and
82
+ `massUpsert`, ids for the id based methods, values in an `IN` filter) is a 400 that names the
83
+ limit, instead of a driver protocol error and a 500.
84
+ - A missing required value (`23502`) is a 400 that names the column.
85
+ - MySQL `ER_DUP_ENTRY` is a 409, and foreign key errors are a 400 on a write and a 409 on a delete.
86
+ - `upsert` no longer updates a row in the trash: the conflict writes nothing and is a 404.
87
+ - With `useReturning: false`, a scoped `upsert` no longer reads back another tenant's row.
88
+ - `restore` on a live row is a 404 before any hook or event runs. It used to succeed and bump
89
+ `updated_at` and the version.
90
+ - `?relation[some]=` and `[none]=` with no column need the relation, or one of its columns, in
91
+ `filterableColumns`.
92
+ - A nested child row cannot set its own generated primary key.
93
+
94
+ ## [3.5.2] - 2026-11-02
95
+
96
+ ### Fixed
97
+
98
+ - `softDelete`, `restore`, `massSoftDelete` and `massRestore` on an entity without soft delete,
99
+ and full-text search on MySQL (`fullTextSearch()` or `search.mode: "fullText"`), now throw a
100
+ `BadRequestException`. They were a plain `Error`, which reached the client as a 500.
101
+ - `options.counts` now applies the tenant scope to the related rows it counts, when the related
102
+ table has the scope column. Before, a parent's count included rows of other tenants.
103
+
104
+ ### Changed
105
+
106
+ - `sortOrder` now defaults to `"asc"` in `findAll` and `findByCursor`, so a bare
107
+ `sortBy: "name"` sorts the same way as `?sortBy=name` over HTTP. Pass `sortOrder: "desc"` to
108
+ keep the old order.
109
+
11
110
  ## [3.5.1] - 2026-11-02
12
111
 
13
112
  ### Changed
package/README.md CHANGED
@@ -1,11 +1,38 @@
1
1
  # nestjs-drizzle-crud
2
2
 
3
3
  [![docs](https://img.shields.io/badge/docs-crud.avytrix.dev-E0234E)](https://crud.avytrix.dev)
4
- [![npm](https://img.shields.io/npm/v/nestjs-drizzle-crud?color=E0234E)](https://www.npmjs.com/package/nestjs-drizzle-crud)
5
- [![license](https://img.shields.io/npm/l/nestjs-drizzle-crud)](./LICENSE)
4
+ [![npm](https://img.shields.io/npm/v/@avytrix/nestjs-drizzle-crud?color=E0234E)](https://www.npmjs.com/package/@avytrix/nestjs-drizzle-crud)
5
+ [![license](https://img.shields.io/npm/l/@avytrix/nestjs-drizzle-crud)](./LICENSE)
6
6
  [![Sponsor @avytrix](https://img.shields.io/badge/Sponsor-%40avytrix-db61a2?style=for-the-badge&logo=githubsponsors&logoColor=white)](https://github.com/sponsors/avytrix)
7
7
 
8
- Built and maintained by **[Avytrix Technologies](https://avytrix.dev)**.
8
+ **nestjs-drizzle-crud is a NestJS + Drizzle ORM CRUD library: extend one base class and every table gets type-safe find, create, update, delete, filtering, pagination, relations and soft delete, on PostgreSQL, MySQL or SQLite.**
9
+
10
+ ```bash
11
+ pnpm add @avytrix/nestjs-drizzle-crud drizzle-orm
12
+ ```
13
+
14
+ > **Renamed.** From 3.5.1 the package is `@avytrix/nestjs-drizzle-crud`; the unscoped `nestjs-drizzle-crud` is deprecated. Change the dependency and the imports, nothing else changes.
15
+
16
+ ```typescript
17
+ // app.module.ts: configure the connection once
18
+ DrizzleCrudModule.forRoot({
19
+ dialect: 'postgresql',
20
+ connectionString: process.env.DATABASE_URL,
21
+ schema,
22
+ });
23
+
24
+ // users.service.ts: a full CRUD service is an empty subclass
25
+ export class UsersService extends SqlBaseCrudService<
26
+ User, CreateUserDto, UpdateUserDto, CrudFilter
27
+ > {}
28
+
29
+ // users.module.ts: bind it to a table
30
+ DrizzleCrudModule.forFeature([{ service: UsersService, table: users }]);
31
+ ```
32
+
33
+ **[Read the docs at crud.avytrix.dev](https://crud.avytrix.dev)**, starting with the [quick start](https://crud.avytrix.dev/docs/quick-start/). Also on [npm](https://www.npmjs.com/package/@avytrix/nestjs-drizzle-crud) and [GitHub](https://github.com/avytrix/nestjs-drizzle-crud).
34
+
35
+ Built and maintained by **[Avytrix Technologies](https://avytrix.dev/?utm_source=nestjs-drizzle-crud&utm_medium=docs&utm_campaign=readme)**. Need help shipping? [Hire the team behind the library](https://crud.avytrix.dev/docs/commercial-support/).
9
36
 
10
37
  ## ❤️ Sponsoring is what keeps this package free
11
38
 
@@ -19,36 +46,13 @@ sponsors are what make keeping it free sustainable.
19
46
 
20
47
  **Even $1 a month helps.** It funds bug fixes, new guides, and the next release.
21
48
 
22
- > Sponsors get a spot on the [sponsor page](https://crud.avytrix.dev/docs/sponsor)
49
+ > Sponsors get a spot on the [sponsor page](https://crud.avytrix.dev/docs/sponsor/)
23
50
  > and our thanks.
24
51
 
25
52
  ### 👉 [Sponsor on GitHub →](https://github.com/sponsors/avytrix) &nbsp;·&nbsp; thank you 🙏
26
53
 
27
54
  ---
28
55
 
29
- A type-safe CRUD layer for [Drizzle ORM](https://orm.drizzle.team/) in [NestJS](https://nestjs.com/) applications.
30
-
31
- **[Documentation](https://crud.avytrix.dev)** · **[npm](https://www.npmjs.com/package/nestjs-drizzle-crud)** · **[GitHub](https://github.com/avytrix/nestjs-drizzle-crud)** · **[Sponsor](https://github.com/sponsors/avytrix)**
32
-
33
- Configure the database connection **once**, then every entity gets full CRUD (find / create / update / delete / soft-delete / bulk / pagination / filtering / full-text search) by extending one base class. No per-service connection wiring.
34
-
35
- ```typescript
36
- // 1. configure once (app.module.ts)
37
- DrizzleCrudModule.forRoot({
38
- dialect: 'postgresql',
39
- connectionString: process.env.DATABASE_URL,
40
- schema,
41
- });
42
-
43
- // 2. a fully-featured CRUD service is just:
44
- export class UsersService extends SqlBaseCrudService<User> {}
45
-
46
- // 3. bind it to a table (users.module.ts)
47
- DrizzleCrudModule.forFeature([{ service: UsersService, table: users }]);
48
- ```
49
-
50
- ---
51
-
52
56
  ## Table of contents
53
57
 
54
58
  - [Features](#features)
@@ -90,7 +94,7 @@ DrizzleCrudModule.forFeature([{ service: UsersService, table: users }]);
90
94
  - 🚀 **CRUD**: `find`, `findOne`, `findAll`, `create`, `update`, `delete`, and more
91
95
  - 🧩 **Configure once**: `forRoot()` owns the connection; services only declare a table
92
96
  - ⚡ **Type-safe**: generics over your entity, create/update DTOs and filter types
93
- - 🗃️ **PostgreSQL & MySQL**: dialect-aware (`RETURNING` vs `insertId`)
97
+ - 🗃️ **PostgreSQL, MySQL & SQLite**: dialect-aware (`RETURNING` vs `insertId`), libSQL and Cloudflare D1 included
94
98
  - 🔄 **Soft delete**: opt-in, with `restore`, a trash view (`onlyDeleted`) and `forceDelete`
95
99
  - 📦 **Bulk operations**: one statement per mass create/update/delete, inside a transaction, plus `upsert`
96
100
  - 🔍 **Filtering**: comparison, `in`/`notIn`, `between`, escaped `contains`/`startsWith`, array and JSON operators, `or`/`not` groups
@@ -116,11 +120,11 @@ DrizzleCrudModule.forFeature([{ service: UsersService, table: users }]);
116
120
  ## Installation
117
121
 
118
122
  ```bash
119
- pnpm add nestjs-drizzle-crud
123
+ pnpm add @avytrix/nestjs-drizzle-crud
120
124
  # or
121
- npm install nestjs-drizzle-crud
125
+ npm install @avytrix/nestjs-drizzle-crud
122
126
  # or
123
- yarn add nestjs-drizzle-crud
127
+ yarn add @avytrix/nestjs-drizzle-crud
124
128
  ```
125
129
 
126
130
  Peer dependencies (install the ones you use):
@@ -132,22 +136,23 @@ pnpm add @nestjs/common @nestjs/core drizzle-orm reflect-metadata
132
136
  # PostgreSQL (also required if you use `connectionString` with dialect 'postgresql')
133
137
  pnpm add postgres
134
138
 
135
- # MySQL
139
+ # MySQL (also required if you use `connectionString` with dialect 'mysql')
136
140
  pnpm add mysql2
137
141
 
142
+ # SQLite / libSQL (you build the Drizzle instance and pass it as `db`)
143
+ pnpm add @libsql/client
144
+
138
145
  # Only for ApiCrudQuery / ApiCrudList (OpenAPI docs)
139
146
  pnpm add @nestjs/swagger
140
147
  ```
141
148
 
142
149
  > Using npm or yarn? Swap `pnpm add` for `npm install` / `yarn add`.
143
150
 
144
- > **pnpm note:** `postgres`/`mysql2` are optional peer dependencies. pnpm enforces
145
- > peer deps strictly, so if you build the connection from a `connectionString`
146
- > make sure the matching driver is installed in your app.
147
-
148
- > `postgres` is an **optional** peer dependency. It's only needed when you let the
149
- > module build the connection from a `connectionString` for the `postgresql` dialect.
150
- > If you pass a pre-built `db` instead, you don't need it.
151
+ > **pnpm note:** `postgres` and `mysql2` are optional peer dependencies. pnpm
152
+ > enforces peer deps strictly, so if you build the connection from a
153
+ > `connectionString` make sure the driver for your dialect is installed in your app.
154
+ > If you pass a pre-built `db` instead, you don't need either. SQLite always takes a
155
+ > `db` you build.
151
156
 
152
157
  ---
153
158
 
@@ -174,7 +179,7 @@ export type User = typeof users.$inferSelect;
174
179
  ```typescript
175
180
  // app.module.ts
176
181
  import { Module } from '@nestjs/common';
177
- import { DrizzleCrudModule } from 'nestjs-drizzle-crud';
182
+ import { DrizzleCrudModule } from '@avytrix/nestjs-drizzle-crud';
178
183
  import { schema } from './db/schema';
179
184
  import { UsersModule } from './users/users.module';
180
185
 
@@ -198,7 +203,7 @@ application shutdown (when the module built it from a `connectionString`).
198
203
 
199
204
  ```typescript
200
205
  // users/users.service.ts
201
- import { SqlBaseCrudService } from 'nestjs-drizzle-crud';
206
+ import { SqlBaseCrudService } from '@avytrix/nestjs-drizzle-crud';
202
207
  import type { User } from '../db/schema';
203
208
 
204
209
  export interface CreateUserDto { name: string; email: string }
@@ -218,7 +223,7 @@ export class UsersService extends SqlBaseCrudService<
218
223
  ```typescript
219
224
  // users/users.module.ts
220
225
  import { Module } from '@nestjs/common';
221
- import { DrizzleCrudModule } from 'nestjs-drizzle-crud';
226
+ import { DrizzleCrudModule } from '@avytrix/nestjs-drizzle-crud';
222
227
  import { users } from '../db/schema';
223
228
  import { UsersController } from './users.controller';
224
229
  import { UsersService } from './users.service';
@@ -280,15 +285,16 @@ export class UsersController {
280
285
 
281
286
  | Field | Type | Description |
282
287
  |---|---|---|
283
- | `dialect` | `'postgresql' \| 'mysql'` | **Required.** Database dialect. |
284
- | `connectionString` | `string` | Connection string. The module builds the connection (PostgreSQL only). |
288
+ | `dialect` | `'postgresql' \| 'mysql' \| 'sqlite'` | **Required.** Database dialect. `'sqlite'` covers SQLite, libSQL and Cloudflare D1. |
289
+ | `connectionString` | `string` | Connection string. The module builds the connection (PostgreSQL with `postgres`, MySQL with `mysql2`). |
290
+ | `connection` | `{ poolSize?, idleTimeout?, ssl? }` | Pool size, idle timeout (seconds) and TLS for the connection built from `connectionString`. |
285
291
  | `db` | `Drizzle instance` | Alternatively, pass a Drizzle instance you built yourself (any dialect). |
286
292
  | `schema` | `Record<string, unknown>` | Drizzle schema, used when building from `connectionString`. |
287
293
  | `defaults.softDelete` | `boolean` | Enable soft delete for all entities (default `true`). |
288
294
  | `defaults.timestamps` | `boolean` | Auto-manage `created_at`/`updated_at` for all entities (default `true`). |
289
295
  | `defaults.pagination` | `{ defaultLimit?, maxLimit? }` | Pagination defaults (default `{ 20, 100 }`). Either key alone keeps the other's default. |
290
296
  | `defaults.sortOrder` | `'asc' \| 'desc'` | Opt-in default sort: entities **without** an explicit `defaultSort` fall back to ordering by their `created_at` column in this direction. Omit it and `findAll` orders by the primary key only. |
291
- | `sql` | `{ caseSensitive?, useReturning? }` | Dialect tuning. `useReturning` defaults to `true` for PostgreSQL, `false` for MySQL. `jsonSupport` and `enableFullTextSearch` are deprecated and do nothing. |
297
+ | `sql` | `{ caseSensitive?, useReturning? }` | Dialect tuning. `useReturning` defaults to `true` for PostgreSQL and SQLite, `false` for MySQL. `jsonSupport` and `enableFullTextSearch` are deprecated and do nothing. |
292
298
  | `hooks` | `{ enableGlobalHooks?, subscribers? }` | Module-level write events on the `CrudEventBus`. See [Events](#events-and-global-hooks). |
293
299
 
294
300
  > **Provide exactly one of `connectionString` or `db`.** If your tables have no
@@ -301,7 +307,7 @@ export class UsersController {
301
307
  > table does not have stops the app at startup, and the message names the key
302
308
  > to use.
303
309
 
304
- **Build the connection yourself (any dialect, recommended for MySQL):**
310
+ **Build the connection yourself (any dialect, required for SQLite):**
305
311
 
306
312
  ```typescript
307
313
  import { drizzle } from 'drizzle-orm/postgres-js';
@@ -327,6 +333,27 @@ DrizzleCrudModule.forRootAsync({
327
333
  });
328
334
  ```
329
335
 
336
+ `forRootAsync` also takes `useClass` or `useExisting` with a class that
337
+ implements `DrizzleCrudOptionsFactory`:
338
+
339
+ ```typescript
340
+ @Injectable()
341
+ class CrudConfig implements DrizzleCrudOptionsFactory {
342
+ constructor(private readonly cfg: ConfigService) {}
343
+ createDrizzleCrudOptions(): DrizzleCrudConfig {
344
+ return { dialect: 'mysql', connectionString: this.cfg.get('DATABASE_URL') };
345
+ }
346
+ }
347
+
348
+ DrizzleCrudModule.forRootAsync({ useClass: CrudConfig });
349
+ ```
350
+
351
+ On SQLite (`dialect: 'sqlite'`), mass writes, nested creates and attach / detach
352
+ / sync need an async transaction: they work on libSQL and throw a
353
+ `TransactionException` on Cloudflare D1 and synchronous drivers such as
354
+ better-sqlite3. Full-text search, row locks and the array and JSON operators
355
+ are a 400 there.
356
+
330
357
  ### `DrizzleCrudModule.forFeature(entities)`
331
358
 
332
359
  Registers one or more services and binds each to its table. Per-entity overrides
@@ -362,7 +389,9 @@ The minimal service is an empty class. Connection, dialect and defaults are
362
389
  injected by the module:
363
390
 
364
391
  ```typescript
365
- export class UsersService extends SqlBaseCrudService<User> {}
392
+ export class UsersService extends SqlBaseCrudService<
393
+ User, CreateUserDto, UpdateUserDto, CrudFilter
394
+ > {}
366
395
  ```
367
396
 
368
397
  Add custom behaviour by overriding hooks (see [Lifecycle hooks](#lifecycle-hooks--validation))
@@ -590,7 +619,7 @@ await service.findAll(
590
619
  // → { data: [...], total: 240, page: 2, limit: 25 }
591
620
  ```
592
621
 
593
- `limit` is capped at `pagination.maxLimit`. `sortOrder` defaults to `'desc'`.
622
+ `limit` is capped at `pagination.maxLimit`. `sortOrder` defaults to `'asc'`.
594
623
  The primary key is always the last `ORDER BY` column, so rows that tie on the
595
624
  sort column never repeat or go missing between pages.
596
625
 
@@ -1104,7 +1133,19 @@ const { data, total } = await service.fullTextSearch(
1104
1133
  ```
1105
1134
 
1106
1135
  Builds `to_tsvector(...) @@ plainto_tsquery(...)` across the given columns and
1107
- orders by `ts_rank`. Throws if the dialect is not `postgresql`.
1136
+ orders by `ts_rank`. Throws a 400 if the dialect is not `postgresql`. The options
1137
+ take `filters`, `relations`, `select` and `counts` as on `findAll`, plus
1138
+ `language` (default `'english'`) and `queryMode: 'websearch'` for
1139
+ `websearch_to_tsquery`:
1140
+
1141
+ ```typescript
1142
+ await service.fullTextSearch('"zero downtime" -kubernetes', ['title', 'body'], {}, {
1143
+ filters: { status: 'published' },
1144
+ relations: ['author'],
1145
+ language: 'english',
1146
+ queryMode: 'websearch',
1147
+ });
1148
+ ```
1108
1149
 
1109
1150
  For paginated list endpoints, prefer `findAll(..., ..., { search })` so search is
1110
1151
  combined with filters and the count query. Use `fullTextSearch` directly when
@@ -1144,7 +1185,7 @@ service stamps `created_at`/`updated_at` automatically.
1144
1185
  ## Testing
1145
1186
 
1146
1187
  ```typescript
1147
- import { TestCrudFactory } from 'nestjs-drizzle-crud';
1188
+ import { TestCrudFactory } from '@avytrix/nestjs-drizzle-crud';
1148
1189
 
1149
1190
  const mockDb = TestCrudFactory.createMockDb();
1150
1191
  const mockTable = TestCrudFactory.createMockTable();
@@ -1164,18 +1205,18 @@ const service = TestCrudFactory.createTestService(UsersService, mockDb, mockTabl
1164
1205
  when they use it. Copy the pointer template to the filename your tool reads:
1165
1206
 
1166
1207
  ```bash
1167
- cp node_modules/nestjs-drizzle-crud/AGENTS.template.md ./AGENTS.md # Codex, Cursor, Copilot, Zed, Cline, Windsurf
1168
- cp node_modules/nestjs-drizzle-crud/CLAUDE.template.md ./CLAUDE.md # Claude Code
1208
+ cp node_modules/@avytrix/nestjs-drizzle-crud/AGENTS.template.md ./AGENTS.md # Codex, Cursor, Copilot, Zed, Cline, Windsurf
1209
+ cp node_modules/@avytrix/nestjs-drizzle-crud/CLAUDE.template.md ./CLAUDE.md # Claude Code
1169
1210
  # Gemini CLI reads ./GEMINI.md, Aider reads ./CONVENTIONS.md
1170
1211
  ```
1171
1212
 
1172
- Each template is one `@node_modules/nestjs-drizzle-crud/AGENT.md` line, so an
1213
+ Each template is one `@node_modules/@avytrix/nestjs-drizzle-crud/AGENT.md` line, so an
1173
1214
  upgrade updates the rules with it and you never hold a stale copy.
1174
1215
 
1175
1216
  ### Or run the MCP server
1176
1217
 
1177
1218
  ```bash
1178
- npx nestjs-drizzle-crud-mcp
1219
+ npx -p @avytrix/nestjs-drizzle-crud nestjs-drizzle-crud-mcp
1179
1220
  ```
1180
1221
 
1181
1222
  A stdio MCP server, zero dependencies, that serves this package's docs to any MCP
@@ -1187,7 +1228,7 @@ client through three tools: `list_docs`, `get_doc` (`agent-guide` / `readme` /
1187
1228
  "mcpServers": {
1188
1229
  "nestjs-drizzle-crud": {
1189
1230
  "command": "npx",
1190
- "args": ["-y", "nestjs-drizzle-crud-mcp"]
1231
+ "args": ["-y", "-p", "@avytrix/nestjs-drizzle-crud", "nestjs-drizzle-crud-mcp"]
1191
1232
  }
1192
1233
  }
1193
1234
  }
@@ -1204,7 +1245,7 @@ read-only and never touches your database. Full setup:
1204
1245
 
1205
1246
  Use these facts instead of guessing.
1206
1247
 
1207
- **Package:** `nestjs-drizzle-crud` · **Peers:** `@nestjs/common`, `@nestjs/core`, `drizzle-orm`, `reflect-metadata`; optional `postgres` (PG) / `mysql2` (MySQL) / `@nestjs/swagger` (for `ApiCrudList`).
1248
+ **Package:** `nestjs-drizzle-crud` · **Peers:** `@nestjs/common`, `@nestjs/core`, `drizzle-orm`, `reflect-metadata`; optional `postgres` (PG `connectionString`) / `mysql2` (MySQL `connectionString`) / `@nestjs/swagger` (for `ApiCrudList`). SQLite drivers are yours, passed as `db`.
1208
1249
 
1209
1250
  **Setup is two steps and no per-service connection wiring:**
1210
1251
  1. `DrizzleCrudModule.forRoot({ dialect, connectionString | db, schema, defaults })` once in `AppModule`.
@@ -1235,15 +1276,17 @@ export class XService extends SqlBaseCrudService<X, CreateXDto, UpdateXDto, XFil
1235
1276
  - Relations: declare in forFeature `config.relations` and eager-load via `options.relations: ['relName']`, where a dotted path nests (`['states', 'states.cities']`, `['state.country']`; the parent path is pulled in automatically). Belongs-to is `{ table, localKey, references? }`, LEFT JOINed into the main query, resolving to an object or `null`. Has-many is `{ table, type: 'hasMany', foreignKey, parentKey?, orderBy? }`. Many-to-many is `{ table, type: 'manyToMany', through, throughLocalKey, throughForeignKey, references?, parentKey?, orderBy? }`. Nested relations live in a `relations` map on the relation. Every collection and every nested level costs exactly one extra query for the whole result set, never one per row. Filter via `findAll({ relName: { col: value } })` with the same operators; collection filters compile to `parentKey IN (SELECT ... WHERE ...)`, so `total` stays correct. Filters go through relation paths at any depth (`{ state: { country: { code: 'IN' } } }`), and collections take `some` / `none` / `every` (`{ posts: { none: { status: 'draft' } } }`), compiled to subqueries.
1236
1277
  - `options.select` narrows the projection and takes dotted relation paths at any depth: `['id', 'name', 'region.code', 'states.cities.name']`. Unknown names are ignored; an all-dotted list leaves the base row complete; the parent key, the child foreign key and any nested key are re-added automatically.
1237
1278
  - Primary keys: `primaryKey` (default `'id'`) per entity in forFeature config (`primaryKeyType` is deprecated and does nothing). UUID works on Postgres via RETURNING; with uuid keys, route params are strings, so don't use `ParseIntPipe`.
1238
- - Full-text search is PostgreSQL-only.
1279
+ - Full-text search is PostgreSQL-only; it takes `filters`, `relations`, `select`, `language` and `queryMode` in its options.
1280
+ - SQLite (`dialect: 'sqlite'`): pass `db`. Bulk writes need libSQL; D1 and sync drivers throw `TransactionException`. Locks, full-text and array/JSON operators are a 400.
1281
+ - Exports (3.7): `DrizzleConnectionOptions`, `DrizzleCrudAsyncOptions`, `DrizzleCrudOptionsFactory`, `FullTextOptions`, `FullTextQueryMode`, `FullTextSearchOptions`.
1239
1282
  - Exports (3.5): `createCrudController`, `CrudContext`, `CrudEventBus`, `ApiCrudQuery`, `ApiCrudList`, `MissingScopeException`, `StaleVersionException`, and the types `CrudControllerOptions`, `CrudEvent`, `CrudSubscriber`, `CrudScope`, `CollectionFilter`, `CountByRow`, `CrudPage`, `UncountedCrudPage`, `RelationCounts`.
1240
- - Exports: `SqlBaseCrudService`, `DrizzleCrudModule`, `DRIZZLE_DB`, `DRIZZLE_CRUD_CONFIG`, `TestCrudFactory`, exceptions (`EntityNotFoundException`, `DuplicateEntityException`, `ForeignKeyViolationException`, `EntityInUseException`, `ValidationFailedException`, `BulkOperationException`, plus the deprecated `DatabaseConnectionException` and `TransactionException`), and types (`SqlCrudConfig`, `SqlOperationOptions`, `DrizzleCrudConfig`, `CrudFeature`, `CrudFeatureConfig`, `PaginationOptions`, `OperationOptions`, `SqlSearchOptions`, `SqlSearchMode`, `BulkOperationResult`, `BulkOperationItemError`, `SqlDialect`, `PrimaryKeyType`, `CrudFilter`, `FilterOperators`, `SortInput`, `CursorPaginationOptions`, `CursorPage`, `CrudAllowlists`, `UpsertOptions`, `BulkMode`, `ParsedCrudQuery`), and `parseCrudQuery`.
1283
+ - Exports: `SqlBaseCrudService`, `DrizzleCrudModule`, `DRIZZLE_DB`, `DRIZZLE_CRUD_CONFIG`, `TestCrudFactory`, exceptions (`EntityNotFoundException`, `DuplicateEntityException`, `ForeignKeyViolationException`, `EntityInUseException`, `ValidationFailedException`, `BulkOperationException`, `TransactionException`, plus the deprecated `DatabaseConnectionException`), and types (`SqlCrudConfig`, `SqlOperationOptions`, `DrizzleCrudConfig`, `CrudFeature`, `CrudFeatureConfig`, `PaginationOptions`, `OperationOptions`, `SqlSearchOptions`, `SqlSearchMode`, `BulkOperationResult`, `BulkOperationItemError`, `SqlDialect`, `PrimaryKeyType`, `CrudFilter`, `FilterOperators`, `SortInput`, `CursorPaginationOptions`, `CursorPage`, `CrudAllowlists`, `UpsertOptions`, `BulkMode`, `ParsedCrudQuery`), and `parseCrudQuery`.
1241
1284
 
1242
1285
  ### `SqlCrudConfig`
1243
1286
 
1244
1287
  ```typescript
1245
1288
  interface SqlCrudConfig {
1246
- dialect: 'postgresql' | 'mysql';
1289
+ dialect: 'postgresql' | 'mysql' | 'sqlite';
1247
1290
  db: any; // Drizzle instance (injected by forFeature)
1248
1291
  table: any; // Drizzle table (set by forFeature)
1249
1292
  primaryKey: string; // default 'id'
@@ -1,4 +1,4 @@
1
- import { type Type } from "@nestjs/common";
1
+ import { type Type, type ValidationPipeOptions } from "@nestjs/common";
2
2
  import type { SqlBaseCrudService } from "../core/abstract/sql-base-crud.service";
3
3
  import type { ParseCrudQueryOptions } from "../core/interfaces/query.interface";
4
4
  import { type ApiCrudSource } from "../openapi/api-crud.decorators";
@@ -14,6 +14,7 @@ export interface CrudControllerOptions {
14
14
  create?: Type<unknown>;
15
15
  update?: Type<unknown>;
16
16
  };
17
+ validation?: ValidationPipeOptions;
17
18
  query?: ParseCrudQueryOptions;
18
19
  routes?: Partial<Record<CrudRouteName, boolean | CrudRouteOptions>>;
19
20
  decorators?: MethodDecorator[];
@@ -10,12 +10,29 @@ const BULK_ROUTES = new Set([
10
10
  "bulkDelete",
11
11
  "bulkRestore",
12
12
  ]);
13
- function idsOf(body) {
13
+ class BulkUpdateBodyPipe {
14
+ dto;
15
+ pipe;
16
+ constructor(dto, options) {
17
+ this.dto = dto;
18
+ this.pipe = new common_1.ValidationPipe(options);
19
+ }
20
+ async transform(body) {
21
+ if (!body?.data || typeof body.data !== "object")
22
+ return body;
23
+ const data = await this.pipe.transform(body.data, {
24
+ type: "body",
25
+ metatype: this.dto,
26
+ });
27
+ return { ...body, data };
28
+ }
29
+ }
30
+ async function idsOf(service, body) {
14
31
  const ids = body?.ids;
15
32
  if (!Array.isArray(ids) || ids.length === 0) {
16
33
  throw new common_1.BadRequestException("ids must be a non-empty array");
17
34
  }
18
- return ids;
35
+ return Promise.all(ids.map((id) => service.parseId(id)));
19
36
  }
20
37
  function createCrudController(service, options = {}) {
21
38
  const routeOptions = (name) => {
@@ -78,21 +95,21 @@ function createCrudController(service, options = {}) {
78
95
  }
79
96
  return this.service.massCreate(body, readOptions(this.service, query));
80
97
  }
81
- bulkUpdate(body, query) {
98
+ async bulkUpdate(body, query) {
82
99
  if (!body?.data || typeof body.data !== "object") {
83
100
  throw new common_1.BadRequestException("data must be an object");
84
101
  }
85
- return this.service.massUpdate(idsOf(body), body.data, readOptions(this.service, query));
102
+ return this.service.massUpdate(await idsOf(this.service, body), body.data, readOptions(this.service, query));
86
103
  }
87
- bulkDelete(body) {
88
- const ids = idsOf(body);
104
+ async bulkDelete(body) {
105
+ const ids = await idsOf(this.service, body);
89
106
  return options.deleteMode === "soft"
90
107
  ? this.service.massSoftDelete(ids)
91
108
  : this.service.massDelete(ids);
92
109
  }
93
- bulkRestore(body, query) {
110
+ async bulkRestore(body, query) {
94
111
  softDeleteOn(this.service);
95
- return this.service.massRestore(idsOf(body), readOptions(this.service, query));
112
+ return this.service.massRestore(await idsOf(this.service, body), readOptions(this.service, query));
96
113
  }
97
114
  async get(rawId, query) {
98
115
  const id = await this.id(rawId);
@@ -148,7 +165,10 @@ function createCrudController(service, options = {}) {
148
165
  http: (0, common_1.Post)("bulk"),
149
166
  params: [
150
167
  options.dto?.create
151
- ? (0, common_1.Body)(new common_1.ParseArrayPipe({ items: options.dto.create }))
168
+ ? (0, common_1.Body)(new common_1.ParseArrayPipe({
169
+ ...options.validation,
170
+ items: options.dto.create,
171
+ }))
152
172
  : (0, common_1.Body)(),
153
173
  (0, common_1.Query)(),
154
174
  ],
@@ -157,7 +177,12 @@ function createCrudController(service, options = {}) {
157
177
  {
158
178
  name: "bulkUpdate",
159
179
  http: (0, common_1.Patch)("bulk"),
160
- params: [(0, common_1.Body)(), (0, common_1.Query)()],
180
+ params: [
181
+ options.dto?.update
182
+ ? (0, common_1.Body)(new BulkUpdateBodyPipe(options.dto.update, options.validation))
183
+ : (0, common_1.Body)(),
184
+ (0, common_1.Query)(),
185
+ ],
161
186
  types: [Object, Object],
162
187
  extra: [(0, common_1.HttpCode)(200)],
163
188
  },
@@ -227,19 +252,22 @@ function createCrudController(service, options = {}) {
227
252
  configurable: true,
228
253
  enumerable: false,
229
254
  });
230
- const descriptor = Object.getOwnPropertyDescriptor(proto, route.name);
255
+ let descriptor = Object.getOwnPropertyDescriptor(proto, route.name);
231
256
  route.params.forEach((decorate, index) => {
232
257
  decorate(proto, route.name, index);
233
258
  });
234
259
  Reflect.defineMetadata("design:paramtypes", route.types, proto, route.name);
235
- for (const decorate of [
260
+ const decorators = [
236
261
  route.http,
237
262
  ...(route.extra ?? []),
238
- ...(options.decorators ?? []),
239
263
  ...(settings.decorators ?? []),
240
- ]) {
241
- decorate(proto, route.name, descriptor);
264
+ ...(options.decorators ?? []),
265
+ ];
266
+ for (const decorate of decorators.reverse()) {
267
+ descriptor =
268
+ decorate(proto, route.name, descriptor) ?? descriptor;
242
269
  }
270
+ Object.defineProperty(proto, route.name, descriptor);
243
271
  }
244
272
  for (const decorate of options.classDecorators ?? []) {
245
273
  decorate(CrudController);