@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.
Files changed (130) hide show
  1. package/README.md +709 -0
  2. package/dist/adapters/index.d.ts +3 -0
  3. package/dist/adapters/index.d.ts.map +1 -0
  4. package/dist/adapters/index.js +3 -0
  5. package/dist/adapters/index.js.map +1 -0
  6. package/dist/adapters/valibot.adapter.d.ts +14 -0
  7. package/dist/adapters/valibot.adapter.d.ts.map +1 -0
  8. package/dist/adapters/valibot.adapter.js +21 -0
  9. package/dist/adapters/valibot.adapter.js.map +1 -0
  10. package/dist/adapters/zod.adapter.d.ts +29 -0
  11. package/dist/adapters/zod.adapter.d.ts.map +1 -0
  12. package/dist/adapters/zod.adapter.js +42 -0
  13. package/dist/adapters/zod.adapter.js.map +1 -0
  14. package/dist/controllers/generic.controller.d.ts +79 -0
  15. package/dist/controllers/generic.controller.d.ts.map +1 -0
  16. package/dist/controllers/generic.controller.js +279 -0
  17. package/dist/controllers/generic.controller.js.map +1 -0
  18. package/dist/controllers/index.d.ts +2 -0
  19. package/dist/controllers/index.d.ts.map +1 -0
  20. package/dist/controllers/index.js +2 -0
  21. package/dist/controllers/index.js.map +1 -0
  22. package/dist/errors/app-errors.d.ts +17 -0
  23. package/dist/errors/app-errors.d.ts.map +1 -0
  24. package/dist/errors/app-errors.js +28 -0
  25. package/dist/errors/app-errors.js.map +1 -0
  26. package/dist/errors/error-messages.d.ts +80 -0
  27. package/dist/errors/error-messages.d.ts.map +1 -0
  28. package/dist/errors/error-messages.js +75 -0
  29. package/dist/errors/error-messages.js.map +1 -0
  30. package/dist/errors/index.d.ts +3 -0
  31. package/dist/errors/index.d.ts.map +1 -0
  32. package/dist/errors/index.js +3 -0
  33. package/dist/errors/index.js.map +1 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +9 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/query-builder/helpers/order-by.helper.d.ts +4 -0
  39. package/dist/query-builder/helpers/order-by.helper.d.ts.map +1 -0
  40. package/dist/query-builder/helpers/order-by.helper.js +24 -0
  41. package/dist/query-builder/helpers/order-by.helper.js.map +1 -0
  42. package/dist/query-builder/helpers/pagination.helper.d.ts +6 -0
  43. package/dist/query-builder/helpers/pagination.helper.d.ts.map +1 -0
  44. package/dist/query-builder/helpers/pagination.helper.js +9 -0
  45. package/dist/query-builder/helpers/pagination.helper.js.map +1 -0
  46. package/dist/query-builder/helpers/where.helper.d.ts +5 -0
  47. package/dist/query-builder/helpers/where.helper.d.ts.map +1 -0
  48. package/dist/query-builder/helpers/where.helper.js +356 -0
  49. package/dist/query-builder/helpers/where.helper.js.map +1 -0
  50. package/dist/query-builder/index.d.ts +3 -0
  51. package/dist/query-builder/index.d.ts.map +1 -0
  52. package/dist/query-builder/index.js +3 -0
  53. package/dist/query-builder/index.js.map +1 -0
  54. package/dist/query-builder/query-builder.d.ts +56 -0
  55. package/dist/query-builder/query-builder.d.ts.map +1 -0
  56. package/dist/query-builder/query-builder.js +149 -0
  57. package/dist/query-builder/query-builder.js.map +1 -0
  58. package/dist/query-builder/query-builder.types.d.ts +83 -0
  59. package/dist/query-builder/query-builder.types.d.ts.map +1 -0
  60. package/dist/query-builder/query-builder.types.js +2 -0
  61. package/dist/query-builder/query-builder.types.js.map +1 -0
  62. package/dist/repositories/crud.repository.prisma.d.ts +44 -0
  63. package/dist/repositories/crud.repository.prisma.d.ts.map +1 -0
  64. package/dist/repositories/crud.repository.prisma.js +246 -0
  65. package/dist/repositories/crud.repository.prisma.js.map +1 -0
  66. package/dist/repositories/index.d.ts +2 -0
  67. package/dist/repositories/index.d.ts.map +1 -0
  68. package/dist/repositories/index.js +2 -0
  69. package/dist/repositories/index.js.map +1 -0
  70. package/dist/services/audit.service.d.ts +32 -0
  71. package/dist/services/audit.service.d.ts.map +1 -0
  72. package/dist/services/audit.service.js +57 -0
  73. package/dist/services/audit.service.js.map +1 -0
  74. package/dist/services/error-handler.service.d.ts +34 -0
  75. package/dist/services/error-handler.service.d.ts.map +1 -0
  76. package/dist/services/error-handler.service.js +159 -0
  77. package/dist/services/error-handler.service.js.map +1 -0
  78. package/dist/services/index.d.ts +3 -0
  79. package/dist/services/index.d.ts.map +1 -0
  80. package/dist/services/index.js +3 -0
  81. package/dist/services/index.js.map +1 -0
  82. package/dist/types/crud-config.d.ts +20 -0
  83. package/dist/types/crud-config.d.ts.map +1 -0
  84. package/dist/types/crud-config.js +2 -0
  85. package/dist/types/crud-config.js.map +1 -0
  86. package/dist/types/generic-repository.d.ts +36 -0
  87. package/dist/types/generic-repository.d.ts.map +1 -0
  88. package/dist/types/generic-repository.js +2 -0
  89. package/dist/types/generic-repository.js.map +1 -0
  90. package/dist/types/index.d.ts +8 -0
  91. package/dist/types/index.d.ts.map +1 -0
  92. package/dist/types/index.js +8 -0
  93. package/dist/types/index.js.map +1 -0
  94. package/dist/types/logger.d.ts +17 -0
  95. package/dist/types/logger.d.ts.map +1 -0
  96. package/dist/types/logger.js +8 -0
  97. package/dist/types/logger.js.map +1 -0
  98. package/dist/types/pagination.d.ts +11 -0
  99. package/dist/types/pagination.d.ts.map +1 -0
  100. package/dist/types/pagination.js +2 -0
  101. package/dist/types/pagination.js.map +1 -0
  102. package/dist/types/prisma-delegate.types.d.ts +36 -0
  103. package/dist/types/prisma-delegate.types.d.ts.map +1 -0
  104. package/dist/types/prisma-delegate.types.js +15 -0
  105. package/dist/types/prisma-delegate.types.js.map +1 -0
  106. package/dist/types/query-params.d.ts +40 -0
  107. package/dist/types/query-params.d.ts.map +1 -0
  108. package/dist/types/query-params.js +2 -0
  109. package/dist/types/query-params.js.map +1 -0
  110. package/dist/types/schema-adapter.d.ts +19 -0
  111. package/dist/types/schema-adapter.d.ts.map +1 -0
  112. package/dist/types/schema-adapter.js +2 -0
  113. package/dist/types/schema-adapter.js.map +1 -0
  114. package/dist/utils/date.utils.d.ts +13 -0
  115. package/dist/utils/date.utils.d.ts.map +1 -0
  116. package/dist/utils/date.utils.js +75 -0
  117. package/dist/utils/date.utils.js.map +1 -0
  118. package/dist/utils/index.d.ts +4 -0
  119. package/dist/utils/index.d.ts.map +1 -0
  120. package/dist/utils/index.js +4 -0
  121. package/dist/utils/index.js.map +1 -0
  122. package/dist/utils/phone.utils.d.ts +7 -0
  123. package/dist/utils/phone.utils.d.ts.map +1 -0
  124. package/dist/utils/phone.utils.js +15 -0
  125. package/dist/utils/phone.utils.js.map +1 -0
  126. package/dist/utils/response.utils.d.ts +5 -0
  127. package/dist/utils/response.utils.d.ts.map +1 -0
  128. package/dist/utils/response.utils.js +13 -0
  129. package/dist/utils/response.utils.js.map +1 -0
  130. 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
@@ -0,0 +1,3 @@
1
+ export { ValibotAdapter, valibotAdapter } from './valibot.adapter.js';
2
+ export { ZodAdapter, zodAdapter } from './zod.adapter.js';
3
+ //# sourceMappingURL=index.d.ts.map