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 CHANGED
@@ -1,18 +1,60 @@
1
- # NestJS Drizzle CRUD
1
+ # nestjs-drizzle-crud
2
2
 
3
- A complete, type-safe CRUD abstraction layer for Drizzle ORM in NestJS applications. Supports PostgreSQL and MySQL with advanced features like soft delete, transactions, bulk operations, and full-text search.
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 Operations** - find, create, update, delete, and more
8
- - ๐Ÿ—ƒ๏ธ **SQL Database Support** - PostgreSQL & MySQL with dialect-specific optimizations
9
- - โšก **Type-Safe** - Full TypeScript support with generics
10
- - ๐Ÿ”„ **Soft Delete** - Built-in soft delete with restore functionality
11
- - ๐Ÿ“ฆ **Bulk Operations** - Mass create, update, delete with transaction support
12
- - ๐Ÿ” **Advanced Querying** - Filtering, pagination, sorting, full-text search
13
- - ๐ŸŽฏ **NestJS Native** - Seamless integration with NestJS dependency injection
14
- - ๐Ÿงช **Test Utilities** - Comprehensive testing helpers
15
- - ๐Ÿ›ก๏ธ **Production Ready** - Error handling, transactions, and validation hooks
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
- # Quick Start
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
- ## 1. Basic Setup
86
+ ### 1. Define your Drizzle schema
26
87
 
