@miguelmorales13/nestkit 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,19 @@
1
1
  # @miguelmorales13/nestkit
2
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
3
+ Bloques de infraestructura reutilizables para servicios NestJS. Pensado para eliminar el boilerplate
6
4
  que se repite al arrancar cada backend nuevo.
7
5
 
6
+ **El esqueleto**: entidades base, `Repository<T>` y CRUD genérico, respuestas estandarizadas, manejo
7
+ de errores, tracking de requests, i18n, y un `applyNestKitDefaults` que lo cablea todo en una línea.
8
+
9
+ **Las conexiones**: Postgres (con aislamiento por tenant vía RLS) y Supabase — el contrato de Mongo
10
+ está listo, la implementación no.
11
+
12
+ **Las integraciones**, cada una en su subpath y sin arrastrar dependencias a quien no la use: acceso
13
+ con Google o Facebook (`auth/oauth`) o con email y contraseña (`auth`), Stripe, Telegram, WhatsApp,
14
+ Instagram y Facebook Pages (`meta`), subida de archivos (`storage`), email (Resend o Nodemailer) y
15
+ lectura de analítica (`umami`).
16
+
8
17
  ## ¿Para qué proyectos es esto?
9
18
 
10
19
  **Sí**: cualquier proyecto NestJS nuevo que arranque con el patrón hexagonal/ports-and-adapters
@@ -31,16 +40,30 @@ tu proyecto realmente use:
31
40
  |---------------------|------------------|
32
41
  | `@nestjs/common`, `@nestjs/core`, `reflect-metadata`, `rxjs` | `pg` → `./database/postgres` |
33
42
  | | `@supabase/supabase-js` → `./database/supabase` |
34
- | | `helmet`, `@nestjs/swagger` → `./bootstrap` (`applyNestKitDefaults`) |
43
+ | | `helmet`, `@nestjs/swagger` → `./bootstrap` |
44
+ | | `telegraf` → `./telegram` |
45
+ | | `stripe` → `./stripe` |
46
+ | | `resend` → `./email/resend` |
47
+ | | `nodemailer` → `./email/nodemailer` |
48
+ | | `@aws-sdk/client-s3`, `@aws-sdk/lib-storage`, `@aws-sdk/s3-request-presigner` → `./storage` (solo el adaptador S3) |
35
49
 
36
50
  ```bash
37
51
  bun add @nestjs/common @nestjs/core reflect-metadata rxjs
38
52
  bun add pg # si usas ./database/postgres
39
53
  bun add @supabase/supabase-js # si usas ./database/supabase
40
54
  bun add helmet @nestjs/swagger # si usas ./bootstrap
55
+ bun add telegraf # si usas ./telegram
56
+ bun add stripe # si usas ./stripe
41
57
  bun add class-validator class-transformer # requeridos en runtime por ValidationPipe
42
58
  ```
43
59
 
60
+ `meta`, `media`, `umami` y `auth/oauth` no piden nada extra: hablan con sus APIs por el `fetch` del
61
+ runtime. `auth/oauth` sí necesita cookies en el adaptador HTTP de tu app —`@fastify/cookie` o
62
+ `cookie-parser`—, que es tuyo, no del paquete.
63
+
64
+ `bcryptjs` y `jsonwebtoken` (los usan `auth` y `auth/oauth`) van como dependencias normales, ocultas:
65
+ no los instalas ni los importas vos.
66
+
44
67
  ## Quickstart (`main.ts`)
45
68
 
46
69
  ```ts
@@ -209,6 +232,31 @@ hace que Row-Level-Security de Postgres vea el usuario correcto: abre una transa
209
232
  commit/rollback — **cualquier query que dependa de una policy RLS basada en `auth.uid()` debe pasar
210
233
  por aquí**, `pool.query()` directo no aplica el `set_config` y verá (o no verá) filas incorrectamente.
211
234
 
235
+ ### `database/typeorm` — DataSource + RLS por tenant, para el que quiere ORM
236
+
237
+ Mismo modelo que `database/postgres`, pero con **TypeORM** para quien prefiere entidades,
238
+ repositorios y migraciones del ORM. `TypeOrmDbModule.forRoot({ entities, migrations })` provee un
239
+ `DataSource` inicializado bajo el token `DATA_SOURCE` (`type: 'postgres'` fijo, `synchronize`
240
+ siempre apagado — los cambios de esquema van por migraciones, jamás por auto-sync). Funciona con
241
+ cualquier Postgres, incluido **Neon**.
242
+
243
+ Lo importante para multi-tenant: el pool del ORM no tiene sesión por-request, así que la RLS no
244
+ vería el tenant. Por eso este subpath trae su propio **`withTenantScope(dataSource, session, fn)`**,
245
+ que abre un QueryRunner, corre `set_config(name, value, true)` por cada variable de sesión, y te pasa
246
+ el `EntityManager` **atado a esa conexión y transacción** — dentro, `manager.getRepository(X).find()`
247
+ corre con la RLS aplicada; fuera, no. Es el equivalente en TypeORM del `withTenantScope` de `pg`, y
248
+ está probado: con una policy sobre un GUC, cada tenant ve solo lo suyo y sin scope no ve nada.
249
+
250
+ ```ts
251
+ await withTenantScope(dataSource, {
252
+ 'request.jwt.claim.sub': userId,
253
+ 'request.jwt.claim.tenant': tenantId,
254
+ }, (manager) => manager.getRepository(Order).find());
255
+ ```
256
+
257
+ Peer dep opcional: `typeorm` (solo si se usa este módulo). Elegí ESTE o `database/postgres`, no los
258
+ dos a la vez — son dos formas de lo mismo.
259
+
212
260
  ### `database/supabase` — clientes anon y service-role
213
261
 
214
262
  ```ts
@@ -449,6 +497,44 @@ razonable para la Cloud API, que de todos modos es un REST plano, así que `What
449
497
  `fetch` global de Node (18+) directo. Solo manda mensajes de texto (`sendTextMessage`) — sin
450
498
  templates, sin media, sin manejo de mensajes entrantes.
451
499
 
500
+ ### `meta` — publicar en Instagram y Facebook Pages
501
+
502
+ Cliente de salida sobre la Graph API, solo para publicar: no lee insights, ni maneja webhooks, ni
503
+ comentarios. Mismo alcance que `telegram`.
504
+
505
+ ```ts
506
+ import { MetaModule } from '@miguelmorales13/nestkit/meta';
507
+
508
+ @Module({ imports: [MetaModule.forRoot()] }) // lee META_ACCESS_TOKEN, META_PAGE_ID, META_IG_USER_ID
509
+ export class AppModule {}
510
+ ```
511
+
512
+ ```ts
513
+ await meta.publishToInstagram(url, 'Texto del pie', 'feed'); // 'feed' | 'stories' | 'reels'
514
+ await meta.publishToPage(url, 'Texto');
515
+ await meta.publishStoryToPage(url);
516
+ await meta.describe(); // a qué cuenta apunta el token, para diagnosticar
517
+ ```
518
+
519
+ Dos cosas de esta API mandan sobre todo lo demás, y conviene saberlas antes:
520
+
521
+ **Las imágenes nunca se suben.** Se pasa una URL pública y Meta la descarga. Solo la necesita
522
+ durante la publicación —después guarda su propia copia y sirve el archivo desde su CDN—, así que
523
+ alcanza con un hosting efímero. Para eso está `media`, en este mismo paquete.
524
+
525
+ **Publicar en una Página exige un Page token, no un user token.** Con un token de usuario Meta
526
+ asume que estás posteando como persona y pide `publish_actions`, un permiso que eliminó en 2018, así
527
+ que el error que devuelve no apunta ni de cerca a la causa real.
528
+
529
+ Permisos: `instagram_basic` e `instagram_content_publish` para Instagram, `pages_manage_posts` para
530
+ Facebook. Publicar en tus propias Páginas no necesita App Review; hacerlo en nombre de terceros sí.
531
+
532
+ Instagram publica en dos pasos (crear contenedor, publicar) y el servicio espera solo a que el
533
+ contenedor esté listo — no hace falta encadenarlo a mano.
534
+
535
+ `forRoot()` sin argumentos toma las credenciales del entorno; pasándoselas explícitas se puede tener
536
+ un proceso publicando en varias cuentas.
537
+
452
538
  ### `stripe` — cliente de la SDK oficial
453
539
 
454
540
  ```ts
