querymongo 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -44
- package/package.json +1 -1
- package/.github/workflows/ci.yml +0 -61
- package/.vscode/settings.json +0 -3
- package/ARCHITECTURE.md +0 -286
- package/examples.ts +0 -79
package/README.md
CHANGED
|
@@ -45,32 +45,35 @@ src/
|
|
|
45
45
|
- **Dependency Inversion**: Se depende de la interface `IConverter`, no de implementaciones
|
|
46
46
|
- **Single Responsibility**: Cada conversor tiene un propósito único
|
|
47
47
|
|
|
48
|
-
##
|
|
48
|
+
## 💻 Uso
|
|
49
|
+
|
|
50
|
+
### 🚀 Instalación Global - CLI
|
|
51
|
+
|
|
52
|
+
Para instalar QueryMongo globalmente en tu máquina:
|
|
49
53
|
|
|
50
54
|
```bash
|
|
51
|
-
npm
|
|
52
|
-
npm run build
|
|
55
|
+
npm i querymongo -g
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
### Modo CLI - Interactivo
|
|
58
|
+
Luego ejecuta desde cualquier directorio:
|
|
58
59
|
|
|
59
60
|
```bash
|
|
60
|
-
|
|
61
|
+
querymongo
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
### 🛠️ Desarrollo Local
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
Para trabajar en el desarrollo del proyecto:
|
|
66
67
|
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/rubsuadav/querymongo
|
|
70
|
+
cd querymongo
|
|
71
|
+
npm install
|
|
72
|
+
npm run dev interactive
|
|
72
73
|
```
|
|
73
74
|
|
|
75
|
+
Esto abrirá la CLI interactiva donde puedes ingresar queries SQL y recibir instantáneamente el equivalente en MongoDB.
|
|
76
|
+
|
|
74
77
|
## 📚 Ejemplos de Conversión
|
|
75
78
|
|
|
76
79
|
### SELECT con proyección
|
|
@@ -141,36 +144,6 @@ npm test
|
|
|
141
144
|
|
|
142
145
|
Cobertura: **100%** en líneas y funciones.
|
|
143
146
|
|
|
144
|
-
## 📖 API Reference
|
|
145
|
-
|
|
146
|
-
### `MongoConverter`
|
|
147
|
-
|
|
148
|
-
Clase principal para conversiones.
|
|
149
|
-
|
|
150
|
-
```typescript
|
|
151
|
-
class MongoConverter {
|
|
152
|
-
convert(sql: string): Record<string, any>;
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
**Parámetros:**
|
|
157
|
-
|
|
158
|
-
- `sql` (string): Query SQL a convertir
|
|
159
|
-
|
|
160
|
-
**Retorna:** Objeto con estructura MongoDB
|
|
161
|
-
|
|
162
|
-
**Lanza:** Error si el query SQL no es soportado
|
|
163
|
-
|
|
164
|
-
### Exports Públicos
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
export {
|
|
168
|
-
MongoConverter,
|
|
169
|
-
mongoConverter,
|
|
170
|
-
} from "./src/application/MongoConverter.ts";
|
|
171
|
-
export type { IConverter } from "./src/domain/interfaces/Converter.ts";
|
|
172
|
-
```
|
|
173
|
-
|
|
174
147
|
## 🔧 Operadores Soportados
|
|
175
148
|
|
|
176
149
|
| SQL | MongoDB |
|
package/package.json
CHANGED
package/.github/workflows/ci.yml
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: ["master"]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: ["master"]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
build:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- name: Checkout repository
|
|
14
|
-
uses: actions/checkout@v6
|
|
15
|
-
|
|
16
|
-
- name: Setup Node.js
|
|
17
|
-
uses: actions/setup-node@v5
|
|
18
|
-
with:
|
|
19
|
-
node-version: 22
|
|
20
|
-
cache: "npm"
|
|
21
|
-
|
|
22
|
-
- name: Install dependencies
|
|
23
|
-
run: npm ci
|
|
24
|
-
|
|
25
|
-
- name: Build
|
|
26
|
-
run: npm run build --if-present
|
|
27
|
-
|
|
28
|
-
- name: Test
|
|
29
|
-
run: npm test
|
|
30
|
-
|
|
31
|
-
release:
|
|
32
|
-
needs: build
|
|
33
|
-
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
|
34
|
-
runs-on: ubuntu-latest
|
|
35
|
-
permissions:
|
|
36
|
-
contents: write
|
|
37
|
-
issues: write
|
|
38
|
-
pull-requests: write
|
|
39
|
-
id-token: write
|
|
40
|
-
steps:
|
|
41
|
-
- name: Checkout repository
|
|
42
|
-
uses: actions/checkout@v6
|
|
43
|
-
with:
|
|
44
|
-
fetch-depth: 0
|
|
45
|
-
|
|
46
|
-
- name: Setup Node.js
|
|
47
|
-
uses: actions/setup-node@v5
|
|
48
|
-
with:
|
|
49
|
-
node-version: 22
|
|
50
|
-
|
|
51
|
-
- name: Install dependencies
|
|
52
|
-
run: npm ci
|
|
53
|
-
|
|
54
|
-
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
|
|
55
|
-
run: npm audit signatures
|
|
56
|
-
|
|
57
|
-
- name: Release
|
|
58
|
-
env:
|
|
59
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
60
|
-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
61
|
-
run: npx semantic-release
|
package/.vscode/settings.json
DELETED
package/ARCHITECTURE.md
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
# 🏗️ Arquitectura y Principios SOLID
|
|
2
|
-
|
|
3
|
-
## Resumen Ejecutivo
|
|
4
|
-
|
|
5
|
-
QueryMongo está diseñado siguiendo los 5 principios SOLID, Clean Code y DRY (Don't Repeat Yourself). El código es modular, escalable y fácil de mantener.
|
|
6
|
-
|
|
7
|
-
## 📐 Principios SOLID Aplicados
|
|
8
|
-
|
|
9
|
-
### 1. **S - Single Responsibility Principle**
|
|
10
|
-
|
|
11
|
-
Cada clase/función tiene una única responsabilidad:
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
// ✅ BIEN: SelectConverter solo convierte SELECT
|
|
15
|
-
export const SelectConverter: IConverter = {
|
|
16
|
-
can: (statement) => statement.toLowerCase().startsWith("select"),
|
|
17
|
-
convert: (query) => {
|
|
18
|
-
/* lógica solo para SELECT */
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// ✅ BIEN: buildProjection solo construye proyecciones
|
|
23
|
-
export const buildProjection = (fields: string[]): Record<string, 1 | 0> => {
|
|
24
|
-
// lógica única
|
|
25
|
-
};
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
### 2. **O - Open/Closed Principle**
|
|
29
|
-
|
|
30
|
-
El código está abierto a extensión pero cerrado a modificación:
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
// Agregar un nuevo tipo de query (ej: TRUNCATE) es solo:
|
|
34
|
-
export const TruncateConverter: IConverter = {
|
|
35
|
-
can: (statement) => statement.toLowerCase().startsWith("truncate"),
|
|
36
|
-
convert: (query) => {
|
|
37
|
-
/* implementación */
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
// Y registrarlo en ConverterFactory sin tocar código existente:
|
|
42
|
-
const CONVERTERS: IConverter[] = [
|
|
43
|
-
SelectConverter,
|
|
44
|
-
CreateConverter,
|
|
45
|
-
UpdateConverter,
|
|
46
|
-
DeleteConverter,
|
|
47
|
-
TruncateConverter, // ← Nueva línea solamente
|
|
48
|
-
];
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### 3. **L - Liskov Substitution Principle**
|
|
52
|
-
|
|
53
|
-
Todos los conversores implementan la interface `IConverter`:
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
interface IConverter {
|
|
57
|
-
can(statement: string): boolean;
|
|
58
|
-
convert(query: any): Record<string, any>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// SelectConverter, CreateConverter, UpdateConverter, DeleteConverter
|
|
62
|
-
// pueden usarse indistintamente donde se espera IConverter
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### 4. **I - Interface Segregation Principle**
|
|
66
|
-
|
|
67
|
-
Las interfaces son mínimas y específicas:
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
// Interface pequeña y enfocada
|
|
71
|
-
interface IConverter {
|
|
72
|
-
can(statement: string): boolean;
|
|
73
|
-
convert(query: any): Record<string, any>;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// No hay métodos innecesarios, solo lo que se necesita
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 5. **D - Dependency Inversion Principle**
|
|
80
|
-
|
|
81
|
-
Dependemos de abstracciones, no de implementaciones concretas:
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
// MongoConverter depende de la interface, no de las implementaciones
|
|
85
|
-
const converter = getConverter(sql); // Retorna IConverter, no una clase específica
|
|
86
|
-
|
|
87
|
-
// Si el parser cambia, solo cambio sqlParser.ts
|
|
88
|
-
// Si un conversor cambia, los otros no se ven afectados
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## 🏛️ Arquitectura en Capas
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
┌─────────────────────────────────────┐
|
|
95
|
-
│ CLI Layer (main.ts) │
|
|
96
|
-
│ - Commander para argumentos │
|
|
97
|
-
│ - Interfaz interactiva │
|
|
98
|
-
└─────────────────────────────────────┘
|
|
99
|
-
↓
|
|
100
|
-
┌─────────────────────────────────────┐
|
|
101
|
-
│ Application Layer │
|
|
102
|
-
│ (MongoConverter.ts) │
|
|
103
|
-
│ - Orquestación del flujo │
|
|
104
|
-
│ - Error handling │
|
|
105
|
-
└─────────────────────────────────────┘
|
|
106
|
-
↓
|
|
107
|
-
┌─────────────────────────────────────┐
|
|
108
|
-
│ Infrastructure Layer │
|
|
109
|
-
│ - Parsers (sqlParser.ts) │
|
|
110
|
-
│ - Conversores (SelectConverter...) │
|
|
111
|
-
│ - Utilidades (queryUtils.ts) │
|
|
112
|
-
└─────────────────────────────────────┘
|
|
113
|
-
↓
|
|
114
|
-
┌─────────────────────────────────────┐
|
|
115
|
-
│ Domain Layer │
|
|
116
|
-
│ - Interfaces (IConverter) │
|
|
117
|
-
│ - Tipos y contratos │
|
|
118
|
-
└─────────────────────────────────────┘
|
|
119
|
-
↓
|
|
120
|
-
┌─────────────────────────────────────┐
|
|
121
|
-
│ External Dependencies │
|
|
122
|
-
│ - node-sql-parser │
|
|
123
|
-
│ - lodash │
|
|
124
|
-
│ - chalk, commander │
|
|
125
|
-
└─────────────────────────────────────┘
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
## 🎯 Patrones de Diseño
|
|
129
|
-
|
|
130
|
-
### Strategy Pattern
|
|
131
|
-
|
|
132
|
-
Cada tipo de SQL es una estrategia diferente:
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
// SelectConverter es una estrategia
|
|
136
|
-
const SelectConverter: IConverter = {
|
|
137
|
-
can: (stmt) => stmt.startsWith("select"),
|
|
138
|
-
convert: (query) => {
|
|
139
|
-
/* convertir SELECT */
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
// UpdateConverter es una estrategia diferente
|
|
144
|
-
const UpdateConverter: IConverter = {
|
|
145
|
-
can: (stmt) => stmt.startsWith("update"),
|
|
146
|
-
convert: (query) => {
|
|
147
|
-
/* convertir UPDATE */
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
// Factory selecciona la estrategia correcta
|
|
152
|
-
const converter = CONVERTERS.find((c) => c.can(sql));
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Factory Pattern
|
|
156
|
-
|
|
157
|
-
`ConverterFactory` determina qué conversor usar:
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
export const getConverter = (statement: string): IConverter | null => {
|
|
161
|
-
return CONVERTERS.find((converter) => converter.can(statement)) || null;
|
|
162
|
-
};
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### Adapter Pattern
|
|
166
|
-
|
|
167
|
-
`sqlParser.ts` adapta `node-sql-parser` a una interfaz limpia:
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
// Internamente usa node-sql-parser
|
|
171
|
-
const sqlParser = new Parser();
|
|
172
|
-
|
|
173
|
-
// Pero expone una interfaz simple
|
|
174
|
-
export const parseSqlQuery = (sql: string) => sqlParser.parse(sql);
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## 🧹 DRY - Don't Repeat Yourself
|
|
178
|
-
|
|
179
|
-
Toda lógica compartida está centralizada:
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
// queryUtils.ts - Funciones reutilizables
|
|
183
|
-
export const normalizeFieldName = (field: string): string => {
|
|
184
|
-
return field.trim().replace(/`|"/g, '');
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
export const buildProjection = (fields: string[]): Record<string, 1 | 0> => {
|
|
188
|
-
if (fields === '*') return {};
|
|
189
|
-
return Object.fromEntries(normalizeFieldNames(fields).map(f => [f, 1]));
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
export const buildFilter = (condition: any): Record<string, any> => {
|
|
193
|
-
// Lógica única para construir filters
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// Los conversores reutilizan estas funciones
|
|
197
|
-
SelectConverter usa buildProjection y buildFilter
|
|
198
|
-
UpdateConverter usa buildFilter
|
|
199
|
-
DeleteConverter usa buildFilter
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## 🎓 Beneficios de esta Arquitectura
|
|
203
|
-
|
|
204
|
-
| Beneficio | Impacto |
|
|
205
|
-
| ------------------ | -------------------------------------------- |
|
|
206
|
-
| **Mantenibilidad** | Cambios localizados, sin efectos secundarios |
|
|
207
|
-
| **Escalabilidad** | Agregar nuevos tipos de query es trivial |
|
|
208
|
-
| **Testabilidad** | 100% cobertura fácil de lograr |
|
|
209
|
-
| **Reutilización** | Utilidades compartidas evitan duplicación |
|
|
210
|
-
| **Legibilidad** | Código claro, intenciones obvias |
|
|
211
|
-
| **Flexibilidad** | Fácil refactorizar componentes |
|
|
212
|
-
|
|
213
|
-
## 📊 Flujo de Conversión
|
|
214
|
-
|
|
215
|
-
```
|
|
216
|
-
SQL Query
|
|
217
|
-
↓
|
|
218
|
-
normalizeQuery()
|
|
219
|
-
↓
|
|
220
|
-
parseSqlQuery()
|
|
221
|
-
↓
|
|
222
|
-
AST (Abstract Syntax Tree)
|
|
223
|
-
↓
|
|
224
|
-
getConverter(statement)
|
|
225
|
-
↓
|
|
226
|
-
Estrategia seleccionada
|
|
227
|
-
↓
|
|
228
|
-
converter.convert(ast)
|
|
229
|
-
↓
|
|
230
|
-
MongoDB Query Object
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
## 🔌 Extensibilidad
|
|
234
|
-
|
|
235
|
-
Para agregar soporte para un nuevo tipo de query:
|
|
236
|
-
|
|
237
|
-
1. **Crear nuevo conversor**:
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
export const GroupByConverter: IConverter = {
|
|
241
|
-
can: (stmt) => stmt.includes("GROUP BY"),
|
|
242
|
-
convert: (query) => {
|
|
243
|
-
/* implementación */
|
|
244
|
-
},
|
|
245
|
-
};
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
2. **Registrar en factory**:
|
|
249
|
-
|
|
250
|
-
```typescript
|
|
251
|
-
const CONVERTERS: IConverter[] = [
|
|
252
|
-
SelectConverter,
|
|
253
|
-
CreateConverter,
|
|
254
|
-
UpdateConverter,
|
|
255
|
-
DeleteConverter,
|
|
256
|
-
GroupByConverter, // ← Agregar aquí
|
|
257
|
-
];
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
3. **Escribir tests** - El resto funciona automáticamente gracias a la arquitectura.
|
|
261
|
-
|
|
262
|
-
## 📈 Métricas de Calidad
|
|
263
|
-
|
|
264
|
-
- **Test Coverage**: 100%
|
|
265
|
-
- **Cyclomatic Complexity**: Bajo (funciones simples y enfocadas)
|
|
266
|
-
- **Lines of Code**: Mínimo (código conciso)
|
|
267
|
-
- **SOLID Score**: 5/5
|
|
268
|
-
- **Code Duplication**: ~0% (todo compartido)
|
|
269
|
-
|
|
270
|
-
## 🚀 Performance
|
|
271
|
-
|
|
272
|
-
- **Parsing**: ~1-2ms por query
|
|
273
|
-
- **Conversión**: <1ms
|
|
274
|
-
- **Total**: <3ms por conversión
|
|
275
|
-
- Sin dependencias pesadas, bundle mínimo
|
|
276
|
-
|
|
277
|
-
## 🔐 Error Handling
|
|
278
|
-
|
|
279
|
-
```typescript
|
|
280
|
-
try {
|
|
281
|
-
const result = mongoConverter.convert(sql);
|
|
282
|
-
} catch (error) {
|
|
283
|
-
// Error descriptivo
|
|
284
|
-
throw new Error(`Unsupported SQL statement: ${sql}...`);
|
|
285
|
-
}
|
|
286
|
-
```
|
package/examples.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Ejemplos de uso de QueryMongo
|
|
5
|
-
* Demuestra diferentes formas de usar la librería
|
|
6
|
-
*/
|
|
7
|
-
import { mongoConverter } from "./src/application/MongoConverter.ts";
|
|
8
|
-
import chalk from "chalk";
|
|
9
|
-
|
|
10
|
-
interface Example {
|
|
11
|
-
title: string;
|
|
12
|
-
sql: string;
|
|
13
|
-
description?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const examples: Example[] = [
|
|
17
|
-
{
|
|
18
|
-
title: "📌 SELECT Simple",
|
|
19
|
-
sql: "SELECT name, email FROM users",
|
|
20
|
-
description: "Selecciona campos específicos",
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
title: "🔍 SELECT con WHERE",
|
|
24
|
-
sql: "SELECT * FROM products WHERE price > 100",
|
|
25
|
-
description: "Filtra documentos con condición",
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
title: "📊 SELECT con WHERE y LIMIT",
|
|
29
|
-
sql: "SELECT name, age FROM users WHERE age >= 18 LIMIT 10",
|
|
30
|
-
description: "Proyección, filtro y límite combinados",
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
title: "➕ INSERT Simple",
|
|
34
|
-
sql: "INSERT INTO users (name, email, age) VALUES ('John Doe', 'john@example.com', 30)",
|
|
35
|
-
description: "Insertar un documento",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
title: "✏️ UPDATE con WHERE",
|
|
39
|
-
sql: 'UPDATE users SET age = 31 WHERE name = "John Doe"',
|
|
40
|
-
description: "Actualizar documentos que cumplen condición",
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
title: "🗑️ DELETE con WHERE",
|
|
44
|
-
sql: "DELETE FROM users WHERE age < 18",
|
|
45
|
-
description: "Eliminar documentos que cumplen condición",
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
console.log(chalk.bold.blue("\n🚀 QueryMongo - Ejemplos de Uso\n"));
|
|
50
|
-
console.log(chalk.gray("Convierte queries SQL a MongoDB format\n"));
|
|
51
|
-
|
|
52
|
-
examples.forEach((example, index) => {
|
|
53
|
-
console.log(chalk.bold.cyan(`\n${index + 1}. ${example.title}`));
|
|
54
|
-
if (example.description) {
|
|
55
|
-
console.log(chalk.gray(` ${example.description}`));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
console.log(chalk.yellow("\n SQL:"));
|
|
59
|
-
console.log(chalk.white(` ${example.sql}`));
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const result = mongoConverter.convert(example.sql);
|
|
63
|
-
|
|
64
|
-
console.log(chalk.green("\n 📋 MongoDB Query:"));
|
|
65
|
-
const resultStr = JSON.stringify(result, null, 4)
|
|
66
|
-
.split("\n")
|
|
67
|
-
.map((line) => ` ${line}`)
|
|
68
|
-
.join("\n");
|
|
69
|
-
console.log(resultStr);
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error(
|
|
72
|
-
chalk.red(
|
|
73
|
-
` ❌ Error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
74
|
-
),
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
console.log(chalk.bold.green("\n✨ Todos los ejemplos completados\n"));
|