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,
};