27
- ``` typescript
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', // or 'mysql'
36
- defaults: {
37
- softDelete: true,
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
- ## 2. Create a CRUD Service
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
- protected async validateCreate(data: CreateUserDto): Promise<void> {
108
- if (!data.email.includes('@')) {
109
- throw new Error('Invalid email format');
110
- }
111
- }
127
+ ### 3. Create a service
112
128
 
113
- protected async validateUpdate(id: number, data: UpdateUserDto): Promise<void> {
114
- if (data.email && !data.email.includes('@')) {
115
- throw new Error('Invalid email format');
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
- protected mapCreateDtoToEntity(data: CreateUserDto): Record<string, any> {
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
- protected mapUpdateDtoToEntity(data: UpdateUserDto): Record<string, any> {
128
- return {
129
- ...data,
130
- updated_at: new Date(),
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
- // Custom methods
135
- async findByEmail(email: string): Promise<User | null> {
136
- return this.findOne({ email });
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
- ## 3. Use in Controller
165
+ ### 5. Use it in a controller
142
166
 
143
- ```
144
- // user.controller.ts
145
- import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
146
- import { UserService } from './user.service';
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 UserController {
150
- constructor(private readonly userService: UserService) {}
173
+ export class UsersController {
174
+ constructor(private readonly users: UsersService) {}
151
175
 
152
176
  @Get()
153
- async findAll(
154
- @Query('page') page: number = 1,
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
- async find(@Param('id') id: string) {
162
- return this.userService.find(+id);
182
+ find(@Param('id', ParseIntPipe) id: number) {
183
+ return this.users.find(id);
163
184
  }
164
185
 
165
186
  @Post()
166
- async create(@Body() createUserDto: CreateUserDto) {
167
- return this.userService.create(createUserDto);
187
+ create(@Body() dto: CreateUserDto) {
188
+ return this.users.create(dto);
168
189
  }
169
190
 
170
191
  @Put(':id')
171
- async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
172
- return this.userService.update(+id, updateUserDto);
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
- async delete(@Param('id') id: string) {
177
- return this.userService.softDelete(+id);
197
+ remove(@Param('id', ParseIntPipe) id: number) {
198
+ return this.users.delete(id);
178
199
  }
179
200
  }
180
201
  ```
181
202
 
182
- ## Advanced Usage
203
+ ---
183
204
 
184
- ### Bulk Operations
205
+ ## Configuration
185
206
 
186
- ```
187
- // Mass create users
188
- const users = await this.userService.massCreate(userDtos);
207
+ ### `DrizzleCrudModule.forRoot(config)`
189
208
 
190
- // Mass update
191
- const updatedUsers = await this.userService.massUpdate(
192
- [1, 2, 3],
193
- { status: 'active' }
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
- // Mass soft delete
197
- await this.userService.massSoftDelete([1, 2, 3]);
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
- ### Full-Text Search (PostgreSQL)
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
- const results = await this.userService.fullTextSearch(
204
- 'john doe',
205
- ['name', 'email', 'bio']
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
- ### Transactions
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
- await this.userService.executeSqlTransaction(async (tx) => {
213
- await this.userService.create(user1, { transaction: tx });
214
- await this.userService.create(user2, { transaction: tx });
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
- ### Advanced Filtering
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
- // With relations and selection
229
- const user = await this.userService.find(1, {
230
- relations: ['profile', 'posts'],
231
- select: ['id', 'name', 'email']
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
- # Module Configuration
236
- ## Async Configuration
373
+ **Operators** (inside an object value):
237
374
 
238
- ```
239
- DrizzleCrudModule.forRootAsync({
240
- imports: [ConfigModule],
241
- useFactory: async (configService: ConfigService) => ({
242
- dialect: configService.get('DATABASE_DIALECT'),
243
- defaults: {
244
- softDelete: true,
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` |
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
- ## Multiple Entities
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
- DrizzleCrudModule.forFeature([
256
- {
257
- name: 'User',
258
- table: usersTable,
259
- service: UserService,
260
- config: { primaryKey: 'id' },
261
- },
262
- {
263
- name: 'Post',
264
- table: postsTable,
265
- service: PostService,
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
- # Testing
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
- // user.service.spec.ts
274
- import { TestCrudFactory } from 'nestjs-drizzle-crud/test-utils';
275
-
276
- describe('UserService', () => {
277
- let service: UserService;
278
- let mockDb: any;
279
-
280
- beforeEach(() => {
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
- });
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
- # API Reference
307
- ## Core Methods
440
+ Pass `{ transaction: tx }` in `options` to any method to enlist it.
308
441
 
309
- * find(id, options?) - Find by primary key
442
+ ---
310
443
 
311
- * findOne(where, options?) - Find by criteria
444
+ ## Full-text search (PostgreSQL)
312
445
 
313
- * findAll(filters?, pagination?, options?) - Find all with filtering & pagination
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
- * create(data, options?) - Create new entity
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
- * update(id, data, options?) - Update entity
457
+ ---
318
458
 
319
- * softDelete(id, options?) - Soft delete entity
459
+ ## Lifecycle hooks & validation
320
460
 
321
- * restore(id, options?) - Restore soft-deleted entity
461
+ Override any of these `protected` methods in your service (all are optional;
462
+ defaults are no-op / pass-through):
322
463
 
323
- * delete(id, options?) - Hard delete entity
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
- ## Bulk Methods
326
- * massCreate(data[], options?) - Create multiple entities
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
- * massUpdate(ids[], data, options?) - Update multiple entities
486
+ ---
329
487
 
330
- * massSoftDelete(ids[], options?) - Soft delete multiple entities
488
+ ## Testing
331
489
 
332
- * massRestore(ids[], options?) - Restore multiple entities
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
- * massDelete(ids[], options?) - Hard delete multiple entities
498
+ `TestCrudFactory` provides `createMockDb()`, `createMockTable()`,
499
+ `createMockEntity()` and `createTestService()` for unit tests without a database.
335
500
 
336
- ## Utility Methods
501
+ ---
337
502
 
338
- * exists(id, options?) - Check if entity exists
503
+ ## For AI agents / LLM tools
339
504
 
340
- * count(filters?, options?) - Count entities
505
+ Concise, accurate facts for code generation. Prefer these over guessing.
341
506
 
342
- * fullTextSearch(term, columns, pagination?, options?) - Full-text search (PostgreSQL)
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
- # Configuration Options
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; // Drizzle database instance
350
- table: any; // Drizzle table
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
- // 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
- };
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
- # Supported Versions
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
- # Contributing
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!