@jmlq/logger-plugin-fs 0.1.0-alpha.2 → 0.1.0-alpha.4
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 +188 -0
- package/dist/application/dto/index.d.ts +1 -0
- package/dist/application/dto/index.js +17 -0
- package/dist/application/dto/save-log.dto.d.ts +4 -0
- package/dist/application/dto/save-log.dto.js +2 -0
- package/dist/application/services/fs-datasource.service.d.ts +9 -0
- package/dist/application/services/fs-datasource.service.js +22 -0
- package/dist/application/services/index.d.ts +1 -0
- package/dist/application/services/index.js +17 -0
- package/dist/application/use-cases/append-log.usecase.d.ts +7 -0
- package/dist/application/use-cases/append-log.usecase.js +19 -0
- package/dist/application/use-cases/index.d.ts +3 -0
- package/dist/application/use-cases/index.js +19 -0
- package/dist/application/use-cases/persist-log.usecase.d.ts +23 -0
- package/dist/application/use-cases/persist-log.usecase.js +74 -0
- package/dist/application/use-cases/rotate-if-needed.usecase.d.ts +10 -0
- package/dist/application/use-cases/rotate-if-needed.usecase.js +39 -0
- package/dist/domain/contracts/clock.contract.d.ts +3 -0
- package/dist/domain/contracts/clock.contract.js +2 -0
- package/dist/domain/contracts/file-rotator.contract.d.ts +7 -0
- package/dist/domain/contracts/file-rotator.contract.js +2 -0
- package/dist/domain/contracts/index.d.ts +4 -0
- package/dist/domain/contracts/index.js +20 -0
- package/dist/domain/contracts/serializer.contract.d.ts +3 -0
- package/dist/domain/contracts/serializer.contract.js +2 -0
- package/dist/domain/contracts/stream-writer.port.d.ts +6 -0
- package/dist/domain/contracts/stream-writer.port.js +2 -0
- package/dist/domain/types/index.d.ts +1 -0
- package/dist/domain/types/index.js +17 -0
- package/dist/domain/types/options.type.d.ts +11 -0
- package/dist/domain/types/options.type.js +5 -0
- package/dist/domain/value-objects/file-name-pattern.vo.d.ts +4 -0
- package/dist/domain/value-objects/file-name-pattern.vo.js +14 -0
- package/dist/domain/value-objects/index.d.ts +2 -0
- package/dist/domain/value-objects/index.js +18 -0
- package/dist/domain/value-objects/rotation-policy.vo.d.ts +7 -0
- package/dist/domain/value-objects/rotation-policy.vo.js +20 -0
- package/dist/index.d.ts +3 -13
- package/dist/index.js +7 -38
- package/dist/infrastructure/datasources/fs.datasource.d.ts +17 -0
- package/dist/infrastructure/datasources/fs.datasource.js +84 -0
- package/dist/infrastructure/datasources/index.d.ts +1 -0
- package/dist/infrastructure/datasources/index.js +17 -0
- package/dist/infrastructure/fs/file-rotator.adapter.d.ts +22 -0
- package/dist/infrastructure/fs/file-rotator.adapter.js +51 -0
- package/dist/infrastructure/fs/fs-provider.d.ts +15 -0
- package/dist/infrastructure/fs/fs-provider.js +55 -0
- package/dist/infrastructure/fs/fs-writer.adapter.d.ts +10 -0
- package/dist/infrastructure/fs/fs-writer.adapter.js +26 -0
- package/dist/infrastructure/fs/index.d.ts +5 -0
- package/dist/infrastructure/fs/index.js +21 -0
- package/dist/infrastructure/fs/node-clock.adapter.d.ts +4 -0
- package/dist/infrastructure/fs/node-clock.adapter.js +10 -0
- package/dist/infrastructure/fs/path-utils.d.ts +6 -0
- package/dist/infrastructure/fs/path-utils.js +26 -0
- package/dist/presentation/factory/create-fs-datasource.d.ts +15 -0
- package/dist/presentation/factory/create-fs-datasource.js +39 -0
- package/dist/presentation/factory/index.d.ts +1 -0
- package/dist/presentation/factory/index.js +17 -0
- package/package.json +15 -3
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# @jmlq/logger-plugin-fs
|
|
2
|
+
|
|
3
|
+
Datasource de `sistema de archivos` para [`@jmlq/logger`](https://www.npmjs.com/package/@jmlq/logger).
|
|
4
|
+
Escribe cada evento de log como **línea** (JSONL por defecto) y soporta `rotación por día` o por `tamaño`, manejo de `backpressure/drain`, `flush()` y `dispose()`.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 📦 Instalación
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# Con npm
|
|
12
|
+
npm i @jmlq/logger @jmlq/logger-plugin-fs
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Este plugin **depende** de [`@jmlq/logger`](https://www.npmjs.com/package/@jmlq/logger). Asegúrate de instalar ambos paquetes.
|
|
17
|
+
|
|
18
|
+
## 🧱 Estructura del paquete
|
|
19
|
+
|
|
20
|
+
### 📝 Resumen rápido
|
|
21
|
+
|
|
22
|
+
> - **`src/domain/`** — Reglas del negocio del plugin (sin dependencias de Node).
|
|
23
|
+
> > - **`contracts/`**
|
|
24
|
+
> > > - `clock.contract.ts` — Puerto que abstrae el tiempo actual (`now()`), permite testear rotación sin depender de `Date.now()`.
|
|
25
|
+
> > > - `file-rotator.port.ts` — Puerto para rotación: calcula path esperado, tamaño, índice, etc.
|
|
26
|
+
> > > - `stream-writer.port.ts` — Puerto para escritura secuencial: `write`, `once("drain")`, `end`.
|
|
27
|
+
> > - **`value-objects/`**
|
|
28
|
+
> > > - `file-name-pattern.vo.ts` — VO que encapsula el patrón de nombres (`{yyyy}{MM}{dd}`) con validación.
|
|
29
|
+
> > > - `rotation-policy.vo.ts` — VO que define estrategia de rotación (`none | day | size`) con invariantes (`maxSizeMB > 0`).
|
|
30
|
+
> > - **`types/`**
|
|
31
|
+
> > > - `options.type.ts` — Define `FsDatasourceOptions` y `IFsSerializer` (contrato de serialización de líneas).
|
|
32
|
+
|
|
33
|
+
> - **`src/application/`** — Casos de uso (orquestan, no dependen de Node).
|
|
34
|
+
> > - **`dto/`**
|
|
35
|
+
> > > - `save-log.dto.ts` — DTO de entrada: `{ log: ILog }`.
|
|
36
|
+
> > - **`use-cases/`**
|
|
37
|
+
> > > - `rotate-if-needed.usecase.ts` — Decide si se rota: compara fecha/tamaño contra `RotationPolicy`.
|
|
38
|
+
> > > - `append-log.usecase.ts` — Serializa y escribe línea; maneja backpressure (`write=false` → espera `drain`).
|
|
39
|
+
> > > - `persist-log.usecase.ts` — Orquesta: asegura writer, rota si corresponde, escribe, dispara hooks.
|
|
40
|
+
> > - **`services/`**
|
|
41
|
+
> > > - `fs-datasource.service.ts` — Implementa `ILogDatasource` del core usando los UC anteriores.
|
|
42
|
+
|
|
43
|
+
> - **`src/infrastructure/`** — Adaptadores técnicos (Node.js).
|
|
44
|
+
> > - **`fs/`**
|
|
45
|
+
> > > - `fs-provider.ts` — Wrapper de `fs`/`fs.promises` (mockeable en tests).
|
|
46
|
+
> > > - `path-utils.ts` — Utilidades para `join`, `splitBaseExt`, `formatPattern` (UTC).
|
|
47
|
+
> > > - `file-rotator.adapter.ts` — Implementa `IFileRotatorPort` usando `fs-provider` y `path-utils`.
|
|
48
|
+
> > > - `fs-writer.adapter.ts` — Implementa `IStreamWriterPort` con `fs.createWriteStream`.
|
|
49
|
+
> > > - `node-clock.adapter.ts` — Implementa `IClock` devolviendo `new Date()`.
|
|
50
|
+
|
|
51
|
+
> - **`src/presentation/`** — API pública (cara del paquete).
|
|
52
|
+
> > - **`factory/`**
|
|
53
|
+
> > > - `create-fs-datasource.ts` — Ensambla VO + adaptadores + casos de uso y devuelve un `ILogDatasource`.
|
|
54
|
+
> > - `index.ts` — Barrel: reexporta la factory y VO/contratos públicos (`RotationPolicy`, `FileNamePattern`).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
### 🧩 Configuración
|
|
59
|
+
|
|
60
|
+
### 🔐 Variables de Entorno (.env)
|
|
61
|
+
|
|
62
|
+
```ini
|
|
63
|
+
# Ruta base (carpeta) para los archivos de log
|
|
64
|
+
LOGGER_FS_PATH=./logs
|
|
65
|
+
|
|
66
|
+
# Patrón (opcional). Si omites, usa "app-{yyyy}{MM}{dd}.log"
|
|
67
|
+
LOGGER_FS_PATTERN=app-{yyyy}{MM}{dd}.log
|
|
68
|
+
|
|
69
|
+
# Rotación: "day" | "size" | "none"
|
|
70
|
+
LOGGER_FS_ROTATION=day
|
|
71
|
+
LOGGER_FS_MAX_SIZE_MB=50
|
|
72
|
+
|
|
73
|
+
# Logger core
|
|
74
|
+
LOGGER_LEVEL=info # trace|debug|info|warn|error|fatal
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### 🚀 Uso del paquete
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { createLogger, LogLevel } from "@jmlq/logger";
|
|
84
|
+
import {
|
|
85
|
+
createFsDatasource,
|
|
86
|
+
RotationPolicy,
|
|
87
|
+
FileNamePattern,
|
|
88
|
+
} from "@jmlq/logger-plugin-fs";
|
|
89
|
+
|
|
90
|
+
const ds = createFsDatasource({
|
|
91
|
+
basePath: "./logs", // Carpeta destino
|
|
92
|
+
mkdir: true, // Crea carpeta si no existe
|
|
93
|
+
fileNamePattern: "app-{yyyy}{MM}{dd}.log", // Rotación diaria por fecha (UTC)
|
|
94
|
+
// Alternativa por tamaño:
|
|
95
|
+
// rotation: { by: "size", maxSizeMB: 50 }
|
|
96
|
+
rotation: { by: "day" },
|
|
97
|
+
// Serializador opcional (por defecto JSON.stringify)
|
|
98
|
+
// serializer: { serialize: (log) => formatMyLine(log) },
|
|
99
|
+
onRotate: (oldP, newP) => console.log("[fs] rotated:", oldP, "->", newP),
|
|
100
|
+
onError: (e) => console.error("[fs] error:", e),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const logger = createLogger(ds, { minLevel: LogLevel.INFO });
|
|
104
|
+
|
|
105
|
+
logger.info("Servidor iniciado", { pid: process.pid });
|
|
106
|
+
|
|
107
|
+
// Cierre elegante
|
|
108
|
+
process.on("SIGTERM", async () => {
|
|
109
|
+
await logger.flush?.();
|
|
110
|
+
await logger.dispose?.();
|
|
111
|
+
process.exit(0);
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Opciones soportadas
|
|
116
|
+
|
|
117
|
+
> - `basePath: string` – carpeta donde se guardan los logs.
|
|
118
|
+
> - `mkdir?: boolean` – crea la carpeta si no existe.
|
|
119
|
+
> - `fileNamePattern?: string` – tokens {yyyy}{MM}{dd} (formateados en UTC).
|
|
120
|
+
> > - Ejemplos: `"app-{yyyy}{MM}{dd}.log"`, `"service.log"`.
|
|
121
|
+
> - `rotation?: { by: "none" | "day" | "size"; maxSizeMB?: number; maxFiles?: number }`
|
|
122
|
+
> > - `day` → rota al cambiar la fecha (UTC).
|
|
123
|
+
> > - `size` → rota al alcanzar `maxSizeMB` (genera app.1.log, app.2.log, …).
|
|
124
|
+
> - `serializer?: { serialize(entry: unknown): string }` – una línea sin `\n`.
|
|
125
|
+
> - `onRotate?: (oldPath, newPath) => void | Promise<void>`
|
|
126
|
+
> - `onError?: (err) => void | Promise<void>`
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### 🗂️ Formato de archivo
|
|
131
|
+
|
|
132
|
+
> - **Una línea por evento** (JSONL por defecto).
|
|
133
|
+
> - El `serializer` determina el formato de cada línea (sin salto).
|
|
134
|
+
> - El plugin agrega `"\n"` y maneja `backpressure`/`drain` del stream.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### 🔁 Rotación
|
|
139
|
+
|
|
140
|
+
**Por día **(`rotation: { by: "day" }`)
|
|
141
|
+
|
|
142
|
+
> - El archivo activo se calcula con `fileNamePattern` usando la `fecha UTC actual`.
|
|
143
|
+
> - Si al persistir cambia el día → `cierra` el stream anterior, abre uno nuevo y dispara `onRotate(old, new)`.
|
|
144
|
+
|
|
145
|
+
**Por tamaño** (`rotation: { by: "size", maxSizeMB }`)
|
|
146
|
+
|
|
147
|
+
> - Comprueba el tamaño del archivo activo. Si `>= maxSizeMB` → rota a `app.1.log`, `app.2.log`, … (buscando el siguiente índice libre).
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### 🧯 Backpressure & drain (cómo evita perder logs)
|
|
152
|
+
|
|
153
|
+
Node.js devuelve `false` en `stream.write()` cuando el **buffer interno está lleno** (backpressure).
|
|
154
|
+
El plugin:
|
|
155
|
+
|
|
156
|
+
1. Serializa el log + `"\n"`.
|
|
157
|
+
2. Llama `write(...)`.
|
|
158
|
+
3. Si devuelve `false`, **espera el evento** `drain` antes de continuar.
|
|
159
|
+
4. `flush()` espera a que se libere el buffer si `writableNeedDrain` es `true`.
|
|
160
|
+
5. `dispose()` cierra el stream actual drenando el buffer pendiente.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### 🌐 Zona horaria
|
|
165
|
+
|
|
166
|
+
> - El formateo `{yyyy}{MM}{dd}` del patrón se realiza en **UTC** para evitar desfases por huso horario (máquinas/CI distintas).
|
|
167
|
+
> - Si necesitas otro criterio (p.ej., “día de Guayaquil”), puedes:
|
|
168
|
+
> > - Ajustar el **IClock** para proveer el “ahora” en otro huso y/o
|
|
169
|
+
> > - Reemplazar el util de formateo si deseas `{…}` en local time.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## ❓ FAQ
|
|
174
|
+
|
|
175
|
+
**¿Puedo usar mi propio serializador?**
|
|
176
|
+
Sí. Pasa serializer: { serialize: (log) => "mi-línea" }. Debe devolver una sola línea sin `\n`.
|
|
177
|
+
|
|
178
|
+
**¿Qué pasa si el proceso se detiene en medio de un write?**
|
|
179
|
+
Usamos los mecanismos de `Writable` (buffer + `drain`) y exponemos `flush()`/`dispose()` para el cierre. Llama a ambos en shutdown.
|
|
180
|
+
|
|
181
|
+
**¿Puedo rotar por hora?**
|
|
182
|
+
No out-of-the-box. Puedes implementar un **FileNamePattern** por hora (ej. `app-{yyyy}{MM}{dd}-{HH}.log`) + un `RotateIfNeededUseCase` extendido.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 📄 Licencia
|
|
187
|
+
|
|
188
|
+
MIT © Mauricio Lahuasi
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./save-log.dto";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./save-log.dto"), exports);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ILogDatasource, ILog } from "@jmlq/logger";
|
|
2
|
+
import { PersistLogUseCase } from "../use-cases";
|
|
3
|
+
export declare class FsDatasourceService implements ILogDatasource {
|
|
4
|
+
private readonly persistUC;
|
|
5
|
+
constructor(persistUC: PersistLogUseCase);
|
|
6
|
+
save(log: ILog): Promise<void>;
|
|
7
|
+
flush(): Promise<void>;
|
|
8
|
+
dispose(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FsDatasourceService = void 0;
|
|
4
|
+
class FsDatasourceService {
|
|
5
|
+
constructor(persistUC) {
|
|
6
|
+
this.persistUC = persistUC;
|
|
7
|
+
}
|
|
8
|
+
// Guarda un log (core llama esto). Orquesta vía PersistLogUseCase.
|
|
9
|
+
async save(log) {
|
|
10
|
+
const dto = { log };
|
|
11
|
+
await this.persistUC.execute(dto);
|
|
12
|
+
}
|
|
13
|
+
// Espera drenaje del buffer (útil para shutdown)
|
|
14
|
+
async flush() {
|
|
15
|
+
await this.persistUC.flush();
|
|
16
|
+
}
|
|
17
|
+
// Cierra recursos (stream)
|
|
18
|
+
async dispose() {
|
|
19
|
+
await this.persistUC.dispose();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.FsDatasourceService = FsDatasourceService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./fs-datasource.service";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./fs-datasource.service"), exports);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { IFsSerializer, IStreamWriterPort } from "../../domain/contracts";
|
|
2
|
+
import { SaveLogDTO } from "../dto";
|
|
3
|
+
export declare class AppendLogUseCase {
|
|
4
|
+
private readonly serializer;
|
|
5
|
+
constructor(serializer: IFsSerializer);
|
|
6
|
+
execute(input: SaveLogDTO, writer: IStreamWriterPort): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Serializa (línea + '\n') y escribe en el stream con manejo de backpressure.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.AppendLogUseCase = void 0;
|
|
5
|
+
class AppendLogUseCase {
|
|
6
|
+
constructor(serializer) {
|
|
7
|
+
this.serializer = serializer;
|
|
8
|
+
}
|
|
9
|
+
async execute(input, writer) {
|
|
10
|
+
// 1) Serializa el log a texto + salto de línea
|
|
11
|
+
const line = this.serializer.serialize(input.log) + "\n";
|
|
12
|
+
// 2) Intenta escribir; si devuelve false, espera 'drain'
|
|
13
|
+
const ok = writer.write(line);
|
|
14
|
+
if (!ok) {
|
|
15
|
+
await new Promise((resolve) => writer.once("drain", resolve));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.AppendLogUseCase = AppendLogUseCase;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./rotate-if-needed.usecase"), exports);
|
|
18
|
+
__exportStar(require("./append-log.usecase"), exports);
|
|
19
|
+
__exportStar(require("./persist-log.usecase"), exports);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AppendLogUseCase, RotateIfNeededUseCase } from ".";
|
|
2
|
+
import { IFileRotatorPort, IStreamWriterPort } from "../../domain/contracts";
|
|
3
|
+
import { SaveLogDTO } from "../dto";
|
|
4
|
+
export declare class PersistLogUseCase {
|
|
5
|
+
private readonly rotateUC;
|
|
6
|
+
private readonly appendUC;
|
|
7
|
+
private readonly rotator;
|
|
8
|
+
private readonly openWriter;
|
|
9
|
+
private readonly onRotate?;
|
|
10
|
+
private readonly onError?;
|
|
11
|
+
constructor(rotateUC: RotateIfNeededUseCase, // caso de uso de rotación
|
|
12
|
+
appendUC: AppendLogUseCase, // caso de uso de escritura
|
|
13
|
+
rotator: IFileRotatorPort, // puerto para actualizar activePath
|
|
14
|
+
openWriter: (path: string) => IStreamWriterPort, // factory: abre stream
|
|
15
|
+
onRotate?: ((oldPath: string, newPath: string) => void | Promise<void>) | undefined, onError?: ((err: unknown) => void | Promise<void>) | undefined);
|
|
16
|
+
private writer;
|
|
17
|
+
private path;
|
|
18
|
+
private ensureWriter;
|
|
19
|
+
private disposeWriter;
|
|
20
|
+
execute(input: SaveLogDTO): Promise<void>;
|
|
21
|
+
flush(): Promise<void>;
|
|
22
|
+
dispose(): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Orquesta: (1) rotar si hace falta, (2) escribir línea, (3) hooks de rotación/errores.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.PersistLogUseCase = void 0;
|
|
5
|
+
class PersistLogUseCase {
|
|
6
|
+
constructor(rotateUC, // caso de uso de rotación
|
|
7
|
+
appendUC, // caso de uso de escritura
|
|
8
|
+
rotator, // puerto para actualizar activePath
|
|
9
|
+
openWriter, // factory: abre stream
|
|
10
|
+
onRotate, onError) {
|
|
11
|
+
this.rotateUC = rotateUC;
|
|
12
|
+
this.appendUC = appendUC;
|
|
13
|
+
this.rotator = rotator;
|
|
14
|
+
this.openWriter = openWriter;
|
|
15
|
+
this.onRotate = onRotate;
|
|
16
|
+
this.onError = onError;
|
|
17
|
+
this.writer = null; // writer activo
|
|
18
|
+
this.path = null; // path activo
|
|
19
|
+
}
|
|
20
|
+
// Inicializa el writer si no existe (idempotente)
|
|
21
|
+
ensureWriter(currentPath) {
|
|
22
|
+
if (!this.writer) {
|
|
23
|
+
this.writer = this.openWriter(currentPath);
|
|
24
|
+
this.path = currentPath;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Cierra writer activo de forma segura
|
|
28
|
+
async disposeWriter() {
|
|
29
|
+
if (!this.writer)
|
|
30
|
+
return;
|
|
31
|
+
await new Promise((resolve) => this.writer.end(resolve));
|
|
32
|
+
this.writer = null;
|
|
33
|
+
}
|
|
34
|
+
// Persistir un log con rotación condicional
|
|
35
|
+
async execute(input) {
|
|
36
|
+
try {
|
|
37
|
+
// 1) Asegurar writer inicial para el path actual del rotator
|
|
38
|
+
const currentPath = this.rotator.getActiveFilePath();
|
|
39
|
+
this.ensureWriter(currentPath);
|
|
40
|
+
// 2) Consultar si debemos rotar → abrir nuevo writer si hace falta
|
|
41
|
+
const nextPath = await this.rotateUC.execute();
|
|
42
|
+
if (nextPath) {
|
|
43
|
+
const old = this.path;
|
|
44
|
+
await this.disposeWriter();
|
|
45
|
+
this.rotator.setActivePath(nextPath);
|
|
46
|
+
this.writer = this.openWriter(nextPath);
|
|
47
|
+
this.path = nextPath;
|
|
48
|
+
// Notificar rotación (no bloqueante)
|
|
49
|
+
Promise.resolve(this.onRotate?.(old, nextPath)).catch((e) => this.onError?.(e));
|
|
50
|
+
}
|
|
51
|
+
// 3) Escribir línea manejando backpressure
|
|
52
|
+
await this.appendUC.execute(input, this.writer);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// Notificar error sin tumbar el proceso (política del plugin)
|
|
56
|
+
await Promise.resolve(this.onError?.(err));
|
|
57
|
+
// Si quieres propagar al core, descomenta:
|
|
58
|
+
// throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Esperar drenaje del buffer
|
|
62
|
+
async flush() {
|
|
63
|
+
if (!this.writer)
|
|
64
|
+
return;
|
|
65
|
+
if (this.writer.writableNeedDrain) {
|
|
66
|
+
await new Promise((resolve) => this.writer.once("drain", resolve));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Cerrar el writer activo
|
|
70
|
+
async dispose() {
|
|
71
|
+
await this.disposeWriter();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.PersistLogUseCase = PersistLogUseCase;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { IClock, IFileRotatorPort } from "../../domain/contracts";
|
|
2
|
+
import { RotationPolicy } from "../../domain/value-objects";
|
|
3
|
+
export declare class RotateIfNeededUseCase {
|
|
4
|
+
private readonly policy;
|
|
5
|
+
private readonly rotator;
|
|
6
|
+
private readonly clock;
|
|
7
|
+
private readonly maxSizeBytes?;
|
|
8
|
+
constructor(policy: RotationPolicy, rotator: IFileRotatorPort, clock: IClock, maxSizeBytes?: number | undefined);
|
|
9
|
+
execute(): Promise<string | null>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Caso de uso: evaluar si corresponde rotar el archivo y, si corresponde, devolver el nuevo path.
|
|
3
|
+
// NO toca node:fs directamente; delega a servicios de infraestructura (rotator).
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.RotateIfNeededUseCase = void 0;
|
|
6
|
+
class RotateIfNeededUseCase {
|
|
7
|
+
// Dependencias: política (VO), puerto de rotación y reloj.
|
|
8
|
+
constructor(policy, rotator, clock, maxSizeBytes // cache de bytes (derivado de policy.maxSizeMB)
|
|
9
|
+
) {
|
|
10
|
+
this.policy = policy;
|
|
11
|
+
this.rotator = rotator;
|
|
12
|
+
this.clock = clock;
|
|
13
|
+
this.maxSizeBytes = maxSizeBytes;
|
|
14
|
+
}
|
|
15
|
+
// Devuelve null si no se requiere rotar; o el "nextPath" a usar si hay rotación.
|
|
16
|
+
async execute() {
|
|
17
|
+
// Selecciona estrategia según la política
|
|
18
|
+
if (this.policy.by === "none")
|
|
19
|
+
return null;
|
|
20
|
+
if (this.policy.by === "day") {
|
|
21
|
+
// Compara path actual vs path esperado para "hoy"
|
|
22
|
+
const now = this.clock.now();
|
|
23
|
+
const expected = this.rotator.expectedPathForToday(now);
|
|
24
|
+
return expected !== this.rotator.getActiveFilePath() ? expected : null;
|
|
25
|
+
}
|
|
26
|
+
if (this.policy.by === "size") {
|
|
27
|
+
// Si el archivo supera el umbral, pedimos un nuevo path indexado
|
|
28
|
+
const current = this.rotator.getActiveFilePath();
|
|
29
|
+
const size = await this.rotator.sizeOf(current);
|
|
30
|
+
const limit = this.maxSizeBytes ?? this.policy.maxSizeMB * 1024 * 1024;
|
|
31
|
+
if ((size ?? 0) >= limit) {
|
|
32
|
+
return await this.rotator.nextIndexedPath();
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
exports.RotateIfNeededUseCase = RotateIfNeededUseCase;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./clock.contract"), exports);
|
|
18
|
+
__exportStar(require("./serializer.contract"), exports);
|
|
19
|
+
__exportStar(require("./file-rotator.contract"), exports);
|
|
20
|
+
__exportStar(require("./stream-writer.port"), exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./options.type";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./options.type"), exports);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { IFsSerializer } from "../contracts";
|
|
2
|
+
import { FileNamePattern, RotationPolicy } from "../value-objects";
|
|
3
|
+
export interface FsDatasourceOptions {
|
|
4
|
+
basePath: string;
|
|
5
|
+
mkdir?: boolean;
|
|
6
|
+
pattern?: FileNamePattern;
|
|
7
|
+
rotation?: RotationPolicy;
|
|
8
|
+
serializer?: IFsSerializer;
|
|
9
|
+
onRotate?: (oldPath: string, newPath: string) => void | Promise<void>;
|
|
10
|
+
onError?: (err: unknown) => void | Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tipo de opciones de alto nivel del plugin, expuesto a la "presentación" (factory).
|
|
3
|
+
// Reúne los contratos/VOs de dominio, pero sin detalles de node:fs.
|
|
4
|
+
// Esto mantiene la API pública limpia y testeable.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileNamePattern = void 0;
|
|
4
|
+
// Value Object para el patrón de nombre del archivo.
|
|
5
|
+
// Soporta tokens {yyyy}, {MM}, {dd}; el formateo real ocurre en infraestructura.
|
|
6
|
+
class FileNamePattern {
|
|
7
|
+
constructor(pattern) {
|
|
8
|
+
this.pattern = pattern;
|
|
9
|
+
if (!pattern || typeof pattern !== "string") {
|
|
10
|
+
throw new Error("[FileNamePattern] pattern inválido");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.FileNamePattern = FileNamePattern;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./file-name-pattern.vo"), exports);
|
|
18
|
+
__exportStar(require("./rotation-policy.vo"), exports);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type RotationBy = "none" | "day" | "size";
|
|
2
|
+
export declare class RotationPolicy {
|
|
3
|
+
readonly by: RotationBy;
|
|
4
|
+
readonly maxSizeMB?: number | undefined;
|
|
5
|
+
readonly maxFiles?: number | undefined;
|
|
6
|
+
constructor(by: RotationBy, maxSizeMB?: number | undefined, maxFiles?: number | undefined);
|
|
7
|
+
}
|