@@ -799,6 +885,82 @@ Bunny y local no requieren nada extra. Variables de entorno reconocidas: `STORAG
799
885
  Storage (no hay firma que oculte la contraseña de la zona); en esos casos se sube por la API, o se
800
886
  usa Bunny Stream/TUS para video grande.
801
887
 
888
+ ### `media` — hosting efímero para APIs que descargan por URL
889
+
890
+ No es almacenamiento. Es el complemento de `meta`: existe porque hay APIs —la de Meta es el caso
891
+ típico— que **rechazan el binario** y exigen bajarse el archivo ellas mismas, pero solo lo necesitan
892
+ durante la llamada. Sin algo así, el apaño habitual es commitear las imágenes al repositorio, que
893
+ las conserva para siempre por una URL que importó un minuto.
894
+
895
+ ```ts
896
+ import { MediaModule } from '@miguelmorales13/nestkit/media';
897
+
898
+ MediaModule.forRoot({
899
+ publicUrl: 'https://api.tuapp.com/api', // ¡con el prefijo global!
900
+ uploadSecret: process.env.MEDIA_SECRET,
901
+ ttlMs: 60 * 60 * 1000, // por defecto una hora
902
+ })
903
+ ```
904
+
905
+ Expone `POST /media` (devuelve `{ url }`) y `GET /media/:id`. La subida va protegida con un secreto
906
+ compartido en la cabecera `x-media-secret`, no con un JWT, porque quien llama es un job de CI y no
907
+ una persona con sesión; sin él cualquiera podría alojar contenido arbitrario en tu dominio. La
908
+ descarga es pública a propósito: los servicios que consumen estas URLs las piden sin credenciales.
909
+
910
+ **Vive en memoria, a propósito.** Entre guardar y publicar pasan segundos, así que un reinicio en el
911
+ medio solo significa reintentar, y no queda nada en disco. Si necesitás que los archivos sobrevivan
912
+ a un reinicio, esta es la herramienta equivocada: usá `storage`.
913
+
914
+ Dos errores que cuestan una tarde:
915
+
916
+ - `publicUrl` **tiene que incluir el prefijo global** de la app. Sin él la URL cae en lo que sirva el
917
+ frontend, que responde 200 con una página HTML, y el consumidor se descarga eso en vez del archivo.
918
+ - Fastify solo parsea JSON de fábrica. Sin registrar un parser para los tipos que vas a aceptar, la
919
+ subida responde 415:
920
+
921
+ ```ts
922
+ app.getHttpAdapter().getInstance().addContentTypeParser(
923
+ /^image\//, { parseAs: 'buffer' }, (_req, body, done) => done(null, body),
924
+ )
925
+ ```
926
+
927
+ ### `umami` — leer la API de analítica
928
+
929
+ Solo lee. Los eventos los manda el script del navegador; un backend escribiéndolos estaría
930
+ reportando tráfico que nunca ocurrió.
931
+
932
+ ```ts
933
+ import { UmamiModule, UmamiService } from '@miguelmorales13/nestkit/umami';
934
+
935
+ UmamiModule.forRoot({
936
+ apiKey: process.env.UMAMI_API_KEY, // Umami Cloud: Settings → API Keys
937
+ websiteId: process.env.UMAMI_WEBSITE_ID,
938
+ timezone: 'America/Mexico_City',
939
+ // baseUrl: 'https://tu-host/api', // solo si es self-hosted
940
+ })
941
+ ```
942
+
943
+ ```ts
944
+ const rango = UmamiService.lastDays(7);
945
+ await umami.stats(rango); // visitantes, pageviews, visitas, rebotes
946
+ await umami.metrics('referrer', rango, 5); // top 5 de dónde llegan
947
+ await umami.eventCount('registro', rango); // cuántas veces disparó un evento
948
+ ```
949
+
950
+ **Todos los métodos devuelven `null` en vez de lanzar** cuando la llamada falla. Un resumen suele
951
+ pedir varias cosas a la vez, y que un endpoint esté caído no debería llevarse el reporte entero; quien
952
+ llama decide qué significa un bloque ausente. Los fallos se loguean, así que uno persistente sigue
953
+ siendo visible.
954
+
955
+ `eventCount` distingue `0` de `null` a propósito: uno es «nadie lo hizo», el otro es «no sabemos».
956
+
957
+ `timezone` no es opcional en la práctica. Umami agrupa por día en esa zona, y dejarlo sin poner
958
+ reporta días UTC en silencio — que fuera de UTC significa un «día» que empieza a media tarde.
959
+
960
+ Deliberadamente no opina sobre qué significan los números: qué eventos forman un embudo, cómo
961
+ llamarlos y cómo redactar un reporte son decisiones de producto que cambian por proyecto. Esto solo
962
+ trae los datos.
963
+
802
964
  ## Subpaths disponibles
803
965
 
804
966
  | Subpath | Qué trae |
@@ -810,10 +972,13 @@ usa Bunny Stream/TUS para video grande.
810
972
  | `@miguelmorales13/nestkit/tracking` | `RequestContext`, `RequestIdMiddleware`, `TrackingModule` |
811
973
  | `@miguelmorales13/nestkit/crud` | `Repository<T>`, `BaseCrudService`, `createCrudController` |
812
974
  | `@miguelmorales13/nestkit/database/postgres` | `PgModule`, `PG_POOL`, `withTenantScope` |
975
+ | `@miguelmorales13/nestkit/database/typeorm` | `TypeOrmDbModule`, `DATA_SOURCE`, `withTenantScope` |
813
976
  | `@miguelmorales13/nestkit/database/supabase` | `SupabaseModule`, `SUPABASE_ANON_CLIENT`, `SUPABASE_SERVICE_ROLE_CLIENT` |
814
977
  | `@miguelmorales13/nestkit/database/mongo` | Solo el contrato `MongoRepositoryPort` — sin implementación |
815
978
  | `@miguelmorales13/nestkit/telegram` | `TelegramModule`, `TELEGRAM_BOT` |
816
979
  | `@miguelmorales13/nestkit/whatsapp` | `WhatsAppModule`, `WHATSAPP_CLIENT`, `WhatsAppClient` |
980
+ | `@miguelmorales13/nestkit/meta` | `MetaModule`, `MetaService` — publicar en Instagram y Facebook Pages |
981
+ | `@miguelmorales13/nestkit/media` | `MediaModule`, `MediaService` — hosting efímero en memoria |
817
982
  | `@miguelmorales13/nestkit/stripe` | `StripeModule`, `STRIPE_CLIENT`, `createStripeWebhookController` |
818
983
  | `@miguelmorales13/nestkit/auth` | `AuthUserPort`, `BaseAuthService`, `createAuthController`, `TokenService`, `JwtAuthGuard`, `RolesGuard`, `RequireTenantGuard`, `CurrentUser`, `CurrentTenant`, `Roles` |
819
984
  | `@miguelmorales13/nestkit/auth/oauth` | `OAuthAuthModule`, `OAuthAuthService`, `createOAuthController`, `googleProvider`, `facebookProvider`, `OAuthStorePort`, `RefreshTokenStorePort` |
@@ -826,7 +991,7 @@ usa Bunny Stream/TUS para video grande.
826
991
 
827
992
  ## Estado del paquete
828
993
 
829
- `0.6.0`. Sin adaptador Mongo real. Sin tests unitarios propios.
994
+ `0.7.0`. Sin adaptador Mongo real. Sin tests unitarios propios.
830
995
 
831
996
  `auth/oauth` y `umami` sí tienen un consumidor real: **nutrimx** corre su acceso con Google y
832
997
  Facebook sobre este módulo en producción. El resto sigue sin ejercitarse de verdad.
@@ -1,11 +1,11 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
- var _chunk54ZXIB5Tcjs = require('../chunk-54ZXIB5T.cjs');
4
- require('../chunk-TJHRABML.cjs');
3
+ var _chunkYARLPYG5cjs = require('../chunk-YARLPYG5.cjs');
5
4
  require('../chunk-J7TURDAL.cjs');
5
+ require('../chunk-TJHRABML.cjs');
6
6
  require('../chunk-ZA56XBCK.cjs');
7
7
  require('../chunk-R7BVS6CI.cjs');
8
8
  require('../chunk-2REOCMUD.cjs');
9
9
 
10
10
 
11
- exports.applyNestKitDefaults = _chunk54ZXIB5Tcjs.applyNestKitDefaults;
11
+ exports.applyNestKitDefaults = _chunkYARLPYG5cjs.applyNestKitDefaults;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  applyNestKitDefaults
3
- } from "../chunk-AOCF5QCZ.js";
4
- import "../chunk-KDAA6GFF.js";
3
+ } from "../chunk-ANQ3YPDI.js";
5
4
  import "../chunk-7I2Y7V52.js";
5
+ import "../chunk-KDAA6GFF.js";
6
6
  import "../chunk-EBO6UKHL.js";
7
7
  import "../chunk-YFYHLYHN.js";
8
8
  import "../chunk-4MGIQFAJ.js";
@@ -0,0 +1,79 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
2
+
3
+ var _chunk2REOCMUDcjs = require('../../chunk-2REOCMUD.cjs');
4
+
5
+ // src/database/typeorm/typeorm.module.ts
6
+ var _common = require('@nestjs/common');
7
+ var _typeorm = require('typeorm');
8
+ var DATA_SOURCE = /* @__PURE__ */ Symbol("DATA_SOURCE");
9
+ function buildDataSource(options) {
10
+ const url = _nullishCoalesce(options.url, () => ( process.env.DATABASE_URL));
11
+ const hasHost = Boolean(options.host);
12
+ if (!url && !hasHost) {
13
+ throw new Error("TypeOrmDbModule: DATABASE_URL is not set and no host was provided.");
14
+ }
15
+ return new (0, _typeorm.DataSource)({
16
+ type: "postgres",
17
+ url,
18
+ synchronize: false,
19
+ ...options
20
+ });
21
+ }
22
+ var TypeOrmDbModule = class {
23
+ static forRoot(options = {}) {
24
+ const provider = {
25
+ provide: DATA_SOURCE,
26
+ useFactory: async () => {
27
+ const dataSource = buildDataSource(options);
28
+ return dataSource.isInitialized ? dataSource : dataSource.initialize();
29
+ }
30
+ };
31
+ return { module: TypeOrmDbModule, providers: [provider], exports: [DATA_SOURCE] };
32
+ }
33
+ static forRootAsync(config) {
34
+ const provider = {
35
+ provide: DATA_SOURCE,
36
+ useFactory: async (...args) => {
37
+ const options = await config.useFactory(...args);
38
+ const dataSource = buildDataSource(options);
39
+ return dataSource.isInitialized ? dataSource : dataSource.initialize();
40
+ },
41
+ inject: _nullishCoalesce(config.inject, () => ( []))
42
+ };
43
+ return {
44
+ module: TypeOrmDbModule,
45
+ imports: _nullishCoalesce(config.imports, () => ( [])),
46
+ providers: [provider],
47
+ exports: [DATA_SOURCE]
48
+ };
49
+ }
50
+ };
51
+ TypeOrmDbModule = exports.TypeOrmDbModule = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
52
+ _common.Global.call(void 0, ),
53
+ _common.Module.call(void 0, {})
54
+ ], TypeOrmDbModule);
55
+
56
+ // src/database/typeorm/tenant-scope.ts
57
+ async function withTenantScope(dataSource, session, fn) {
58
+ const runner = dataSource.createQueryRunner();
59
+ await runner.connect();
60
+ await runner.startTransaction();
61
+ try {
62
+ for (const [name, value] of Object.entries(session)) {
63
+ await runner.query("SELECT set_config($1, $2, true)", [name, value]);
64
+ }
65
+ const result = await fn(runner.manager);
66
+ await runner.commitTransaction();
67
+ return result;
68
+ } catch (error) {
69
+ await runner.rollbackTransaction();
70
+ throw error;
71
+ } finally {
72
+ await runner.release();
73
+ }
74
+ }
75
+
76
+
77
+
78
+
79
+ exports.DATA_SOURCE = DATA_SOURCE; exports.TypeOrmDbModule = TypeOrmDbModule; exports.withTenantScope = withTenantScope;
@@ -0,0 +1,3 @@
1
+ export { DATA_SOURCE, TypeOrmDbModule } from './typeorm.module.js';
2
+ export type { TypeOrmDbOptions } from './typeorm.module.js';
3
+ export { withTenantScope } from './tenant-scope.js';
@@ -0,0 +1,79 @@
1
+ import {
2
+ __decorateClass
3
+ } from "../../chunk-4MGIQFAJ.js";
4
+
5
+ // src/database/typeorm/typeorm.module.ts
6
+ import { Global, Module } from "@nestjs/common";
7
+ import { DataSource } from "typeorm";
8
+ var DATA_SOURCE = /* @__PURE__ */ Symbol("DATA_SOURCE");
9
+ function buildDataSource(options) {
10
+ const url = options.url ?? process.env.DATABASE_URL;
11
+ const hasHost = Boolean(options.host);
12
+ if (!url && !hasHost) {
13
+ throw new Error("TypeOrmDbModule: DATABASE_URL is not set and no host was provided.");
14
+ }
15
+ return new DataSource({
16
+ type: "postgres",
17
+ url,
18
+ synchronize: false,
19
+ ...options
20
+ });
21
+ }
22
+ var TypeOrmDbModule = class {
23
+ static forRoot(options = {}) {
24
+ const provider = {
25
+ provide: DATA_SOURCE,
26
+ useFactory: async () => {
27
+ const dataSource = buildDataSource(options);
28
+ return dataSource.isInitialized ? dataSource : dataSource.initialize();
29
+ }
30
+ };
31
+ return { module: TypeOrmDbModule, providers: [provider], exports: [DATA_SOURCE] };
32
+ }
33
+ static forRootAsync(config) {
34
+ const provider = {
35
+ provide: DATA_SOURCE,
36
+ useFactory: async (...args) => {
37
+ const options = await config.useFactory(...args);
38
+ const dataSource = buildDataSource(options);
39
+ return dataSource.isInitialized ? dataSource : dataSource.initialize();
40
+ },
41
+ inject: config.inject ?? []
42
+ };
43
+ return {
44
+ module: TypeOrmDbModule,
45
+ imports: config.imports ?? [],
46
+ providers: [provider],
47
+ exports: [DATA_SOURCE]
48
+ };
49
+ }
50
+ };
51
+ TypeOrmDbModule = __decorateClass([
52
+ Global(),
53
+ Module({})
54
+ ], TypeOrmDbModule);
55
+
56
+ // src/database/typeorm/tenant-scope.ts
57
+ async function withTenantScope(dataSource, session, fn) {
58
+ const runner = dataSource.createQueryRunner();
59
+ await runner.connect();
60
+ await runner.startTransaction();
61
+ try {
62
+ for (const [name, value] of Object.entries(session)) {
63
+ await runner.query("SELECT set_config($1, $2, true)", [name, value]);
64
+ }
65
+ const result = await fn(runner.manager);
66
+ await runner.commitTransaction();
67
+ return result;
68
+ } catch (error) {
69
+ await runner.rollbackTransaction();
70
+ throw error;
71
+ } finally {
72
+ await runner.release();
73
+ }
74
+ }
75
+ export {
76
+ DATA_SOURCE,
77
+ TypeOrmDbModule,
78
+ withTenantScope
79
+ };
@@ -0,0 +1,18 @@
1
+ import type { DataSource, EntityManager } from 'typeorm';
2
+ /**
3
+ * Runs `fn` inside a transaction whose Postgres session variables are set first,
4
+ * so Row-Level-Security policies keyed on those variables see the right tenant.
5
+ * This is the TypeORM equivalent of the `pg` `withTenantScope`: the ORM's own
6
+ * pool would otherwise run each query on an arbitrary connection with no session
7
+ * context, and RLS would see nothing (or the wrong rows).
8
+ *
9
+ * The `EntityManager` handed to `fn` is bound to that one connection and
10
+ * transaction — so `manager.getRepository(X).find()` inside runs scoped, and
11
+ * anything outside `fn` does NOT. Never leak the manager past the callback.
12
+ *
13
+ * `session` maps GUC name → value, e.g.
14
+ * `{ 'request.jwt.claim.sub': userId, 'request.jwt.claim.tenant': tenantId }`.
15
+ * Each is applied with `set_config(name, value, true)` (transaction-local), which
16
+ * is why the whole thing has to be one transaction on one connection.
17
+ */
18
+ export declare function withTenantScope<T>(dataSource: DataSource, session: Record<string, string>, fn: (manager: EntityManager) => Promise<T>): Promise<T>;
@@ -0,0 +1,35 @@
1
+ import { type DynamicModule } from '@nestjs/common';
2
+ import { type DataSourceOptions } from 'typeorm';
3
+ /** DI token for the shared TypeORM {@link DataSource}. Inject with `@Inject(DATA_SOURCE)`. */
4
+ export declare const DATA_SOURCE: unique symbol;
5
+ /**
6
+ * Options for the DataSource. Everything TypeORM's Postgres options accept, minus
7
+ * the bits this module fixes: `type` is always `postgres`, and `synchronize` is
8
+ * forced off (schema changes go through migrations, never an auto-sync that would
9
+ * happily drop a column in production). `url` defaults to `DATABASE_URL`.
10
+ */
11
+ export type TypeOrmDbOptions = Partial<Omit<DataSourceOptions, 'type' | 'synchronize'>>;
12
+ /**
13
+ * Provides a single, initialized TypeORM {@link DataSource} for the app, on
14
+ * Postgres (works with any Postgres, including Neon). Register once in the root
15
+ * module; inject the DataSource with `@Inject(DATA_SOURCE)`.
16
+ *
17
+ * For multi-tenant apps that rely on Postgres RLS, pair it with `withTenantScope`
18
+ * (same folder): the ORM pool alone has no per-request session, so tenant-scoped
19
+ * work must go through that helper.
20
+ *
21
+ * ```ts
22
+ * TypeOrmDbModule.forRoot({ entities: [User, Order], migrations: ['dist/migrations/*.js'] })
23
+ * ```
24
+ *
25
+ * `synchronize` is always off and `type` is always `postgres` — schema changes go
26
+ * through migrations. Install `typeorm` (optional peer dep) to use this module.
27
+ */
28
+ export declare class TypeOrmDbModule {
29
+ static forRoot(options?: TypeOrmDbOptions): DynamicModule;
30
+ static forRootAsync(config: {
31
+ imports?: any[];
32
+ inject?: any[];
33
+ useFactory: (...args: any[]) => TypeOrmDbOptions | Promise<TypeOrmDbOptions>;
34
+ }): DynamicModule;
35
+ }
package/dist/index.cjs CHANGED
@@ -9,6 +9,7 @@ var _chunkSPTDXUEDcjs = require('./chunk-SPTDXUED.cjs');
9
9
 
10
10
 
11
11
  var _chunkRF75KC63cjs = require('./chunk-RF75KC63.cjs');
12
+ require('./chunk-7SOM7EZP.cjs');
12
13
  require('./chunk-MR2IFCZE.cjs');
13
14
 
14
15
 
@@ -17,38 +18,37 @@ require('./chunk-MR2IFCZE.cjs');
17
18
  var _chunkV2M75FN3cjs = require('./chunk-V2M75FN3.cjs');
18
19
 
19
20
 
20
- var _chunk54ZXIB5Tcjs = require('./chunk-54ZXIB5T.cjs');
21
21
 
22
22
 
23
- var _chunkTJHRABMLcjs = require('./chunk-TJHRABML.cjs');
24
23
 
25
24
 
25
+ var _chunkFDNGAYTZcjs = require('./chunk-FDNGAYTZ.cjs');
26
26
 
27
- var _chunk3CLYZC3Tcjs = require('./chunk-3CLYZC3T.cjs');
28
27
 
28
+ var _chunkYARLPYG5cjs = require('./chunk-YARLPYG5.cjs');
29
29
 
30
- var _chunkQS2W5XCQcjs = require('./chunk-QS2W5XCQ.cjs');
31
30
 
31
+ var _chunkJ7TURDALcjs = require('./chunk-J7TURDAL.cjs');
32
32
 
33
33
 
34
- var _chunkM3EL5O6Tcjs = require('./chunk-M3EL5O6T.cjs');
35
- require('./chunk-7SOM7EZP.cjs');
34
+ var _chunkTJHRABMLcjs = require('./chunk-TJHRABML.cjs');
36
35
 
37
36
 
37
+ var _chunkZA56XBCKcjs = require('./chunk-ZA56XBCK.cjs');
38
38
 
39
39
 
40
+ var _chunkR7BVS6CIcjs = require('./chunk-R7BVS6CI.cjs');
40
41
 
41
42
 
42
- var _chunkFDNGAYTZcjs = require('./chunk-FDNGAYTZ.cjs');
43
43
 
44
+ var _chunk3CLYZC3Tcjs = require('./chunk-3CLYZC3T.cjs');
44
45
 
45
- var _chunkJ7TURDALcjs = require('./chunk-J7TURDAL.cjs');
46
46
 
47
+ var _chunkQS2W5XCQcjs = require('./chunk-QS2W5XCQ.cjs');
47
48
 
48
- var _chunkZA56XBCKcjs = require('./chunk-ZA56XBCK.cjs');
49
49
 
50
50
 
51
- var _chunkR7BVS6CIcjs = require('./chunk-R7BVS6CI.cjs');
51
+ var _chunkM3EL5O6Tcjs = require('./chunk-M3EL5O6T.cjs');
52
52
  require('./chunk-2REOCMUD.cjs');
53
53
 
54
54
 
@@ -75,4 +75,4 @@ require('./chunk-2REOCMUD.cjs');
75
75
 
76
76
 
77
77
 
78
- exports.AppException = _chunkR7BVS6CIcjs.AppException; exports.BaseCrudService = _chunk3CLYZC3Tcjs.BaseCrudService; exports.BaseResponseDto = _chunkQS2W5XCQcjs.BaseResponseDto; exports.ConflictAppException = _chunkFDNGAYTZcjs.ConflictAppException; exports.ForbiddenAppException = _chunkFDNGAYTZcjs.ForbiddenAppException; exports.GlobalExceptionFilter = _chunkJ7TURDALcjs.GlobalExceptionFilter; exports.I18nModule = _chunkM3EL5O6Tcjs.I18nModule; exports.NotFoundAppException = _chunkFDNGAYTZcjs.NotFoundAppException; exports.PG_POOL = _chunkRF75KC63cjs.PG_POOL; exports.PgModule = _chunkRF75KC63cjs.PgModule; exports.REQUEST_ID_HEADER = _chunkV2M75FN3cjs.REQUEST_ID_HEADER; exports.RequestContext = _chunkZA56XBCKcjs.RequestContext; exports.RequestIdMiddleware = _chunkV2M75FN3cjs.RequestIdMiddleware; exports.ResponseInterceptor = _chunkTJHRABMLcjs.ResponseInterceptor; exports.SUPABASE_ANON_CLIENT = _chunkSPTDXUEDcjs.SUPABASE_ANON_CLIENT; exports.SUPABASE_SERVICE_ROLE_CLIENT = _chunkSPTDXUEDcjs.SUPABASE_SERVICE_ROLE_CLIENT; exports.SupabaseModule = _chunkSPTDXUEDcjs.SupabaseModule; exports.TrackingModule = _chunkV2M75FN3cjs.TrackingModule; exports.UnauthorizedAppException = _chunkFDNGAYTZcjs.UnauthorizedAppException; exports.ValidationAppException = _chunkFDNGAYTZcjs.ValidationAppException; exports.applyNestKitDefaults = _chunk54ZXIB5Tcjs.applyNestKitDefaults; exports.createCrudController = _chunk3CLYZC3Tcjs.createCrudController; exports.translateOr = _chunkM3EL5O6Tcjs.translateOr; exports.withTenantScope = _chunkRF75KC63cjs.withTenantScope;
78
+ exports.AppException = _chunkR7BVS6CIcjs.AppException; exports.BaseCrudService = _chunk3CLYZC3Tcjs.BaseCrudService; exports.BaseResponseDto = _chunkQS2W5XCQcjs.BaseResponseDto; exports.ConflictAppException = _chunkFDNGAYTZcjs.ConflictAppException; exports.ForbiddenAppException = _chunkFDNGAYTZcjs.ForbiddenAppException; exports.GlobalExceptionFilter = _chunkJ7TURDALcjs.GlobalExceptionFilter; exports.I18nModule = _chunkM3EL5O6Tcjs.I18nModule; exports.NotFoundAppException = _chunkFDNGAYTZcjs.NotFoundAppException; exports.PG_POOL = _chunkRF75KC63cjs.PG_POOL; exports.PgModule = _chunkRF75KC63cjs.PgModule; exports.REQUEST_ID_HEADER = _chunkV2M75FN3cjs.REQUEST_ID_HEADER; exports.RequestContext = _chunkZA56XBCKcjs.RequestContext; exports.RequestIdMiddleware = _chunkV2M75FN3cjs.RequestIdMiddleware; exports.ResponseInterceptor = _chunkTJHRABMLcjs.ResponseInterceptor; exports.SUPABASE_ANON_CLIENT = _chunkSPTDXUEDcjs.SUPABASE_ANON_CLIENT; exports.SUPABASE_SERVICE_ROLE_CLIENT = _chunkSPTDXUEDcjs.SUPABASE_SERVICE_ROLE_CLIENT; exports.SupabaseModule = _chunkSPTDXUEDcjs.SupabaseModule; exports.TrackingModule = _chunkV2M75FN3cjs.TrackingModule; exports.UnauthorizedAppException = _chunkFDNGAYTZcjs.UnauthorizedAppException; exports.ValidationAppException = _chunkFDNGAYTZcjs.ValidationAppException; exports.applyNestKitDefaults = _chunkYARLPYG5cjs.applyNestKitDefaults; exports.createCrudController = _chunk3CLYZC3Tcjs.createCrudController; exports.translateOr = _chunkM3EL5O6Tcjs.translateOr; exports.withTenantScope = _chunkRF75KC63cjs.withTenantScope;
package/dist/index.js CHANGED
@@ -9,30 +9,13 @@ import {
9
9
  PgModule,
10
10
  withTenantScope
11
11
  } from "./chunk-VKOPDDCC.js";
12
+ import "./chunk-DQYAIQQ5.js";
12
13
  import "./chunk-NAK4WDKS.js";
13
14
  import {
14
15
  REQUEST_ID_HEADER,
15
16
  RequestIdMiddleware,
16
17
  TrackingModule
17
18
  } from "./chunk-EYURGACO.js";
18
- import {
19
- applyNestKitDefaults
20
- } from "./chunk-AOCF5QCZ.js";
21
- import {
22
- ResponseInterceptor
23
- } from "./chunk-KDAA6GFF.js";
24
- import {
25
- BaseCrudService,
26
- createCrudController
27
- } from "./chunk-JOVBJDJ2.js";
28
- import {
29
- BaseResponseDto
30
- } from "./chunk-XX2HPTRU.js";
31
- import {
32
- I18nModule,
33
- translateOr
34
- } from "./chunk-IYUUYCP5.js";
35
- import "./chunk-DQYAIQQ5.js";
36
19
  import {
37
20
  ConflictAppException,
38
21
  ForbiddenAppException,
@@ -40,15 +23,32 @@ import {
40
23
  UnauthorizedAppException,
41
24
  ValidationAppException
42
25
  } from "./chunk-ORWJ7LES.js";
26
+ import {
27
+ applyNestKitDefaults
28
+ } from "./chunk-ANQ3YPDI.js";
43
29
  import {
44
30
  GlobalExceptionFilter
45
31
  } from "./chunk-7I2Y7V52.js";
32
+ import {
33
+ ResponseInterceptor
34
+ } from "./chunk-KDAA6GFF.js";
46
35
  import {
47
36
  RequestContext
48
37
  } from "./chunk-EBO6UKHL.js";
49
38
  import {
50
39
  AppException
51
40
  } from "./chunk-YFYHLYHN.js";
41
+ import {
42
+ BaseCrudService,
43
+ createCrudController
44
+ } from "./chunk-JOVBJDJ2.js";
45
+ import {
46
+ BaseResponseDto
47
+ } from "./chunk-XX2HPTRU.js";
48
+ import {
49
+ I18nModule,
50
+ translateOr
51
+ } from "./chunk-IYUUYCP5.js";
52
52
  import "./chunk-4MGIQFAJ.js";
53
53
  export {
54
54
  AppException,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miguelmorales13/nestkit",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,6 +51,11 @@
51
51
  "import": "./dist/database/postgres/index.js",
52
52
  "require": "./dist/database/postgres/index.cjs"
53
53
  },
54
+ "./database/typeorm": {
55
+ "types": "./dist/database/typeorm/index.d.ts",
56
+ "import": "./dist/database/typeorm/index.js",
57
+ "require": "./dist/database/typeorm/index.cjs"
58
+ },
54
59
  "./database/supabase": {
55
60
  "types": "./dist/database/supabase/index.d.ts",
56
61
  "import": "./dist/database/supabase/index.js",
@@ -149,7 +154,8 @@
149
154
  "nodemailer": "^9.0.5",
150
155
  "@aws-sdk/client-s3": "^3.0.0",
151
156
  "@aws-sdk/s3-request-presigner": "^3.0.0",
152
- "@aws-sdk/lib-storage": "^3.0.0"
157
+ "@aws-sdk/lib-storage": "^3.0.0",
158
+ "typeorm": "^0.3.0 || ^1.0.0"
153
159
  },
154
160
  "peerDependenciesMeta": {
155
161
  "@aws-sdk/client-s3": {
@@ -190,6 +196,9 @@
190
196
  },
191
197
  "nodemailer": {
192
198
  "optional": true
199
+ },
200
+ "typeorm": {
201
+ "optional": true
193
202
  }
194
203
  },
195
204
  "dependencies": {
@@ -222,6 +231,7 @@
222
231
  "stripe": "^22.5.0",
223
232
  "telegraf": "^4.16.3",
224
233
  "tsup": "^8.5.1",
234
+ "typeorm": "^1.1.0",
225
235
  "typescript": "^5.6.0"
226
236
  },
227
237
  "overrides": {
@@ -1,9 +1,9 @@
1
- import {
2
- ResponseInterceptor
3
- } from "./chunk-KDAA6GFF.js";
4
1
  import {
5
2
  GlobalExceptionFilter
6
3
  } from "./chunk-7I2Y7V52.js";
4
+ import {
5
+ ResponseInterceptor
6
+ } from "./chunk-KDAA6GFF.js";
7
7
 
8
8
  // src/bootstrap/apply-defaults.ts
9
9
  import {
@@ -1,9 +1,9 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkTJHRABMLcjs = require('./chunk-TJHRABML.cjs');
3
+ var _chunkJ7TURDALcjs = require('./chunk-J7TURDAL.cjs');
4
4
 
5
5
 
6
- var _chunkJ7TURDALcjs = require('./chunk-J7TURDAL.cjs');
6
+ var _chunkTJHRABMLcjs = require('./chunk-TJHRABML.cjs');
7
7
 
8
8
  // src/bootstrap/apply-defaults.ts
9
9