nestjs-drizzle-crud 1.0.6 โ 2.0.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/README.md +439 -293
- package/dist/core/abstract/sql-base-crud.service.d.ts +6 -4
- package/dist/core/abstract/sql-base-crud.service.js +64 -40
- package/dist/core/abstract/sql-base-crud.service.js.map +1 -1
- package/dist/core/interfaces/drizzle-crud-config.interface.d.ts +8 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- 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 +2 -4
- package/dist/modules/drizzle-crud.module.js +92 -49
- package/dist/modules/drizzle-crud.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -1,18 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# nestjs-drizzle-crud
|
|
2
2
|
|
|
3
|
-
A complete, type-safe CRUD abstraction layer for Drizzle ORM in NestJS applications.
|
|
3
|
+
A complete, type-safe CRUD abstraction layer for [Drizzle ORM](https://orm.drizzle.team/) in [NestJS](https://nestjs.com/) applications.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// 1. configure once (app.module.ts)
|
|
9
|
+
DrizzleCrudModule.forRoot({
|
|
10
|
+
dialect: 'postgresql',
|
|
11
|
+
connectionString: process.env.DATABASE_URL,
|
|
12
|
+
schema,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// 2. a fully-featured CRUD service is just:
|
|
16
|
+
export class UsersService extends SqlBaseCrudService<User> {}
|
|
17
|
+
|
|
18
|
+
// 3. bind it to a table (users.module.ts)
|
|
19
|
+
DrizzleCrudModule.forFeature([{ service: UsersService, table: users }]);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Table of contents
|
|
25
|
+
|
|
26
|
+
- [Features](#features)
|
|
27
|
+
- [Installation](#installation)
|
|
28
|
+
- [Quick start](#quick-start)
|
|
29
|
+
- [Configuration](#configuration)
|
|
30
|
+
- [Defining services](#defining-services)
|
|
31
|
+
- [API reference](#api-reference)
|
|
32
|
+
- [Filtering](#filtering)
|
|
33
|
+
- [Pagination & sorting](#pagination--sorting)
|
|
34
|
+
- [Soft delete](#soft-delete)
|
|
35
|
+
- [Bulk operations](#bulk-operations)
|
|
36
|
+
- [Transactions](#transactions)
|
|
37
|
+
- [Full-text search (PostgreSQL)](#full-text-search-postgresql)
|
|
38
|
+
- [Lifecycle hooks & validation](#lifecycle-hooks--validation)
|
|
39
|
+
- [Testing](#testing)
|
|
40
|
+
- [For AI agents / LLM tools](#for-ai-agents--llm-tools)
|
|
41
|
+
|
|
42
|
+
---
|
|
4
43
|
|
|
5
44
|
## Features
|
|
6
45
|
|
|
7
|
-
- ๐ **Complete CRUD
|
|
8
|
-
-
|
|
9
|
-
- โก **Type-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
46
|
+
- ๐ **Complete CRUD** โ `find`, `findOne`, `findAll`, `create`, `update`, `delete`, and more
|
|
47
|
+
- ๐งฉ **Configure once** โ `forRoot()` owns the connection; services only declare a table
|
|
48
|
+
- โก **Type-safe** โ generics over your entity, create/update DTOs and filter types
|
|
49
|
+
- ๐๏ธ **PostgreSQL & MySQL** โ dialect-aware (`RETURNING` vs `insertId`)
|
|
50
|
+
- ๐ **Soft delete** โ opt-in soft delete with `restore`
|
|
51
|
+
- ๐ฆ **Bulk operations** โ mass create/update/delete inside a transaction
|
|
52
|
+
- ๐ **Rich filtering** โ equality, `in`, comparison operators, `like`/`ilike`, null checks
|
|
53
|
+
- ๐ **Full-text search** โ PostgreSQL `tsvector`/`tsquery`
|
|
54
|
+
- ๐ช **Hooks & validation** โ `before*`/`after*` hooks, `validateCreate`/`validateUpdate`
|
|
55
|
+
- ๐งช **Test utilities** โ mock db/table/entity factories
|
|
56
|
+
|
|
57
|
+
---
|
|
16
58
|
|
|
17
59
|
## Installation
|
|
18
60
|
|
|
@@ -20,385 +62,489 @@ A complete, type-safe CRUD abstraction layer for Drizzle ORM in NestJS applicati
|
|
|
20
62
|
npm install nestjs-drizzle-crud
|
|
21
63
|
```
|
|
22
64
|
|
|
23
|
-
|
|
65
|
+
Peer dependencies (install the ones you use):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# always
|
|
69
|
+
npm install @nestjs/common @nestjs/core drizzle-orm reflect-metadata
|
|
70
|
+
|
|
71
|
+
# PostgreSQL (also required if you use `connectionString` with dialect 'postgresql')
|
|
72
|
+
npm install postgres
|
|
73
|
+
|
|
74
|
+
# MySQL
|
|
75
|
+
npm install mysql2
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
> `postgres` is an **optional** peer dependency. It's only needed when you let the
|
|
79
|
+
> module build the connection from a `connectionString` for the `postgresql` dialect.
|
|
80
|
+
> If you pass a pre-built `db` instead, you don't need it.
|
|
81
|
+
|
|
82
|
+
---
|
|
24
83
|
|
|
25
|
-
##
|
|
84
|
+
## Quick start
|
|
26
85
|
|
|
27
|
-
|
|
86
|
+
### 1. Define your Drizzle schema
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// db/schema.ts
|
|
90
|
+
import { pgTable, serial, varchar } from 'drizzle-orm/pg-core';
|
|
91
|
+
|
|
92
|
+
export const users = pgTable('users', {
|
|
93
|
+
id: serial('id').primaryKey(),
|
|
94
|
+
name: varchar('name', { length: 100 }).notNull(),
|
|
95
|
+
email: varchar('email', { length: 255 }).notNull().unique(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
export const schema = { users };
|
|
99
|
+
export type User = typeof users.$inferSelect;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Configure the module once
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
28
105
|
// app.module.ts
|
|
29
106
|
import { Module } from '@nestjs/common';
|
|
30
107
|
import { DrizzleCrudModule } from 'nestjs-drizzle-crud';
|
|
108
|
+
import { schema } from './db/schema';
|
|
109
|
+
import { UsersModule } from './users/users.module';
|
|
31
110
|
|
|
32
111
|
@Module({
|
|
33
112
|
imports: [
|
|
34
113
|
DrizzleCrudModule.forRoot({
|
|
35
|
-
dialect: 'postgresql',
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
timestamps: true,
|
|
39
|
-
pagination: { defaultLimit: 20, maxLimit: 100 },
|
|
40
|
-
},
|
|
114
|
+
dialect: 'postgresql',
|
|
115
|
+
connectionString: process.env.DATABASE_URL,
|
|
116
|
+
schema,
|
|
41
117
|
}),
|
|
118
|
+
UsersModule,
|
|
42
119
|
],
|
|
43
120
|
})
|
|
44
121
|
export class AppModule {}
|
|
45
122
|
```
|
|
46
123
|
|
|
47
|
-
|
|
124
|
+
The connection is created here, exposed globally, and closed automatically on
|
|
125
|
+
application shutdown (when the module built it from a `connectionString`).
|
|
126
|
+
|
|
127
|
+
### 3. Create a service
|
|
48
128
|
|
|
49
|
-
```
|
|
50
|
-
//
|
|
51
|
-
import { Injectable } from '@nestjs/common';
|
|
129
|
+
```typescript
|
|
130
|
+
// users/users.service.ts
|
|
52
131
|
import { SqlBaseCrudService } from 'nestjs-drizzle-crud';
|
|
132
|
+
import type { User } from '../db/schema';
|
|
133
|
+
|
|
134
|
+
export interface CreateUserDto { name: string; email: string }
|
|
135
|
+
export interface UpdateUserDto { name?: string; email?: string }
|
|
136
|
+
export interface UserFilters { name?: string; email?: string }
|
|
137
|
+
|
|
138
|
+
export class UsersService extends SqlBaseCrudService<
|
|
139
|
+
User,
|
|
140
|
+
CreateUserDto,
|
|
141
|
+
UpdateUserDto,
|
|
142
|
+
UserFilters
|
|
143
|
+
> {}
|
|
144
|
+
```
|
|
53
145
|
|
|
54
|
-
|
|
55
|
-
export const users = {
|
|
56
|
-
id: 'id',
|
|
57
|
-
name: 'name',
|
|
58
|
-
email: 'email',
|
|
59
|
-
password: 'password',
|
|
60
|
-
created_at: 'created_at',
|
|
61
|
-
updated_at: 'updated_at',
|
|
62
|
-
deleted_at: 'deleted_at',
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// DTOs and interfaces
|
|
66
|
-
export interface User {
|
|
67
|
-
id: number;
|
|
68
|
-
name: string;
|
|
69
|
-
email: string;
|
|
70
|
-
password: string;
|
|
71
|
-
created_at: Date;
|
|
72
|
-
updated_at: Date;
|
|
73
|
-
deleted_at: Date | null;
|
|
74
|
-
}
|
|
146
|
+
### 4. Bind the service to a table
|
|
75
147
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
148
|
+
```typescript
|
|
149
|
+
// users/users.module.ts
|
|
150
|
+
import { Module } from '@nestjs/common';
|
|
151
|
+
import { DrizzleCrudModule } from 'nestjs-drizzle-crud';
|
|
152
|
+
import { users } from '../db/schema';
|
|
153
|
+
import { UsersController } from './users.controller';
|
|
154
|
+
import { UsersService } from './users.service';
|
|
81
155
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
156
|
+
@Module({
|
|
157
|
+
imports: [
|
|
158
|
+
DrizzleCrudModule.forFeature([{ service: UsersService, table: users }]),
|
|
159
|
+
],
|
|
160
|
+
controllers: [UsersController],
|
|
161
|
+
})
|
|
162
|
+
export class UsersModule {}
|
|
163
|
+
```
|
|
87
164
|
|
|
88
|
-
|
|
89
|
-
name?: string;
|
|
90
|
-
email?: string;
|
|
91
|
-
}
|
|
165
|
+
### 5. Use it in a controller
|
|
92
166
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
dialect: 'postgresql',
|
|
98
|
-
db,
|
|
99
|
-
table: users,
|
|
100
|
-
primaryKey: 'id',
|
|
101
|
-
primaryKeyType: 'serial',
|
|
102
|
-
softDelete: { enabled: true, column: 'deleted_at' },
|
|
103
|
-
timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' },
|
|
104
|
-
});
|
|
105
|
-
}
|
|
167
|
+
```typescript
|
|
168
|
+
// users/users.controller.ts
|
|
169
|
+
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, Query } from '@nestjs/common';
|
|
170
|
+
import { CreateUserDto, UpdateUserDto, UsersService } from './users.service';
|
|
106
171
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
172
|
+
@Controller('users')
|
|
173
|
+
export class UsersController {
|
|
174
|
+
constructor(private readonly users: UsersService) {}
|
|
175
|
+
|
|
176
|
+
@Get()
|
|
177
|
+
findAll(@Query('page') page = '1', @Query('limit') limit = '20') {
|
|
178
|
+
return this.users.findAll({}, { page: +page, limit: +limit });
|
|
111
179
|
}
|
|
112
180
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
181
|
+
@Get(':id')
|
|
182
|
+
find(@Param('id', ParseIntPipe) id: number) {
|
|
183
|
+
return this.users.find(id);
|
|
117
184
|
}
|
|
118
185
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
created_at: new Date(),
|
|
123
|
-
updated_at: new Date(),
|
|
124
|
-
};
|
|
186
|
+
@Post()
|
|
187
|
+
create(@Body() dto: CreateUserDto) {
|
|
188
|
+
return this.users.create(dto);
|
|
125
189
|
}
|
|
126
190
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
updated_at: new Date(),
|
|
131
|
-
};
|
|
191
|
+
@Put(':id')
|
|
192
|
+
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateUserDto) {
|
|
193
|
+
return this.users.update(id, dto);
|
|
132
194
|
}
|
|
133
195
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return this.
|
|
196
|
+
@Delete(':id')
|
|
197
|
+
remove(@Param('id', ParseIntPipe) id: number) {
|
|
198
|
+
return this.users.delete(id);
|
|
137
199
|
}
|
|
138
200
|
}
|
|
139
201
|
```
|
|
140
202
|
|
|
141
|
-
|
|
203
|
+
---
|
|
142
204
|
|
|
143
|
-
|
|
144
|
-
// user.controller.ts
|
|
145
|
-
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
|
|
146
|
-
import { UserService } from './user.service';
|
|
205
|
+
## Configuration
|
|
147
206
|
|
|
148
|
-
|
|
149
|
-
export class UserController {
|
|
150
|
-
constructor(private readonly userService: UserService) {}
|
|
207
|
+
### `DrizzleCrudModule.forRoot(config)`
|
|
151
208
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
209
|
+
| Field | Type | Description |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `dialect` | `'postgresql' \| 'mysql'` | **Required.** Database dialect. |
|
|
212
|
+
| `connectionString` | `string` | Connection string. The module builds the connection (PostgreSQL only). |
|
|
213
|
+
| `db` | `Drizzle instance` | Alternatively, pass a Drizzle instance you built yourself (any dialect). |
|
|
214
|
+
| `schema` | `Record<string, unknown>` | Drizzle schema, used when building from `connectionString`. |
|
|
215
|
+
| `defaults.softDelete` | `boolean` | Enable soft delete for all entities (default `true`). |
|
|
216
|
+
| `defaults.timestamps` | `boolean` | Auto-manage `created_at`/`updated_at` for all entities (default `true`). |
|
|
217
|
+
| `defaults.pagination` | `{ defaultLimit, maxLimit }` | Pagination defaults (default `{ 20, 100 }`). |
|
|
218
|
+
| `sql` | `{ caseSensitive, useReturning, jsonSupport, enableFullTextSearch }` | Dialect tuning. `useReturning` defaults to `true` for PostgreSQL, `false` for MySQL. |
|
|
159
219
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
220
|
+
> **Provide exactly one of `connectionString` or `db`.** If your tables have no
|
|
221
|
+
> `created_at`/`updated_at`/`deleted_at` columns, set
|
|
222
|
+
> `defaults: { softDelete: false, timestamps: false }`.
|
|
164
223
|
|
|
165
|
-
|
|
166
|
-
async create(@Body() createUserDto: CreateUserDto) {
|
|
167
|
-
return this.userService.create(createUserDto);
|
|
168
|
-
}
|
|
224
|
+
**Build the connection yourself (any dialect, recommended for MySQL):**
|
|
169
225
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
226
|
+
```typescript
|
|
227
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
228
|
+
import postgres from 'postgres';
|
|
229
|
+
|
|
230
|
+
DrizzleCrudModule.forRoot({
|
|
231
|
+
dialect: 'postgresql',
|
|
232
|
+
db: drizzle(postgres(process.env.DATABASE_URL!), { schema }),
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### `DrizzleCrudModule.forRootAsync(options)`
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
DrizzleCrudModule.forRootAsync({
|
|
240
|
+
imports: [ConfigModule],
|
|
241
|
+
inject: [ConfigService],
|
|
242
|
+
useFactory: (cfg: ConfigService) => ({
|
|
243
|
+
dialect: 'postgresql',
|
|
244
|
+
connectionString: cfg.get('DATABASE_URL'),
|
|
245
|
+
schema,
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### `DrizzleCrudModule.forFeature(entities)`
|
|
251
|
+
|
|
252
|
+
Registers one or more services and binds each to its table. Per-entity overrides
|
|
253
|
+
go in `config`:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
DrizzleCrudModule.forFeature([
|
|
257
|
+
{ service: UsersService, table: users },
|
|
258
|
+
{
|
|
259
|
+
service: PostsService,
|
|
260
|
+
table: posts,
|
|
261
|
+
config: {
|
|
262
|
+
primaryKey: 'uuid',
|
|
263
|
+
primaryKeyType: 'uuid',
|
|
264
|
+
softDelete: { enabled: true, column: 'deleted_at' },
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
]);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Anything in `config` overrides the project defaults for that entity. The shape is
|
|
271
|
+
[`SqlCrudConfig`](#sqlcrudconfig) minus `db`/`dialect`.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Defining services
|
|
276
|
+
|
|
277
|
+
The minimal service is an empty class โ connection, dialect and defaults are
|
|
278
|
+
injected by the module:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
export class UsersService extends SqlBaseCrudService<User> {}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Add custom behaviour by overriding hooks (see [Lifecycle hooks](#lifecycle-hooks--validation))
|
|
285
|
+
or add your own methods:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
export class UsersService extends SqlBaseCrudService<User, CreateUserDto, UpdateUserDto, UserFilters> {
|
|
289
|
+
findByEmail(email: string) {
|
|
290
|
+
return this.findOne({ email } as Partial<User>);
|
|
173
291
|
}
|
|
174
292
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return this.userService.softDelete(+id);
|
|
293
|
+
protected async validateCreate(data: CreateUserDto): Promise<void> {
|
|
294
|
+
if (!data.email.includes('@')) throw new Error('Invalid email');
|
|
178
295
|
}
|
|
179
296
|
}
|
|
180
297
|
```
|
|
181
298
|
|
|
182
|
-
|
|
299
|
+
---
|
|
183
300
|
|
|
184
|
-
|
|
301
|
+
## API reference
|
|
185
302
|
|
|
186
|
-
|
|
187
|
-
// Mass create users
|
|
188
|
-
const users = await this.userService.massCreate(userDtos);
|
|
303
|
+
`SqlBaseCrudService<T, CreateDto = Partial<T>, UpdateDto = Partial<T>, FilterDto = Partial<T>>`
|
|
189
304
|
|
|
190
|
-
|
|
191
|
-
const updatedUsers = await this.userService.massUpdate(
|
|
192
|
-
[1, 2, 3],
|
|
193
|
-
{ status: 'active' }
|
|
194
|
-
);
|
|
305
|
+
### Read
|
|
195
306
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
307
|
+
| Method | Returns | Notes |
|
|
308
|
+
|---|---|---|
|
|
309
|
+
| `find(id, options?)` | `Promise<T \| null>` | By primary key. Skips soft-deleted rows. |
|
|
310
|
+
| `findOne(where, options?)` | `Promise<T \| null>` | `where` is a `Partial<T>` (equality only). |
|
|
311
|
+
| `findAll(filters?, pagination?, options?)` | `Promise<{ data: T[]; total: number; page: number; limit: number }>` | See [Filtering](#filtering) / [Pagination](#pagination--sorting). |
|
|
312
|
+
| `exists(id, options?)` | `Promise<boolean>` | |
|
|
313
|
+
| `count(filters?, options?)` | `Promise<number>` | |
|
|
199
314
|
|
|
200
|
-
###
|
|
315
|
+
### Write
|
|
201
316
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
207
|
-
|
|
317
|
+
| Method | Returns | Notes |
|
|
318
|
+
|---|---|---|
|
|
319
|
+
| `create(data, options?)` | `Promise<T>` | Runs `validateCreate` โ `beforeCreate` โ insert โ `afterCreate`. |
|
|
320
|
+
| `update(id, data, options?)` | `Promise<T>` | Throws `EntityNotFoundException` if missing. |
|
|
321
|
+
| `delete(id, options?)` | `Promise<boolean>` | Hard delete. |
|
|
322
|
+
| `softDelete(id, options?)` | `Promise<boolean>` | Requires soft delete enabled. |
|
|
323
|
+
| `restore(id, options?)` | `Promise<T>` | Clears the soft-delete column. |
|
|
208
324
|
|
|
209
|
-
###
|
|
325
|
+
### Bulk (run inside a transaction)
|
|
210
326
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
327
|
+
| Method | Returns |
|
|
328
|
+
|---|---|
|
|
329
|
+
| `massCreate(data[], options?)` | `Promise<T[]>` |
|
|
330
|
+
| `massUpdate(ids[], data, options?)` | `Promise<T[]>` |
|
|
331
|
+
| `massSoftDelete(ids[], options?)` | `Promise<boolean>` |
|
|
332
|
+
| `massRestore(ids[], options?)` | `Promise<T[]>` |
|
|
333
|
+
| `massDelete(ids[], options?)` | `Promise<boolean>` |
|
|
334
|
+
|
|
335
|
+
### Search
|
|
336
|
+
|
|
337
|
+
| Method | Returns |
|
|
338
|
+
|---|---|
|
|
339
|
+
| `fullTextSearch(term, columns, pagination?, options?)` | `Promise<{ data: T[]; total: number }>` (PostgreSQL only) |
|
|
340
|
+
|
|
341
|
+
### `SqlOperationOptions`
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
interface SqlOperationOptions {
|
|
345
|
+
transaction?: any; // run within an existing transaction
|
|
346
|
+
select?: string[]; // return only these columns
|
|
347
|
+
relations?: string[]; // reserved โ not yet implemented (no-op)
|
|
348
|
+
hooks?: { skipBefore?: boolean; skipAfter?: boolean };
|
|
349
|
+
lock?: 'update' | 'share' | 'none';
|
|
350
|
+
forNoKeyUpdate?: boolean;
|
|
351
|
+
}
|
|
216
352
|
```
|
|
217
353
|
|
|
218
|
-
|
|
354
|
+
> โ ๏ธ `relations` is reserved for future use and currently does nothing.
|
|
219
355
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
});
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Filtering
|
|
359
|
+
|
|
360
|
+
`findAll(filters)` / `count(filters)` accept an object keyed by column name.
|
|
361
|
+
Unknown keys and `null`/`undefined` values are ignored.
|
|
227
362
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
363
|
+
```typescript
|
|
364
|
+
await service.findAll({
|
|
365
|
+
status: 'active', // string: exact match (case-insensitive when sql.caseSensitive === false)
|
|
366
|
+
role: ['admin', 'editor'], // array: IN (...)
|
|
367
|
+
age: { gte: 18, lt: 65 }, // comparison operators
|
|
368
|
+
name: { ilike: 'jo%' }, // pattern match โ you supply the wildcards
|
|
369
|
+
deleted_at: { isNull: true }, // null checks
|
|
232
370
|
});
|
|
233
371
|
```
|
|
234
372
|
|
|
235
|
-
|
|
236
|
-
## Async Configuration
|
|
373
|
+
**Operators** (inside an object value):
|
|
237
374
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
timestamps: true,
|
|
246
|
-
},
|
|
247
|
-
}),
|
|
248
|
-
inject: [ConfigService],
|
|
249
|
-
}),
|
|
250
|
-
```
|
|
375
|
+
| Operator | SQL |
|
|
376
|
+
|---|---|
|
|
377
|
+
| `gt` / `gte` / `lt` / `lte` | `>` `>=` `<` `<=` |
|
|
378
|
+
| `neq` | `<>` |
|
|
379
|
+
| `like` / `ilike` | `LIKE` / `ILIKE` โ **pass your own `%` wildcards** |
|
|
380
|
+
| `in` | `IN (...)` |
|
|
381
|
+
| `isNull` / `isNotNull` | `IS NULL` / `IS NOT NULL` |
|
|
251
382
|
|
|
252
|
-
|
|
383
|
+
> A bare string value is an **exact** match. When `sql.caseSensitive` is `false`
|
|
384
|
+
> (the default) it uses `ILIKE` *without* wildcards (case-insensitive exact match).
|
|
385
|
+
> For partial matching, use the explicit `like`/`ilike` operators with wildcards.
|
|
253
386
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
},
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
table: postsTable,
|
|
265
|
-
service: PostService,
|
|
266
|
-
config: { softDelete: { enabled: false } },
|
|
267
|
-
},
|
|
268
|
-
]),
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Pagination & sorting
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
await service.findAll(
|
|
393
|
+
{},
|
|
394
|
+
{ page: 2, limit: 25, sortBy: 'created_at', sortOrder: 'desc' },
|
|
395
|
+
);
|
|
396
|
+
// โ { data: [...], total: 240, page: 2, limit: 25 }
|
|
269
397
|
```
|
|
270
398
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
mockDb = TestCrudFactory.createMockDb();
|
|
282
|
-
const mockTable = TestCrudFactory.createMockTable();
|
|
283
|
-
|
|
284
|
-
service = TestCrudFactory.createTestService(
|
|
285
|
-
UserService,
|
|
286
|
-
mockDb,
|
|
287
|
-
mockTable
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('should create user', async () => {
|
|
292
|
-
const createDto = { name: 'John', email: 'john@test.com' };
|
|
293
|
-
const mockEntity = TestCrudFactory.createMockEntity();
|
|
294
|
-
|
|
295
|
-
mockDb.insert.mockReturnValue({
|
|
296
|
-
values: jest.fn().mockReturnThis(),
|
|
297
|
-
returning: jest.fn().mockResolvedValue([mockEntity]),
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
const result = await service.create(createDto);
|
|
301
|
-
expect(result).toEqual(mockEntity);
|
|
302
|
-
});
|
|
303
|
-
});
|
|
399
|
+
`limit` is capped at `pagination.maxLimit`. `sortOrder` defaults to `'desc'`.
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Soft delete
|
|
404
|
+
|
|
405
|
+
Enable per-project via `defaults.softDelete` or per-entity via `forFeature` config:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
{ service: UsersService, table: users, config: { softDelete: { enabled: true, column: 'deleted_at' } } }
|
|
304
409
|
```
|
|
305
410
|
|
|
306
|
-
|
|
307
|
-
|
|
411
|
+
- `softDelete(id)` sets the column to the current timestamp.
|
|
412
|
+
- `restore(id)` sets it back to `null`.
|
|
413
|
+
- `find`/`findOne`/`findAll`/`count` automatically exclude soft-deleted rows.
|
|
308
414
|
|
|
309
|
-
|
|
415
|
+
---
|
|
310
416
|
|
|
311
|
-
|
|
417
|
+
## Bulk operations
|
|
312
418
|
|
|
313
|
-
|
|
419
|
+
```typescript
|
|
420
|
+
await service.massCreate([dto1, dto2, dto3]);
|
|
421
|
+
await service.massUpdate([1, 2, 3], { status: 'archived' });
|
|
422
|
+
await service.massSoftDelete([1, 2, 3]);
|
|
423
|
+
```
|
|
314
424
|
|
|
315
|
-
|
|
425
|
+
All bulk methods run inside a single transaction; if any row fails, a
|
|
426
|
+
`BulkOperationException` (carrying the per-row errors) is thrown and the
|
|
427
|
+
transaction rolls back.
|
|
316
428
|
|
|
317
|
-
|
|
429
|
+
---
|
|
318
430
|
|
|
319
|
-
|
|
431
|
+
## Transactions
|
|
320
432
|
|
|
321
|
-
|
|
433
|
+
```typescript
|
|
434
|
+
await service.executeSqlTransaction(async (tx) => {
|
|
435
|
+
const user = await service.create(userDto, { transaction: tx });
|
|
436
|
+
await profileService.create({ userId: user.id }, { transaction: tx });
|
|
437
|
+
});
|
|
438
|
+
```
|
|
322
439
|
|
|
323
|
-
|
|
440
|
+
Pass `{ transaction: tx }` in `options` to any method to enlist it.
|
|
324
441
|
|
|
325
|
-
|
|
326
|
-
* massCreate(data[], options?) - Create multiple entities
|
|
442
|
+
---
|
|
327
443
|
|
|
328
|
-
|
|
444
|
+
## Full-text search (PostgreSQL)
|
|
329
445
|
|
|
330
|
-
|
|
446
|
+
```typescript
|
|
447
|
+
const { data, total } = await service.fullTextSearch(
|
|
448
|
+
'john doe',
|
|
449
|
+
['name', 'email', 'bio'],
|
|
450
|
+
{ page: 1, limit: 20 },
|
|
451
|
+
);
|
|
452
|
+
```
|
|
331
453
|
|
|
332
|
-
|
|
454
|
+
Builds `to_tsvector(...) @@ plainto_tsquery(...)` across the given columns and
|
|
455
|
+
orders by `ts_rank`. Throws if the dialect is not `postgresql`.
|
|
333
456
|
|
|
334
|
-
|
|
457
|
+
---
|
|
335
458
|
|
|
336
|
-
##
|
|
459
|
+
## Lifecycle hooks & validation
|
|
337
460
|
|
|
338
|
-
|
|
461
|
+
Override any of these `protected` methods in your service (all are optional;
|
|
462
|
+
defaults are no-op / pass-through):
|
|
339
463
|
|
|
340
|
-
|
|
464
|
+
```typescript
|
|
465
|
+
protected validateCreate(data: CreateDto): Promise<void>
|
|
466
|
+
protected validateUpdate(id: any, data: UpdateDto): Promise<void>
|
|
467
|
+
protected mapCreateDtoToEntity(data: CreateDto): Record<string, any>
|
|
468
|
+
protected mapUpdateDtoToEntity(data: UpdateDto): Record<string, any>
|
|
341
469
|
|
|
342
|
-
|
|
470
|
+
protected beforeCreate(data: CreateDto): Promise<CreateDto>
|
|
471
|
+
protected afterCreate(entity: T): Promise<void>
|
|
472
|
+
protected beforeUpdate(id: any, data: UpdateDto): Promise<UpdateDto>
|
|
473
|
+
protected afterUpdate(entity: T): Promise<void>
|
|
474
|
+
protected beforeDelete(id: any): Promise<void>
|
|
475
|
+
protected afterDelete(id: any): Promise<void>
|
|
476
|
+
protected beforeSoftDelete(id: any): Promise<void>
|
|
477
|
+
protected afterSoftDelete(id: any): Promise<void>
|
|
478
|
+
protected beforeRestore(id: any): Promise<void>
|
|
479
|
+
protected afterRestore(entity: T): Promise<void>
|
|
480
|
+
```
|
|
343
481
|
|
|
482
|
+
`mapCreateDtoToEntity` / `mapUpdateDtoToEntity` transform the incoming DTO into the
|
|
483
|
+
row to persist (default returns a shallow copy). When `timestamps` is enabled, the
|
|
484
|
+
service stamps `created_at`/`updated_at` automatically.
|
|
344
485
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
// Soft delete
|
|
357
|
-
softDelete?: {
|
|
358
|
-
enabled: boolean;
|
|
359
|
-
column: string;
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
// Timestamps
|
|
363
|
-
timestamps?: {
|
|
364
|
-
createdAt: string;
|
|
365
|
-
updatedAt: string;
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
// Pagination
|
|
369
|
-
pagination?: {
|
|
370
|
-
defaultLimit: number;
|
|
371
|
-
maxLimit: number;
|
|
372
|
-
};
|
|
373
|
-
}
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Testing
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
import { TestCrudFactory } from 'nestjs-drizzle-crud';
|
|
492
|
+
|
|
493
|
+
const mockDb = TestCrudFactory.createMockDb();
|
|
494
|
+
const mockTable = TestCrudFactory.createMockTable();
|
|
495
|
+
const service = TestCrudFactory.createTestService(UsersService, mockDb, mockTable);
|
|
374
496
|
```
|
|
375
497
|
|
|
376
|
-
|
|
377
|
-
|
|
498
|
+
`TestCrudFactory` provides `createMockDb()`, `createMockTable()`,
|
|
499
|
+
`createMockEntity()` and `createTestService()` for unit tests without a database.
|
|
378
500
|
|
|
379
|
-
|
|
501
|
+
---
|
|
380
502
|
|
|
381
|
-
|
|
503
|
+
## For AI agents / LLM tools
|
|
382
504
|
|
|
383
|
-
|
|
505
|
+
Concise, accurate facts for code generation. Prefer these over guessing.
|
|
384
506
|
|
|
385
|
-
|
|
507
|
+
**Package:** `nestjs-drizzle-crud` ยท **Peers:** `@nestjs/common`, `@nestjs/core`, `drizzle-orm`, `reflect-metadata`; optional `postgres` (PG) / `mysql2` (MySQL).
|
|
386
508
|
|
|
387
|
-
|
|
388
|
-
|
|
509
|
+
**Setup is two steps and no per-service connection wiring:**
|
|
510
|
+
1. `DrizzleCrudModule.forRoot({ dialect, connectionString | db, schema, defaults })` once in `AppModule`.
|
|
511
|
+
2. `DrizzleCrudModule.forFeature([{ service, table, config? }])` in each feature module.
|
|
389
512
|
|
|
390
|
-
|
|
391
|
-
|
|
513
|
+
**A service is an empty subclass โ do NOT inject the db or pass `dialect`/`db`:**
|
|
514
|
+
```typescript
|
|
515
|
+
export class XService extends SqlBaseCrudService<X, CreateXDto, UpdateXDto, XFilters> {}
|
|
516
|
+
```
|
|
392
517
|
|
|
518
|
+
**Rules / gotchas:**
|
|
519
|
+
- Generics order: `SqlBaseCrudService<Entity, CreateDto, UpdateDto, FilterDto>`. All but `Entity` default to `Partial<Entity>`.
|
|
520
|
+
- Do **not** add an `@Inject('DRIZZLE_DB')` constructor โ `forFeature` constructs the service for you. Adding a constructor that calls `super({...})` is the legacy/manual pattern and is unnecessary.
|
|
521
|
+
- The table is passed in `forFeature`, **not** in the service.
|
|
522
|
+
- If tables lack timestamp/soft-delete columns, set `defaults: { softDelete: false, timestamps: false }`, else inserts will reference non-existent columns.
|
|
523
|
+
- `findAll` returns `{ data, total, page, limit }` โ not a bare array.
|
|
524
|
+
- Filter operators live inside an object value: `{ age: { gte: 18 } }`. `like`/`ilike` require caller-supplied `%` wildcards; a bare string is exact match.
|
|
525
|
+
- `delete`/`softDelete` return `boolean`; `update`/`restore` return the entity and throw `EntityNotFoundException` when missing.
|
|
526
|
+
- `relations` option is not implemented (no-op).
|
|
527
|
+
- Full-text search is PostgreSQL-only.
|
|
528
|
+
- Exports: `SqlBaseCrudService`, `DrizzleCrudModule`, `DRIZZLE_DB`, `DRIZZLE_CRUD_CONFIG`, `TestCrudFactory`, exceptions (`EntityNotFoundException`, `BulkOperationException`, โฆ), and types (`SqlCrudConfig`, `SqlOperationOptions`, `DrizzleCrudConfig`, `CrudFeature`, `SqlDialect`, `PrimaryKeyType`).
|
|
529
|
+
|
|
530
|
+
### `SqlCrudConfig`
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
interface SqlCrudConfig {
|
|
534
|
+
dialect: 'postgresql' | 'mysql';
|
|
535
|
+
db: any; // Drizzle instance (injected by forFeature)
|
|
536
|
+
table: any; // Drizzle table (set by forFeature)
|
|
537
|
+
primaryKey: string; // default 'id'
|
|
538
|
+
primaryKeyType: 'serial' | 'bigserial' | 'int' | 'bigint' | 'uuid';
|
|
539
|
+
softDelete?: { enabled: boolean; column: string };
|
|
540
|
+
timestamps?: { createdAt: string; updatedAt: string };
|
|
541
|
+
pagination?: { defaultLimit: number; maxLimit: number };
|
|
542
|
+
sql?: { caseSensitive: boolean; useReturning: boolean; jsonSupport: boolean; enableFullTextSearch: boolean };
|
|
543
|
+
}
|
|
544
|
+
```
|
|
393
545
|
|
|
394
|
-
|
|
546
|
+
---
|
|
395
547
|
|
|
396
|
-
|
|
397
|
-
2. **Quick start guide** with code examples
|
|
398
|
-
3. **Advanced usage patterns**
|
|
399
|
-
4. **Comprehensive API documentation**
|
|
400
|
-
5. **Testing examples**
|
|
401
|
-
6. **Configuration reference**
|
|
402
|
-
7. **Version compatibility**
|
|
548
|
+
## License
|
|
403
549
|
|
|
404
|
-
|
|
550
|
+
MIT
|