andres-db 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +202 -0
  2. package/index.js +1151 -0
  3. package/package.json +35 -0
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+
2
+ # 🗄️ EasyDB
3
+
4
+ ![License](https://img.shields.io/badge/license-MIT-green.svg)
5
+ ![Node](https://img.shields.io/badge/node-%3E%3D16-brightgreen)
6
+ ![SQLite](https://img.shields.io/badge/database-SQLite-blue)
7
+ ![npm](https://img.shields.io/badge/npm-ready-red)
8
+
9
+ **EasyDB** es una interfaz **simple, moderna y poderosa** para trabajar con **SQLite** en Node.js usando `better-sqlite3`.
10
+
11
+ Diseñada para ofrecer **respuestas JSON limpias**, **creación automática de tablas**, **operaciones CRUD intuitivas** y utilidades avanzadas sin escribir SQL complejo.
12
+
13
+ ---
14
+
15
+ ## ✨ Características
16
+
17
+ - 📦 SQLite rápido con `better-sqlite3`
18
+ - ⚡ CRUD completo
19
+ - 🧠 Creación automática de tablas
20
+ - 🧩 Columnas dinámicas
21
+ - 🔎 Búsquedas avanzadas
22
+ - 🔁 Transacciones seguras
23
+ - 📤 Exportar / importar JSON
24
+ - 💾 Backups automáticos
25
+ - 📊 Estadísticas del sistema
26
+ - 🧾 Respuestas JSON estándar
27
+ - 🛠️ Consultas SQL personalizadas
28
+
29
+ ---
30
+
31
+ ## 📦 Instalación
32
+
33
+ ```bash
34
+ npm install andres-db
35
+ ```
36
+
37
+ Requiere **Node.js v16+**
38
+
39
+ ---
40
+
41
+ ## 🚀 Uso rápido
42
+
43
+ ```js
44
+ const db = require('andres-db');
45
+
46
+ db.create('users', {
47
+ name: 'Juan',
48
+ email: 'juan@email.com',
49
+ age: 25
50
+ });
51
+
52
+ const users = db.read('users');
53
+ console.log(users.data);
54
+ ```
55
+
56
+ ---
57
+
58
+ ## ⚙️ Configuración
59
+
60
+ ```js
61
+ const db = require('andres-db').createInstance({
62
+ filename: './db/app.db',
63
+ verbose: true,
64
+ jsonResponse: true
65
+ });
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 🧱 Crear registros
71
+
72
+ ```js
73
+ db.create('products', {
74
+ name: 'Laptop',
75
+ price: 1200,
76
+ stock: 10
77
+ });
78
+ ```
79
+
80
+ ### Múltiples registros
81
+
82
+ ```js
83
+ db.create('users', [
84
+ { name: 'Ana', age: 22 },
85
+ { name: 'Luis', age: 30 }
86
+ ]);
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 📖 Leer datos
92
+
93
+ ```js
94
+ db.read('users', { age: { $gte: 18 } });
95
+ ```
96
+
97
+ ```js
98
+ db.readOne('users', { id: 1 });
99
+ ```
100
+
101
+ ---
102
+
103
+ ## ✏️ Actualizar
104
+
105
+ ```js
106
+ db.update('users', { id: 1 }, { age: 26 });
107
+ ```
108
+
109
+ ---
110
+
111
+ ## ❌ Eliminar
112
+
113
+ ```js
114
+ db.delete('users', { id: 1 });
115
+ ```
116
+
117
+ ---
118
+
119
+ ## 🔎 Buscar texto
120
+
121
+ ```js
122
+ db.search('users', ['name', 'email'], 'juan');
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 🧩 Agregar columnas
128
+
129
+ ```js
130
+ db.addColumn('users', 'phone', 'TEXT');
131
+ ```
132
+
133
+ ```js
134
+ db.addColumns('users', [
135
+ { name: 'city', type: 'TEXT' },
136
+ { name: 'active', type: 'INTEGER', defaultValue: 1 }
137
+ ]);
138
+ ```
139
+
140
+ ---
141
+
142
+ ## 🔁 Transacciones
143
+
144
+ ```js
145
+ db.transaction((db) => {
146
+ db.create('accounts', { name: 'Cuenta A' });
147
+ db.create('accounts', { name: 'Cuenta B' });
148
+ });
149
+ ```
150
+
151
+ ---
152
+
153
+ ## 🧪 SQL personalizado
154
+
155
+ ```js
156
+ db.query('SELECT * FROM users WHERE age > ?', [20]);
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 📤 Exportar / 📥 Importar
162
+
163
+ ```js
164
+ db.exportToJSON();
165
+ ```
166
+
167
+ ```js
168
+ db.importFromJSON({ users: [{ name: 'Carlos', age: 28 }] });
169
+ ```
170
+
171
+ ---
172
+
173
+ ## 💾 Backup
174
+
175
+ ```js
176
+ db.backup('./backup/db_backup.db');
177
+ ```
178
+
179
+ ---
180
+
181
+ ## 📊 Estadísticas
182
+
183
+ ```js
184
+ db.getStats();
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 🧑‍💻 Autor
190
+
191
+ **ANDRES CARVAJAL**
192
+
193
+ 🌐 https://andres-carvajal.vercel.app
194
+ 📸 Instagram: [@05_carvajal](https://instagram.com/05_carvajal)
195
+
196
+ ---
197
+
198
+ ## 📄 Licencia
199
+
200
+ ![MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
201
+
202
+ Este proyecto está bajo la licencia **MIT**.
package/index.js ADDED
@@ -0,0 +1,1151 @@
1
+ /**
2
+ * db.js - Base de Datos SQL Simplificada
3
+ * @version 2.2.0
4
+ * @license MIT
5
+ * @description Interfaz JavaScript simple para bases de datos SQLite con respuestas JSON
6
+ */
7
+
8
+ const Database = require('better-sqlite3');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ // ===========================================
13
+ // VARIABLE GLOBAL PARA RUTA DE LA BASE DE DATOS
14
+ // ===========================================
15
+ const DEFAULT_DB_PATH = './db/database.db'; // <-- VARIABLE GLOBAL
16
+
17
+ // Función para asegurar que el directorio existe
18
+ function ensureDirectoryExists(filePath) {
19
+ const dir = path.dirname(filePath);
20
+ if (!fs.existsSync(dir)) {
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ console.log(`Directorio creado: ${path.resolve(dir)}`);
23
+ }
24
+ return dir;
25
+ }
26
+
27
+ class EasyDB {
28
+ /**
29
+ * Crea una nueva instancia de EasyDB
30
+ * @param {string|Object} config - Configuración de la base de datos
31
+ * @param {string} [config.filename=DEFAULT_DB_PATH] - Nombre del archivo
32
+ * @param {boolean} [config.memory=false] - Usar base de datos en memoria
33
+ * @param {boolean} [config.readonly=false] - Modo solo lectura
34
+ * @param {boolean} [config.verbose=false] - Mostrar logs SQL
35
+ * @param {boolean} [config.jsonResponse=true] - Formatear respuestas como JSON limpio
36
+ * @param {boolean} [config.autoCreateDir=true] - Crear directorio automáticamente si no existe
37
+ */
38
+ constructor(config = DEFAULT_DB_PATH) {
39
+ this.config = {
40
+ filename: DEFAULT_DB_PATH,
41
+ memory: false,
42
+ readonly: false,
43
+ verbose: false,
44
+ jsonResponse: true,
45
+ autoCreateDir: true,
46
+ ...(typeof config === 'string' ? { filename: config } : config)
47
+ };
48
+
49
+ // Crear directorio automáticamente si es necesario
50
+ if (!this.config.memory && this.config.autoCreateDir) {
51
+ ensureDirectoryExists(this.config.filename);
52
+ }
53
+
54
+ // Opciones de conexión
55
+ const dbOptions = {
56
+ verbose: this.config.verbose ? console.log : null,
57
+ readonly: this.config.readonly
58
+ };
59
+
60
+ // Establecer conexión
61
+ if (this.config.memory) {
62
+ this.db = new Database(':memory:', dbOptions);
63
+ if (this.config.verbose) console.log('EasyDB: Conectado a base de datos en memoria');
64
+ } else {
65
+ try {
66
+ this.db = new Database(this.config.filename, dbOptions);
67
+ if (this.config.verbose) {
68
+ console.log(`EasyDB: Conectado a ${this.config.filename}`);
69
+ console.log(`Ruta completa: ${path.resolve(this.config.filename)}`);
70
+ }
71
+ } catch (error) {
72
+ console.error(`Error conectando a ${this.config.filename}:`, error.message);
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ // Optimizaciones
78
+ this.db.pragma('foreign_keys = ON');
79
+ this.db.pragma('journal_mode = WAL');
80
+ this.db.pragma('synchronous = NORMAL');
81
+
82
+ // Inicializar sistema de metadatos
83
+ this._initMetadata();
84
+ }
85
+
86
+ /**
87
+ * Inicializa la tabla de metadatos
88
+ * @private
89
+ */
90
+ _initMetadata() {
91
+ const sql = `
92
+ CREATE TABLE IF NOT EXISTS _easydb_meta (
93
+ key TEXT PRIMARY KEY,
94
+ value TEXT,
95
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
96
+ )
97
+ `;
98
+ this.db.exec(sql);
99
+ this._setMeta('version', '2.2.0');
100
+ this._setMeta('created_at', new Date().toISOString());
101
+ this._setMeta('db_path', this.config.filename);
102
+ }
103
+
104
+ /**
105
+ * Establece un metadato
106
+ * @private
107
+ */
108
+ _setMeta(key, value) {
109
+ const sql = `INSERT OR REPLACE INTO _easydb_meta (key, value) VALUES (?, ?)`;
110
+ this.db.prepare(sql).run(key, value);
111
+ }
112
+
113
+ /**
114
+ * Obtiene un metadato
115
+ * @private
116
+ */
117
+ _getMeta(key) {
118
+ const sql = 'SELECT value FROM _easydb_meta WHERE key = ?';
119
+ const result = this.db.prepare(sql).get(key);
120
+ return result ? result.value : null;
121
+ }
122
+
123
+ /**
124
+ * Verifica si una tabla existe
125
+ * @param {string} tableName - Nombre de la tabla
126
+ * @returns {boolean}
127
+ */
128
+ tableExists(tableName) {
129
+ const sql = `SELECT name FROM sqlite_master WHERE type='table' AND name = ?`;
130
+ return !!this.db.prepare(sql).get(tableName);
131
+ }
132
+
133
+ /**
134
+ * Crea una tabla automáticamente basada en datos de muestra
135
+ * @private
136
+ */
137
+ _autoCreateTable(tableName, sampleData) {
138
+ const columns = ['id INTEGER PRIMARY KEY AUTOINCREMENT'];
139
+
140
+ // Mapear tipos JavaScript a tipos SQL
141
+ for (const [key, value] of Object.entries(sampleData)) {
142
+ let columnType = 'TEXT';
143
+
144
+ if (value === null || value === undefined) {
145
+ columnType = 'TEXT';
146
+ } else if (typeof value === 'number') {
147
+ columnType = Number.isInteger(value) ? 'INTEGER' : 'REAL';
148
+ } else if (typeof value === 'boolean') {
149
+ columnType = 'INTEGER';
150
+ } else if (value instanceof Date) {
151
+ columnType = 'TEXT';
152
+ } else if (typeof value === 'object') {
153
+ columnType = 'TEXT'; // JSON
154
+ }
155
+
156
+ columns.push(`${key} ${columnType}`);
157
+ }
158
+
159
+ columns.push('created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
160
+ columns.push('updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP');
161
+
162
+ // Crear tabla
163
+ const sql = `CREATE TABLE ${tableName} (${columns.join(', ')})`;
164
+ this.db.exec(sql);
165
+
166
+ // Crear trigger para updated_at
167
+ const triggerSql = `
168
+ CREATE TRIGGER IF NOT EXISTS ${tableName}_updated
169
+ AFTER UPDATE ON ${tableName}
170
+ BEGIN
171
+ UPDATE ${tableName}
172
+ SET updated_at = CURRENT_TIMESTAMP
173
+ WHERE id = NEW.id;
174
+ END
175
+ `;
176
+ this.db.exec(triggerSql);
177
+
178
+ if (this.config.verbose) {
179
+ console.log(`EasyDB: Tabla '${tableName}' creada automáticamente`);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Agrega una nueva columna a una tabla existente
185
+ * @param {string} tableName - Nombre de la tabla
186
+ * @param {string} columnName - Nombre de la nueva columna
187
+ * @param {string} columnType - Tipo de dato (INTEGER, TEXT, REAL, BLOB, etc.)
188
+ * @param {*} [defaultValue] - Valor por defecto (opcional)
189
+ * @returns {Object} - Respuesta JSON con resultado de la operación
190
+ */
191
+ addColumn(tableName, columnName, columnType, defaultValue = null) {
192
+ try {
193
+ if (!this.tableExists(tableName)) {
194
+ throw new Error(`La tabla '${tableName}' no existe`);
195
+ }
196
+
197
+ // Verificar si la columna ya existe
198
+ const tableInfo = this.tableInfo(tableName);
199
+ if (tableInfo.success) {
200
+ const columnExists = tableInfo.columns.some(col => col.name === columnName);
201
+ if (columnExists) {
202
+ throw new Error(`La columna '${columnName}' ya existe en la tabla '${tableName}'`);
203
+ }
204
+ }
205
+
206
+ // Construir la sentencia ALTER TABLE
207
+ let sql = `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType}`;
208
+
209
+ if (defaultValue !== null) {
210
+ const defaultValueStr = typeof defaultValue === 'string'
211
+ ? `'${defaultValue.replace(/'/g, "''")}'`
212
+ : defaultValue;
213
+ sql += ` DEFAULT ${defaultValueStr}`;
214
+ }
215
+
216
+ if (this.config.verbose) console.log(`SQL: ${sql}`);
217
+
218
+ this.db.exec(sql);
219
+
220
+ return {
221
+ success: true,
222
+ operation: 'addColumn',
223
+ table: tableName,
224
+ column: columnName,
225
+ type: columnType,
226
+ defaultValue: defaultValue,
227
+ message: `Columna '${columnName}' agregada exitosamente a la tabla '${tableName}'`,
228
+ timestamp: new Date().toISOString()
229
+ };
230
+
231
+ } catch (error) {
232
+ return {
233
+ success: false,
234
+ operation: 'addColumn',
235
+ error: error.message,
236
+ timestamp: new Date().toISOString()
237
+ };
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Agrega múltiples columnas a una tabla existente
243
+ * @param {string} tableName - Nombre de la tabla
244
+ * @param {Array} columns - Array de objetos {name, type, defaultValue}
245
+ * @returns {Object} - Respuesta JSON con resultado de la operación
246
+ */
247
+ addColumns(tableName, columns) {
248
+ try {
249
+ const results = [];
250
+
251
+ for (const column of columns) {
252
+ const result = this.addColumn(
253
+ tableName,
254
+ column.name,
255
+ column.type,
256
+ column.defaultValue
257
+ );
258
+ results.push(result);
259
+ }
260
+
261
+ const successful = results.filter(r => r.success).length;
262
+
263
+ return {
264
+ success: successful === columns.length,
265
+ operation: 'addColumns',
266
+ table: tableName,
267
+ results: results,
268
+ added: successful,
269
+ failed: columns.length - successful,
270
+ message: `Agregadas ${successful} de ${columns.length} columnas a la tabla '${tableName}'`,
271
+ timestamp: new Date().toISOString()
272
+ };
273
+
274
+ } catch (error) {
275
+ return {
276
+ success: false,
277
+ operation: 'addColumns',
278
+ error: error.message,
279
+ timestamp: new Date().toISOString()
280
+ };
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Construye cláusula WHERE a partir de condiciones
286
+ * @private
287
+ */
288
+ _buildWhereClause(conditions) {
289
+ if (!conditions || Object.keys(conditions).length === 0) {
290
+ return { clause: '', params: [] };
291
+ }
292
+
293
+ const clauses = [];
294
+ const params = [];
295
+
296
+ for (const [key, value] of Object.entries(conditions)) {
297
+ if (typeof value === 'object' && value !== null) {
298
+ // Operadores especiales
299
+ if (value.$eq !== undefined) {
300
+ clauses.push(`${key} = ?`);
301
+ params.push(value.$eq);
302
+ } else if (value.$ne !== undefined) {
303
+ clauses.push(`${key} != ?`);
304
+ params.push(value.$ne);
305
+ } else if (value.$gt !== undefined) {
306
+ clauses.push(`${key} > ?`);
307
+ params.push(value.$gt);
308
+ } else if (value.$gte !== undefined) {
309
+ clauses.push(`${key} >= ?`);
310
+ params.push(value.$gte);
311
+ } else if (value.$lt !== undefined) {
312
+ clauses.push(`${key} < ?`);
313
+ params.push(value.$lt);
314
+ } else if (value.$lte !== undefined) {
315
+ clauses.push(`${key} <= ?`);
316
+ params.push(value.$lte);
317
+ } else if (value.$in !== undefined) {
318
+ const placeholders = value.$in.map(() => '?').join(', ');
319
+ clauses.push(`${key} IN (${placeholders})`);
320
+ params.push(...value.$in);
321
+ } else if (value.$like !== undefined) {
322
+ clauses.push(`${key} LIKE ?`);
323
+ params.push(value.$like);
324
+ } else if (value.$between !== undefined && Array.isArray(value.$between)) {
325
+ clauses.push(`${key} BETWEEN ? AND ?`);
326
+ params.push(value.$between[0], value.$between[1]);
327
+ } else if (value.$not !== undefined) {
328
+ clauses.push(`${key} != ?`);
329
+ params.push(value.$not);
330
+ }
331
+ } else {
332
+ clauses.push(`${key} = ?`);
333
+ params.push(value);
334
+ }
335
+ }
336
+
337
+ return {
338
+ clause: clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '',
339
+ params
340
+ };
341
+ }
342
+
343
+ /**
344
+ * Formatea respuesta como JSON limpio
345
+ * @private
346
+ */
347
+ _formatResponse(data, operation, table = null) {
348
+ if (!this.config.jsonResponse) return data;
349
+
350
+ const response = {
351
+ success: true,
352
+ operation: operation,
353
+ timestamp: new Date().toISOString(),
354
+ data: data
355
+ };
356
+
357
+ if (table) response.table = table;
358
+
359
+ // Para operaciones de creación
360
+ if (operation === 'create' && data && data.id) {
361
+ response.message = `Registro creado con ID: ${data.id}`;
362
+ response.recordId = data.id;
363
+ }
364
+
365
+ // Para operaciones de actualización/eliminación
366
+ if (operation === 'update' && data.changes !== undefined) {
367
+ response.message = `Registros actualizados: ${data.changes}`;
368
+ response.changes = data.changes;
369
+ }
370
+
371
+ if (operation === 'delete' && data.deleted !== undefined) {
372
+ response.message = `Registros eliminados: ${data.deleted}`;
373
+ response.deleted = data.deleted;
374
+ }
375
+
376
+ // Para operaciones de lectura
377
+ if (operation === 'read' || operation === 'search') {
378
+ response.count = Array.isArray(data) ? data.length : 1;
379
+ response.message = `Registros encontrados: ${response.count}`;
380
+ }
381
+
382
+ return response;
383
+ }
384
+
385
+ /**
386
+ * Crea uno o múltiples registros
387
+ * @param {string} table - Nombre de la tabla
388
+ * @param {Object|Array} data - Datos a insertar
389
+ * @returns {Object} - Respuesta JSON con registro(s) creado(s)
390
+ */
391
+ create(table, data) {
392
+ try {
393
+ // Crear tabla si no existe
394
+ if (!this.tableExists(table)) {
395
+ const sampleData = Array.isArray(data) ? data[0] : data;
396
+ this._autoCreateTable(table, sampleData);
397
+ }
398
+
399
+ const isArray = Array.isArray(data);
400
+ const records = isArray ? data : [data];
401
+ const results = [];
402
+
403
+ for (const record of records) {
404
+ const filtered = {};
405
+ for (const [key, value] of Object.entries(record)) {
406
+ if (value !== undefined) filtered[key] = value;
407
+ }
408
+
409
+ const keys = Object.keys(filtered);
410
+ const values = Object.values(filtered);
411
+ const placeholders = keys.map(() => '?').join(', ');
412
+
413
+ const sql = `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`;
414
+ if (this.config.verbose) console.log(`SQL: ${sql}`, values);
415
+
416
+ const stmt = this.db.prepare(sql);
417
+ const result = stmt.run(...values);
418
+
419
+ const newRecord = {
420
+ id: result.lastInsertRowid,
421
+ ...filtered,
422
+ created_at: new Date().toISOString()
423
+ };
424
+
425
+ results.push(newRecord);
426
+ }
427
+
428
+ const responseData = isArray ? results : results[0];
429
+ return this._formatResponse(responseData, 'create', table);
430
+
431
+ } catch (error) {
432
+ return {
433
+ success: false,
434
+ operation: 'create',
435
+ error: error.message,
436
+ timestamp: new Date().toISOString()
437
+ };
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Lee registros de una tabla
443
+ * @param {string} table - Nombre de la tabla
444
+ * @param {Object} [conditions={}] - Condiciones de filtro
445
+ * @param {Object} [options={}] - Opciones de consulta
446
+ * @param {string} [options.orderBy] - Campo para ordenar
447
+ * @param {string} [options.order='ASC'] - Dirección del orden
448
+ * @param {number} [options.limit] - Límite de resultados
449
+ * @param {number} [options.offset] - Desplazamiento
450
+ * @returns {Object} - Respuesta JSON con registros encontrados
451
+ */
452
+ read(table, conditions = {}, options = {}) {
453
+ try {
454
+ if (!this.tableExists(table)) {
455
+ return this._formatResponse([], 'read', table);
456
+ }
457
+
458
+ const { clause, params } = this._buildWhereClause(conditions);
459
+ let sql = `SELECT * FROM ${table} ${clause}`;
460
+
461
+ if (options.orderBy) {
462
+ sql += ` ORDER BY ${options.orderBy} ${options.order || 'ASC'}`;
463
+ }
464
+
465
+ if (options.limit) {
466
+ sql += ` LIMIT ${options.limit}`;
467
+ }
468
+
469
+ if (options.offset) {
470
+ sql += ` OFFSET ${options.offset}`;
471
+ }
472
+
473
+ if (this.config.verbose) console.log(`SQL: ${sql}`, params);
474
+
475
+ const stmt = this.db.prepare(sql);
476
+ const results = stmt.all(...params);
477
+
478
+ return this._formatResponse(results, 'read', table);
479
+
480
+ } catch (error) {
481
+ return {
482
+ success: false,
483
+ operation: 'read',
484
+ error: error.message,
485
+ timestamp: new Date().toISOString()
486
+ };
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Lee un solo registro
492
+ * @param {string} table - Nombre de la tabla
493
+ * @param {Object} [conditions={}] - Condiciones de filtro
494
+ * @returns {Object} - Respuesta JSON con registro encontrado o null
495
+ */
496
+ readOne(table, conditions = {}) {
497
+ try {
498
+ const result = this.read(table, conditions, { limit: 1 });
499
+ if (result.success && Array.isArray(result.data)) {
500
+ result.data = result.data[0] || null;
501
+ result.count = result.data ? 1 : 0;
502
+ result.message = result.data ? 'Registro encontrado' : 'No se encontró el registro';
503
+ }
504
+ return result;
505
+ } catch (error) {
506
+ return {
507
+ success: false,
508
+ operation: 'readOne',
509
+ error: error.message,
510
+ timestamp: new Date().toISOString()
511
+ };
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Actualiza registros
517
+ * @param {string} table - Nombre de la tabla
518
+ * @param {Object} conditions - Condiciones para seleccionar registros
519
+ * @param {Object} data - Datos a actualizar
520
+ * @returns {Object} - Respuesta JSON con resultado de la operación
521
+ */
522
+ update(table, conditions, data) {
523
+ try {
524
+ if (!this.tableExists(table)) {
525
+ throw new Error(`La tabla '${table}' no existe`);
526
+ }
527
+
528
+ const updateData = {};
529
+ for (const [key, value] of Object.entries(data)) {
530
+ if (value !== undefined) updateData[key] = value;
531
+ }
532
+
533
+ if (Object.keys(updateData).length === 0) {
534
+ throw new Error('No hay datos para actualizar');
535
+ }
536
+
537
+ const setClause = Object.keys(updateData)
538
+ .map(key => `${key} = ?`)
539
+ .join(', ');
540
+
541
+ const { clause: whereClause, params: whereParams } = this._buildWhereClause(conditions);
542
+
543
+ if (!whereClause) {
544
+ throw new Error('Se requieren condiciones para actualizar');
545
+ }
546
+
547
+ const params = [
548
+ ...Object.values(updateData),
549
+ ...whereParams
550
+ ];
551
+
552
+ const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
553
+ if (this.config.verbose) console.log(`SQL: ${sql}`, params);
554
+
555
+ const stmt = this.db.prepare(sql);
556
+ const result = stmt.run(...params);
557
+
558
+ const responseData = {
559
+ changes: result.changes,
560
+ message: `Actualizados ${result.changes} registro(s)`
561
+ };
562
+
563
+ return this._formatResponse(responseData, 'update', table);
564
+
565
+ } catch (error) {
566
+ return {
567
+ success: false,
568
+ operation: 'update',
569
+ error: error.message,
570
+ timestamp: new Date().toISOString()
571
+ };
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Elimina registros
577
+ * @param {string} table - Nombre de la tabla
578
+ * @param {Object} conditions - Condiciones para seleccionar registros
579
+ * @returns {Object} - Respuesta JSON con resultado de la operación
580
+ */
581
+ delete(table, conditions) {
582
+ try {
583
+ if (!this.tableExists(table)) {
584
+ throw new Error(`La tabla '${table}' no existe`);
585
+ }
586
+
587
+ const { clause, params } = this._buildWhereClause(conditions);
588
+
589
+ if (!clause) {
590
+ throw new Error('Se requieren condiciones para eliminar');
591
+ }
592
+
593
+ const sql = `DELETE FROM ${table} ${clause}`;
594
+ if (this.config.verbose) console.log(`SQL: ${sql}`, params);
595
+
596
+ const stmt = this.db.prepare(sql);
597
+ const result = stmt.run(...params);
598
+
599
+ const responseData = {
600
+ deleted: result.changes,
601
+ message: `Eliminados ${result.changes} registro(s)`
602
+ };
603
+
604
+ return this._formatResponse(responseData, 'delete', table);
605
+
606
+ } catch (error) {
607
+ return {
608
+ success: false,
609
+ operation: 'delete',
610
+ error: error.message,
611
+ timestamp: new Date().toISOString()
612
+ };
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Busca registros por texto
618
+ * @param {string} table - Nombre de la tabla
619
+ * @param {string|Array} fields - Campo(s) donde buscar
620
+ * @param {string} searchTerm - Término de búsqueda
621
+ * @returns {Object} - Respuesta JSON con registros encontrados
622
+ */
623
+ search(table, fields, searchTerm) {
624
+ try {
625
+ if (!this.tableExists(table)) {
626
+ return this._formatResponse([], 'search', table);
627
+ }
628
+
629
+ const fieldList = Array.isArray(fields) ? fields : [fields];
630
+ const conditions = fieldList.map(field => `${field} LIKE ?`).join(' OR ');
631
+ const params = fieldList.map(() => `%${searchTerm}%`);
632
+
633
+ const sql = `SELECT * FROM ${table} WHERE ${conditions}`;
634
+ if (this.config.verbose) console.log(`SQL: ${sql}`, params);
635
+
636
+ const stmt = this.db.prepare(sql);
637
+ const results = stmt.all(...params);
638
+
639
+ return this._formatResponse(results, 'search', table);
640
+
641
+ } catch (error) {
642
+ return {
643
+ success: false,
644
+ operation: 'search',
645
+ error: error.message,
646
+ timestamp: new Date().toISOString()
647
+ };
648
+ }
649
+ }
650
+
651
+ /**
652
+ * Cuenta registros
653
+ * @param {string} table - Nombre de la tabla
654
+ * @param {Object} [conditions={}] - Condiciones de filtro
655
+ * @returns {Object} - Respuesta JSON con cantidad de registros
656
+ */
657
+ count(table, conditions = {}) {
658
+ try {
659
+ if (!this.tableExists(table)) {
660
+ return {
661
+ success: true,
662
+ operation: 'count',
663
+ count: 0,
664
+ message: 'La tabla no existe',
665
+ timestamp: new Date().toISOString()
666
+ };
667
+ }
668
+
669
+ const { clause, params } = this._buildWhereClause(conditions);
670
+ const sql = `SELECT COUNT(*) as total FROM ${table} ${clause}`;
671
+ if (this.config.verbose) console.log(`SQL: ${sql}`, params);
672
+
673
+ const stmt = this.db.prepare(sql);
674
+ const result = stmt.get(...params);
675
+ const count = result ? result.total : 0;
676
+
677
+ return {
678
+ success: true,
679
+ operation: 'count',
680
+ count: count,
681
+ message: `Total de registros: ${count}`,
682
+ timestamp: new Date().toISOString()
683
+ };
684
+
685
+ } catch (error) {
686
+ return {
687
+ success: false,
688
+ operation: 'count',
689
+ error: error.message,
690
+ timestamp: new Date().toISOString()
691
+ };
692
+ }
693
+ }
694
+
695
+ /**
696
+ * Ejecuta una consulta SQL personalizada
697
+ * @param {string} sql - Consulta SQL
698
+ * @param {Array} [params=[]] - Parámetros para la consulta
699
+ * @returns {Object} - Respuesta JSON con resultado de la consulta
700
+ */
701
+ query(sql, params = []) {
702
+ try {
703
+ const stmt = this.db.prepare(sql);
704
+ const isSelect = sql.trim().toUpperCase().startsWith('SELECT');
705
+
706
+ if (this.config.verbose) console.log(`SQL: ${sql}`, params);
707
+
708
+ if (isSelect) {
709
+ const results = stmt.all(...params);
710
+ return {
711
+ success: true,
712
+ operation: 'query',
713
+ type: 'SELECT',
714
+ data: results,
715
+ count: results.length,
716
+ message: `Consulta ejecutada: ${results.length} resultados`,
717
+ timestamp: new Date().toISOString()
718
+ };
719
+ } else {
720
+ const result = stmt.run(...params);
721
+ return {
722
+ success: true,
723
+ operation: 'query',
724
+ type: 'EXECUTE',
725
+ changes: result.changes,
726
+ lastInsertRowid: result.lastInsertRowid,
727
+ message: `Consulta ejecutada: ${result.changes} cambios`,
728
+ timestamp: new Date().toISOString()
729
+ };
730
+ }
731
+
732
+ } catch (error) {
733
+ return {
734
+ success: false,
735
+ operation: 'query',
736
+ error: error.message,
737
+ timestamp: new Date().toISOString()
738
+ };
739
+ }
740
+ }
741
+
742
+ /**
743
+ * Ejecuta operaciones en una transacción
744
+ * @param {Function} operations - Función con operaciones a ejecutar
745
+ * @returns {Object} - Respuesta JSON con resultado de la transacción
746
+ */
747
+ transaction(operations) {
748
+ try {
749
+ this.db.exec('BEGIN TRANSACTION');
750
+ let result;
751
+
752
+ try {
753
+ result = operations(this);
754
+ this.db.exec('COMMIT');
755
+ return {
756
+ success: true,
757
+ operation: 'transaction',
758
+ data: result,
759
+ message: 'Transacción completada exitosamente',
760
+ timestamp: new Date().toISOString()
761
+ };
762
+ } catch (error) {
763
+ this.db.exec('ROLLBACK');
764
+ throw error;
765
+ }
766
+
767
+ } catch (error) {
768
+ return {
769
+ success: false,
770
+ operation: 'transaction',
771
+ error: error.message,
772
+ timestamp: new Date().toISOString()
773
+ };
774
+ }
775
+ }
776
+
777
+ /**
778
+ * Lista todas las tablas
779
+ * @returns {Object} - Respuesta JSON con nombres de las tablas
780
+ */
781
+ listTables() {
782
+ try {
783
+ const sql = `
784
+ SELECT name FROM sqlite_master
785
+ WHERE type='table'
786
+ AND name NOT LIKE 'sqlite_%'
787
+ AND name NOT LIKE '_easydb_%'
788
+ ORDER BY name
789
+ `;
790
+ const stmt = this.db.prepare(sql);
791
+ const tables = stmt.all().map(table => table.name);
792
+
793
+ return {
794
+ success: true,
795
+ operation: 'listTables',
796
+ tables: tables,
797
+ count: tables.length,
798
+ message: `Tablas encontradas: ${tables.length}`,
799
+ timestamp: new Date().toISOString()
800
+ };
801
+ } catch (error) {
802
+ return {
803
+ success: false,
804
+ operation: 'listTables',
805
+ error: error.message,
806
+ timestamp: new Date().toISOString()
807
+ };
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Obtiene información de una tabla
813
+ * @param {string} table - Nombre de la tabla
814
+ * @returns {Object} - Respuesta JSON con información de columnas
815
+ */
816
+ tableInfo(table) {
817
+ try {
818
+ if (!this.tableExists(table)) {
819
+ throw new Error(`La tabla '${table}' no existe`);
820
+ }
821
+
822
+ const sql = `PRAGMA table_info(${table})`;
823
+ const stmt = this.db.prepare(sql);
824
+ const columns = stmt.all();
825
+
826
+ return {
827
+ success: true,
828
+ operation: 'tableInfo',
829
+ table: table,
830
+ columns: columns,
831
+ count: columns.length,
832
+ message: `Columnas en ${table}: ${columns.length}`,
833
+ timestamp: new Date().toISOString()
834
+ };
835
+ } catch (error) {
836
+ return {
837
+ success: false,
838
+ operation: 'tableInfo',
839
+ error: error.message,
840
+ timestamp: new Date().toISOString()
841
+ };
842
+ }
843
+ }
844
+
845
+ /**
846
+ * Exporta toda la base de datos a JSON
847
+ * @returns {Object} - Respuesta JSON con datos exportados
848
+ */
849
+ exportToJSON() {
850
+ try {
851
+ const tablesResult = this.listTables();
852
+ if (!tablesResult.success) throw new Error(tablesResult.error);
853
+
854
+ const tables = tablesResult.tables;
855
+ const exportData = {};
856
+
857
+ for (const table of tables) {
858
+ const result = this.read(table);
859
+ if (result.success) {
860
+ exportData[table] = result.data;
861
+ }
862
+ }
863
+
864
+ return {
865
+ success: true,
866
+ operation: 'exportToJSON',
867
+ metadata: {
868
+ exported_at: new Date().toISOString(),
869
+ version: this._getMeta('version'),
870
+ tables: tables.length
871
+ },
872
+ data: exportData,
873
+ message: `Base de datos exportada: ${tables.length} tablas`,
874
+ timestamp: new Date().toISOString()
875
+ };
876
+ } catch (error) {
877
+ return {
878
+ success: false,
879
+ operation: 'exportToJSON',
880
+ error: error.message,
881
+ timestamp: new Date().toISOString()
882
+ };
883
+ }
884
+ }
885
+
886
+ /**
887
+ * Importa datos desde JSON
888
+ * @param {Object} data - Datos a importar
889
+ * @returns {Object} - Respuesta JSON con resultado de la importación
890
+ */
891
+ importFromJSON(data) {
892
+ try {
893
+ let imported = 0;
894
+ let created = 0;
895
+
896
+ for (const [table, records] of Object.entries(data)) {
897
+ if (!this.tableExists(table) && records.length > 0) {
898
+ this._autoCreateTable(table, records[0]);
899
+ created++;
900
+ }
901
+
902
+ for (const record of records) {
903
+ const result = this.create(table, record);
904
+ if (result.success) imported++;
905
+ }
906
+ }
907
+
908
+ return {
909
+ success: true,
910
+ operation: 'importFromJSON',
911
+ imported: imported,
912
+ tables_created: created,
913
+ message: `Importados ${imported} registros, creadas ${created} tablas`,
914
+ timestamp: new Date().toISOString()
915
+ };
916
+ } catch (error) {
917
+ return {
918
+ success: false,
919
+ operation: 'importFromJSON',
920
+ error: error.message,
921
+ timestamp: new Date().toISOString()
922
+ };
923
+ }
924
+ }
925
+
926
+ /**
927
+ * Crea un backup de la base de datos
928
+ * @param {string} [backupPath] - Ruta para el backup
929
+ * @returns {Object} - Respuesta JSON con resultado del backup
930
+ */
931
+ backup(backupPath) {
932
+ try {
933
+ const defaultPath = `backup_${Date.now()}.db`;
934
+ const targetPath = backupPath || defaultPath;
935
+
936
+ // Asegurar que el directorio del backup existe
937
+ ensureDirectoryExists(targetPath);
938
+
939
+ if (this.config.memory) {
940
+ const jsonData = this.exportToJSON();
941
+ fs.writeFileSync(
942
+ targetPath.replace('.db', '.json'),
943
+ JSON.stringify(jsonData, null, 2)
944
+ );
945
+
946
+ return {
947
+ success: true,
948
+ operation: 'backup',
949
+ path: path.resolve(targetPath.replace('.db', '.json')),
950
+ format: 'json',
951
+ message: 'Backup en memoria exportado a JSON',
952
+ timestamp: new Date().toISOString()
953
+ };
954
+ }
955
+
956
+ fs.copyFileSync(this.config.filename, targetPath);
957
+
958
+ return {
959
+ success: true,
960
+ operation: 'backup',
961
+ path: path.resolve(targetPath),
962
+ format: 'sqlite',
963
+ size: fs.statSync(targetPath).size,
964
+ message: 'Backup de base de datos creado',
965
+ timestamp: new Date().toISOString()
966
+ };
967
+
968
+ } catch (error) {
969
+ return {
970
+ success: false,
971
+ operation: 'backup',
972
+ error: error.message,
973
+ timestamp: new Date().toISOString()
974
+ };
975
+ }
976
+ }
977
+
978
+ /**
979
+ * Cierra la conexión a la base de datos
980
+ * @returns {Object} - Respuesta JSON de confirmación
981
+ */
982
+ close() {
983
+ try {
984
+ this.db.close();
985
+ return {
986
+ success: true,
987
+ operation: 'close',
988
+ message: 'Conexión cerrada exitosamente',
989
+ timestamp: new Date().toISOString()
990
+ };
991
+ } catch (error) {
992
+ return {
993
+ success: false,
994
+ operation: 'close',
995
+ error: error.message,
996
+ timestamp: new Date().toISOString()
997
+ };
998
+ }
999
+ }
1000
+
1001
+ /**
1002
+ * Obtiene estadísticas de la base de datos
1003
+ * @returns {Object} - Respuesta JSON con estadísticas
1004
+ */
1005
+ getStats() {
1006
+ try {
1007
+ const tablesResult = this.listTables();
1008
+ if (!tablesResult.success) throw new Error(tablesResult.error);
1009
+
1010
+ const tables = tablesResult.tables;
1011
+ const stats = {
1012
+ tables: tables.length,
1013
+ total_records: 0,
1014
+ table_stats: {}
1015
+ };
1016
+
1017
+ for (const table of tables) {
1018
+ const countResult = this.count(table);
1019
+ if (countResult.success) {
1020
+ stats.total_records += countResult.count;
1021
+ const infoResult = this.tableInfo(table);
1022
+ stats.table_stats[table] = {
1023
+ records: countResult.count,
1024
+ columns: infoResult.success ? infoResult.count : 0
1025
+ };
1026
+ }
1027
+ }
1028
+
1029
+ return {
1030
+ success: true,
1031
+ operation: 'getStats',
1032
+ stats: stats,
1033
+ metadata: {
1034
+ filename: path.resolve(this.config.filename),
1035
+ memory: this.config.memory,
1036
+ version: this._getMeta('version'),
1037
+ created: this._getMeta('created_at')
1038
+ },
1039
+ message: `Estadísticas: ${stats.tables} tablas, ${stats.total_records} registros`,
1040
+ timestamp: new Date().toISOString()
1041
+ };
1042
+ } catch (error) {
1043
+ return {
1044
+ success: false,
1045
+ operation: 'getStats',
1046
+ error: error.message,
1047
+ timestamp: new Date().toISOString()
1048
+ };
1049
+ }
1050
+ }
1051
+
1052
+ /**
1053
+ * Obtiene información del archivo de la base de datos
1054
+ * @returns {Object} - Información del archivo
1055
+ */
1056
+ getFileInfo() {
1057
+ try {
1058
+ if (this.config.memory) {
1059
+ return {
1060
+ success: true,
1061
+ is_memory: true,
1062
+ message: 'Base de datos en memoria',
1063
+ timestamp: new Date().toISOString()
1064
+ };
1065
+ }
1066
+
1067
+ const filePath = this.config.filename;
1068
+ const exists = fs.existsSync(filePath);
1069
+ const info = exists ? fs.statSync(filePath) : null;
1070
+
1071
+ return {
1072
+ success: true,
1073
+ path: path.resolve(filePath),
1074
+ exists: exists,
1075
+ size: exists ? info.size : 0,
1076
+ created: exists ? info.birthtime : null,
1077
+ modified: exists ? info.mtime : null,
1078
+ is_memory: false,
1079
+ timestamp: new Date().toISOString()
1080
+ };
1081
+ } catch (error) {
1082
+ return {
1083
+ success: false,
1084
+ error: error.message,
1085
+ timestamp: new Date().toISOString()
1086
+ };
1087
+ }
1088
+ }
1089
+ }
1090
+
1091
+ // ======================
1092
+ // INSTANCIA GLOBAL
1093
+ // ======================
1094
+
1095
+ /**
1096
+ * Crea una instancia de EasyDB
1097
+ * @param {string|Object} config - Configuración
1098
+ * @returns {EasyDB} - Instancia de EasyDB
1099
+ */
1100
+ function createInstance(config = DEFAULT_DB_PATH) {
1101
+ return new EasyDB(config);
1102
+ }
1103
+
1104
+ // Instancia global predeterminada
1105
+ const defaultInstance = createInstance();
1106
+
1107
+ // API global para acceso rápido con respuestas JSON
1108
+ const db = {
1109
+ // Operaciones básicas
1110
+ create: (table, data) => defaultInstance.create(table, data),
1111
+ read: (table, conditions, options) => defaultInstance.read(table, conditions, options),
1112
+ readOne: (table, conditions) => defaultInstance.readOne(table, conditions),
1113
+ update: (table, conditions, data) => defaultInstance.update(table, conditions, data),
1114
+ delete: (table, conditions) => defaultInstance.delete(table, conditions),
1115
+
1116
+ // Operaciones avanzadas
1117
+ search: (table, fields, searchTerm) => defaultInstance.search(table, fields, searchTerm),
1118
+ count: (table, conditions) => defaultInstance.count(table, conditions),
1119
+ query: (sql, params) => defaultInstance.query(sql, params),
1120
+ transaction: (operations) => defaultInstance.transaction(operations),
1121
+
1122
+ // Nueva funcionalidad para agregar columnas
1123
+ addColumn: (tableName, columnName, columnType, defaultValue) =>
1124
+ defaultInstance.addColumn(tableName, columnName, columnType, defaultValue),
1125
+ addColumns: (tableName, columns) => defaultInstance.addColumns(tableName, columns),
1126
+
1127
+ // Metadatos y utilidades
1128
+ listTables: () => defaultInstance.listTables(),
1129
+ tableInfo: (table) => defaultInstance.tableInfo(table),
1130
+ exportToJSON: () => defaultInstance.exportToJSON(),
1131
+ importFromJSON: (data) => defaultInstance.importFromJSON(data),
1132
+ backup: (backupPath) => defaultInstance.backup(backupPath),
1133
+ close: () => defaultInstance.close(),
1134
+ getStats: () => defaultInstance.getStats(),
1135
+ getFileInfo: () => defaultInstance.getFileInfo(),
1136
+
1137
+ // Información del sistema
1138
+ getVersion: () => defaultInstance._getMeta('version'),
1139
+
1140
+ // Crear nueva instancia
1141
+ createInstance: (config) => createInstance(config),
1142
+
1143
+ // Variables globales (exportadas para uso externo)
1144
+ DB_PATH: DEFAULT_DB_PATH,
1145
+ ensureDirectoryExists: ensureDirectoryExists,
1146
+
1147
+ // Acceso directo a la instancia (opcional)
1148
+ _instance: defaultInstance
1149
+ };
1150
+
1151
+ module.exports = db;
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "andres-db",
3
+ "version": "2.2.0",
4
+ "description": "Interfaz JavaScript simplificada para bases de datos SQLite con respuestas JSON estructuradas",
5
+ "main": "index.js",
6
+ "engines": {
7
+ "node": ">=14.0.0"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "keywords": [
13
+ "sqlite",
14
+ "database",
15
+ "json",
16
+ "crud",
17
+ "nodejs",
18
+ "andres-db",
19
+ "simpledb",
20
+ "db"
21
+ ],
22
+ "author": "ANDRES CARVAJAL OROZCO <https://andres-carvajal.vercel.app/>",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "better-sqlite3": "^8.5.0"
26
+ },
27
+ "files": [
28
+ "index.js",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }