@miguelmorales13/nestkit 0.4.0 → 0.5.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
@@ -685,6 +685,45 @@ Requiere `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`; `SMTP_SECURE` e
685
685
  para forzar TLS directo). Elegí Nodemailer cuando ya tenés un proveedor SMTP propio (Gmail, SES,
686
686
  Mailgun, tu propio servidor) en vez de sumar otra cuenta de terceros.
687
687
 
688
+ ### `storage` — subida de archivos agnóstica del proveedor
689
+
690
+ Almacenamiento de archivos con un puerto único (`StoragePort`) y varios adaptadores intercambiables:
691
+ **local** (disco, para desarrollo), **S3-compatible** (AWS S3 y **Cloudflare R2**) y **Bunny.net**.
692
+ Sirve cualquier tipo de archivo — imágenes, PDF y otros documentos, video, audio — sin suponer nada
693
+ del contenido. Cambiar de R2 a Bunny (o al revés) es **una línea** en `StorageModule.forRoot(...)`:
694
+ nada depende de un proveedor concreto, todo depende del puerto.
695
+
696
+ Trae streaming en ambos sentidos (`putStream` para subir grandes/video sin cargarlos en memoria,
697
+ `getStream` con **rango de bytes** para el seek de video/audio), subida **directa firmada** al
698
+ proveedor (`signedUploadUrl`, para que un archivo pesado no pase por tu API), URLs de lectura
699
+ firmadas (`signedReadUrl`), y `stat`/`exists`/`list`/`delete`.
700
+
701
+ ```ts
702
+ // En el módulo raíz — elegí el proveedor acá y nada más cambia:
703
+ StorageModule.forRoot({ provider: 'r2' }) // lee R2_* del entorno
704
+ StorageModule.forRoot({ provider: 'bunny' }) // lee BUNNY_* del entorno
705
+ StorageModule.forRoot({ provider: 'local', local: { root: '.storage' } })
706
+ StorageModule.forRootAsync({ inject: [Config], useFactory: (c) => ({ provider: c.storageProvider }) })
707
+
708
+ // En cualquier servicio:
709
+ constructor(@Inject(STORAGE) private readonly storage: StoragePort) {}
710
+
711
+ await this.storage.put(`${tenantId}/platillos/${id}.webp`, buffer, { contentType: 'image/webp' });
712
+ const { stream, totalSize, contentRange } = await this.storage.getStream(key, { start, end }); // video
713
+ const target = await this.storage.signedUploadUrl(key, { contentType: 'video/mp4' }); // subida directa
714
+ const url = this.storage.publicUrl(key) ?? (await this.storage.signedReadUrl(key));
715
+ ```
716
+
717
+ Los adaptadores S3/R2 cargan `@aws-sdk/client-s3` **de forma diferida** (solo si se usa ese
718
+ proveedor), así que un proyecto con Bunny o local no necesita el SDK. Peer deps opcionales:
719
+ `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner`, `@aws-sdk/lib-storage` (solo para S3/R2);
720
+ Bunny y local no requieren nada extra. Variables de entorno reconocidas: `STORAGE_PROVIDER`,
721
+ `STORAGE_LOCAL_ROOT`/`STORAGE_PUBLIC_BASE_URL`, `S3_*`, `R2_*`, `BUNNY_*` (ver `resolveStorageOptions`).
722
+
723
+ `signedUploadUrl` no existe en el adaptador local (un cliente no puede subir a tu disco) ni en Bunny
724
+ Storage (no hay firma que oculte la contraseña de la zona); en esos casos se sube por la API, o se
725
+ usa Bunny Stream/TUS para video grande.
726
+
688
727
  ## Subpaths disponibles
689
728
 
690
729
  | Subpath | Qué trae |
@@ -702,6 +741,7 @@ Mailgun, tu propio servidor) en vez de sumar otra cuenta de terceros.
702
741
  | `@miguelmorales13/nestkit/whatsapp` | `WhatsAppModule`, `WHATSAPP_CLIENT`, `WhatsAppClient` |
