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 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
+ ---
24
83
 
25
- ## 1. Basic Setup
84
+ ## Quick start
26
85
 
27
- ``` typescript
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', // 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
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
- ``` typescript
50
- // user.service.ts
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
- // 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
- }
146
+ ### 4. Bind the service to a table
75
147
 
76
- export interface CreateUserDto {
77
- name: string;
78
- email: string;
79
- password: string;
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
- export interface UpdateUserDto {
83
- name?: string;
84
- email?: string;
85
- password?: string;
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
- export interface UserFilters {
89
- name?: string;
90
- email?: string;
91
- }
165
+ ### 5. Use it in a controller
92
166
 
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
- }
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
- protected async validateCreate(data: CreateUserDto): Promise<void> {
108
- if (!data.email.includes('@')) {
109
- throw new Error('Invalid email format');
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
- protected async validateUpdate(id: number, data: UpdateUserDto): Promise<void> {
114
- if (data.email && !data.email.includes('@')) {
115
- throw new Error('Invalid email format');
116
- }
181
+ @Get(':id')
182
+ find(@Param('id', ParseIntPipe) id: number) {
183
+ return this.users.find(id);
117
184
  }
118
185
 
119
- protected mapCreateDtoToEntity(data: CreateUserDto): Record<string, any> {
120
- return {
121
- ...data,
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
- protected mapUpdateDtoToEntity(data: UpdateUserDto): Record<string, any> {
128
- return {
129
- ...data,
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
- // Custom methods
135
- async findByEmail(email: string): Promise<User | null> {
136
- return this.findOne({ email });
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
- ## 3. Use in Controller
203
+ ---
142
204
 
143
- ``` typescript
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
- @Controller('users')
149
- export class UserController {
150
- constructor(private readonly userService: UserService) {}
207
+ ### `DrizzleCrudModule.forRoot(config)`
151
208
 
152
- @Get()
153
- async findAll(
154
- @Query('page') page: number = 1,
155
- @Query('limit') limit: number = 20
156
- ) {
157
- return this.userService.findAll({}, { page, limit });
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
- @Get(':id')
161
- async find(@Param('id') id: string) {
162
- return this.userService.find(+id);
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
- @Post()
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
- @Put(':id')
171
- async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
172
- return this.userService.update(+id, updateUserDto);
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
- @Delete(':id')
176
- async delete(@Param('id') id: string) {
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
- ## Advanced Usage
299
+ ---
183
300
 
184
- ### Bulk Operations
301
+ ## API reference
185
302
 
186
- ``` typescript
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
- // Mass update
191
- const updatedUsers = await this.userService.massUpdate(
192
- [1, 2, 3],
193
- { status: 'active' }
194
- );
305
+ ### Read
195
306
 
196
- // Mass soft delete
197
- await this.userService.massSoftDelete([1, 2, 3]);
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
- ### Full-Text Search (PostgreSQL)
315
+ ### Write
201
316
 
202
- ``` typescript
203
- const results = await this.userService.fullTextSearch(
204
- 'john doe',
205
- ['name', 'email', 'bio']
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
- ### Transactions
325
+ ### Bulk (run inside a transaction)
210
326
 
211
- ``` typescript
212
- await this.userService.executeSqlTransaction(async (tx) => {
213
- await this.userService.create(user1, { transaction: tx });
214
- await this.userService.create(user2, { transaction: tx });
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
- ### Advanced Filtering
354
+ > โš ๏ธ `relations` is reserved for future use and currently does nothing.
219
355
 
220
- ``` typescript
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
- });
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
- // With relations and selection
229
- const user = await this.userService.find(1, {
230
- relations: ['profile', 'posts'],
231
- select: ['id', 'name', 'email']
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
- ``` typescript
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` |
251
382
 
252
- ## Multiple Entities
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
- ``` typescript
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
- ]),
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
- # Testing
272
- ``` typescript
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
- });
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
- # API Reference
307
- ## Core Methods
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
- * find(id, options?) - Find by primary key
415
+ ---
310
416
 
311
- * findOne(where, options?) - Find by criteria
417
+ ## Bulk operations
312
418
 
313
- * findAll(filters?, pagination?, options?) - Find all with filtering & pagination
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
- * create(data, options?) - Create new entity
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
- * update(id, data, options?) - Update entity
429
+ ---
318
430
 
319
- * softDelete(id, options?) - Soft delete entity
431
+ ## Transactions
320
432
 
321
- * restore(id, options?) - Restore soft-deleted entity
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
- * delete(id, options?) - Hard delete entity
440
+ Pass `{ transaction: tx }` in `options` to any method to enlist it.
324
441
 
325
- ## Bulk Methods
326
- * massCreate(data[], options?) - Create multiple entities
442
+ ---
327
443
 
328
- * massUpdate(ids[], data, options?) - Update multiple entities
444
+ ## Full-text search (PostgreSQL)
329
445
 
330
- * massSoftDelete(ids[], options?) - Soft delete multiple entities
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
- * massRestore(ids[], options?) - Restore multiple entities
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
- * massDelete(ids[], options?) - Hard delete multiple entities
457
+ ---
335
458
 
336
- ## Utility Methods
459
+ ## Lifecycle hooks & validation
337
460
 
338
- * exists(id, options?) - Check if entity exists
461
+ Override any of these `protected` methods in your service (all are optional;
462
+ defaults are no-op / pass-through):
339
463
 
340
- * count(filters?, options?) - Count entities
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
- * fullTextSearch(term, columns, pagination?, options?) - Full-text search (PostgreSQL)
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
- # Configuration Options
346
- ``` typescript
347
- interface SqlCrudConfig {
348
- dialect: 'postgresql' | 'mysql';
349
- db: any; // Drizzle database instance
350
- table: any; // Drizzle table
351
-
352
- // Primary key configuration
353
- primaryKey: string;
354
- 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
- };
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
- # Supported Versions
377
- * NestJS: >=10.0.0
498
+ `TestCrudFactory` provides `createMockDb()`, `createMockTable()`,
499
+ `createMockEntity()` and `createTestService()` for unit tests without a database.
378
500
 
379
- * Drizzle ORM: >=0.28.0
501
+ ---
380
502
 
381
- * Node.js: >=18.0.0
503
+ ## For AI agents / LLM tools
382
504
 
383
- * PostgreSQL: >=12.0
505
+ Concise, accurate facts for code generation. Prefer these over guessing.
384
506
 
385
- * MySQL: >=8.0
507
+ **Package:** `nestjs-drizzle-crud` ยท **Peers:** `@nestjs/common`, `@nestjs/core`, `drizzle-orm`, `reflect-metadata`; optional `postgres` (PG) / `mysql2` (MySQL).
386
508
 
387
- # Contributing
388
- Contributions are welcome! Please feel free to submit a Pull Request.
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
- # License
391
- MIT
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
- This README provides:
546
+ ---
395
547
 
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**
548
+ ## License
403
549
 
404
- It's ready to use and will help users understand how to implement your package quickly!
550
+ MIT