@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 +20 -12
- package/CHANGELOG.md +100 -1
- package/README.md +102 -59
- package/dist/controller/crud-controller.factory.d.ts +2 -1
- package/dist/controller/crud-controller.factory.js +43 -15
- package/dist/controller/crud-controller.factory.js.map +1 -1
- package/dist/core/abstract/sql-base-crud.service.d.ts +23 -7
- package/dist/core/abstract/sql-base-crud.service.js +258 -119
- package/dist/core/abstract/sql-base-crud.service.js.map +1 -1
- package/dist/core/interfaces/drizzle-crud-config.interface.d.ts +16 -0
- package/dist/core/interfaces/sql-crud-config.interface.d.ts +9 -1
- package/dist/core/types/sql.types.d.ts +1 -1
- package/dist/core/utils/query-parser.d.ts +2 -0
- package/dist/core/utils/query-parser.js +36 -2
- package/dist/core/utils/query-parser.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/drizzle-connection.d.ts +3 -1
- package/dist/modules/drizzle-connection.js +49 -2
- package/dist/modules/drizzle-connection.js.map +1 -1
- package/dist/modules/drizzle-crud.module.d.ts +2 -6
- package/dist/modules/drizzle-crud.module.js +28 -6
- package/dist/modules/drizzle-crud.module.js.map +1 -1
- package/package.json +25 -8
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`
|
|
66
|
-
the connection from a `connectionString`
|
|
67
|
-
`
|
|
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
|
|
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 `'
|
|
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.
|
|
456
|
-
covers the service's own table only (`FOR UPDATE OF
|
|
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.
|
|
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
|
[](https://crud.avytrix.dev)
|
|
4
|
-
[](https://www.npmjs.com/package/nestjs-drizzle-crud)
|
|
5
|
-
[](./LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/@avytrix/nestjs-drizzle-crud)
|
|
5
|
+
[](./LICENSE)
|
|
6
6
|
[](https://github.com/sponsors/avytrix)
|
|
7
7
|
|
|
8
|
-
|
|
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) · 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 &
|
|
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
|
|
145
|
-
> peer deps strictly, so if you build the connection from a
|
|
146
|
-
> make sure the
|
|
147
|
-
|
|
148
|
-
> `
|
|
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
|
|
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,
|
|
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<
|
|
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 `'
|
|
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`
|
|
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
|
-
|
|
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({
|
|
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: [
|
|
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
|
-
|
|
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
|
-
|
|
260
|
+
const decorators = [
|
|
236
261
|
route.http,
|
|
237
262
|
...(route.extra ?? []),
|
|
238
|
-
...(options.decorators ?? []),
|
|
239
263
|
...(settings.decorators ?? []),
|
|
240
|
-
|
|
241
|
-
|
|
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);
|