voltjs-framework 1.0.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/LICENSE +21 -0
- package/README.md +1265 -0
- package/bin/volt.js +139 -0
- package/package.json +56 -0
- package/src/api/graphql.js +399 -0
- package/src/api/rest.js +204 -0
- package/src/api/websocket.js +285 -0
- package/src/cli/build.js +111 -0
- package/src/cli/create.js +371 -0
- package/src/cli/db.js +106 -0
- package/src/cli/dev.js +114 -0
- package/src/cli/generate.js +278 -0
- package/src/cli/lint.js +172 -0
- package/src/cli/routes.js +118 -0
- package/src/cli/start.js +42 -0
- package/src/cli/test.js +138 -0
- package/src/core/app.js +701 -0
- package/src/core/config.js +232 -0
- package/src/core/middleware.js +133 -0
- package/src/core/plugins.js +88 -0
- package/src/core/react-renderer.js +244 -0
- package/src/core/renderer.js +337 -0
- package/src/core/router.js +183 -0
- package/src/database/index.js +461 -0
- package/src/database/migration.js +192 -0
- package/src/database/model.js +285 -0
- package/src/database/query.js +394 -0
- package/src/database/seeder.js +89 -0
- package/src/index.js +156 -0
- package/src/security/auth.js +425 -0
- package/src/security/cors.js +80 -0
- package/src/security/csrf.js +125 -0
- package/src/security/encryption.js +110 -0
- package/src/security/helmet.js +103 -0
- package/src/security/index.js +75 -0
- package/src/security/rateLimit.js +119 -0
- package/src/security/sanitizer.js +113 -0
- package/src/security/xss.js +110 -0
- package/src/ui/component.js +224 -0
- package/src/ui/reactive.js +503 -0
- package/src/ui/template.js +448 -0
- package/src/utils/cache.js +216 -0
- package/src/utils/collection.js +772 -0
- package/src/utils/cron.js +213 -0
- package/src/utils/date.js +223 -0
- package/src/utils/events.js +181 -0
- package/src/utils/excel.js +482 -0
- package/src/utils/form.js +547 -0
- package/src/utils/hash.js +121 -0
- package/src/utils/http.js +461 -0
- package/src/utils/logger.js +186 -0
- package/src/utils/mail.js +347 -0
- package/src/utils/paginator.js +179 -0
- package/src/utils/pdf.js +417 -0
- package/src/utils/queue.js +199 -0
- package/src/utils/schema.js +985 -0
- package/src/utils/sms.js +243 -0
- package/src/utils/storage.js +348 -0
- package/src/utils/string.js +236 -0
- package/src/utils/validation.js +318 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Database Module
|
|
3
|
+
*
|
|
4
|
+
* Universal database abstraction supporting SQLite, MySQL, PostgreSQL.
|
|
5
|
+
* Includes ORM, query builder, migrations, and seeding.
|
|
6
|
+
* Uses native Node.js features — zero dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { Model } = require('./model');
|
|
12
|
+
const { QueryBuilder } = require('./query');
|
|
13
|
+
const { Migration } = require('./migration');
|
|
14
|
+
const { Seeder } = require('./seeder');
|
|
15
|
+
|
|
16
|
+
class Database {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this._config = {
|
|
19
|
+
driver: config.driver || 'memory',
|
|
20
|
+
host: config.host || 'localhost',
|
|
21
|
+
port: config.port || null,
|
|
22
|
+
name: config.name || 'volt_db',
|
|
23
|
+
user: config.user || null,
|
|
24
|
+
password: config.password || null,
|
|
25
|
+
filename: config.filename || ':memory:',
|
|
26
|
+
pool: config.pool || { min: 2, max: 10 },
|
|
27
|
+
...config,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
this._connection = null;
|
|
31
|
+
this._tables = new Map(); // In-memory store
|
|
32
|
+
this._connected = false;
|
|
33
|
+
this._queryLog = [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Connect to database */
|
|
37
|
+
async connect() {
|
|
38
|
+
const driver = this._config.driver;
|
|
39
|
+
|
|
40
|
+
switch (driver) {
|
|
41
|
+
case 'memory':
|
|
42
|
+
this._connection = this._tables;
|
|
43
|
+
this._connected = true;
|
|
44
|
+
break;
|
|
45
|
+
case 'sqlite':
|
|
46
|
+
await this._connectSQLite();
|
|
47
|
+
break;
|
|
48
|
+
case 'mysql':
|
|
49
|
+
await this._connectMySQL();
|
|
50
|
+
break;
|
|
51
|
+
case 'postgres':
|
|
52
|
+
case 'postgresql':
|
|
53
|
+
await this._connectPostgres();
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
// Default to in-memory
|
|
57
|
+
this._connection = this._tables;
|
|
58
|
+
this._connected = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Get a query builder for a table */
|
|
65
|
+
table(tableName) {
|
|
66
|
+
return new QueryBuilder(tableName, this);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Create a model class bound to this database */
|
|
70
|
+
model(tableName, schema = {}) {
|
|
71
|
+
return Model.create(tableName, schema, this);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Run a raw query */
|
|
75
|
+
async query(sql, params = []) {
|
|
76
|
+
this._logQuery(sql, params);
|
|
77
|
+
|
|
78
|
+
if (this._config.driver === 'memory') {
|
|
79
|
+
return this._memoryQuery(sql, params);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// For real database drivers
|
|
83
|
+
if (this._connection && typeof this._connection.query === 'function') {
|
|
84
|
+
return this._connection.query(sql, params);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error('Database not connected');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Create table (in-memory) */
|
|
91
|
+
async createTable(tableName, schema) {
|
|
92
|
+
if (this._config.driver === 'memory') {
|
|
93
|
+
this._tables.set(tableName, {
|
|
94
|
+
schema,
|
|
95
|
+
data: [],
|
|
96
|
+
autoIncrement: 1,
|
|
97
|
+
});
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Generate CREATE TABLE SQL
|
|
102
|
+
const columns = Object.entries(schema).map(([name, def]) => {
|
|
103
|
+
return this._columnToSQL(name, def);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (${columns.join(', ')})`;
|
|
107
|
+
return this.query(sql);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Drop table */
|
|
111
|
+
async dropTable(tableName) {
|
|
112
|
+
if (this._config.driver === 'memory') {
|
|
113
|
+
this._tables.delete(tableName);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return this.query(`DROP TABLE IF EXISTS ${tableName}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Check if table exists */
|
|
120
|
+
async tableExists(tableName) {
|
|
121
|
+
if (this._config.driver === 'memory') {
|
|
122
|
+
return this._tables.has(tableName);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
await this.query(`SELECT 1 FROM ${tableName} LIMIT 1`);
|
|
126
|
+
return true;
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Run migrations */
|
|
133
|
+
async migrate(migrationsDir) {
|
|
134
|
+
const migration = new Migration(this, migrationsDir);
|
|
135
|
+
return migration.run();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Rollback migrations */
|
|
139
|
+
async rollback(migrationsDir) {
|
|
140
|
+
const migration = new Migration(this, migrationsDir);
|
|
141
|
+
return migration.rollback();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Run seeders */
|
|
145
|
+
async seed(seedersDir) {
|
|
146
|
+
const seeder = new Seeder(this, seedersDir);
|
|
147
|
+
return seeder.run();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Close connection */
|
|
151
|
+
async close() {
|
|
152
|
+
if (this._connection && typeof this._connection.end === 'function') {
|
|
153
|
+
await this._connection.end();
|
|
154
|
+
}
|
|
155
|
+
this._connected = false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Get query log */
|
|
159
|
+
getQueryLog() {
|
|
160
|
+
return [...this._queryLog];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Clear query log */
|
|
164
|
+
clearQueryLog() {
|
|
165
|
+
this._queryLog = [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Transaction support */
|
|
169
|
+
async transaction(callback) {
|
|
170
|
+
if (this._config.driver === 'memory') {
|
|
171
|
+
// Simple transaction for in-memory: snapshot and restore on error
|
|
172
|
+
const snapshot = new Map();
|
|
173
|
+
for (const [name, table] of this._tables) {
|
|
174
|
+
snapshot.set(name, { ...table, data: [...table.data.map(r => ({ ...r }))] });
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const result = await callback(this);
|
|
178
|
+
return result;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// Rollback
|
|
181
|
+
for (const [name, table] of snapshot) {
|
|
182
|
+
this._tables.set(name, table);
|
|
183
|
+
}
|
|
184
|
+
throw err;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await this.query('BEGIN');
|
|
189
|
+
try {
|
|
190
|
+
const result = await callback(this);
|
|
191
|
+
await this.query('COMMIT');
|
|
192
|
+
return result;
|
|
193
|
+
} catch (err) {
|
|
194
|
+
await this.query('ROLLBACK');
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ===== IN-MEMORY DATABASE ENGINE =====
|
|
200
|
+
|
|
201
|
+
_memoryQuery(sql, params) {
|
|
202
|
+
// Basic SQL parser for in-memory database
|
|
203
|
+
const normalized = sql.trim();
|
|
204
|
+
|
|
205
|
+
if (normalized.toUpperCase().startsWith('INSERT')) {
|
|
206
|
+
return this._memoryInsert(normalized, params);
|
|
207
|
+
}
|
|
208
|
+
if (normalized.toUpperCase().startsWith('SELECT')) {
|
|
209
|
+
return this._memorySelect(normalized, params);
|
|
210
|
+
}
|
|
211
|
+
if (normalized.toUpperCase().startsWith('UPDATE')) {
|
|
212
|
+
return this._memoryUpdate(normalized, params);
|
|
213
|
+
}
|
|
214
|
+
if (normalized.toUpperCase().startsWith('DELETE')) {
|
|
215
|
+
return this._memoryDelete(normalized, params);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { success: true };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_memoryInsert(sql, params) {
|
|
222
|
+
const match = sql.match(/INSERT\s+INTO\s+(\w+)\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)/i);
|
|
223
|
+
if (!match) throw new Error('Invalid INSERT syntax');
|
|
224
|
+
|
|
225
|
+
const tableName = match[1];
|
|
226
|
+
const table = this._tables.get(tableName);
|
|
227
|
+
if (!table) throw new Error(`Table "${tableName}" not found`);
|
|
228
|
+
|
|
229
|
+
const columns = match[2].split(',').map(c => c.trim());
|
|
230
|
+
const values = match[3].split(',').map((v, i) => {
|
|
231
|
+
if (v.trim() === '?') return params[i];
|
|
232
|
+
return v.trim().replace(/^'|'$/g, '');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const row = { id: table.autoIncrement++ };
|
|
236
|
+
columns.forEach((col, i) => {
|
|
237
|
+
row[col] = values[i];
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Set timestamps
|
|
241
|
+
if (table.schema.created_at || table.schema.createdAt) {
|
|
242
|
+
row.created_at = row.created_at || new Date().toISOString();
|
|
243
|
+
}
|
|
244
|
+
if (table.schema.updated_at || table.schema.updatedAt) {
|
|
245
|
+
row.updated_at = new Date().toISOString();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
table.data.push(row);
|
|
249
|
+
return { insertId: row.id, affectedRows: 1 };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_memorySelect(sql, params) {
|
|
253
|
+
const match = sql.match(/SELECT\s+(.+?)\s+FROM\s+(\w+)(?:\s+WHERE\s+(.+?))?(?:\s+ORDER\s+BY\s+(.+?))?(?:\s+LIMIT\s+(\d+))?(?:\s+OFFSET\s+(\d+))?$/i);
|
|
254
|
+
if (!match) return [];
|
|
255
|
+
|
|
256
|
+
const tableName = match[2];
|
|
257
|
+
const table = this._tables.get(tableName);
|
|
258
|
+
if (!table) return [];
|
|
259
|
+
|
|
260
|
+
let results = [...table.data];
|
|
261
|
+
|
|
262
|
+
// Apply WHERE
|
|
263
|
+
if (match[3]) {
|
|
264
|
+
results = this._applyWhere(results, match[3], params);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Apply ORDER BY
|
|
268
|
+
if (match[4]) {
|
|
269
|
+
const [field, direction] = match[4].trim().split(/\s+/);
|
|
270
|
+
results.sort((a, b) => {
|
|
271
|
+
if (direction?.toUpperCase() === 'DESC') return a[field] > b[field] ? -1 : 1;
|
|
272
|
+
return a[field] > b[field] ? 1 : -1;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Apply LIMIT & OFFSET
|
|
277
|
+
const offset = match[6] ? parseInt(match[6]) : 0;
|
|
278
|
+
const limit = match[5] ? parseInt(match[5]) : results.length;
|
|
279
|
+
results = results.slice(offset, offset + limit);
|
|
280
|
+
|
|
281
|
+
// Apply column selection
|
|
282
|
+
if (match[1].trim() !== '*') {
|
|
283
|
+
const columns = match[1].split(',').map(c => c.trim());
|
|
284
|
+
results = results.map(row => {
|
|
285
|
+
const filtered = {};
|
|
286
|
+
columns.forEach(col => { filtered[col] = row[col]; });
|
|
287
|
+
return filtered;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return results;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_memoryUpdate(sql, params) {
|
|
295
|
+
const match = sql.match(/UPDATE\s+(\w+)\s+SET\s+(.+?)(?:\s+WHERE\s+(.+))?$/i);
|
|
296
|
+
if (!match) throw new Error('Invalid UPDATE syntax');
|
|
297
|
+
|
|
298
|
+
const tableName = match[1];
|
|
299
|
+
const table = this._tables.get(tableName);
|
|
300
|
+
if (!table) throw new Error(`Table "${tableName}" not found`);
|
|
301
|
+
|
|
302
|
+
const setParts = match[2].split(',').map(s => s.trim());
|
|
303
|
+
let paramIndex = 0;
|
|
304
|
+
const updates = {};
|
|
305
|
+
for (const part of setParts) {
|
|
306
|
+
const [col, val] = part.split('=').map(s => s.trim());
|
|
307
|
+
updates[col] = val === '?' ? params[paramIndex++] : val.replace(/^'|'$/g, '');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let affected = 0;
|
|
311
|
+
for (const row of table.data) {
|
|
312
|
+
if (!match[3] || this._matchesWhere(row, match[3], params.slice(paramIndex))) {
|
|
313
|
+
Object.assign(row, updates);
|
|
314
|
+
row.updated_at = new Date().toISOString();
|
|
315
|
+
affected++;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { affectedRows: affected };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_memoryDelete(sql, params) {
|
|
323
|
+
const match = sql.match(/DELETE\s+FROM\s+(\w+)(?:\s+WHERE\s+(.+))?$/i);
|
|
324
|
+
if (!match) throw new Error('Invalid DELETE syntax');
|
|
325
|
+
|
|
326
|
+
const tableName = match[1];
|
|
327
|
+
const table = this._tables.get(tableName);
|
|
328
|
+
if (!table) throw new Error(`Table "${tableName}" not found`);
|
|
329
|
+
|
|
330
|
+
const before = table.data.length;
|
|
331
|
+
if (match[2]) {
|
|
332
|
+
table.data = table.data.filter(row => !this._matchesWhere(row, match[2], params));
|
|
333
|
+
} else {
|
|
334
|
+
table.data = [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { affectedRows: before - table.data.length };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_applyWhere(data, whereClause, params) {
|
|
341
|
+
return data.filter(row => this._matchesWhere(row, whereClause, params));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
_matchesWhere(row, whereClause, params) {
|
|
345
|
+
// Simple WHERE parser: supports AND, OR, =, !=, >, <, >=, <=, LIKE
|
|
346
|
+
const conditions = whereClause.split(/\s+AND\s+/i);
|
|
347
|
+
let paramIdx = 0;
|
|
348
|
+
|
|
349
|
+
return conditions.every(condition => {
|
|
350
|
+
const match = condition.match(/(\w+)\s*(=|!=|<>|>=|<=|>|<|LIKE)\s*(.+)/i);
|
|
351
|
+
if (!match) return true;
|
|
352
|
+
|
|
353
|
+
const field = match[1].trim();
|
|
354
|
+
const op = match[2].toUpperCase();
|
|
355
|
+
let value = match[3].trim();
|
|
356
|
+
|
|
357
|
+
if (value === '?') {
|
|
358
|
+
value = params[paramIdx++];
|
|
359
|
+
} else {
|
|
360
|
+
value = value.replace(/^'|'$/g, '');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const rowVal = row[field];
|
|
364
|
+
|
|
365
|
+
switch (op) {
|
|
366
|
+
case '=': return rowVal == value;
|
|
367
|
+
case '!=': case '<>': return rowVal != value;
|
|
368
|
+
case '>': return rowVal > value;
|
|
369
|
+
case '<': return rowVal < value;
|
|
370
|
+
case '>=': return rowVal >= value;
|
|
371
|
+
case '<=': return rowVal <= value;
|
|
372
|
+
case 'LIKE':
|
|
373
|
+
const pattern = value.replace(/%/g, '.*').replace(/_/g, '.');
|
|
374
|
+
return new RegExp(`^${pattern}$`, 'i').test(String(rowVal));
|
|
375
|
+
default: return true;
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_columnToSQL(name, def) {
|
|
381
|
+
if (typeof def === 'string') {
|
|
382
|
+
return `${name} ${def}`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let sql = `${name} ${def.type || 'TEXT'}`;
|
|
386
|
+
if (def.primary) sql += ' PRIMARY KEY';
|
|
387
|
+
if (def.autoIncrement) sql += ' AUTOINCREMENT';
|
|
388
|
+
if (def.notNull || def.required) sql += ' NOT NULL';
|
|
389
|
+
if (def.unique) sql += ' UNIQUE';
|
|
390
|
+
if (def.default !== undefined) {
|
|
391
|
+
sql += ` DEFAULT ${typeof def.default === 'string' ? `'${def.default}'` : def.default}`;
|
|
392
|
+
}
|
|
393
|
+
if (def.references) {
|
|
394
|
+
sql += ` REFERENCES ${def.references.table}(${def.references.column || 'id'})`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return sql;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_logQuery(sql, params) {
|
|
401
|
+
if (this._queryLog.length < 1000) { // Prevent memory leak
|
|
402
|
+
this._queryLog.push({ sql, params, timestamp: new Date() });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ===== DRIVER CONNECTIONS =====
|
|
407
|
+
|
|
408
|
+
async _connectSQLite() {
|
|
409
|
+
try {
|
|
410
|
+
// Try to use better-sqlite3 or sqlite3
|
|
411
|
+
const sqlite3 = require('better-sqlite3');
|
|
412
|
+
this._connection = sqlite3(this._config.filename);
|
|
413
|
+
this._connected = true;
|
|
414
|
+
} catch {
|
|
415
|
+
console.warn('\x1b[33mSQLite driver not found. Install: npm install better-sqlite3\x1b[0m');
|
|
416
|
+
console.warn('\x1b[33mFalling back to in-memory database.\x1b[0m');
|
|
417
|
+
this._connection = this._tables;
|
|
418
|
+
this._connected = true;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async _connectMySQL() {
|
|
423
|
+
try {
|
|
424
|
+
const mysql = require('mysql2/promise');
|
|
425
|
+
this._connection = await mysql.createPool({
|
|
426
|
+
host: this._config.host,
|
|
427
|
+
port: this._config.port || 3306,
|
|
428
|
+
database: this._config.name,
|
|
429
|
+
user: this._config.user,
|
|
430
|
+
password: this._config.password,
|
|
431
|
+
...this._config.pool,
|
|
432
|
+
});
|
|
433
|
+
this._connected = true;
|
|
434
|
+
} catch {
|
|
435
|
+
console.warn('\x1b[33mMySQL driver not found. Install: npm install mysql2\x1b[0m');
|
|
436
|
+
this._connection = this._tables;
|
|
437
|
+
this._connected = true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async _connectPostgres() {
|
|
442
|
+
try {
|
|
443
|
+
const { Pool } = require('pg');
|
|
444
|
+
this._connection = new Pool({
|
|
445
|
+
host: this._config.host,
|
|
446
|
+
port: this._config.port || 5432,
|
|
447
|
+
database: this._config.name,
|
|
448
|
+
user: this._config.user,
|
|
449
|
+
password: this._config.password,
|
|
450
|
+
...this._config.pool,
|
|
451
|
+
});
|
|
452
|
+
this._connected = true;
|
|
453
|
+
} catch {
|
|
454
|
+
console.warn('\x1b[33mPostgreSQL driver not found. Install: npm install pg\x1b[0m');
|
|
455
|
+
this._connection = this._tables;
|
|
456
|
+
this._connected = true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = { Database };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Migration System
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* // migrations/001_create_users.js
|
|
6
|
+
* module.exports = {
|
|
7
|
+
* up: async (db) => {
|
|
8
|
+
* await db.createTable('users', {
|
|
9
|
+
* id: { type: 'INTEGER', primary: true, autoIncrement: true },
|
|
10
|
+
* name: { type: 'TEXT', required: true },
|
|
11
|
+
* email: { type: 'TEXT', unique: true },
|
|
12
|
+
* created_at: 'DATETIME',
|
|
13
|
+
* updated_at: 'DATETIME',
|
|
14
|
+
* });
|
|
15
|
+
* },
|
|
16
|
+
* down: async (db) => {
|
|
17
|
+
* await db.dropTable('users');
|
|
18
|
+
* }
|
|
19
|
+
* };
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
class Migration {
|
|
28
|
+
constructor(db, migrationsDir) {
|
|
29
|
+
this._db = db;
|
|
30
|
+
this._dir = migrationsDir || path.join(process.cwd(), 'migrations');
|
|
31
|
+
this._tableName = '_volt_migrations';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Run all pending migrations */
|
|
35
|
+
async run() {
|
|
36
|
+
await this._ensureMigrationTable();
|
|
37
|
+
const completed = await this._getCompleted();
|
|
38
|
+
const files = this._getMigrationFiles();
|
|
39
|
+
const pending = files.filter(f => !completed.includes(f));
|
|
40
|
+
|
|
41
|
+
if (pending.length === 0) {
|
|
42
|
+
console.log('\x1b[33m No pending migrations.\x1b[0m');
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const results = [];
|
|
47
|
+
for (const file of pending) {
|
|
48
|
+
try {
|
|
49
|
+
const migration = require(path.join(this._dir, file));
|
|
50
|
+
console.log(`\x1b[36m Migrating: ${file}\x1b[0m`);
|
|
51
|
+
await migration.up(this._db);
|
|
52
|
+
await this._markCompleted(file);
|
|
53
|
+
console.log(`\x1b[32m ✓ Migrated: ${file}\x1b[0m`);
|
|
54
|
+
results.push({ file, status: 'success' });
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(`\x1b[31m ✗ Migration failed: ${file} — ${err.message}\x1b[0m`);
|
|
57
|
+
results.push({ file, status: 'error', error: err.message });
|
|
58
|
+
break; // Stop on first failure
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Rollback last batch of migrations */
|
|
66
|
+
async rollback() {
|
|
67
|
+
await this._ensureMigrationTable();
|
|
68
|
+
const completed = await this._getCompleted();
|
|
69
|
+
|
|
70
|
+
if (completed.length === 0) {
|
|
71
|
+
console.log('\x1b[33m Nothing to rollback.\x1b[0m');
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Rollback last migration
|
|
76
|
+
const lastFile = completed[completed.length - 1];
|
|
77
|
+
try {
|
|
78
|
+
const migration = require(path.join(this._dir, lastFile));
|
|
79
|
+
if (migration.down) {
|
|
80
|
+
console.log(`\x1b[36m Rolling back: ${lastFile}\x1b[0m`);
|
|
81
|
+
await migration.down(this._db);
|
|
82
|
+
await this._markRolledBack(lastFile);
|
|
83
|
+
console.log(`\x1b[32m ✓ Rolled back: ${lastFile}\x1b[0m`);
|
|
84
|
+
}
|
|
85
|
+
return [{ file: lastFile, status: 'rolled_back' }];
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error(`\x1b[31m ✗ Rollback failed: ${lastFile} — ${err.message}\x1b[0m`);
|
|
88
|
+
return [{ file: lastFile, status: 'error', error: err.message }];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Get migration files sorted by name */
|
|
93
|
+
_getMigrationFiles() {
|
|
94
|
+
try {
|
|
95
|
+
if (!fs.existsSync(this._dir)) {
|
|
96
|
+
fs.mkdirSync(this._dir, { recursive: true });
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return fs.readdirSync(this._dir)
|
|
100
|
+
.filter(f => f.endsWith('.js'))
|
|
101
|
+
.sort();
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Ensure migration tracking table exists */
|
|
108
|
+
async _ensureMigrationTable() {
|
|
109
|
+
if (this._db._config.driver === 'memory') {
|
|
110
|
+
if (!this._db._tables.has(this._tableName)) {
|
|
111
|
+
this._db._tables.set(this._tableName, {
|
|
112
|
+
schema: { id: 'INTEGER', name: 'TEXT', ran_at: 'DATETIME' },
|
|
113
|
+
data: [],
|
|
114
|
+
autoIncrement: 1,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
await this._db.query(`CREATE TABLE IF NOT EXISTS ${this._tableName} (
|
|
119
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
120
|
+
name TEXT NOT NULL,
|
|
121
|
+
ran_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
122
|
+
)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Get list of completed migrations */
|
|
127
|
+
async _getCompleted() {
|
|
128
|
+
if (this._db._config.driver === 'memory') {
|
|
129
|
+
const table = this._db._tables.get(this._tableName);
|
|
130
|
+
return table ? table.data.map(r => r.name) : [];
|
|
131
|
+
}
|
|
132
|
+
const results = await this._db.query(`SELECT name FROM ${this._tableName} ORDER BY id`);
|
|
133
|
+
return results.map(r => r.name);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Mark a migration as completed */
|
|
137
|
+
async _markCompleted(name) {
|
|
138
|
+
if (this._db._config.driver === 'memory') {
|
|
139
|
+
const table = this._db._tables.get(this._tableName);
|
|
140
|
+
table.data.push({ id: table.autoIncrement++, name, ran_at: new Date().toISOString() });
|
|
141
|
+
} else {
|
|
142
|
+
await this._db.query(`INSERT INTO ${this._tableName} (name) VALUES (?)`, [name]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Mark a migration as rolled back */
|
|
147
|
+
async _markRolledBack(name) {
|
|
148
|
+
if (this._db._config.driver === 'memory') {
|
|
149
|
+
const table = this._db._tables.get(this._tableName);
|
|
150
|
+
table.data = table.data.filter(r => r.name !== name);
|
|
151
|
+
} else {
|
|
152
|
+
await this._db.query(`DELETE FROM ${this._tableName} WHERE name = ?`, [name]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Generate a new migration file */
|
|
157
|
+
static generate(name, migrationsDir) {
|
|
158
|
+
const dir = migrationsDir || path.join(process.cwd(), 'migrations');
|
|
159
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
160
|
+
|
|
161
|
+
const timestamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
162
|
+
const filename = `${timestamp}_${name}.js`;
|
|
163
|
+
const filepath = path.join(dir, filename);
|
|
164
|
+
|
|
165
|
+
const template = `/**
|
|
166
|
+
* Migration: ${name}
|
|
167
|
+
* Created: ${new Date().toISOString()}
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
async up(db) {
|
|
172
|
+
await db.createTable('${name}', {
|
|
173
|
+
id: { type: 'INTEGER', primary: true, autoIncrement: true },
|
|
174
|
+
// Add your columns here
|
|
175
|
+
created_at: 'DATETIME',
|
|
176
|
+
updated_at: 'DATETIME',
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async down(db) {
|
|
181
|
+
await db.dropTable('${name}');
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
fs.writeFileSync(filepath, template);
|
|
187
|
+
console.log(`\x1b[32m ✓ Created migration: ${filename}\x1b[0m`);
|
|
188
|
+
return filepath;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = { Migration };
|