703
742
  | `@miguelmorales13/nestkit/stripe` | `StripeModule`, `STRIPE_CLIENT`, `createStripeWebhookController` |
704
743
  | `@miguelmorales13/nestkit/auth` | `AuthUserPort`, `BaseAuthService`, `createAuthController`, `TokenService`, `JwtAuthGuard`, `RolesGuard`, `RequireTenantGuard`, `CurrentUser`, `CurrentTenant`, `Roles` |
744
+ | `@miguelmorales13/nestkit/storage` | `StorageModule`, `STORAGE`, `StoragePort`, adaptadores `LocalStorage`/`S3Storage`/`BunnyStorage` |
705
745
  | `@miguelmorales13/nestkit/email/resend` | `ResendModule`, `RESEND_CLIENT` |
706
746
  | `@miguelmorales13/nestkit/email/nodemailer` | `NodemailerModule`, `NODEMAILER_TRANSPORT` |
707
747
  | `@miguelmorales13/nestkit/i18n` | `I18nModule` (wrapper de `nestjs-i18n`), `translateOr` |
@@ -709,7 +749,5 @@ Mailgun, tu propio servidor) en vez de sumar otra cuenta de terceros.
709
749
 
710
750
  ## Estado del paquete
711
751
 
712
- `0.3.0`, no publicado en npm todavía (repo privado, se prueba localmente con `bun link` antes de
713
- publicar). Sin adaptador Mongo real. Sin tests unitarios propios es un paquete nuevo sin
714
- consumidor productivo todavía; la primera integración real (ej. en `hr-pymes-saas`) es la que va a
715
- ejercitar el código de verdad.
752
+ `0.5.0`. Sin adaptador Mongo real. Sin tests unitarios propios es un paquete nuevo; la primera
753
+ integración real (ej. en `hr-pymes-saas` o `cuota`) es la que ejercita el código de verdad.
package/dist/index.cjs CHANGED
@@ -3,18 +3,21 @@
3
3
 
4
4
 
5
5
  var _chunkRF75KC63cjs = require('./chunk-RF75KC63.cjs');
6
- require('./chunk-MR2IFCZE.cjs');
6
+ require('./chunk-KVBQBT3D.cjs');
7
7
 
8
8
 
9
9
 
10
10
 
11
- var _chunkV2M75FN3cjs = require('./chunk-V2M75FN3.cjs');
12
- require('./chunk-KVBQBT3D.cjs');
11
+ var _chunkSPTDXUEDcjs = require('./chunk-SPTDXUED.cjs');
12
+
13
+
14
+ var _chunkQS2W5XCQcjs = require('./chunk-QS2W5XCQ.cjs');
15
+ require('./chunk-MR2IFCZE.cjs');
13
16
 
14
17
 
15
18
 
16
19
 
17
- var _chunkSPTDXUEDcjs = require('./chunk-SPTDXUED.cjs');
20
+ var _chunkV2M75FN3cjs = require('./chunk-V2M75FN3.cjs');
18
21
 
19
22
 
20
23
  var _chunk54ZXIB5Tcjs = require('./chunk-54ZXIB5T.cjs');
@@ -46,9 +49,6 @@ var _chunkR7BVS6CIcjs = require('./chunk-R7BVS6CI.cjs');
46
49
 
47
50
 
48
51
  var _chunkM3EL5O6Tcjs = require('./chunk-M3EL5O6T.cjs');
49
-
50
-
51
- var _chunkQS2W5XCQcjs = require('./chunk-QS2W5XCQ.cjs');
52
52
  require('./chunk-2REOCMUD.cjs');
53
53
 
54
54
 
package/dist/index.js CHANGED
@@ -3,18 +3,21 @@ import {
3
3
  PgModule,
4
4
  withTenantScope
5
5
  } from "./chunk-VKOPDDCC.js";
6
- import "./chunk-NAK4WDKS.js";
7
- import {
8
- REQUEST_ID_HEADER,
9
- RequestIdMiddleware,
10
- TrackingModule
11
- } from "./chunk-EYURGACO.js";
12
6
  import "./chunk-EPVKCBPT.js";
13
7
  import {
14
8
  SUPABASE_ANON_CLIENT,
15
9
  SUPABASE_SERVICE_ROLE_CLIENT,
16
10
  SupabaseModule
17
11
  } from "./chunk-PA24P76K.js";
12
+ import {
13
+ BaseResponseDto
14
+ } from "./chunk-XX2HPTRU.js";
15
+ import "./chunk-NAK4WDKS.js";
16
+ import {
17
+ REQUEST_ID_HEADER,
18
+ RequestIdMiddleware,
19
+ TrackingModule
20
+ } from "./chunk-EYURGACO.js";
18
21
  import {
19
22
  applyNestKitDefaults
20
23
  } from "./chunk-AOCF5QCZ.js";
@@ -46,9 +49,6 @@ import {
46
49
  I18nModule,
47
50
  translateOr
48
51
  } from "./chunk-IYUUYCP5.js";
49
- import {
50
- BaseResponseDto
51
- } from "./chunk-XX2HPTRU.js";
52
52
  import "./chunk-4MGIQFAJ.js";
53
53
  export {
54
54
  AppException,
@@ -22,6 +22,12 @@ var _common = require('@nestjs/common');
22
22
 
23
23
  // src/media/media.service.ts
24
24
 
25
+
26
+ // src/media/media.options.ts
27
+ var MEDIA_OPTIONS = /* @__PURE__ */ Symbol("MEDIA_OPTIONS");
28
+ var MEDIA_TTL = /* @__PURE__ */ Symbol("MEDIA_TTL");
29
+
30
+ // src/media/media.service.ts
25
31
  var _crypto = require('crypto');
26
32
  var MediaService = class {
27
33
  constructor(ttlMs) {
@@ -29,9 +35,6 @@ var MediaService = class {
29
35
  this.logger = new (0, _common.Logger)(MediaService.name);
30
36
  this.store = /* @__PURE__ */ new Map();
31
37
  }
32
- store_(data, contentType) {
33
- return this.put(data, contentType);
34
- }
35
38
  put(data, contentType) {
36
39
  this.sweep();
37
40
  const id = _crypto.randomUUID.call(void 0, ).replace(/-/g, "");
@@ -57,14 +60,16 @@ var MediaService = class {
57
60
  }
58
61
  };
59
62
  MediaService = exports.MediaService = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
60
- _common.Injectable.call(void 0, )
63
+ _common.Injectable.call(void 0, ),
64
+ _chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _common.Inject.call(void 0, MEDIA_TTL))
61
65
  ], MediaService);
62
66
 
63
- // src/media/media.options.ts
64
- var MEDIA_OPTIONS = /* @__PURE__ */ Symbol("MEDIA_OPTIONS");
65
-
66
67
  // src/media/media.controller.ts
67
68
  var MediaController = class {
69
+ // Both dependencies are injected by explicit token, like the rest of this
70
+ // package. The bundler does not emit decorator metadata, so injecting by type
71
+ // alone leaves Nest unable to resolve the constructor at runtime — and the
72
+ // failure only shows when the consuming app boots, never at build time.
68
73
  constructor(media, options) {
69
74
  this.media = media;
70
75
  this.options = options;
@@ -99,6 +104,7 @@ _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
99
104
  ], MediaController.prototype, "serve", 1);
100
105
  MediaController = _chunk2REOCMUDcjs.__decorateClass.call(void 0, [
101
106
  _common.Controller.call(void 0, ),
107
+ _chunk2REOCMUDcjs.__decorateParam.call(void 0, 0, _common.Inject.call(void 0, MediaService)),
102
108
  _chunk2REOCMUDcjs.__decorateParam.call(void 0, 1, _common.Inject.call(void 0, MEDIA_OPTIONS))
103
109
  ], MediaController);
104
110
 
