Spaces:
Sleeping
Sleeping
File size: 6,637 Bytes
f9dc8ac | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | // api/db.js β NT DB Core Engine
import { readFile, writeFile } from 'fs/promises';
import { existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import crypto from 'crypto';
const DB_DIR = process.env.DB_DIR || join(process.cwd(), 'db');
if (!existsSync(DB_DIR)) mkdirSync(DB_DIR, { recursive: true });
// ββ File helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
async function readTable(table) {
try { return JSON.parse(await readFile(join(DB_DIR, `${table}.json`), 'utf8')); }
catch { return []; }
}
async function writeTable(table, rows) {
await writeFile(join(DB_DIR, `${table}.json`), JSON.stringify(rows, null, 2), 'utf8');
}
export async function readSchema() {
try { return JSON.parse(await readFile(join(DB_DIR, '_schema.json'), 'utf8')); }
catch { return { tables: {} }; }
}
export async function writeSchema(schema) {
await writeFile(join(DB_DIR, '_schema.json'), JSON.stringify(schema, null, 2), 'utf8');
}
// ββ Query helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function applyFilters(rows, filters) {
return rows.filter(row => filters.every(({ col, op, val }) => {
const cell = row[col];
switch (op) {
case 'eq': return String(cell) == String(val);
case 'neq': return String(cell) != String(val);
case 'gt': return Number(cell) > Number(val);
case 'gte': return Number(cell) >= Number(val);
case 'lt': return Number(cell) < Number(val);
case 'lte': return Number(cell) <= Number(val);
case 'like': return String(cell).toLowerCase().includes(String(val).replace(/%/g, '').toLowerCase());
case 'in': return Array.isArray(val) && val.includes(cell);
default: return true;
}
}));
}
function applySelect(rows, columns) {
if (!columns || columns === '*') return rows;
const cols = columns.split(',').map(c => c.trim());
return rows.map(row => Object.fromEntries(cols.filter(c => c in row).map(c => [c, row[c]])));
}
function applyOrder(rows, col, dir = 'asc') {
return [...rows].sort((a, b) => {
if (a[col] < b[col]) return dir === 'asc' ? -1 : 1;
if (a[col] > b[col]) return dir === 'asc' ? 1 : -1;
return 0;
});
}
function autoFill(record, tableDef) {
const out = { ...record };
for (const [col, def] of Object.entries(tableDef?.columns || {})) {
if (def.auto) {
if (def.type === 'uuid' && def.primaryKey) out[col] = out[col] || crypto.randomUUID();
if (def.type === 'timestamp') out[col] = out[col] || new Date().toISOString();
}
}
return out;
}
function validate(record, tableDef) {
const errors = [];
for (const [col, def] of Object.entries(tableDef?.columns || {})) {
if (def.required && !def.auto && !(col in record)) errors.push(`Missing required field: ${col}`);
}
return errors;
}
// ββ Query Builder βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class QueryBuilder {
constructor(table) {
this._table = table;
this._filters = [];
this._select = '*';
this._orderCol = null;
this._orderDir = 'asc';
this._limit = null;
this._offset = 0;
this._op = 'select';
this._payload = null;
}
select(cols = '*') { this._select = cols; return this; }
insert(data) { this._op = 'insert'; this._payload = data; return this; }
update(data) { this._op = 'update'; this._payload = data; return this; }
delete() { this._op = 'delete'; return this; }
eq(col, val) { this._filters.push({ col, op: 'eq', val }); return this; }
neq(col, val) { this._filters.push({ col, op: 'neq', val }); return this; }
gt(col, val) { this._filters.push({ col, op: 'gt', val }); return this; }
gte(col, val) { this._filters.push({ col, op: 'gte', val }); return this; }
lt(col, val) { this._filters.push({ col, op: 'lt', val }); return this; }
lte(col, val) { this._filters.push({ col, op: 'lte', val }); return this; }
like(col, val) { this._filters.push({ col, op: 'like', val }); return this; }
in(col, val) { this._filters.push({ col, op: 'in', val }); return this; }
order(col, dir = 'asc') { this._orderCol = col; this._orderDir = dir; return this; }
limit(n) { this._limit = n; return this; }
offset(n) { this._offset = n; return this; }
async execute() {
const schema = await readSchema();
const tableDef = schema.tables?.[this._table];
// INSERT
if (this._op === 'insert') {
const rows = await readTable(this._table);
const records = Array.isArray(this._payload) ? this._payload : [this._payload];
const inserted = [];
for (const rec of records) {
const errors = validate(rec, tableDef);
if (errors.length) throw new Error(errors.join(', '));
const filled = autoFill(rec, tableDef);
rows.push(filled);
inserted.push(filled);
}
await writeTable(this._table, rows);
return { data: inserted, error: null };
}
// UPDATE
if (this._op === 'update') {
const all = await readTable(this._table);
const updated = all.map(row => {
if (applyFilters([row], this._filters).length === 0) return row;
return { ...row, ...this._payload, updated_at: new Date().toISOString() };
});
await writeTable(this._table, updated);
return { data: applyFilters(updated, this._filters), error: null };
}
// DELETE
if (this._op === 'delete') {
const all = await readTable(this._table);
const gone = applyFilters(all, this._filters);
await writeTable(this._table, all.filter(row => !gone.includes(row)));
return { data: gone, error: null };
}
// SELECT
let rows = applyFilters(await readTable(this._table), this._filters);
if (this._orderCol) rows = applyOrder(rows, this._orderCol, this._orderDir);
if (this._limit) rows = rows.slice(this._offset, this._offset + this._limit);
else if (this._offset) rows = rows.slice(this._offset);
return { data: applySelect(rows, this._select), error: null };
}
then(resolve, reject) { return this.execute().then(resolve, reject); }
}
export default {
from: (table) => new QueryBuilder(table),
schema: readSchema,
}; |