nestjs-drizzle-crud 1.0.5 โ 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 +437 -291
- 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
|
+
---
|
|
83
|
+
|
|
84
|
+
## Quick start
|
|
24
85
|
|
|
25
|
-
|
|
86
|
+
### 1. Define your Drizzle schema
|
|
26
87
|
|
|
27
|
-
```
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
// user.service.ts
|
|
51
|
-
import { Injectable } from '@nestjs/common';
|
|
52
|
-
import { SqlBaseCrudService } from 'nestjs-drizzle-crud';
|
|
53
|
-
|
|
54
|
-
// Your Drizzle table schema
|
|
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
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface CreateUserDto {
|
|
77
|
-
name: string;
|
|
78
|
-
email: string;
|
|
79
|
-
password: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface UpdateUserDto {
|
|
83
|
-
name?: string;
|
|
84
|
-
email?: string;
|
|
85
|
-
password?: string;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export interface UserFilters {
|
|
89
|
-
name?: string;
|
|
90
|
-
email?: string;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
@Injectable()
|
|
94
|
-
export class UserService extends SqlBaseCrudService<User, CreateUserDto, UpdateUserDto, UserFilters> {
|
|
95
|
-
constructor(@Inject('DRIZZLE_DB') db: any) {
|
|
96
|
-
super({
|
|
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
|
-
}
|
|
124
|
+
The connection is created here, exposed globally, and closed automatically on
|
|
125
|
+
application shutdown (when the module built it from a `connectionString`).
|
|
106
126
|
|
|
107
|
-
|
|
108
|
-
if (!data.email.includes('@')) {
|
|
109
|
-
throw new Error('Invalid email format');
|
|
110
|
-
}
|
|
111
|
-
}
|
|
127
|
+
### 3. Create a service
|
|
112
128
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
129
|
+
```typescript
|
|
130
|
+
// users/users.service.ts
|
|
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
|
+
```
|
|
118
145
|
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
...data,
|
|
122
|
-
created_at: new Date(),
|
|
123
|
-
updated_at: new Date(),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
146
|
+
### 4. Bind the service to a table
|
|
126
147
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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';
|
|
133
155
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
156
|
+
@Module({
|
|
157
|
+
imports: [
|
|
158
|
+
DrizzleCrudModule.forFeature([{ service: UsersService, table: users }]),
|
|
159
|
+
],
|
|
160
|
+
controllers: [UsersController],
|
|
161
|
+
})
|
|
162
|
+
export class UsersModule {}
|
|
139
163
|
```
|
|
140
164
|
|
|
141
|
-
|
|
165
|
+
### 5. Use it in a controller
|
|
142
166
|
|
|
143
|
-
```
|
|
144
|
-
//
|
|
145
|
-
import { Controller,
|
|
146
|
-
import {
|
|
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';
|
|
147
171
|
|
|
148
172
|
@Controller('users')
|
|
149
|
-
export class
|
|
150
|
-
constructor(private readonly
|
|
173
|
+
export class UsersController {
|
|
174
|
+
constructor(private readonly users: UsersService) {}
|
|
151
175
|
|
|
152
176
|
@Get()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@Query('limit') limit: number = 20
|
|
156
|
-
) {
|
|
157
|
-
return this.userService.findAll({}, { page, limit });
|
|
177
|
+
findAll(@Query('page') page = '1', @Query('limit') limit = '20') {
|
|
178
|
+
return this.users.findAll({}, { page: +page, limit: +limit });
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
@Get(':id')
|
|
161
|
-
|
|
162
|
-
return this.
|
|
182
|
+
find(@Param('id', ParseIntPipe) id: number) {
|
|
183
|
+
return this.users.find(id);
|
|
163
184
|
}
|
|
164
185
|
|
|
165
186
|
@Post()
|
|
166
|
-
|
|
167
|
-
return this.
|
|
187
|
+
create(@Body() dto: CreateUserDto) {
|
|
188
|
+
return this.users.create(dto);
|
|
168
189
|
}
|
|
169
190
|
|
|
170
191
|
@Put(':id')
|
|
171
|
-
|
|
172
|
-
return this.
|
|
192
|
+
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateUserDto) {
|
|
193
|
+
return this.users.update(id, dto);
|
|
173
194
|
}
|
|
174
195
|
|
|
175
196
|
@Delete(':id')
|
|
176
|
-
|
|
177
|
-
return this.
|
|
197
|
+
remove(@Param('id', ParseIntPipe) id: number) {
|
|
198
|
+
return this.users.delete(id);
|
|
178
199
|
}
|
|
179
200
|
}
|
|
180
201
|
```
|
|
181
202
|
|
|
182
|
-
|
|
203
|
+
---
|
|
183
204
|
|
|
184
|
-
|
|
205
|
+
## Configuration
|
|
185
206
|
|
|
186
|
-
|
|
187
|
-
// Mass create users
|
|
188
|
-
const users = await this.userService.massCreate(userDtos);
|
|
207
|
+
### `DrizzleCrudModule.forRoot(config)`
|
|
189
208
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
)
|
|
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. |
|
|
219
|
+
|
|
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 }`.
|
|
223
|
+
|
|
224
|
+
**Build the connection yourself (any dialect, recommended for MySQL):**
|
|
195
225
|
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
});
|
|
198
234
|
```
|
|
199
235
|
|
|
200
|
-
###
|
|
236
|
+
### `DrizzleCrudModule.forRootAsync(options)`
|
|
201
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
|
+
});
|
|
202
248
|
```
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
]);
|
|
207
268
|
```
|
|
208
269
|
|
|
209
|
-
|
|
270
|
+
Anything in `config` overrides the project defaults for that entity. The shape is
|
|
271
|
+
[`SqlCrudConfig`](#sqlcrudconfig) minus `db`/`dialect`.
|
|
272
|
+
|
|
273
|
+
---
|
|
210
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> {}
|
|
211
282
|
```
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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>);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
protected async validateCreate(data: CreateUserDto): Promise<void> {
|
|
294
|
+
if (!data.email.includes('@')) throw new Error('Invalid email');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
216
297
|
```
|
|
217
298
|
|
|
218
|
-
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## API reference
|
|
302
|
+
|
|
303
|
+
`SqlBaseCrudService<T, CreateDto = Partial<T>, UpdateDto = Partial<T>, FilterDto = Partial<T>>`
|
|
304
|
+
|
|
305
|
+
### Read
|
|
306
|
+
|
|
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>` | |
|
|
219
314
|
|
|
315
|
+
### Write
|
|
316
|
+
|
|
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. |
|
|
324
|
+
|
|
325
|
+
### Bulk (run inside a transaction)
|
|
326
|
+
|
|
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
|
+
}
|
|
220
352
|
```
|
|
221
|
-
// Complex filters
|
|
222
|
-
const results = await this.userService.findAll({
|
|
223
|
-
name: { like: 'John%' },
|
|
224
|
-
age: { gt: 18, lt: 65 },
|
|
225
|
-
status: { in: ['active', 'pending'] }
|
|
226
|
-
});
|
|
227
353
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
354
|
+
> โ ๏ธ `relations` is reserved for future use and currently does nothing.
|
|
355
|
+
|
|
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.
|
|
362
|
+
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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` |
|
|
382
|
+
|
|
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.
|
|
386
|
+
|
|
387
|
+
---
|
|
251
388
|
|
|
252
|
-
##
|
|
389
|
+
## Pagination & sorting
|
|
253
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 }
|
|
254
397
|
```
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
config: { softDelete: { enabled: false } },
|
|
267
|
-
},
|
|
268
|
-
]),
|
|
398
|
+
|
|
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' } } }
|
|
269
409
|
```
|
|
270
410
|
|
|
271
|
-
|
|
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.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Bulk operations
|
|
418
|
+
|
|
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]);
|
|
272
423
|
```
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
});
|
|
424
|
+
|
|
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.
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Transactions
|
|
432
|
+
|
|
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 });
|
|
303
437
|
});
|
|
304
438
|
```
|
|
305
439
|
|
|
306
|
-
|
|
307
|
-
## Core Methods
|
|
440
|
+
Pass `{ transaction: tx }` in `options` to any method to enlist it.
|
|
308
441
|
|
|
309
|
-
|
|
442
|
+
---
|
|
310
443
|
|
|
311
|
-
|
|
444
|
+
## Full-text search (PostgreSQL)
|
|
312
445
|
|
|
313
|
-
|
|
446
|
+
```typescript
|
|
447
|
+
const { data, total } = await service.fullTextSearch(
|
|
448
|
+
'john doe',
|
|
449
|
+
['name', 'email', 'bio'],
|
|
450
|
+
{ page: 1, limit: 20 },
|
|
451
|
+
);
|
|
452
|
+
```
|
|
314
453
|
|
|
315
|
-
|
|
454
|
+
Builds `to_tsvector(...) @@ plainto_tsquery(...)` across the given columns and
|
|
455
|
+
orders by `ts_rank`. Throws if the dialect is not `postgresql`.
|
|
316
456
|
|
|
317
|
-
|
|
457
|
+
---
|
|
318
458
|
|
|
319
|
-
|
|
459
|
+
## Lifecycle hooks & validation
|
|
320
460
|
|
|
321
|
-
|
|
461
|
+
Override any of these `protected` methods in your service (all are optional;
|
|
462
|
+
defaults are no-op / pass-through):
|
|
322
463
|
|
|
323
|
-
|
|
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>
|
|
324
469
|
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
```
|
|
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.
|
|
327
485
|
|
|
328
|
-
|
|
486
|
+
---
|
|
329
487
|
|
|
330
|
-
|
|
488
|
+
## Testing
|
|
331
489
|
|
|
332
|
-
|
|
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);
|
|
496
|
+
```
|
|
333
497
|
|
|
334
|
-
|
|
498
|
+
`TestCrudFactory` provides `createMockDb()`, `createMockTable()`,
|
|
499
|
+
`createMockEntity()` and `createTestService()` for unit tests without a database.
|
|
335
500
|
|
|
336
|
-
|
|
501
|
+
---
|
|
337
502
|
|
|
338
|
-
|
|
503
|
+
## For AI agents / LLM tools
|
|
339
504
|
|
|
340
|
-
|
|
505
|
+
Concise, accurate facts for code generation. Prefer these over guessing.
|
|
341
506
|
|
|
342
|
-
|
|
507
|
+
**Package:** `nestjs-drizzle-crud` ยท **Peers:** `@nestjs/common`, `@nestjs/core`, `drizzle-orm`, `reflect-metadata`; optional `postgres` (PG) / `mysql2` (MySQL).
|
|
343
508
|
|
|
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.
|
|
344
512
|
|
|
345
|
-
|
|
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> {}
|
|
346
516
|
```
|
|
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
|
|
347
533
|
interface SqlCrudConfig {
|
|
348
534
|
dialect: 'postgresql' | 'mysql';
|
|
349
|
-
db: any;
|
|
350
|
-
table: any;
|
|
351
|
-
|
|
352
|
-
// Primary key configuration
|
|
353
|
-
primaryKey: string;
|
|
535
|
+
db: any; // Drizzle instance (injected by forFeature)
|
|
536
|
+
table: any; // Drizzle table (set by forFeature)
|
|
537
|
+
primaryKey: string; // default 'id'
|
|
354
538
|
primaryKeyType: 'serial' | 'bigserial' | 'int' | 'bigint' | 'uuid';
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
};
|
|
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 };
|
|
373
543
|
}
|
|
374
544
|
```
|
|
375
545
|
|
|
376
|
-
|
|
377
|
-
* NestJS: >=10.0.0
|
|
378
|
-
|
|
379
|
-
* Drizzle ORM: >=0.28.0
|
|
380
|
-
|
|
381
|
-
* Node.js: >=18.0.0
|
|
382
|
-
|
|
383
|
-
* PostgreSQL: >=12.0
|
|
384
|
-
|
|
385
|
-
* MySQL: >=8.0
|
|
546
|
+
---
|
|
386
547
|
|
|
387
|
-
|
|
388
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
548
|
+
## License
|
|
389
549
|
|
|
390
|
-
# License
|
|
391
550
|
MIT
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
This README provides:
|
|
395
|
-
|
|
396
|
-
1. **Clear installation instructions**
|
|
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**
|
|
403
|
-
|
|
404
|
-
It's ready to use and will help users understand how to implement your package quickly!
|