@angelps/prisma-query-builder 0.1.1
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 +709 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/valibot.adapter.d.ts +14 -0
- package/dist/adapters/valibot.adapter.d.ts.map +1 -0
- package/dist/adapters/valibot.adapter.js +21 -0
- package/dist/adapters/valibot.adapter.js.map +1 -0
- package/dist/adapters/zod.adapter.d.ts +29 -0
- package/dist/adapters/zod.adapter.d.ts.map +1 -0
- package/dist/adapters/zod.adapter.js +42 -0
- package/dist/adapters/zod.adapter.js.map +1 -0
- package/dist/controllers/generic.controller.d.ts +79 -0
- package/dist/controllers/generic.controller.d.ts.map +1 -0
- package/dist/controllers/generic.controller.js +279 -0
- package/dist/controllers/generic.controller.js.map +1 -0
- package/dist/controllers/index.d.ts +2 -0
- package/dist/controllers/index.d.ts.map +1 -0
- package/dist/controllers/index.js +2 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/errors/app-errors.d.ts +17 -0
- package/dist/errors/app-errors.d.ts.map +1 -0
- package/dist/errors/app-errors.js +28 -0
- package/dist/errors/app-errors.js.map +1 -0
- package/dist/errors/error-messages.d.ts +80 -0
- package/dist/errors/error-messages.d.ts.map +1 -0
- package/dist/errors/error-messages.js +75 -0
- package/dist/errors/error-messages.js.map +1 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/query-builder/helpers/order-by.helper.d.ts +4 -0
- package/dist/query-builder/helpers/order-by.helper.d.ts.map +1 -0
- package/dist/query-builder/helpers/order-by.helper.js +24 -0
- package/dist/query-builder/helpers/order-by.helper.js.map +1 -0
- package/dist/query-builder/helpers/pagination.helper.d.ts +6 -0
- package/dist/query-builder/helpers/pagination.helper.d.ts.map +1 -0
- package/dist/query-builder/helpers/pagination.helper.js +9 -0
- package/dist/query-builder/helpers/pagination.helper.js.map +1 -0
- package/dist/query-builder/helpers/where.helper.d.ts +5 -0
- package/dist/query-builder/helpers/where.helper.d.ts.map +1 -0
- package/dist/query-builder/helpers/where.helper.js +356 -0
- package/dist/query-builder/helpers/where.helper.js.map +1 -0
- package/dist/query-builder/index.d.ts +3 -0
- package/dist/query-builder/index.d.ts.map +1 -0
- package/dist/query-builder/index.js +3 -0
- package/dist/query-builder/index.js.map +1 -0
- package/dist/query-builder/query-builder.d.ts +56 -0
- package/dist/query-builder/query-builder.d.ts.map +1 -0
- package/dist/query-builder/query-builder.js +149 -0
- package/dist/query-builder/query-builder.js.map +1 -0
- package/dist/query-builder/query-builder.types.d.ts +83 -0
- package/dist/query-builder/query-builder.types.d.ts.map +1 -0
- package/dist/query-builder/query-builder.types.js +2 -0
- package/dist/query-builder/query-builder.types.js.map +1 -0
- package/dist/repositories/crud.repository.prisma.d.ts +44 -0
- package/dist/repositories/crud.repository.prisma.d.ts.map +1 -0
- package/dist/repositories/crud.repository.prisma.js +246 -0
- package/dist/repositories/crud.repository.prisma.js.map +1 -0
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.d.ts.map +1 -0
- package/dist/repositories/index.js +2 -0
- package/dist/repositories/index.js.map +1 -0
- package/dist/services/audit.service.d.ts +32 -0
- package/dist/services/audit.service.d.ts.map +1 -0
- package/dist/services/audit.service.js +57 -0
- package/dist/services/audit.service.js.map +1 -0
- package/dist/services/error-handler.service.d.ts +34 -0
- package/dist/services/error-handler.service.d.ts.map +1 -0
- package/dist/services/error-handler.service.js +159 -0
- package/dist/services/error-handler.service.js.map +1 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +3 -0
- package/dist/services/index.js.map +1 -0
- package/dist/types/crud-config.d.ts +20 -0
- package/dist/types/crud-config.d.ts.map +1 -0
- package/dist/types/crud-config.js +2 -0
- package/dist/types/crud-config.js.map +1 -0
- package/dist/types/generic-repository.d.ts +36 -0
- package/dist/types/generic-repository.d.ts.map +1 -0
- package/dist/types/generic-repository.js +2 -0
- package/dist/types/generic-repository.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/logger.d.ts +17 -0
- package/dist/types/logger.d.ts.map +1 -0
- package/dist/types/logger.js +8 -0
- package/dist/types/logger.js.map +1 -0
- package/dist/types/pagination.d.ts +11 -0
- package/dist/types/pagination.d.ts.map +1 -0
- package/dist/types/pagination.js +2 -0
- package/dist/types/pagination.js.map +1 -0
- package/dist/types/prisma-delegate.types.d.ts +36 -0
- package/dist/types/prisma-delegate.types.d.ts.map +1 -0
- package/dist/types/prisma-delegate.types.js +15 -0
- package/dist/types/prisma-delegate.types.js.map +1 -0
- package/dist/types/query-params.d.ts +40 -0
- package/dist/types/query-params.d.ts.map +1 -0
- package/dist/types/query-params.js +2 -0
- package/dist/types/query-params.js.map +1 -0
- package/dist/types/schema-adapter.d.ts +19 -0
- package/dist/types/schema-adapter.d.ts.map +1 -0
- package/dist/types/schema-adapter.js +2 -0
- package/dist/types/schema-adapter.js.map +1 -0
- package/dist/utils/date.utils.d.ts +13 -0
- package/dist/utils/date.utils.d.ts.map +1 -0
- package/dist/utils/date.utils.js +75 -0
- package/dist/utils/date.utils.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/phone.utils.d.ts +7 -0
- package/dist/utils/phone.utils.d.ts.map +1 -0
- package/dist/utils/phone.utils.js +15 -0
- package/dist/utils/phone.utils.js.map +1 -0
- package/dist/utils/response.utils.d.ts +5 -0
- package/dist/utils/response.utils.d.ts.map +1 -0
- package/dist/utils/response.utils.js +13 -0
- package/dist/utils/response.utils.js.map +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
# @angelps/prisma-query-builder
|
|
2
|
+
|
|
3
|
+
Core library for backend projects built with Prisma + Express. Provides a query builder for HTTP parameters, a generic CRUD repository, a generic REST controller, an audit service, and shared utilities.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @angelps/prisma-query-builder
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Peer dependencies
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @prisma/client express valibot
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> **Note**: `valibot` is the default validation library, but you can use **Zod** or any other validator by providing a custom schema adapter. See [Schema Adapters](#8-schema-adapters).
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Table of contents
|
|
22
|
+
|
|
23
|
+
1. [PrismaQueryBuilder](#1-prismaquerybuildert)
|
|
24
|
+
2. [CrudRepository](#2-crudrepositoryt)
|
|
25
|
+
3. [GenericController](#3-genericcontrollert)
|
|
26
|
+
4. [AuditService](#4-auditservice)
|
|
27
|
+
5. [Error handling](#5-error-handling)
|
|
28
|
+
6. [Utilities](#6-utilities)
|
|
29
|
+
7. [Types reference](#7-types-reference)
|
|
30
|
+
8. [Schema Adapters](#8-schema-adapters)
|
|
31
|
+
9. [Configurable Logger](#9-configurable-logger)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 1. PrismaQueryBuilder\<T\>
|
|
36
|
+
|
|
37
|
+
Translates HTTP query parameters into Prisma `where`, `orderBy`, `skip`, and `take` arguments.
|
|
38
|
+
|
|
39
|
+
### Options
|
|
40
|
+
|
|
41
|
+
| Option | Type | Description |
|
|
42
|
+
| ------------------ | ------------------------ | ------------------------------------------- |
|
|
43
|
+
| `filterableFields` | `(keyof T)[]` | Fields that accept exact-match filtering |
|
|
44
|
+
| `likeFields` | `(keyof T)[]` | Fields included in `search` (OR contains) |
|
|
45
|
+
| `notLikeFields` | `(keyof T)[]` | Fields excluded by `notLike` |
|
|
46
|
+
| `sortableFields` | `(keyof T)[]` | Fields allowed in `sort` |
|
|
47
|
+
| `searchDateFields` | `(keyof T)[]` | Fields filtered by `from`/`to` date range |
|
|
48
|
+
| `operationFields` | `(keyof T)[]` | Fields filtered by `greaterThan`/`lessThan` |
|
|
49
|
+
| `omitFields` | `(keyof T)[]` | Fields excluded from all filters |
|
|
50
|
+
| `orderBy` | `{ field, direction }[]` | Default sort when none is provided |
|
|
51
|
+
|
|
52
|
+
### Supported query params
|
|
53
|
+
|
|
54
|
+
| Param | Behavior |
|
|
55
|
+
| -------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| `page` | Page number (default: 1) |
|
|
57
|
+
| `size` | Page size (default: 10) |
|
|
58
|
+
| `sort` | Comma-separated fields. Prefix `-` for desc: `sort=-createdAt,name` |
|
|
59
|
+
| `search` | OR contains across all `likeFields` |
|
|
60
|
+
| `notLike` | AND NOT contains across `notLikeFields`. Supports CSV |
|
|
61
|
+
| `from` / `to` | Date range filter on `searchDateFields` |
|
|
62
|
+
| `greaterThan` / `lessThan` | Numeric comparison on `operationFields` |
|
|
63
|
+
| `cursor` | Cursor for cursor-based pagination (last seen ID) |
|
|
64
|
+
| `fields` | Comma-separated fields to return (`select`). Ignored if `extraArgs` already has `select`/`include` |
|
|
65
|
+
| Any `filterableField` | Exact match. Auto-casts numbers, booleans, and CSV → `in` |
|
|
66
|
+
|
|
67
|
+
### Field selection
|
|
68
|
+
|
|
69
|
+
Return only the fields you need via the `fields` query param. Translates to Prisma `select`:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
GET /users?fields=id,name,email
|
|
73
|
+
→ select: { id: true, name: true, email: true }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> **Note**: `fields` is ignored when the repository's `extraFindManyArgs` already contains a `select` or `include` key, since Prisma does not allow both at the same level.
|
|
77
|
+
|
|
78
|
+
### Operator suffix filters
|
|
79
|
+
|
|
80
|
+
Apply operators directly to filterable fields via suffixes:
|
|
81
|
+
|
|
82
|
+
| Suffix | Prisma Operator | Example |
|
|
83
|
+
| ------------- | -------------------------- | ---------------------------------------- |
|
|
84
|
+
| `_gte` | `gte` | `GET /products?price_gte=100` |
|
|
85
|
+
| `_lte` | `lte` | `GET /products?price_lte=500` |
|
|
86
|
+
| `_gt` | `gt` | `GET /products?price_gt=0` |
|
|
87
|
+
| `_lt` | `lt` | `GET /products?price_lt=1000` |
|
|
88
|
+
| `_contains` | `contains` (insensitive) | `GET /products?name_contains=shirt` |
|
|
89
|
+
| `_startsWith` | `startsWith` (insensitive) | `GET /products?name_startsWith=A` |
|
|
90
|
+
| `_in` | `in` (CSV → array) | `GET /products?status_in=ACTIVE,PENDING` |
|
|
91
|
+
| `_not` | `not` | `GET /products?status_not=DELETED` |
|
|
92
|
+
|
|
93
|
+
### Null / Not-null filters
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
GET /users?deletedAt=null → WHERE deletedAt IS NULL
|
|
97
|
+
GET /users?deletedAt=!null → WHERE deletedAt IS NOT NULL
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Nested / relational filters
|
|
101
|
+
|
|
102
|
+
Filter by related model fields using dot notation:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
GET /users?orders.status=PENDING
|
|
106
|
+
→ WHERE orders: { some: { status: 'PENDING' } }
|
|
107
|
+
|
|
108
|
+
GET /users?orders.items.productId=5
|
|
109
|
+
→ WHERE orders: { some: { items: { some: { productId: 5 } } } }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### OR compound conditions
|
|
113
|
+
|
|
114
|
+
Combine arbitrary filters with OR using indexed syntax:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
GET /users?or[0][status]=ACTIVE&or[0][role]=ADMIN&or[1][status]=PENDING
|
|
118
|
+
→ WHERE OR: [{ status: 'ACTIVE', role: 'ADMIN' }, { status: 'PENDING' }]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Simplified instantiation
|
|
122
|
+
|
|
123
|
+
Instead of specifying all Prisma types manually, use `PrismaQueryBuilder.from()`:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { PrismaQueryBuilder } from "@angelps/prisma-query-builder";
|
|
127
|
+
|
|
128
|
+
// ✅ NEW — types inferred from delegate
|
|
129
|
+
const qb = PrismaQueryBuilder.from(prisma.user, config, options);
|
|
130
|
+
|
|
131
|
+
// ❌ OLD — still works, but verbose
|
|
132
|
+
const qb = new PrismaQueryBuilder<
|
|
133
|
+
User,
|
|
134
|
+
Prisma.UserWhereInput,
|
|
135
|
+
Prisma.UserOrderByWithRelationInput
|
|
136
|
+
>(config, options);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Direct usage
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { PrismaQueryBuilder } from "@angelps/prisma-query-builder";
|
|
143
|
+
|
|
144
|
+
type User = {
|
|
145
|
+
id: number;
|
|
146
|
+
name: string;
|
|
147
|
+
email: string;
|
|
148
|
+
active: boolean;
|
|
149
|
+
createdAt: Date;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const qb = PrismaQueryBuilder.from(
|
|
153
|
+
prisma.user,
|
|
154
|
+
{ softDeleteField: "deletedAt", excludeSoftDeleted: true },
|
|
155
|
+
{
|
|
156
|
+
filterableFields: ["active", "id"],
|
|
157
|
+
likeFields: ["name", "email"],
|
|
158
|
+
sortableFields: ["name", "createdAt"],
|
|
159
|
+
searchDateFields: ["createdAt"],
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Build where clause from HTTP params
|
|
164
|
+
const where = qb.buildWhere({ active: "true", search: "angel" });
|
|
165
|
+
// → { deletedAt: null, active: true, OR: [{ name: { contains: 'angel' } }, ...] }
|
|
166
|
+
|
|
167
|
+
// Paginated result (offset-based)
|
|
168
|
+
const result = await qb.findAllAndCount({
|
|
169
|
+
delegate: prisma.user,
|
|
170
|
+
queryParams: req.query,
|
|
171
|
+
buildWhere: (p) => qb.buildWhere(p),
|
|
172
|
+
buildOrderBy: (s) => qb.buildOrderBy(s),
|
|
173
|
+
extraArgs: { include: { posts: true } },
|
|
174
|
+
});
|
|
175
|
+
// result → { items: User[], pagination: { count, pages, size, current } }
|
|
176
|
+
|
|
177
|
+
// Cursor-based pagination (better for large datasets)
|
|
178
|
+
const cursorResult = await qb.findAllWithCursor({
|
|
179
|
+
delegate: prisma.user,
|
|
180
|
+
queryParams: { cursor: 42, size: 10 },
|
|
181
|
+
buildWhere: (p) => qb.buildWhere(p),
|
|
182
|
+
});
|
|
183
|
+
// cursorResult → { items: User[], nextCursor: number | null, hasMore: boolean }
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 2. CrudRepository\<T\>
|
|
189
|
+
|
|
190
|
+
Generic Prisma repository with pagination, soft delete, validation, and audit fields (`createdBy`, `updatedBy`, `deletedBy`).
|
|
191
|
+
|
|
192
|
+
### Setup
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { CrudRepository } from "@angelps/prisma-query-builder";
|
|
196
|
+
import { object, string, number, pipe, minLength } from "valibot";
|
|
197
|
+
|
|
198
|
+
export class UserRepository extends CrudRepository<
|
|
199
|
+
Prisma.User,
|
|
200
|
+
Prisma.UserWhereInput,
|
|
201
|
+
Prisma.UserOrderByWithRelationInput,
|
|
202
|
+
Prisma.UserFindManyArgs
|
|
203
|
+
> {
|
|
204
|
+
createSchema = object({
|
|
205
|
+
name: pipe(string(), minLength(2)),
|
|
206
|
+
email: string(),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
updateSchema = object({
|
|
210
|
+
name: pipe(string(), minLength(2)),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
constructor() {
|
|
214
|
+
super(
|
|
215
|
+
prisma.user, // Prisma delegate
|
|
216
|
+
{
|
|
217
|
+
softDeleteField: "deletedAt",
|
|
218
|
+
excludeSoftDeleted: true,
|
|
219
|
+
primaryKeyField: "id",
|
|
220
|
+
defaultOrderBy: [{ field: "createdAt", direction: "desc" }],
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
filterableFields: ["active", "companyId"],
|
|
224
|
+
likeFields: ["name", "email"],
|
|
225
|
+
sortableFields: ["name", "createdAt"],
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Optional: transform data before hitting Prisma (e.g. string → Date)
|
|
231
|
+
protected transformData(data: Record<string, unknown>) {
|
|
232
|
+
return {
|
|
233
|
+
...data,
|
|
234
|
+
birthDate: data.birthDate
|
|
235
|
+
? new Date(data.birthDate as string)
|
|
236
|
+
: undefined,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export const userRepository = new UserRepository();
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Methods
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// Standard CRUD
|
|
248
|
+
await userRepository.findAll(req.query, options); // PaginationModel<User>
|
|
249
|
+
await userRepository.findById(1, options); // User | null
|
|
250
|
+
await userRepository.findById(1, { include: { posts: true } }); // User with relations
|
|
251
|
+
await userRepository.findOne({ email: "a@b.com" }); // User | null
|
|
252
|
+
await userRepository.create(req.body, options); // User
|
|
253
|
+
await userRepository.update(1, req.body, options); // void
|
|
254
|
+
await userRepository.delete(1, options); // void (soft delete by default)
|
|
255
|
+
|
|
256
|
+
// Bulk operations
|
|
257
|
+
await userRepository.createMany([...items], options); // { count: number }
|
|
258
|
+
await userRepository.updateMany([1, 2, 3], data, opts); // { count: number }
|
|
259
|
+
await userRepository.deleteMany([1, 2], options); // { count: number } (soft delete)
|
|
260
|
+
await userRepository.upsert(data, options); // User
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Using Zod instead of Valibot
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { z } from 'zod';
|
|
267
|
+
import { CrudRepository, ZodAdapter } from '@angelps/prisma-query-builder';
|
|
268
|
+
|
|
269
|
+
class UserRepo extends CrudRepository<...> {
|
|
270
|
+
createSchema = z.object({ name: z.string().min(2), email: z.string().email() });
|
|
271
|
+
updateSchema = z.object({ name: z.string().min(2) });
|
|
272
|
+
|
|
273
|
+
constructor() {
|
|
274
|
+
super(prisma.user, config, options, { schemaAdapter: new ZodAdapter() });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 3. GenericController\<T\>
|
|
282
|
+
|
|
283
|
+
Express controller that wires HTTP methods to `IGenericRepository`. Handles validation, error mapping, audit, configurable user-field injection, and lifecycle hooks.
|
|
284
|
+
|
|
285
|
+
### Setup
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import { GenericController } from "@angelps/prisma-query-builder";
|
|
289
|
+
import { userRepository } from "./user.repository.js";
|
|
290
|
+
import { MyAuditService } from "./audit.service.js";
|
|
291
|
+
import { Router } from "express";
|
|
292
|
+
|
|
293
|
+
class UserController extends GenericController<User> {
|
|
294
|
+
constructor() {
|
|
295
|
+
super(userRepository, "User", {
|
|
296
|
+
audit: true,
|
|
297
|
+
auditService: new MyAuditService(),
|
|
298
|
+
// Inject fields from req.user into query params
|
|
299
|
+
injectFromUser: [
|
|
300
|
+
{ userField: "companyIds", queryField: "companyId" },
|
|
301
|
+
{ userField: "branchIds", queryField: "branchId" },
|
|
302
|
+
],
|
|
303
|
+
// Lifecycle hooks
|
|
304
|
+
hooks: {
|
|
305
|
+
beforeCreate: (req, data) => {
|
|
306
|
+
// Modify data before create
|
|
307
|
+
return { ...data, source: "api" };
|
|
308
|
+
},
|
|
309
|
+
afterCreate: (req, result) => {
|
|
310
|
+
// Send notification, etc.
|
|
311
|
+
},
|
|
312
|
+
beforeUpdate: (req, id, data) => {
|
|
313
|
+
return { ...data, lastModifiedSource: "api" };
|
|
314
|
+
},
|
|
315
|
+
afterDelete: (req, id) => {
|
|
316
|
+
// Cleanup related resources
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const controller = new UserController();
|
|
324
|
+
const router = Router();
|
|
325
|
+
|
|
326
|
+
router.get("/", (req, res, next) => controller.getAll(req, res, next));
|
|
327
|
+
router.get("/:id", (req, res, next) => controller.getById(req, res, next));
|
|
328
|
+
router.post("/", (req, res, next) => controller.create(req, res, next));
|
|
329
|
+
router.put("/:id", (req, res, next) => controller.update(req, res, next));
|
|
330
|
+
router.delete("/:id", (req, res, next) => controller.delete(req, res, next));
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Custom operations with audit
|
|
334
|
+
|
|
335
|
+
Use `executeWithAudit` in subclass methods for non-standard operations:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
class UserController extends GenericController<User> {
|
|
339
|
+
async activate(req: Request, res: Response, next: NextFunction) {
|
|
340
|
+
return this.executeWithAudit(
|
|
341
|
+
req,
|
|
342
|
+
res,
|
|
343
|
+
next,
|
|
344
|
+
async (id, options) => {
|
|
345
|
+
await userRepository.update(id, { active: true }, options);
|
|
346
|
+
},
|
|
347
|
+
"UPDATE",
|
|
348
|
+
"User activated successfully",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Config options
|
|
355
|
+
|
|
356
|
+
| Option | Type | Default | Description |
|
|
357
|
+
| ------------------------- | ---------------------- | --------------- | ------------------------------------------ |
|
|
358
|
+
| `entityName` | `string` | constructor arg | Name used in response messages and audit |
|
|
359
|
+
| `audit` | `boolean` | `false` | Enable audit logging |
|
|
360
|
+
| `auditService` | `IAuditService` | — | Required when `audit: true` |
|
|
361
|
+
| `injectFromUser` | `UserFieldInjection[]` | `[]` | Inject `req.user` fields into query params |
|
|
362
|
+
| `hooks` | `ControllerHooks<T>` | `{}` | Lifecycle hooks (before/after each op) |
|
|
363
|
+
| ~~`filterByUserBranch`~~ | `boolean` | `false` | **Deprecated** — use `injectFromUser` |
|
|
364
|
+
| ~~`filterByUserCompany`~~ | `boolean` | `false` | **Deprecated** — use `injectFromUser` |
|
|
365
|
+
|
|
366
|
+
### Lifecycle hooks
|
|
367
|
+
|
|
368
|
+
| Hook | Signature | Description |
|
|
369
|
+
| -------------- | ------------------------- | ---------------------------------- |
|
|
370
|
+
| `beforeCreate` | `(req, data) => data` | Modify/validate data before create |
|
|
371
|
+
| `afterCreate` | `(req, result) => void` | Run side-effects after create |
|
|
372
|
+
| `beforeUpdate` | `(req, id, data) => data` | Modify/validate data before update |
|
|
373
|
+
| `afterUpdate` | `(req, id) => void` | Run side-effects after update |
|
|
374
|
+
| `beforeDelete` | `(req, id) => void` | Guard or cleanup before delete |
|
|
375
|
+
| `afterDelete` | `(req, id) => void` | Run side-effects after delete |
|
|
376
|
+
|
|
377
|
+
### Express type augmentation
|
|
378
|
+
|
|
379
|
+
The library augments `req.user` and `req.audit`. To have your auth middleware populate them, declare them in your project:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
// src/types/express.d.ts (in your project)
|
|
383
|
+
import "@angelps/prisma-query-builder";
|
|
384
|
+
|
|
385
|
+
// Optionally extend with your own fields:
|
|
386
|
+
declare global {
|
|
387
|
+
namespace Express {
|
|
388
|
+
interface Request {
|
|
389
|
+
user?: {
|
|
390
|
+
id: number;
|
|
391
|
+
branchIds?: number | number[];
|
|
392
|
+
companyIds?: number | number[];
|
|
393
|
+
// add your own fields here
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## 4. AuditService
|
|
403
|
+
|
|
404
|
+
Extend `BaseAuditService` and implement `persist()` with your own Prisma model.
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import {
|
|
408
|
+
BaseAuditService,
|
|
409
|
+
type AuditLogInput,
|
|
410
|
+
} from "@angelps/prisma-query-builder";
|
|
411
|
+
|
|
412
|
+
export class AuditService extends BaseAuditService {
|
|
413
|
+
protected async persist(input: AuditLogInput): Promise<void> {
|
|
414
|
+
await prisma.auditLog.create({
|
|
415
|
+
data: {
|
|
416
|
+
action: input.action,
|
|
417
|
+
entity: input.entity,
|
|
418
|
+
entityId: input.entityId != null ? String(input.entityId) : null,
|
|
419
|
+
userId: input.options?.userId ?? null,
|
|
420
|
+
companyId: input.options?.companyId ?? null,
|
|
421
|
+
requestId: input.options?.requestId ?? null,
|
|
422
|
+
ip: input.options?.ip ?? null,
|
|
423
|
+
userAgent: input.options?.userAgent ?? null,
|
|
424
|
+
path: input.options?.path ?? null,
|
|
425
|
+
method: input.options?.method ?? null,
|
|
426
|
+
before: input.before as any,
|
|
427
|
+
after: input.after as any,
|
|
428
|
+
metadata: input.metadata as any,
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export const auditService = new AuditService();
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`BaseAuditService` automatically:
|
|
438
|
+
|
|
439
|
+
- Strips sensitive fields: `password`, `passwordHash`, `refreshToken`, `accessToken`, `token`
|
|
440
|
+
- Converts `Prisma.Decimal` → `number`
|
|
441
|
+
- Converts `Date` → ISO string
|
|
442
|
+
|
|
443
|
+
### Prisma schema
|
|
444
|
+
|
|
445
|
+
```prisma
|
|
446
|
+
model AuditLog {
|
|
447
|
+
id Int @id @default(autoincrement())
|
|
448
|
+
action String
|
|
449
|
+
entity String
|
|
450
|
+
entityId String?
|
|
451
|
+
userId Int?
|
|
452
|
+
companyId Int?
|
|
453
|
+
requestId String?
|
|
454
|
+
ip String?
|
|
455
|
+
userAgent String?
|
|
456
|
+
path String?
|
|
457
|
+
method String?
|
|
458
|
+
before Json?
|
|
459
|
+
after Json?
|
|
460
|
+
metadata Json?
|
|
461
|
+
createdAt DateTime @default(now())
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## 5. Error handling
|
|
468
|
+
|
|
469
|
+
`ErrorHandlerService` maps errors to HTTP responses automatically.
|
|
470
|
+
|
|
471
|
+
### Handled error codes
|
|
472
|
+
|
|
473
|
+
| Error type | HTTP status |
|
|
474
|
+
| ------------------------------------------------------------------------- | ------------------------------ |
|
|
475
|
+
| `AppError` subclass (`ConflictError`, `BadRequestError`, `NotFoundError`) | statusCode of the error |
|
|
476
|
+
| `PrismaClientKnownRequestError` P2002 | 409 Conflict |
|
|
477
|
+
| `PrismaClientKnownRequestError` P2003 | 422 Unprocessable |
|
|
478
|
+
| `PrismaClientKnownRequestError` P2011 | 422 Null constraint violation |
|
|
479
|
+
| `PrismaClientKnownRequestError` P2012 | 422 Missing required value |
|
|
480
|
+
| `PrismaClientKnownRequestError` P2014 | 422 Required relation violated |
|
|
481
|
+
| `PrismaClientKnownRequestError` P2016 | 422 Query interpretation error |
|
|
482
|
+
| `PrismaClientKnownRequestError` P2021 | 500 Table does not exist |
|
|
483
|
+
| `PrismaClientKnownRequestError` P2025 | 404 Not Found |
|
|
484
|
+
| `ValiError` (valibot) | 422 with field-level errors |
|
|
485
|
+
| Known `ErrorMessages` string | matching statusCode |
|
|
486
|
+
| Unknown error | 500 Internal Server Error |
|
|
487
|
+
|
|
488
|
+
### Custom errors
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
import {
|
|
492
|
+
AppError,
|
|
493
|
+
BadRequestError,
|
|
494
|
+
ConflictError,
|
|
495
|
+
NotFoundError,
|
|
496
|
+
} from "@angelps/prisma-query-builder";
|
|
497
|
+
|
|
498
|
+
throw new BadRequestError("Invalid date range"); // 400
|
|
499
|
+
throw new ConflictError("Email already exists"); // 409
|
|
500
|
+
throw new NotFoundError("User not found"); // 404
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Customizable error messages (i18n)
|
|
504
|
+
|
|
505
|
+
Override default Prisma error messages for localization:
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
import { ErrorHandlerService } from "@angelps/prisma-query-builder";
|
|
509
|
+
|
|
510
|
+
const errorHandler = new ErrorHandlerService({
|
|
511
|
+
messages: {
|
|
512
|
+
P2002: "A record with this data already exists",
|
|
513
|
+
P2025: "The requested record was not found",
|
|
514
|
+
P2003: "The referenced record does not exist",
|
|
515
|
+
VALIDATION_FAILED: "Validation failed",
|
|
516
|
+
INTERNAL_SERVER_ERROR: "Internal server error",
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
---
|
|
522
|
+
|
|
523
|
+
## 6. Utilities
|
|
524
|
+
|
|
525
|
+
### Date utils
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
import {
|
|
529
|
+
startDateOfDay,
|
|
530
|
+
endDateOfDay,
|
|
531
|
+
formatDate,
|
|
532
|
+
getTimeFromDateString,
|
|
533
|
+
formatTimeWithTimezone,
|
|
534
|
+
} from "@angelps/prisma-query-builder";
|
|
535
|
+
|
|
536
|
+
startDateOfDay("2024-01-15"); // '2024-01-15T00:00:00.000Z'
|
|
537
|
+
endDateOfDay("2024-01-15"); // '2024-01-15T23:59:59.999Z'
|
|
538
|
+
formatDate(new Date(), "DD-MM-YYYY"); // '15-01-2024'
|
|
539
|
+
formatDate(new Date(), "YYYY-MM-DD", true); // '2024-01-15 14:30'
|
|
540
|
+
getTimeFromDateString("2024-01-15T14:30:00Z"); // '14:30'
|
|
541
|
+
formatTimeWithTimezone(new Date(), "America/New_York"); // '09:30'
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
### Phone utils
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import { extractCountryCodeAndNumber } from "@angelps/prisma-query-builder";
|
|
548
|
+
|
|
549
|
+
extractCountryCodeAndNumber("8091234567", "DO");
|
|
550
|
+
// → { prefix: '+1', number: '8091234567', country: 'DO' }
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### HTTP response utils
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
import {
|
|
557
|
+
apiResponse,
|
|
558
|
+
apiPaginationResponse,
|
|
559
|
+
} from "@angelps/prisma-query-builder";
|
|
560
|
+
|
|
561
|
+
apiResponse(res, data, "Created successfully", 201);
|
|
562
|
+
apiPaginationResponse(res, paginationModel, "Users retrieved", 200);
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## 7. Types reference
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
// Pagination result (offset-based)
|
|
571
|
+
type PaginationModel<T> = {
|
|
572
|
+
items: T[];
|
|
573
|
+
total?: number;
|
|
574
|
+
pagination: { count: number; pages: number; size: number; current: number };
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// Cursor pagination result
|
|
578
|
+
type CursorPaginationResult<T> = {
|
|
579
|
+
items: T[];
|
|
580
|
+
nextCursor: string | number | null;
|
|
581
|
+
hasMore: boolean;
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// Query params (generic for type-safe field filtering)
|
|
585
|
+
type QueryParams<T = Record<string, unknown>> = BaseQueryParams & {
|
|
586
|
+
[K in keyof T]?: T[K] | string;
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Options passed from controller to repository
|
|
590
|
+
type RepositoryOptions = {
|
|
591
|
+
userId?: number;
|
|
592
|
+
companyId?: number;
|
|
593
|
+
requestId?: string;
|
|
594
|
+
ip?: string;
|
|
595
|
+
userAgent?: string;
|
|
596
|
+
path?: string;
|
|
597
|
+
method?: string;
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// Generic repository contract
|
|
601
|
+
interface IGenericRepository<T> {
|
|
602
|
+
findAll(queryParams?: QueryParams, options?: RepositoryOptions): Promise<PaginationModel<T>>;
|
|
603
|
+
findById(id: number, options?: FindByIdOptions): Promise<T | null>;
|
|
604
|
+
findOne(where: Partial<T>, options?: RepositoryOptions): Promise<T | null>;
|
|
605
|
+
create(data: unknown, options?: RepositoryOptions): Promise<T>;
|
|
606
|
+
update(id: number, data: unknown, options?: RepositoryOptions): Promise<void>;
|
|
607
|
+
delete(id: number, options?: RepositoryOptions): Promise<void>;
|
|
608
|
+
createMany(data: unknown[], options?: RepositoryOptions): Promise<{ count: number }>;
|
|
609
|
+
updateMany(ids: number[], data: unknown, options?: RepositoryOptions): Promise<{ count: number }>;
|
|
610
|
+
deleteMany(ids: number[], options?: RepositoryOptions): Promise<{ count: number }>;
|
|
611
|
+
upsert(data: unknown, options?: RepositoryOptions): Promise<T>;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Audit service contract
|
|
615
|
+
interface IAuditService {
|
|
616
|
+
log(input: AuditLogInput): Promise<void>;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
type AuditAction = 'CREATE' | 'UPDATE' | 'DELETE' | 'LOGIN' | 'LOGOUT';
|
|
620
|
+
|
|
621
|
+
// Utility types — infer Prisma types from delegate
|
|
622
|
+
type InferResult<D> // → Model type
|
|
623
|
+
type InferWhere<D> // → WhereInput
|
|
624
|
+
type InferOrderBy<D> // → OrderByWithRelationInput
|
|
625
|
+
type InferFindManyArgs<D> // → FindManyArgs
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
## 8. Schema Adapters
|
|
631
|
+
|
|
632
|
+
The library ships with a pluggable validation layer. By default it uses **Valibot**, but you can switch to **Zod** or implement your own adapter.
|
|
633
|
+
|
|
634
|
+
### Valibot (default)
|
|
635
|
+
|
|
636
|
+
No configuration needed — this is the default behavior:
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
import { CrudRepository } from '@angelps/prisma-query-builder';
|
|
640
|
+
import { object, string } from 'valibot';
|
|
641
|
+
|
|
642
|
+
class UserRepo extends CrudRepository<...> {
|
|
643
|
+
createSchema = object({ name: string() });
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Zod
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
import { CrudRepository, ZodAdapter } from '@angelps/prisma-query-builder';
|
|
651
|
+
import { z } from 'zod';
|
|
652
|
+
|
|
653
|
+
class UserRepo extends CrudRepository<...> {
|
|
654
|
+
createSchema = z.object({ name: z.string().min(2) });
|
|
655
|
+
updateSchema = z.object({ name: z.string().min(2) });
|
|
656
|
+
|
|
657
|
+
constructor() {
|
|
658
|
+
super(prisma.user, config, options, { schemaAdapter: new ZodAdapter() });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Custom adapter
|
|
664
|
+
|
|
665
|
+
Implement the `SchemaAdapter` interface:
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
import type { SchemaAdapter } from "@angelps/prisma-query-builder";
|
|
669
|
+
|
|
670
|
+
class YupAdapter implements SchemaAdapter {
|
|
671
|
+
parse<T>(schema: unknown, data: unknown): T {
|
|
672
|
+
return (schema as any).validateSync(data);
|
|
673
|
+
}
|
|
674
|
+
isValidSchema(schema: unknown): boolean {
|
|
675
|
+
return !!schema && typeof (schema as any).validateSync === "function";
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## 9. Configurable Logger
|
|
683
|
+
|
|
684
|
+
Replace `console.*` calls with your preferred logging library (winston, pino, etc.):
|
|
685
|
+
|
|
686
|
+
```typescript
|
|
687
|
+
import { CrudRepository } from '@angelps/prisma-query-builder';
|
|
688
|
+
import type { Logger } from '@angelps/prisma-query-builder';
|
|
689
|
+
import pino from 'pino';
|
|
690
|
+
|
|
691
|
+
const pinoLogger = pino();
|
|
692
|
+
|
|
693
|
+
const logger: Logger = {
|
|
694
|
+
warn: (msg, ...args) => pinoLogger.warn(msg, ...args),
|
|
695
|
+
error: (msg, ...args) => pinoLogger.error(msg, ...args),
|
|
696
|
+
info: (msg, ...args) => pinoLogger.info(msg, ...args),
|
|
697
|
+
debug: (msg, ...args) => pinoLogger.debug(msg, ...args),
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
class UserRepo extends CrudRepository<...> {
|
|
701
|
+
constructor() {
|
|
702
|
+
super(prisma.user, { logger, ...otherConfig }, options);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
By default the library uses `console.*` — no configuration required.
|
|
708
|
+
|
|
709
|
+
Holaa, solo probando :D
|