@@ -111,7 +117,8 @@ var MediaModule = class {
111
117
  controllers: [MediaController],
112
118
  providers: [
113
119
  { provide: MEDIA_OPTIONS, useValue: options },
114
- { provide: MediaService, useFactory: () => new MediaService(_nullishCoalesce(options.ttlMs, () => ( ONE_HOUR))) }
120
+ { provide: MEDIA_TTL, useValue: _nullishCoalesce(options.ttlMs, () => ( ONE_HOUR)) },
121
+ MediaService
115
122
  ],
116
123
  exports: [MediaService]
117
124
  };
@@ -124,4 +131,5 @@ MediaModule = exports.MediaModule = _chunk2REOCMUDcjs.__decorateClass.call(void
124
131
 
125
132
 
126
133
 
127
- exports.MEDIA_OPTIONS = MEDIA_OPTIONS; exports.MediaModule = MediaModule; exports.MediaService = MediaService;
134
+
135
+ exports.MEDIA_OPTIONS = MEDIA_OPTIONS; exports.MEDIA_TTL = MEDIA_TTL; exports.MediaModule = MediaModule; exports.MediaService = MediaService;
@@ -1,5 +1,5 @@
1
1
  export { MediaModule } from './media.module.js';
2
2
  export { MediaService } from './media.service.js';
3
- export { MEDIA_OPTIONS } from './media.options.js';
3
+ export { MEDIA_OPTIONS, MEDIA_TTL } from './media.options.js';
4
4
  export type { MediaOptions } from './media.options.js';
5
5
  export type { StoredMedia } from './media.service.js';
@@ -11,7 +11,7 @@ import {
11
11
  Controller,
12
12
  Get,
13
13
  Headers,
14
- Inject,
14
+ Inject as Inject2,
15
15
  NotFoundException,
16
16
  Param,
17
17
  Post,
@@ -21,7 +21,13 @@ import {
21
21
  } from "@nestjs/common";
22
22
 
23
23
  // src/media/media.service.ts
24
- import { Injectable, Logger } from "@nestjs/common";
24
+ import { Inject, Injectable, Logger } from "@nestjs/common";
25
+
26
+ // src/media/media.options.ts
27
+ var MEDIA_OPTIONS = /* @__PURE__ */ Symbol("MEDIA_OPTIONS");
28
+ var MEDIA_TTL = /* @__PURE__ */ Symbol("MEDIA_TTL");
29
+
30
+ // src/media/media.service.ts
25
31
  import { randomUUID } from "crypto";
26
32
  var MediaService = class {
27
33
  constructor(ttlMs) {
@@ -29,9 +35,6 @@ var MediaService = class {
29
35
  this.logger = new Logger(MediaService.name);
30
36
  this.store = /* @__PURE__ */ new Map();
31
37
  }
32
- store_(data, contentType) {
33
- return this.put(data, contentType);
34
- }
35
38
  put(data, contentType) {
36
39
  this.sweep();
37
40
  const id = randomUUID().replace(/-/g, "");
@@ -57,14 +60,16 @@ var MediaService = class {
57
60
  }
58
61
  };
59
62
  MediaService = __decorateClass([
60
- Injectable()
63
+ Injectable(),
64
+ __decorateParam(0, Inject(MEDIA_TTL))
61
65
  ], MediaService);
62
66
 
63
- // src/media/media.options.ts
64
- var MEDIA_OPTIONS = /* @__PURE__ */ Symbol("MEDIA_OPTIONS");
65
-
66
67
  // src/media/media.controller.ts
67
68
  var MediaController = class {
69
+ // Both dependencies are injected by explicit token, like the rest of this
70
+ // package. The bundler does not emit decorator metadata, so injecting by type
71
+ // alone leaves Nest unable to resolve the constructor at runtime — and the
72
+ // failure only shows when the consuming app boots, never at build time.
68
73
  constructor(media, options) {
69
74
  this.media = media;
70
75
  this.options = options;
@@ -99,7 +104,8 @@ __decorateClass([
99
104
  ], MediaController.prototype, "serve", 1);
100
105
  MediaController = __decorateClass([
101
106
  Controller(),
102
- __decorateParam(1, Inject(MEDIA_OPTIONS))
107
+ __decorateParam(0, Inject2(MediaService)),
108
+ __decorateParam(1, Inject2(MEDIA_OPTIONS))
103
109
  ], MediaController);
104
110
 
105
111
  // src/media/media.module.ts
@@ -111,7 +117,8 @@ var MediaModule = class {
111
117
  controllers: [MediaController],
112
118
  providers: [
113
119
  { provide: MEDIA_OPTIONS, useValue: options },
114
- { provide: MediaService, useFactory: () => new MediaService(options.ttlMs ?? ONE_HOUR) }
120
+ { provide: MEDIA_TTL, useValue: options.ttlMs ?? ONE_HOUR },
121
+ MediaService
115
122
  ],
116
123
  exports: [MediaService]
117
124
  };
@@ -122,6 +129,7 @@ MediaModule = __decorateClass([
122
129
  ], MediaModule);
123
130
  export {
124
131
  MEDIA_OPTIONS,
132
+ MEDIA_TTL,
125
133
  MediaModule,
126
134
  MediaService
127
135
  };
@@ -1,4 +1,5 @@
1
1
  export declare const MEDIA_OPTIONS: unique symbol;
2
+ export declare const MEDIA_TTL: unique symbol;
2
3
  export interface MediaOptions {
3
4
  /**
4
5
  * Base URL the generated links point to, **including the global prefix** if
@@ -21,7 +21,6 @@ export declare class MediaService {
21
21
  private readonly logger;
22
22
  private readonly store;
23
23
  constructor(ttlMs: number);
24
- store_(data: Buffer, contentType: string): string;
25
24
  put(data: Buffer, contentType: string): string;
26
25
  get(id: string): StoredMedia | undefined;
27
26
  /** Swept on write, which is the only moment the map can have grown. */
@@ -0,0 +1,34 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { BunnyStorageOptions } from '../storage.options.js';
3
+ import type { ByteRange, ListResult, ObjectInfo, PutOptions, ReadStream, SignedUploadTarget, StoragePort } from '../storage.port.js';
4
+ /**
5
+ * Bunny.net Storage adapter. Objects live in a Storage Zone (write/read via the
6
+ * Storage API) and are served publicly through the Pull Zone bound to it.
7
+ *
8
+ * Uses `fetch` only — no SDK. Streaming works both ways: `putStream` streams the
9
+ * body up, `getStream` streams it down. Direct signed UPLOADS are not offered:
10
+ * Bunny Storage has no presigned-PUT that avoids exposing the zone password, so
11
+ * uploads go through your API (or use Bunny Stream/TUS for large video).
12
+ */
13
+ export declare class BunnyStorage implements StoragePort {
14
+ private readonly base;
15
+ private readonly apiKey;
16
+ private readonly pullZoneUrl?;
17
+ private readonly tokenSecurityKey?;
18
+ constructor(options: BunnyStorageOptions);
19
+ private url;
20
+ put(key: string, data: Buffer | Uint8Array, options?: PutOptions): Promise<ObjectInfo>;
21
+ putStream(key: string, stream: Readable, options?: PutOptions): Promise<ObjectInfo>;
22
+ get(key: string): Promise<Buffer>;
23
+ getStream(key: string, range?: ByteRange): Promise<ReadStream>;
24
+ stat(key: string): Promise<ObjectInfo | null>;
25
+ exists(key: string): Promise<boolean>;
26
+ delete(key: string): Promise<void>;
27
+ private listRaw;
28
+ list(prefix: string, options?: {
29
+ limit?: number;
30
+ }): Promise<ListResult>;
31
+ publicUrl(key: string): string | null;
32
+ signedReadUrl(key: string, ttlSeconds?: number): Promise<string>;
33
+ signedUploadUrl(): Promise<SignedUploadTarget>;
34
+ }
@@ -0,0 +1,31 @@
1
+ import type { Readable } from 'node:stream';
2
+ import type { LocalStorageOptions } from '../storage.options.js';
3
+ import type { ByteRange, ListResult, ObjectInfo, ReadStream, SignedUploadTarget, StoragePort } from '../storage.port.js';
4
+ /**
5
+ * Filesystem-backed storage for development. Serves streams and byte ranges from
6
+ * disk. Content-Type is not persisted (the filesystem has no place for it), so
7
+ * `stat`/`getStream` return it only when the caller can infer it elsewhere.
8
+ *
9
+ * Not for production: there is no durability story, no CDN, and `signedUploadUrl`
10
+ * is unsupported (a client can't PUT to your disk). Use S3/R2 or Bunny there.
11
+ */
12
+ export declare class LocalStorage implements StoragePort {
13
+ private readonly root;
14
+ private readonly publicBaseUrl?;
15
+ constructor(options: LocalStorageOptions);
16
+ /** Resolve a key to an absolute path, refusing anything that escapes the root. */
17
+ private pathOf;
18
+ put(key: string, data: Buffer | Uint8Array): Promise<ObjectInfo>;
19
+ putStream(key: string, stream: Readable): Promise<ObjectInfo>;
20
+ get(key: string): Promise<Buffer>;
21
+ getStream(key: string, range?: ByteRange): Promise<ReadStream>;
22
+ stat(key: string): Promise<ObjectInfo | null>;
23
+ exists(key: string): Promise<boolean>;
24
+ delete(key: string): Promise<void>;
25
+ list(prefix: string, options?: {
26
+ limit?: number;
27
+ }): Promise<ListResult>;
28
+ publicUrl(key: string): string | null;
29
+ signedReadUrl(key: string): Promise<string>;
30
+ signedUploadUrl(): Promise<SignedUploadTarget>;
31
+ }
@@ -0,0 +1,37 @@
1
+ import type { Readable } from 'node:stream';
2
+ import type { S3StorageOptions } from '../storage.options.js';
3
+ import type { ByteRange, ListResult, ObjectInfo, PutOptions, ReadStream, SignedUploadTarget, StoragePort } from '../storage.port.js';
4
+ /**
5
+ * S3-compatible storage. Works with AWS S3 and any S3 API implementation —
6
+ * notably Cloudflare R2 (see `createR2Storage`) and MinIO.
7
+ *
8
+ * The AWS SDK is imported LAZILY, the first time it's needed, so an app that only
9
+ * uses the Bunny or local adapter never loads `@aws-sdk/*`. Install
10
+ * `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner` and `@aws-sdk/lib-storage`
11
+ * (optional peer deps) to use this adapter.
12
+ */
13
+ export declare class S3Storage implements StoragePort {
14
+ private readonly bucket;
15
+ private readonly publicBaseUrl?;
16
+ private clientPromise?;
17
+ private readonly config;
18
+ constructor(options: S3StorageOptions);
19
+ /** Lazily construct (and memoize) the S3 client, loading the SDK on first use. */
20
+ private client;
21
+ put(key: string, data: Buffer | Uint8Array, options?: PutOptions): Promise<ObjectInfo>;
22
+ putStream(key: string, stream: Readable, options?: PutOptions): Promise<ObjectInfo>;
23
+ get(key: string): Promise<Buffer>;
24
+ getStream(key: string, range?: ByteRange): Promise<ReadStream>;
25
+ stat(key: string): Promise<ObjectInfo | null>;
26
+ exists(key: string): Promise<boolean>;
27
+ delete(key: string): Promise<void>;
28
+ list(prefix: string, options?: {
29
+ limit?: number;
30
+ cursor?: string;
31
+ }): Promise<ListResult>;
32
+ signedReadUrl(key: string, ttlSeconds?: number): Promise<string>;
33
+ signedUploadUrl(key: string, options?: PutOptions & {
34
+ ttlSeconds?: number;
35
+ }): Promise<SignedUploadTarget>;
36
+ publicUrl(key: string): string | null;
37
+ }