zero-http 0.2.4 → 0.3.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 +1250 -283
- package/documentation/config/db.js +25 -0
- package/documentation/config/middleware.js +44 -0
- package/documentation/config/tls.js +12 -0
- package/documentation/controllers/cookies.js +34 -0
- package/documentation/controllers/tasks.js +108 -0
- package/documentation/full-server.js +28 -177
- package/documentation/models/Task.js +21 -0
- package/documentation/public/data/api.json +404 -24
- package/documentation/public/data/docs.json +1139 -0
- package/documentation/public/data/examples.json +80 -2
- package/documentation/public/data/options.json +23 -8
- package/documentation/public/index.html +138 -99
- package/documentation/public/scripts/app.js +1 -3
- package/documentation/public/scripts/custom-select.js +189 -0
- package/documentation/public/scripts/data-sections.js +233 -250
- package/documentation/public/scripts/playground.js +270 -0
- package/documentation/public/scripts/ui.js +4 -3
- package/documentation/public/styles.css +56 -5
- package/documentation/public/vendor/icons/compress.svg +17 -17
- package/documentation/public/vendor/icons/database.svg +21 -0
- package/documentation/public/vendor/icons/env.svg +21 -0
- package/documentation/public/vendor/icons/fetch.svg +11 -14
- package/documentation/public/vendor/icons/security.svg +15 -0
- package/documentation/public/vendor/icons/sse.svg +12 -13
- package/documentation/public/vendor/icons/static.svg +12 -26
- package/documentation/public/vendor/icons/stream.svg +7 -13
- package/documentation/public/vendor/icons/validate.svg +17 -0
- package/documentation/routes/api.js +41 -0
- package/documentation/routes/core.js +20 -0
- package/documentation/routes/playground.js +29 -0
- package/documentation/routes/realtime.js +49 -0
- package/documentation/routes/uploads.js +71 -0
- package/index.js +62 -1
- package/lib/app.js +200 -8
- package/lib/body/json.js +28 -5
- package/lib/body/multipart.js +29 -1
- package/lib/body/raw.js +1 -1
- package/lib/body/sendError.js +1 -0
- package/lib/body/text.js +1 -1
- package/lib/body/typeMatch.js +6 -2
- package/lib/body/urlencoded.js +5 -2
- package/lib/debug.js +345 -0
- package/lib/env/index.js +440 -0
- package/lib/errors.js +231 -0
- package/lib/http/request.js +219 -1
- package/lib/http/response.js +410 -6
- package/lib/middleware/compress.js +39 -6
- package/lib/middleware/cookieParser.js +237 -0
- package/lib/middleware/cors.js +13 -2
- package/lib/middleware/csrf.js +135 -0
- package/lib/middleware/errorHandler.js +90 -0
- package/lib/middleware/helmet.js +176 -0
- package/lib/middleware/index.js +7 -2
- package/lib/middleware/rateLimit.js +12 -1
- package/lib/middleware/requestId.js +54 -0
- package/lib/middleware/static.js +95 -11
- package/lib/middleware/timeout.js +72 -0
- package/lib/middleware/validator.js +257 -0
- package/lib/orm/adapters/json.js +215 -0
- package/lib/orm/adapters/memory.js +383 -0
- package/lib/orm/adapters/mongo.js +444 -0
- package/lib/orm/adapters/mysql.js +272 -0
- package/lib/orm/adapters/postgres.js +394 -0
- package/lib/orm/adapters/sql-base.js +142 -0
- package/lib/orm/adapters/sqlite.js +311 -0
- package/lib/orm/index.js +276 -0
- package/lib/orm/model.js +895 -0
- package/lib/orm/query.js +807 -0
- package/lib/orm/schema.js +172 -0
- package/lib/router/index.js +136 -47
- package/lib/sse/stream.js +15 -3
- package/lib/ws/connection.js +19 -3
- package/lib/ws/handshake.js +3 -0
- package/lib/ws/index.js +3 -1
- package/lib/ws/room.js +222 -0
- package/package.json +15 -5
- package/types/app.d.ts +120 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +147 -0
- package/types/fetch.d.ts +43 -0
- package/types/index.d.ts +135 -0
- package/types/middleware.d.ts +292 -0
- package/types/orm.d.ts +610 -0
- package/types/request.d.ts +99 -0
- package/types/response.d.ts +142 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/websocket.d.ts +119 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module orm/adapters/json
|
|
3
|
+
* @description JSON file-backed database adapter.
|
|
4
|
+
* Persists data to JSON files on disk — one file per table.
|
|
5
|
+
* Zero-dependency, suitable for prototyping, small apps, and
|
|
6
|
+
* embedded scenarios. Uses atomic writes for safety.
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const MemoryAdapter = require('./memory');
|
|
11
|
+
|
|
12
|
+
class JsonAdapter extends MemoryAdapter
|
|
13
|
+
{
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} options
|
|
16
|
+
* @param {string} options.dir - Directory to store JSON files. Created if needed.
|
|
17
|
+
* @param {boolean} [options.pretty=true] - Pretty-print JSON files.
|
|
18
|
+
* @param {number} [options.flushInterval=50] - Debounce interval in ms for writes.
|
|
19
|
+
* @param {boolean} [options.autoFlush=true] - Automatically flush writes (set false for manual flush()).
|
|
20
|
+
*/
|
|
21
|
+
constructor(options = {})
|
|
22
|
+
{
|
|
23
|
+
super();
|
|
24
|
+
if (!options.dir) throw new Error('JsonAdapter requires a "dir" option');
|
|
25
|
+
|
|
26
|
+
/** @private */ this._dir = path.resolve(options.dir);
|
|
27
|
+
/** @private */ this._pretty = options.pretty !== false;
|
|
28
|
+
/** @private */ this._dirty = new Set();
|
|
29
|
+
/** @private */ this._flushTimer = null;
|
|
30
|
+
/** @private */ this._flushInterval = options.flushInterval || 50;
|
|
31
|
+
/** @private */ this._autoFlush = options.autoFlush !== false;
|
|
32
|
+
|
|
33
|
+
// Ensure directory exists
|
|
34
|
+
if (!fs.existsSync(this._dir))
|
|
35
|
+
{
|
|
36
|
+
fs.mkdirSync(this._dir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Load any existing tables
|
|
40
|
+
try
|
|
41
|
+
{
|
|
42
|
+
const files = fs.readdirSync(this._dir).filter(f => f.endsWith('.json'));
|
|
43
|
+
for (const file of files)
|
|
44
|
+
{
|
|
45
|
+
const table = path.basename(file, '.json');
|
|
46
|
+
const content = fs.readFileSync(path.join(this._dir, file), 'utf8');
|
|
47
|
+
const parsed = JSON.parse(content);
|
|
48
|
+
this._tables.set(table, parsed.rows || []);
|
|
49
|
+
this._autoIncrements.set(table, parsed.autoIncrement || 1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch (e) { /* fresh start */ }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @override */
|
|
56
|
+
async createTable(table, schema)
|
|
57
|
+
{
|
|
58
|
+
await super.createTable(table, schema);
|
|
59
|
+
this._scheduleSave(table);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** @override */
|
|
63
|
+
async dropTable(table)
|
|
64
|
+
{
|
|
65
|
+
await super.dropTable(table);
|
|
66
|
+
const filePath = path.join(this._dir, `${table}.json`);
|
|
67
|
+
try { fs.unlinkSync(filePath); } catch (e) { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @override */
|
|
71
|
+
async insert(table, data)
|
|
72
|
+
{
|
|
73
|
+
const result = await super.insert(table, data);
|
|
74
|
+
this._scheduleSave(table);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** @override */
|
|
79
|
+
async update(table, pk, pkVal, data)
|
|
80
|
+
{
|
|
81
|
+
await super.update(table, pk, pkVal, data);
|
|
82
|
+
this._scheduleSave(table);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @override */
|
|
86
|
+
async updateWhere(table, conditions, data)
|
|
87
|
+
{
|
|
88
|
+
const count = await super.updateWhere(table, conditions, data);
|
|
89
|
+
if (count > 0) this._scheduleSave(table);
|
|
90
|
+
return count;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @override */
|
|
94
|
+
async remove(table, pk, pkVal)
|
|
95
|
+
{
|
|
96
|
+
await super.remove(table, pk, pkVal);
|
|
97
|
+
this._scheduleSave(table);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @override */
|
|
101
|
+
async deleteWhere(table, conditions)
|
|
102
|
+
{
|
|
103
|
+
const count = await super.deleteWhere(table, conditions);
|
|
104
|
+
if (count > 0) this._scheduleSave(table);
|
|
105
|
+
return count;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @override */
|
|
109
|
+
async clear()
|
|
110
|
+
{
|
|
111
|
+
await super.clear();
|
|
112
|
+
for (const table of this._tables.keys()) this._scheduleSave(table);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Immediately flush all pending writes.
|
|
117
|
+
*/
|
|
118
|
+
async flush()
|
|
119
|
+
{
|
|
120
|
+
for (const table of this._dirty) this._saveTable(table);
|
|
121
|
+
this._dirty.clear();
|
|
122
|
+
if (this._flushTimer) { clearTimeout(this._flushTimer); this._flushTimer = null; }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** @private Schedule a debounced save for the given table. */
|
|
126
|
+
_scheduleSave(table)
|
|
127
|
+
{
|
|
128
|
+
this._dirty.add(table);
|
|
129
|
+
if (this._autoFlush && !this._flushTimer)
|
|
130
|
+
{
|
|
131
|
+
this._flushTimer = setTimeout(() =>
|
|
132
|
+
{
|
|
133
|
+
this.flush();
|
|
134
|
+
}, this._flushInterval);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @private Write table data to JSON file. */
|
|
139
|
+
_saveTable(table)
|
|
140
|
+
{
|
|
141
|
+
const filePath = path.join(this._dir, `${table}.json`);
|
|
142
|
+
const data = {
|
|
143
|
+
autoIncrement: this._autoIncrements.get(table) || 1,
|
|
144
|
+
rows: this._tables.get(table) || [],
|
|
145
|
+
};
|
|
146
|
+
const json = this._pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
147
|
+
|
|
148
|
+
// Atomic write: write to temp file then rename
|
|
149
|
+
const tmpPath = filePath + '.tmp';
|
|
150
|
+
fs.writeFileSync(tmpPath, json, 'utf8');
|
|
151
|
+
fs.renameSync(tmpPath, filePath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// -- JSON Adapter Utilities --------------------------
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the directory where JSON files are stored.
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
get directory() { return this._dir; }
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get the total size of all JSON files in bytes.
|
|
164
|
+
* @returns {number}
|
|
165
|
+
*/
|
|
166
|
+
fileSize()
|
|
167
|
+
{
|
|
168
|
+
let total = 0;
|
|
169
|
+
try
|
|
170
|
+
{
|
|
171
|
+
const files = fs.readdirSync(this._dir).filter(f => f.endsWith('.json'));
|
|
172
|
+
for (const file of files)
|
|
173
|
+
{
|
|
174
|
+
total += fs.statSync(path.join(this._dir, file)).size;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch { /* empty dir, no files */ }
|
|
178
|
+
return total;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if there are pending writes that haven't been flushed.
|
|
183
|
+
* @returns {boolean}
|
|
184
|
+
*/
|
|
185
|
+
get hasPendingWrites() { return this._dirty.size > 0; }
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Compact a specific table's JSON file (re-serialize, removes whitespace bloat).
|
|
189
|
+
* @param {string} table
|
|
190
|
+
*/
|
|
191
|
+
compact(table)
|
|
192
|
+
{
|
|
193
|
+
if (this._tables.has(table)) this._saveTable(table);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Back up the entire data directory to a target path.
|
|
198
|
+
* @param {string} destDir - Destination directory (will be created).
|
|
199
|
+
*/
|
|
200
|
+
backup(destDir)
|
|
201
|
+
{
|
|
202
|
+
const dest = path.resolve(destDir);
|
|
203
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
204
|
+
const files = fs.readdirSync(this._dir).filter(f => f.endsWith('.json'));
|
|
205
|
+
for (const file of files)
|
|
206
|
+
{
|
|
207
|
+
fs.copyFileSync(
|
|
208
|
+
path.join(this._dir, file),
|
|
209
|
+
path.join(dest, file)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = JsonAdapter;
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module orm/adapters/memory
|
|
3
|
+
* @description In-memory database adapter.
|
|
4
|
+
* Zero-dependency, perfect for testing, prototyping, and
|
|
5
|
+
* applications that don't need persistence beyond the process lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* All data is stored in plain JavaScript Maps and arrays.
|
|
8
|
+
* Supports full CRUD, filtering, ordering, pagination, and counting.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class MemoryAdapter
|
|
12
|
+
{
|
|
13
|
+
constructor()
|
|
14
|
+
{
|
|
15
|
+
/** @private */ this._tables = new Map();
|
|
16
|
+
/** @private */ this._autoIncrements = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a table (register schema).
|
|
21
|
+
* @param {string} table - Table name.
|
|
22
|
+
* @param {object} schema - Column definitions.
|
|
23
|
+
*/
|
|
24
|
+
async createTable(table, schema)
|
|
25
|
+
{
|
|
26
|
+
if (!this._tables.has(table))
|
|
27
|
+
{
|
|
28
|
+
this._tables.set(table, []);
|
|
29
|
+
this._autoIncrements.set(table, 1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Drop a table.
|
|
35
|
+
* @param {string} table
|
|
36
|
+
*/
|
|
37
|
+
async dropTable(table)
|
|
38
|
+
{
|
|
39
|
+
this._tables.delete(table);
|
|
40
|
+
this._autoIncrements.delete(table);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Insert a row.
|
|
45
|
+
* @param {string} table - Table name.
|
|
46
|
+
* @param {object} data - Row data.
|
|
47
|
+
* @returns {Promise<object>} Inserted row (with auto-increment ID if applicable).
|
|
48
|
+
*/
|
|
49
|
+
async insert(table, data)
|
|
50
|
+
{
|
|
51
|
+
const rows = this._getTable(table);
|
|
52
|
+
const row = { ...data };
|
|
53
|
+
|
|
54
|
+
// Auto-increment: find any key not provided
|
|
55
|
+
if (row.id === undefined || row.id === null)
|
|
56
|
+
{
|
|
57
|
+
row.id = this._autoIncrements.get(table) || 1;
|
|
58
|
+
this._autoIncrements.set(table, row.id + 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Serialize Date objects
|
|
62
|
+
for (const [k, v] of Object.entries(row))
|
|
63
|
+
{
|
|
64
|
+
if (v instanceof Date) row[k] = v.toISOString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
rows.push(row);
|
|
68
|
+
return row;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Update a row by primary key.
|
|
73
|
+
* @param {string} table - Table name.
|
|
74
|
+
* @param {string} pk - Primary key column name.
|
|
75
|
+
* @param {*} pkVal - Primary key value.
|
|
76
|
+
* @param {object} data - Fields to update.
|
|
77
|
+
*/
|
|
78
|
+
async update(table, pk, pkVal, data)
|
|
79
|
+
{
|
|
80
|
+
const rows = this._getTable(table);
|
|
81
|
+
const row = rows.find(r => r[pk] === pkVal);
|
|
82
|
+
if (row)
|
|
83
|
+
{
|
|
84
|
+
for (const [k, v] of Object.entries(data))
|
|
85
|
+
{
|
|
86
|
+
row[k] = v instanceof Date ? v.toISOString() : v;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Update all rows matching conditions.
|
|
93
|
+
* @param {string} table - Table name.
|
|
94
|
+
* @param {object} conditions - WHERE conditions.
|
|
95
|
+
* @param {object} data - Fields to update.
|
|
96
|
+
* @returns {Promise<number>} Number of updated rows.
|
|
97
|
+
*/
|
|
98
|
+
async updateWhere(table, conditions, data)
|
|
99
|
+
{
|
|
100
|
+
const rows = this._getTable(table);
|
|
101
|
+
let count = 0;
|
|
102
|
+
for (const row of rows)
|
|
103
|
+
{
|
|
104
|
+
if (this._matchConditions(row, conditions))
|
|
105
|
+
{
|
|
106
|
+
for (const [k, v] of Object.entries(data))
|
|
107
|
+
{
|
|
108
|
+
row[k] = v instanceof Date ? v.toISOString() : v;
|
|
109
|
+
}
|
|
110
|
+
count++;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Remove a row by primary key.
|
|
118
|
+
* @param {string} table
|
|
119
|
+
* @param {string} pk
|
|
120
|
+
* @param {*} pkVal
|
|
121
|
+
*/
|
|
122
|
+
async remove(table, pk, pkVal)
|
|
123
|
+
{
|
|
124
|
+
const rows = this._getTable(table);
|
|
125
|
+
const idx = rows.findIndex(r => r[pk] === pkVal);
|
|
126
|
+
if (idx !== -1) rows.splice(idx, 1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Delete all rows matching conditions.
|
|
131
|
+
* @param {string} table
|
|
132
|
+
* @param {object} conditions
|
|
133
|
+
* @returns {Promise<number>}
|
|
134
|
+
*/
|
|
135
|
+
async deleteWhere(table, conditions)
|
|
136
|
+
{
|
|
137
|
+
const rows = this._getTable(table);
|
|
138
|
+
let count = 0;
|
|
139
|
+
for (let i = rows.length - 1; i >= 0; i--)
|
|
140
|
+
{
|
|
141
|
+
if (this._matchConditions(rows[i], conditions))
|
|
142
|
+
{
|
|
143
|
+
rows.splice(i, 1);
|
|
144
|
+
count++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return count;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Execute a query descriptor (from the Query builder).
|
|
152
|
+
* @param {object} descriptor - Abstract query descriptor.
|
|
153
|
+
* @returns {Promise<Array|number>}
|
|
154
|
+
*/
|
|
155
|
+
async execute(descriptor)
|
|
156
|
+
{
|
|
157
|
+
const { action, table, fields, where, orderBy, limit, offset, distinct, includeDeleted } = descriptor;
|
|
158
|
+
let rows = [...this._getTable(table)];
|
|
159
|
+
|
|
160
|
+
// Apply WHERE filters
|
|
161
|
+
if (where && where.length > 0)
|
|
162
|
+
{
|
|
163
|
+
rows = rows.filter(row => this._applyWhereChain(row, where));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Count action
|
|
167
|
+
if (action === 'count') return rows.length;
|
|
168
|
+
|
|
169
|
+
// ORDER BY
|
|
170
|
+
if (orderBy && orderBy.length > 0)
|
|
171
|
+
{
|
|
172
|
+
rows.sort((a, b) =>
|
|
173
|
+
{
|
|
174
|
+
for (const { field, dir } of orderBy)
|
|
175
|
+
{
|
|
176
|
+
const av = a[field], bv = b[field];
|
|
177
|
+
if (av < bv) return dir === 'ASC' ? -1 : 1;
|
|
178
|
+
if (av > bv) return dir === 'ASC' ? 1 : -1;
|
|
179
|
+
}
|
|
180
|
+
return 0;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// OFFSET
|
|
185
|
+
if (offset) rows = rows.slice(offset);
|
|
186
|
+
|
|
187
|
+
// LIMIT
|
|
188
|
+
if (limit) rows = rows.slice(0, limit);
|
|
189
|
+
|
|
190
|
+
// SELECT specific fields
|
|
191
|
+
if (fields && fields.length > 0)
|
|
192
|
+
{
|
|
193
|
+
rows = rows.map(row =>
|
|
194
|
+
{
|
|
195
|
+
const filtered = {};
|
|
196
|
+
for (const f of fields) filtered[f] = row[f];
|
|
197
|
+
return filtered;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// DISTINCT
|
|
202
|
+
if (distinct)
|
|
203
|
+
{
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
rows = rows.filter(row =>
|
|
206
|
+
{
|
|
207
|
+
const key = JSON.stringify(row);
|
|
208
|
+
if (seen.has(key)) return false;
|
|
209
|
+
seen.add(key);
|
|
210
|
+
return true;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return rows;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Clear all data (for testing).
|
|
219
|
+
*/
|
|
220
|
+
async clear()
|
|
221
|
+
{
|
|
222
|
+
for (const key of this._tables.keys())
|
|
223
|
+
{
|
|
224
|
+
this._tables.set(key, []);
|
|
225
|
+
this._autoIncrements.set(key, 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// -- Internal Helpers -------------------------------
|
|
230
|
+
|
|
231
|
+
/** @private Get or create table array. */
|
|
232
|
+
_getTable(table)
|
|
233
|
+
{
|
|
234
|
+
if (!this._tables.has(table)) this._tables.set(table, []);
|
|
235
|
+
return this._tables.get(table);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @private Match simple object conditions { key: value }. */
|
|
239
|
+
_matchConditions(row, conditions)
|
|
240
|
+
{
|
|
241
|
+
if (!conditions || typeof conditions !== 'object') return true;
|
|
242
|
+
for (const [k, v] of Object.entries(conditions))
|
|
243
|
+
{
|
|
244
|
+
if (row[k] !== v) return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** @private Apply the where chain from query builder. */
|
|
250
|
+
_applyWhereChain(row, where)
|
|
251
|
+
{
|
|
252
|
+
let result = true;
|
|
253
|
+
for (let i = 0; i < where.length; i++)
|
|
254
|
+
{
|
|
255
|
+
const clause = where[i];
|
|
256
|
+
// Skip raw SQL clauses — not supported in memory adapter
|
|
257
|
+
if (clause.raw) continue;
|
|
258
|
+
const matches = this._matchClause(row, clause);
|
|
259
|
+
|
|
260
|
+
if (i === 0 || clause.logic === 'AND')
|
|
261
|
+
{
|
|
262
|
+
result = i === 0 ? matches : (result && matches);
|
|
263
|
+
}
|
|
264
|
+
else if (clause.logic === 'OR')
|
|
265
|
+
{
|
|
266
|
+
result = result || matches;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** @private Match a single WHERE clause. */
|
|
273
|
+
_matchClause(row, clause)
|
|
274
|
+
{
|
|
275
|
+
const val = row[clause.field];
|
|
276
|
+
const { op, value } = clause;
|
|
277
|
+
|
|
278
|
+
switch (op)
|
|
279
|
+
{
|
|
280
|
+
case '=': return val === value;
|
|
281
|
+
case '!=':
|
|
282
|
+
case '<>': return val !== value;
|
|
283
|
+
case '>': return val > value;
|
|
284
|
+
case '<': return val < value;
|
|
285
|
+
case '>=': return val >= value;
|
|
286
|
+
case '<=': return val <= value;
|
|
287
|
+
case 'LIKE':
|
|
288
|
+
{
|
|
289
|
+
// Simple LIKE: % = any, _ = single char
|
|
290
|
+
const pattern = String(value).replace(/%/g, '.*').replace(/_/g, '.');
|
|
291
|
+
return new RegExp('^' + pattern + '$', 'i').test(String(val));
|
|
292
|
+
}
|
|
293
|
+
case 'IN': return Array.isArray(value) && value.includes(val);
|
|
294
|
+
case 'NOT IN': return Array.isArray(value) && !value.includes(val);
|
|
295
|
+
case 'BETWEEN': return Array.isArray(value) && val >= value[0] && val <= value[1];
|
|
296
|
+
case 'NOT BETWEEN': return Array.isArray(value) && (val < value[0] || val > value[1]);
|
|
297
|
+
case 'IS NULL': return val === null || val === undefined;
|
|
298
|
+
case 'IS NOT NULL': return val !== null && val !== undefined;
|
|
299
|
+
default: return val === value;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// -- Memory Adapter Utilities ------------------------
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* List all registered table names.
|
|
307
|
+
* @returns {string[]}
|
|
308
|
+
*/
|
|
309
|
+
tables()
|
|
310
|
+
{
|
|
311
|
+
return [...this._tables.keys()];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get the total number of rows across all tables.
|
|
316
|
+
* @returns {number}
|
|
317
|
+
*/
|
|
318
|
+
totalRows()
|
|
319
|
+
{
|
|
320
|
+
let total = 0;
|
|
321
|
+
for (const rows of this._tables.values()) total += rows.length;
|
|
322
|
+
return total;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get memory usage stats.
|
|
327
|
+
* @returns {{ tables: number, totalRows: number, estimatedBytes: number }}
|
|
328
|
+
*/
|
|
329
|
+
stats()
|
|
330
|
+
{
|
|
331
|
+
const tables = this._tables.size;
|
|
332
|
+
let totalRows = 0;
|
|
333
|
+
let estimatedBytes = 0;
|
|
334
|
+
for (const rows of this._tables.values())
|
|
335
|
+
{
|
|
336
|
+
totalRows += rows.length;
|
|
337
|
+
estimatedBytes += JSON.stringify(rows).length * 2; // rough UTF-16 estimate
|
|
338
|
+
}
|
|
339
|
+
return { tables, totalRows, estimatedBytes };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Export all data as a plain object.
|
|
344
|
+
* @returns {object} { tableName: rows[], ... }
|
|
345
|
+
*/
|
|
346
|
+
toJSON()
|
|
347
|
+
{
|
|
348
|
+
const out = {};
|
|
349
|
+
for (const [table, rows] of this._tables) out[table] = [...rows];
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Import data from a plain object, merging with existing data.
|
|
355
|
+
* @param {object} data - { tableName: rows[], ... }
|
|
356
|
+
*/
|
|
357
|
+
fromJSON(data)
|
|
358
|
+
{
|
|
359
|
+
for (const [table, rows] of Object.entries(data))
|
|
360
|
+
{
|
|
361
|
+
if (!this._tables.has(table)) this._tables.set(table, []);
|
|
362
|
+
const existing = this._tables.get(table);
|
|
363
|
+
for (const row of rows) existing.push({ ...row });
|
|
364
|
+
// Update auto-increment
|
|
365
|
+
const maxId = rows.reduce((max, r) => Math.max(max, r.id || 0), 0);
|
|
366
|
+
const currentAi = this._autoIncrements.get(table) || 1;
|
|
367
|
+
if (maxId >= currentAi) this._autoIncrements.set(table, maxId + 1);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Clone the entire database state (deep copy).
|
|
373
|
+
* @returns {MemoryAdapter}
|
|
374
|
+
*/
|
|
375
|
+
clone()
|
|
376
|
+
{
|
|
377
|
+
const copy = new MemoryAdapter();
|
|
378
|
+
copy.fromJSON(this.toJSON());
|
|
379
|
+
return copy;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = MemoryAdapter;
|