@miguelmorales13/nestkit 0.1.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.
Files changed (59) hide show
  1. package/README.md +411 -0
  2. package/dist/bootstrap/apply-defaults.d.ts +23 -0
  3. package/dist/bootstrap/index.d.ts +2 -0
  4. package/dist/bootstrap/index.js +10 -0
  5. package/dist/chunk-4MGIQFAJ.js +16 -0
  6. package/dist/chunk-EBO6UKHL.js +20 -0
  7. package/dist/chunk-EPVKCBPT.js +0 -0
  8. package/dist/chunk-EQXYK6AL.js +81 -0
  9. package/dist/chunk-EYURGACO.js +40 -0
  10. package/dist/chunk-HGJX2GPE.js +39 -0
  11. package/dist/chunk-IYUUYCP5.js +36 -0
  12. package/dist/chunk-JOVBJDJ2.js +104 -0
  13. package/dist/chunk-KDAA6GFF.js +29 -0
  14. package/dist/chunk-KP7GRCZW.js +32 -0
  15. package/dist/chunk-NAK4WDKS.js +0 -0
  16. package/dist/chunk-PA24P76K.js +39 -0
  17. package/dist/chunk-VKOPDDCC.js +50 -0
  18. package/dist/chunk-XX2HPTRU.js +12 -0
  19. package/dist/crud/crud.controller.d.ts +39 -0
  20. package/dist/crud/crud.service.d.ts +29 -0
  21. package/dist/crud/index.d.ts +5 -0
  22. package/dist/crud/index.js +9 -0
  23. package/dist/crud/repository.port.d.ts +14 -0
  24. package/dist/database/mongo/index.d.ts +1 -0
  25. package/dist/database/mongo/index.js +1 -0
  26. package/dist/database/mongo/repository.port.d.ts +1 -0
  27. package/dist/database/postgres/index.d.ts +2 -0
  28. package/dist/database/postgres/index.js +11 -0
  29. package/dist/database/postgres/postgres.module.d.ts +4 -0
  30. package/dist/database/postgres/tenant-scope.d.ts +11 -0
  31. package/dist/database/supabase/index.d.ts +1 -0
  32. package/dist/database/supabase/index.js +11 -0
  33. package/dist/database/supabase/supabase.module.d.ts +6 -0
  34. package/dist/entities/base-response.dto.d.ts +15 -0
  35. package/dist/entities/base.entity.d.ts +11 -0
  36. package/dist/entities/index.d.ts +3 -0
  37. package/dist/entities/index.js +7 -0
  38. package/dist/entities/soft-delete.entity.d.ts +8 -0
  39. package/dist/errors/app.exception.d.ts +11 -0
  40. package/dist/errors/common.exceptions.d.ts +16 -0
  41. package/dist/errors/global-exception.filter.d.ts +13 -0
  42. package/dist/errors/index.d.ts +3 -0
  43. package/dist/errors/index.js +22 -0
  44. package/dist/i18n/i18n.module.d.ts +18 -0
  45. package/dist/i18n/index.d.ts +3 -0
  46. package/dist/i18n/index.js +9 -0
  47. package/dist/i18n/translate.helper.d.ts +7 -0
  48. package/dist/index.d.ts +10 -0
  49. package/dist/index.js +75 -0
  50. package/dist/response/api-response.d.ts +20 -0
  51. package/dist/response/index.d.ts +2 -0
  52. package/dist/response/index.js +9 -0
  53. package/dist/response/response.interceptor.d.ts +13 -0
  54. package/dist/tracking/index.d.ts +4 -0
  55. package/dist/tracking/index.js +15 -0
  56. package/dist/tracking/request-context.d.ts +14 -0
  57. package/dist/tracking/request-id.middleware.d.ts +17 -0
  58. package/dist/tracking/tracking.module.d.ts +9 -0
  59. package/package.json +112 -0
package/README.md ADDED
@@ -0,0 +1,411 @@
1
+ # @miguelmorales13/nestkit
2
+
3
+ Bloques de infraestructura reutilizables para servicios NestJS: i18n, manejo de errores,
4
+ respuestas estandarizadas, tracking de requests, entidades base, CRUD genérico, y conexión a
5
+ Postgres/Supabase (con el contrato listo para Mongo a futuro). Pensado para eliminar el boilerplate
6
+ que se repite al arrancar cada backend nuevo.
7
+
8
+ ## ¿Para qué proyectos es esto?
9
+
10
+ **Sí**: cualquier proyecto NestJS nuevo que arranque con el patrón hexagonal/ports-and-adapters
11
+ (entidad → `Repository<T>` como puerto → adaptador de infraestructura), como `hr-pymes-saas`,
12
+ `schools-saas`, `restaurant-inventory-saas`, o cualquier proyecto futuro que empiece de cero.
13
+
14
+ **No**: proyectos existentes con convenciones ya establecidas y opuestas — por ejemplo `ms-pigmento`,
15
+ que usa Mongoose directo sin capa de Repository y sin DTOs con `class-validator` (ver la skill
16
+ `nestjs-backend` de ese proyecto). Meter `nestkit` ahí introduciría dos formas distintas de hacer lo
17
+ mismo en el mismo código base. Si dudas si un proyecto califica, pregúntate: ¿los servicios llaman al
18
+ ORM/cliente de DB directamente, o pasan por una interfaz de Repository? Si es lo primero, esta
19
+ librería no es para ese proyecto.
20
+
21
+ ## Instalación
22
+
23
+ ```bash
24
+ bun add @miguelmorales13/nestkit
25
+ ```
26
+
27
+ `nestkit` declara sus dependencias de framework como **peer dependencies** — instala solo las que
28
+ tu proyecto realmente use:
29
+
30
+ | Siempre necesarios | Solo si usas... |
31
+ |---------------------|------------------|
32
+ | `@nestjs/common`, `@nestjs/core`, `reflect-metadata`, `rxjs` | `pg` → `./database/postgres` |
33
+ | | `@supabase/supabase-js` → `./database/supabase` |
34
+ | | `helmet`, `@nestjs/swagger` → `./bootstrap` (`applyNestKitDefaults`) |
35
+
36
+ ```bash
37
+ bun add @nestjs/common @nestjs/core reflect-metadata rxjs
38
+ bun add pg # si usas ./database/postgres
39
+ bun add @supabase/supabase-js # si usas ./database/supabase
40
+ bun add helmet @nestjs/swagger # si usas ./bootstrap
41
+ bun add class-validator class-transformer # requeridos en runtime por ValidationPipe
42
+ ```
43
+
44
+ ## Quickstart (`main.ts`)
45
+
46
+ ```ts
47
+ import { NestFactory } from '@nestjs/core';
48
+ import { applyNestKitDefaults } from '@miguelmorales13/nestkit/bootstrap';
49
+ import { AppModule } from './app.module';
50
+
51
+ async function bootstrap() {
52
+ const app = await NestFactory.create(AppModule);
53
+
54
+ applyNestKitDefaults(app, {
55
+ swagger: { title: 'Mi API' },
56
+ });
57
+
58
+ await app.listen(process.env.PORT ?? 3000);
59
+ }
60
+
61
+ bootstrap();
62
+ ```
63
+
64
+ `applyNestKitDefaults` aplica, en orden: `helmet`, CORS, `ValidationPipe` global
65
+ (`whitelist + transform`), `GlobalExceptionFilter`, `ResponseInterceptor` y (salvo que pases
66
+ `swagger: false`) Swagger. Cada pieza también se puede usar suelta si no quieres el paquete completo
67
+ — ver cada módulo abajo.
68
+
69
+ No olvides importar `TrackingModule` en tu `AppModule` para que `ResponseInterceptor` y
70
+ `GlobalExceptionFilter` tengan un `requestId` que leer:
71
+
72
+ ```ts
73
+ import { Module } from '@nestjs/common';
74
+ import { TrackingModule } from '@miguelmorales13/nestkit/tracking';
75
+
76
+ @Module({
77
+ imports: [TrackingModule],
78
+ })
79
+ export class AppModule {}
80
+ ```
81
+
82
+ Con esto ya tienes: cualquier respuesta de cualquier controller sale envuelta en
83
+ `{ success: true, data, requestId, timestamp }`, cualquier excepción sale como
84
+ `{ success: false, error: { code, message, details? }, requestId, timestamp }`, con el header
85
+ `X-Request-Id` reflejado en la respuesta.
86
+
87
+ ## Módulos
88
+
89
+ ### `entities` — entidades y DTOs base
90
+
91
+ ```ts
92
+ import type { BaseEntity, SoftDeleteEntity } from '@miguelmorales13/nestkit/entities';
93
+ import { BaseResponseDto } from '@miguelmorales13/nestkit/entities';
94
+
95
+ // Entidad de dominio normal — id/createdAt/updatedAt vienen de BaseEntity
96
+ export interface Widget extends BaseEntity {
97
+ name: string;
98
+ }
99
+
100
+ // Entidad con borrado lógico — agrega deletedAt: Date | null
101
+ export interface Widget extends SoftDeleteEntity {
102
+ name: string;
103
+ }
104
+
105
+ // DTO de respuesta que mapea 1:1 la entidad, sin repetir constructor
106
+ export class WidgetDto extends BaseResponseDto {
107
+ name!: string;
108
+ }
109
+
110
+ const dto = WidgetDto.fromEntity(widget); // instancia de WidgetDto, no de BaseResponseDto
111
+ ```
112
+
113
+ `SoftDeleteEntity` es opcional — solo la usan las entidades que de verdad necesitan borrado lógico.
114
+ Una entidad con `BaseEntity` a secas no carga el campo `deletedAt`.
115
+
116
+ ### `response` — envelope estandarizado
117
+
118
+ Normalmente no construyes esto a mano: `ResponseInterceptor` (aplicado por `applyNestKitDefaults` o
119
+ manualmente con `app.useGlobalInterceptors(new ResponseInterceptor())`) envuelve automáticamente
120
+ cualquier valor que un controller retorne. Los tipos existen para cuando necesitas tipar el lado del
121
+ cliente/consumidor de la API:
122
+
123
+ ```ts
124
+ import type { ApiResponse, ApiSuccessResponse, ApiErrorResponse } from '@miguelmorales13/nestkit/response';
125
+
126
+ async function fetchWidget(id: string): Promise<ApiResponse<Widget>> {
127
+ const res = await fetch(`/widgets/${id}`);
128
+ return res.json(); // { success: true, data: Widget, requestId, timestamp } | ApiErrorResponse
129
+ }
130
+ ```
131
+
132
+ ### `errors` — excepciones tipadas + filtro global
133
+
134
+ ```ts
135
+ import { NotFoundAppException, ConflictAppException } from '@miguelmorales13/nestkit/errors';
136
+
137
+ async findOrFail(id: string): Promise<Widget> {
138
+ const widget = await this.repository.findById(id);
139
+ if (!widget) {
140
+ throw new NotFoundAppException(`Widget ${id} not found`, { id });
141
+ }
142
+ return widget;
143
+ }
144
+ ```
145
+
146
+ Subclases incluidas: `NotFoundAppException` (404), `ConflictAppException` (409),
147
+ `ValidationAppException` (400), `UnauthorizedAppException` (401), `ForbiddenAppException` (403) — cada
148
+ una acepta `(message?, details?, code?)`, con un `code` por default (ej. `'NOT_FOUND'`) que puedes
149
+ sobreescribir si necesitas un código más específico del dominio. Para un error que no encaja en
150
+ ninguna, usa `AppException` directo: `new AppException('INSUFFICIENT_STOCK', 'No hay suficiente inventario', HttpStatus.CONFLICT, { available: 3 })`.
151
+
152
+ `GlobalExceptionFilter` (aplicado por `applyNestKitDefaults`) traduce **cualquier** excepción —
153
+ `AppException`, cualquier `HttpException` nativa de Nest, o un error no controlado — al mismo
154
+ `ApiErrorResponse`. Los errores 5xx se loggean con el stack completo vía el `Logger` de Nest, pero el
155
+ stack **nunca** llega al cliente en el body de la respuesta.
156
+
157
+ ### `tracking` — request id por request
158
+
159
+ ```ts
160
+ import { RequestContext } from '@miguelmorales13/nestkit/tracking';
161
+
162
+ @Injectable()
163
+ export class WidgetsService {
164
+ async create(data: CreateWidgetDto): Promise<Widget> {
165
+ this.logger.log(`Creating widget, request ${RequestContext.getRequestId()}`);
166
+ // ...
167
+ }
168
+ }
169
+ ```
170
+
171
+ Cualquier capa (servicio, repositorio, logger) puede leer `RequestContext.getRequestId()` sin que se
172
+ lo pasen por parámetro — vive en un `AsyncLocalStorage` que `RequestIdMiddleware` inicializa al
173
+ principio de cada request (`TrackingModule` lo registra globalmente). El middleware respeta el header
174
+ `X-Request-Id` si el cliente ya lo mandó (útil para tracing distribuido entre servicios), o genera uno
175
+ nuevo con `crypto.randomUUID()`.
176
+
177
+ ### `database/postgres` — Pool + aislamiento por tenant (RLS)
178
+
179
+ ```ts
180
+ import { Module } from '@nestjs/common';
181
+ import { PgModule, PG_POOL, withTenantScope } from '@miguelmorales13/nestkit/database/postgres';
182
+
183
+ @Module({ imports: [PgModule] })
184
+ export class AppModule {}
185
+ ```
186
+
187
+ ```ts
188
+ // dentro de un repository o guard
189
+ import { Inject, Injectable } from '@nestjs/common';
190
+ import { PG_POOL, withTenantScope } from '@miguelmorales13/nestkit/database/postgres';
191
+ import type { Pool } from 'pg';
192
+
193
+ @Injectable()
194
+ export class WidgetRepository {
195
+ constructor(@Inject(PG_POOL) private readonly pool: Pool) {}
196
+
197
+ async findAllForTenant(authUserId: string) {
198
+ return withTenantScope(this.pool, authUserId, (client) =>
199
+ client.query('SELECT * FROM widgets'), // RLS solo deja ver lo que auth.uid() puede ver
200
+ );
201
+ }
202
+ }
203
+ ```
204
+
205
+ `PgModule` lee `DATABASE_URL` del entorno y lanza un error explícito al arrancar si falta —
206
+ falla rápido en vez de fallar silenciosamente en el primer query. `withTenantScope` es la pieza que
207
+ hace que Row-Level-Security de Postgres vea el usuario correcto: abre una transacción, corre
208
+ `SELECT set_config('request.jwt.claim.sub', authUserId, true)`, ejecuta tu función, y hace
209
+ commit/rollback — **cualquier query que dependa de una policy RLS basada en `auth.uid()` debe pasar
210
+ por aquí**, `pool.query()` directo no aplica el `set_config` y verá (o no verá) filas incorrectamente.
211
+
212
+ ### `database/supabase` — clientes anon y service-role
213
+
214
+ ```ts
215
+ import { Module } from '@nestjs/common';
216
+ import { SupabaseModule, SUPABASE_ANON_CLIENT, SUPABASE_SERVICE_ROLE_CLIENT } from '@miguelmorales13/nestkit/database/supabase';
217
+
218
+ @Module({ imports: [SupabaseModule] })
219
+ export class AppModule {}
220
+ ```
221
+
222
+ ```ts
223
+ import { Inject, Injectable } from '@nestjs/common';
224
+ import { SUPABASE_SERVICE_ROLE_CLIENT } from '@miguelmorales13/nestkit/database/supabase';
225
+ import type { SupabaseClient } from '@supabase/supabase-js';
226
+
227
+ @Injectable()
228
+ export class StorageService {
229
+ constructor(@Inject(SUPABASE_SERVICE_ROLE_CLIENT) private readonly supabase: SupabaseClient) {}
230
+ // service-role: bypasea RLS — úsalo solo server-side, nunca lo expongas al cliente
231
+ }
232
+ ```
233
+
234
+ Requiere `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY` en el entorno — igual que
235
+ `PgModule`, falla explícito al arrancar si falta alguna. Usa el cliente `anon` cuando quieras que las
236
+ policies RLS apliquen tal cual verían al usuario final; usa `service-role` solo para operaciones
237
+ administrativas server-side (nunca en un contexto que un cliente externo pueda influenciar).
238
+
239
+ ### `database/mongo` — solo el contrato, todavía
240
+
241
+ ```ts
242
+ import type { MongoRepositoryPort } from '@miguelmorales13/nestkit/database/mongo';
243
+ ```
244
+
245
+ Es un alias del mismo `Repository<T>` de `crud` — no hay `MongoModule` ni una implementación
246
+ concreta todavía porque ningún proyecto del portafolio usa Mongo hoy. El día que haga falta, se
247
+ implementa un adaptador que satisfaga `Repository<T>` con Mongoose por debajo — el CRUD genérico y
248
+ las entidades base no cambian, porque nunca dependieron de Postgres/Supabase directamente.
249
+
250
+ ### `crud` — Repository, servicio y controller genéricos
251
+
252
+ ```ts
253
+ // widget.repository.ts
254
+ import { Injectable, Inject } from '@nestjs/common';
255
+ import type { Repository } from '@miguelmorales13/nestkit/crud';
256
+ import { PG_POOL } from '@miguelmorales13/nestkit/database/postgres';
257
+ import type { Pool } from 'pg';
258
+ import type { Widget } from './widget.entity';
259
+
260
+ @Injectable()
261
+ export class WidgetRepository implements Repository<Widget> {
262
+ constructor(@Inject(PG_POOL) private readonly pool: Pool) {}
263
+
264
+ async findById(id: string): Promise<Widget | null> {
265
+ const { rows } = await this.pool.query('SELECT * FROM widgets WHERE id = $1', [id]);
266
+ return rows[0] ?? null;
267
+ }
268
+
269
+ async findAll(): Promise<Widget[]> {
270
+ const { rows } = await this.pool.query('SELECT * FROM widgets');
271
+ return rows;
272
+ }
273
+
274
+ async create(data: Omit<Widget, 'id' | 'createdAt' | 'updatedAt'>): Promise<Widget> {
275
+ const { rows } = await this.pool.query(
276
+ 'INSERT INTO widgets (name) VALUES ($1) RETURNING *',
277
+ [data.name],
278
+ );
279
+ return rows[0];
280
+ }
281
+
282
+ async update(id: string, data: Partial<Widget>): Promise<Widget> {
283
+ const { rows } = await this.pool.query(
284
+ 'UPDATE widgets SET name = COALESCE($2, name) WHERE id = $1 RETURNING *',
285
+ [id, data.name],
286
+ );
287
+ return rows[0];
288
+ }
289
+
290
+ async delete(id: string): Promise<void> {
291
+ await this.pool.query('DELETE FROM widgets WHERE id = $1', [id]);
292
+ }
293
+ }
294
+ ```
295
+
296
+ ```ts
297
+ // widgets.module.ts
298
+ import { Module } from '@nestjs/common';
299
+ import { BaseCrudService, createCrudController } from '@miguelmorales13/nestkit/crud';
300
+ import { WidgetRepository } from './widget.repository';
301
+
302
+ const WIDGETS_SERVICE = 'WIDGETS_SERVICE';
303
+
304
+ class WidgetsController extends createCrudController(WIDGETS_SERVICE as any, { path: 'widgets' }) {}
305
+
306
+ @Module({
307
+ controllers: [WidgetsController],
308
+ providers: [
309
+ WidgetRepository,
310
+ { provide: WIDGETS_SERVICE, useFactory: (repo: WidgetRepository) => new BaseCrudService(repo), inject: [WidgetRepository] },
311
+ ],
312
+ })
313
+ export class WidgetsModule {}
314
+ ```
315
+
316
+ > En la práctica, usa una subclase concreta de `BaseCrudService` como provider/token (en vez de un
317
+ > string token) para que `createCrudController` infiera los tipos sin castear. `createCrudController`
318
+ > es composición opcional: si prefieres tu propio controller, simplemente consume
319
+ > `BaseCrudService`/`Repository<T>` directamente y no lo uses.
320
+
321
+ **Borrado lógico**: pasa `deleteStrategy: 'soft'` al construir el servicio para una entidad
322
+ `SoftDeleteEntity` — `delete()` hace un `update(id, { deletedAt: new Date() })` en vez de borrar la
323
+ fila/documento:
324
+
325
+ ```ts
326
+ new BaseCrudService(widgetRepository, { deleteStrategy: 'soft' })
327
+ ```
328
+
329
+ Por defecto (`'hard'` o sin especificar) `delete()` llama a `repository.delete()` tal cual. Si usas
330
+ `'soft'`, tu `Repository<T>` debe estar tipado sobre una entidad que extienda `SoftDeleteEntity` y tu
331
+ `findAll`/`findById` deben filtrar `deletedAt IS NULL` por su cuenta — el servicio genérico no hace
332
+ ese filtro automáticamente, solo decide cómo borra.
333
+
334
+ **Multi-tenant**: por defecto `createCrudController` **no aísla por tenant** — llama al servicio sin
335
+ `tenantId` (igual que si lo omitieras a mano), correcto para un recurso single-tenant. Si tu
336
+ `Repository<T>`/tabla sí necesita aislar por tenant (ej. RLS de Postgres, o un filtro
337
+ `WHERE company_id = ?` en cualquier otro motor), pasa `tenantIdExtractor` para que los 5 métodos
338
+ generados lo saquen del request y se lo pasen al servicio automáticamente — normalmente lee un valor
339
+ que un guard de auth ya dejó en el request (como `req.companyId` en el guard de `hr-pymes-saas`):
340
+
341
+ ```ts
342
+ class WidgetsController extends createCrudController(WIDGETS_SERVICE as any, {
343
+ path: 'widgets',
344
+ tenantIdExtractor: (req) => (req as AuthenticatedRequest).companyId,
345
+ }) {}
346
+ ```
347
+
348
+ Sin esta opción, un `Repository<T>` que solo filtra correctamente cuando recibe `tenantId` **no
349
+ queda protegido automáticamente** por el CRUD genérico — es responsabilidad de quien monta el
350
+ controller pasarla si el recurso es multi-tenant.
351
+
352
+ ### `i18n` — mensajes en JSON
353
+
354
+ ```ts
355
+ // app.module.ts
356
+ import { Module } from '@nestjs/common';
357
+ import { I18nModule } from '@miguelmorales13/nestkit/i18n';
358
+
359
+ @Module({
360
+ imports: [I18nModule.forRoot({ path: './i18n/', fallbackLanguage: 'es' })],
361
+ })
362
+ export class AppModule {}
363
+ ```
364
+
365
+ ```json
366
+ // i18n/es/widgets.json
367
+ { "not_found": "No se encontró el widget {id}" }
368
+ ```
369
+
370
+ ```json
371
+ // i18n/en/widgets.json
372
+ { "not_found": "Widget {id} not found" }
373
+ ```
374
+
375
+ ```ts
376
+ import { I18nService } from 'nestjs-i18n';
377
+ import { translateOr } from '@miguelmorales13/nestkit/i18n';
378
+ import { NotFoundAppException } from '@miguelmorales13/nestkit/errors';
379
+
380
+ throw new NotFoundAppException(
381
+ translateOr(this.i18n, 'widgets.not_found', `Widget ${id} not found`, { id }),
382
+ );
383
+ ```
384
+
385
+ El idioma se resuelve por el `Accept-Language` del request (comportamiento por defecto de
386
+ `nestjs-i18n`, que `I18nModule.forRoot` no cambia). `translateOr` nunca lanza si la key no existe —
387
+ regresa el `fallback`, así una traducción faltante no se convierte en un error 500 encima del error
388
+ original.
389
+
390
+ ## Subpaths disponibles
391
+
392
+ | Subpath | Qué trae |
393
+ |---------|----------|
394
+ | `@miguelmorales13/nestkit` | Reexporta todo lo demás |
395
+ | `@miguelmorales13/nestkit/entities` | `BaseEntity`, `SoftDeleteEntity`, `BaseResponseDto` |
396
+ | `@miguelmorales13/nestkit/response` | `ApiResponse`/`ApiSuccessResponse`/`ApiErrorResponse`, `ResponseInterceptor` |
397
+ | `@miguelmorales13/nestkit/errors` | `AppException` y subclases comunes, `GlobalExceptionFilter` |
398
+ | `@miguelmorales13/nestkit/tracking` | `RequestContext`, `RequestIdMiddleware`, `TrackingModule` |
399
+ | `@miguelmorales13/nestkit/crud` | `Repository<T>`, `BaseCrudService`, `createCrudController` |
400
+ | `@miguelmorales13/nestkit/database/postgres` | `PgModule`, `PG_POOL`, `withTenantScope` |
401
+ | `@miguelmorales13/nestkit/database/supabase` | `SupabaseModule`, `SUPABASE_ANON_CLIENT`, `SUPABASE_SERVICE_ROLE_CLIENT` |
402
+ | `@miguelmorales13/nestkit/database/mongo` | Solo el contrato `MongoRepositoryPort` — sin implementación |
403
+ | `@miguelmorales13/nestkit/i18n` | `I18nModule` (wrapper de `nestjs-i18n`), `translateOr` |
404
+ | `@miguelmorales13/nestkit/bootstrap` | `applyNestKitDefaults` |
405
+
406
+ ## Estado del paquete
407
+
408
+ `0.1.0`, no publicado en npm todavía (repo privado, se prueba localmente con `bun link` antes de
409
+ publicar). Sin adaptador Mongo real. Sin tests unitarios propios — es un paquete nuevo sin
410
+ consumidor productivo todavía; la primera integración real (ej. en `hr-pymes-saas`) es la que va a
411
+ ejercitar el código de verdad.
@@ -0,0 +1,23 @@
1
+ import { type INestApplication, type ValidationPipeOptions } from '@nestjs/common';
2
+ import type { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
3
+ export interface SwaggerBootstrapOptions {
4
+ title: string;
5
+ path?: string;
6
+ }
7
+ export interface ApplyNestKitDefaultsOptions {
8
+ cors?: CorsOptions;
9
+ /** Pass `false` to skip mounting Swagger entirely. */
10
+ swagger?: SwaggerBootstrapOptions | false;
11
+ validation?: ValidationPipeOptions;
12
+ }
13
+ /**
14
+ * Applies nestkit's opinionated defaults to a Nest application, in order:
15
+ * helmet -> CORS -> global ValidationPipe -> GlobalExceptionFilter ->
16
+ * ResponseInterceptor -> (optionally) Swagger. Call once from `main.ts`
17
+ * before `app.listen()`.
18
+ *
19
+ * Requires `helmet` and, unless `swagger: false`, `@nestjs/swagger` to be
20
+ * installed — both are optional peer dependencies of the package overall,
21
+ * required only when importing this `bootstrap` subpath.
22
+ */
23
+ export declare function applyNestKitDefaults(app: INestApplication, options?: ApplyNestKitDefaultsOptions): void;
@@ -0,0 +1,2 @@
1
+ export { applyNestKitDefaults } from './apply-defaults.js';
2
+ export type { ApplyNestKitDefaultsOptions, SwaggerBootstrapOptions } from './apply-defaults.js';
@@ -0,0 +1,10 @@
1
+ import {
2
+ applyNestKitDefaults
3
+ } from "../chunk-KP7GRCZW.js";
4
+ import "../chunk-KDAA6GFF.js";
5
+ import "../chunk-EQXYK6AL.js";
6
+ import "../chunk-EBO6UKHL.js";
7
+ import "../chunk-4MGIQFAJ.js";
8
+ export {
9
+ applyNestKitDefaults
10
+ };
@@ -0,0 +1,16 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
12
+
13
+ export {
14
+ __decorateClass,
15
+ __decorateParam
16
+ };
@@ -0,0 +1,20 @@
1
+ // src/tracking/request-context.ts
2
+ import { AsyncLocalStorage } from "async_hooks";
3
+ var RequestContext = class {
4
+ static {
5
+ this.storage = new AsyncLocalStorage();
6
+ }
7
+ static run(store, fn) {
8
+ return this.storage.run(store, fn);
9
+ }
10
+ static getStore() {
11
+ return this.storage.getStore();
12
+ }
13
+ static getRequestId() {
14
+ return this.storage.getStore()?.requestId;
15
+ }
16
+ };
17
+
18
+ export {
19
+ RequestContext
20
+ };
File without changes
@@ -0,0 +1,81 @@
1
+ import {
2
+ RequestContext
3
+ } from "./chunk-EBO6UKHL.js";
4
+ import {
5
+ __decorateClass
6
+ } from "./chunk-4MGIQFAJ.js";
7
+
8
+ // src/errors/app.exception.ts
9
+ import { HttpException } from "@nestjs/common";
10
+ var AppException = class extends HttpException {
11
+ constructor(code, message, status, details) {
12
+ super({ code, message, details }, status);
13
+ this.code = code;
14
+ this.details = details;
15
+ }
16
+ };
17
+
18
+ // src/errors/global-exception.filter.ts
19
+ import {
20
+ Catch,
21
+ HttpException as HttpException2,
22
+ HttpStatus,
23
+ Logger
24
+ } from "@nestjs/common";
25
+ var GlobalExceptionFilter = class {
26
+ constructor() {
27
+ this.logger = new Logger(GlobalExceptionFilter.name);
28
+ }
29
+ catch(exception, host) {
30
+ const response = host.switchToHttp().getResponse();
31
+ const resolved = this.resolve(exception);
32
+ if (resolved.status >= HttpStatus.INTERNAL_SERVER_ERROR) {
33
+ const stack = exception instanceof Error ? exception.stack : void 0;
34
+ this.logger.error(`Unhandled exception: ${resolved.message}`, stack);
35
+ }
36
+ const body = {
37
+ success: false,
38
+ requestId: RequestContext.getRequestId() ?? "unknown",
39
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
40
+ error: {
41
+ code: resolved.code,
42
+ message: resolved.message,
43
+ details: resolved.details
44
+ }
45
+ };
46
+ response.status(resolved.status).send(body);
47
+ }
48
+ resolve(exception) {
49
+ if (exception instanceof AppException) {
50
+ return {
51
+ status: exception.getStatus(),
52
+ code: exception.code,
53
+ message: exception.message,
54
+ details: exception.details
55
+ };
56
+ }
57
+ if (exception instanceof HttpException2) {
58
+ const status = exception.getStatus();
59
+ const payload = exception.getResponse();
60
+ const rawMessage = typeof payload === "string" ? payload : payload.message ?? exception.message;
61
+ return {
62
+ status,
63
+ code: HttpStatus[status] ?? "HTTP_ERROR",
64
+ message: Array.isArray(rawMessage) ? rawMessage.join(", ") : rawMessage
65
+ };
66
+ }
67
+ return {
68
+ status: HttpStatus.INTERNAL_SERVER_ERROR,
69
+ code: "INTERNAL_ERROR",
70
+ message: "Internal server error"
71
+ };
72
+ }
73
+ };
74
+ GlobalExceptionFilter = __decorateClass([
75
+ Catch()
76
+ ], GlobalExceptionFilter);
77
+
78
+ export {
79
+ AppException,
80
+ GlobalExceptionFilter
81
+ };
@@ -0,0 +1,40 @@
1
+ import {
2
+ RequestContext
3
+ } from "./chunk-EBO6UKHL.js";
4
+ import {
5
+ __decorateClass
6
+ } from "./chunk-4MGIQFAJ.js";
7
+
8
+ // src/tracking/request-id.middleware.ts
9
+ import { randomUUID } from "crypto";
10
+ import { Injectable } from "@nestjs/common";
11
+ var REQUEST_ID_HEADER = "x-request-id";
12
+ var RequestIdMiddleware = class {
13
+ use(req, res, next) {
14
+ const incoming = req.headers[REQUEST_ID_HEADER];
15
+ const requestId = (Array.isArray(incoming) ? incoming[0] : incoming) ?? randomUUID();
16
+ req.requestId = requestId;
17
+ res.setHeader(REQUEST_ID_HEADER, requestId);
18
+ RequestContext.run({ requestId }, () => next());
19
+ }
20
+ };
21
+ RequestIdMiddleware = __decorateClass([
22
+ Injectable()
23
+ ], RequestIdMiddleware);
24
+
25
+ // src/tracking/tracking.module.ts
26
+ import { Module } from "@nestjs/common";
27
+ var TrackingModule = class {
28
+ configure(consumer) {
29
+ consumer.apply(RequestIdMiddleware).forRoutes("*");
30
+ }
31
+ };
32
+ TrackingModule = __decorateClass([
33
+ Module({})
34
+ ], TrackingModule);
35
+
36
+ export {
37
+ REQUEST_ID_HEADER,
38
+ RequestIdMiddleware,
39
+ TrackingModule
40
+ };
@@ -0,0 +1,39 @@
1
+ import {
2
+ AppException
3
+ } from "./chunk-EQXYK6AL.js";
4
+
5
+ // src/errors/common.exceptions.ts
6
+ import { HttpStatus } from "@nestjs/common";
7
+ var NotFoundAppException = class extends AppException {
8
+ constructor(message = "Resource not found", details, code = "NOT_FOUND") {
9
+ super(code, message, HttpStatus.NOT_FOUND, details);
10
+ }
11
+ };
12
+ var ConflictAppException = class extends AppException {
13
+ constructor(message = "Conflict", details, code = "CONFLICT") {
14
+ super(code, message, HttpStatus.CONFLICT, details);
15
+ }
16
+ };
17
+ var ValidationAppException = class extends AppException {
18
+ constructor(message = "Validation failed", details, code = "VALIDATION_ERROR") {
19
+ super(code, message, HttpStatus.BAD_REQUEST, details);
20
+ }
21
+ };
22
+ var UnauthorizedAppException = class extends AppException {
23
+ constructor(message = "Unauthorized", details, code = "UNAUTHORIZED") {
24
+ super(code, message, HttpStatus.UNAUTHORIZED, details);
25
+ }
26
+ };
27
+ var ForbiddenAppException = class extends AppException {
28
+ constructor(message = "Forbidden", details, code = "FORBIDDEN") {
29
+ super(code, message, HttpStatus.FORBIDDEN, details);
30
+ }
31
+ };
32
+
33
+ export {
34
+ NotFoundAppException,
35
+ ConflictAppException,
36
+ ValidationAppException,
37
+ UnauthorizedAppException,
38
+ ForbiddenAppException
39
+ };
@@ -0,0 +1,36 @@
1
+ import {
2
+ __decorateClass
3
+ } from "./chunk-4MGIQFAJ.js";
4
+
5
+ // src/i18n/i18n.module.ts
6
+ import { Module } from "@nestjs/common";
7
+ import { I18nModule as BaseI18nModule, I18nJsonLoader } from "nestjs-i18n";
8
+ var I18nModule = class {
9
+ static forRoot(options = {}) {
10
+ return BaseI18nModule.forRoot({
11
+ fallbackLanguage: options.fallbackLanguage ?? "es",
12
+ loaderOptions: {
13
+ path: options.path ?? "./i18n/",
14
+ watch: true
15
+ },
16
+ loader: I18nJsonLoader
17
+ });
18
+ }
19
+ };
20
+ I18nModule = __decorateClass([
21
+ Module({})
22
+ ], I18nModule);
23
+
24
+ // src/i18n/translate.helper.ts
25
+ function translateOr(i18n, key, fallback, args) {
26
+ try {
27
+ return i18n.t(key, { args, defaultValue: fallback });
28
+ } catch {
29
+ return fallback;
30
+ }
31
+ }
32
+
33
+ export {
34
+ I18nModule,
35
+ translateOr
36
+ };