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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1265 -0
  3. package/bin/volt.js +139 -0
  4. package/package.json +56 -0
  5. package/src/api/graphql.js +399 -0
  6. package/src/api/rest.js +204 -0
  7. package/src/api/websocket.js +285 -0
  8. package/src/cli/build.js +111 -0
  9. package/src/cli/create.js +371 -0
  10. package/src/cli/db.js +106 -0
  11. package/src/cli/dev.js +114 -0
  12. package/src/cli/generate.js +278 -0
  13. package/src/cli/lint.js +172 -0
  14. package/src/cli/routes.js +118 -0
  15. package/src/cli/start.js +42 -0
  16. package/src/cli/test.js +138 -0
  17. package/src/core/app.js +701 -0
  18. package/src/core/config.js +232 -0
  19. package/src/core/middleware.js +133 -0
  20. package/src/core/plugins.js +88 -0
  21. package/src/core/react-renderer.js +244 -0
  22. package/src/core/renderer.js +337 -0
  23. package/src/core/router.js +183 -0
  24. package/src/database/index.js +461 -0
  25. package/src/database/migration.js +192 -0
  26. package/src/database/model.js +285 -0
  27. package/src/database/query.js +394 -0
  28. package/src/database/seeder.js +89 -0
  29. package/src/index.js +156 -0
  30. package/src/security/auth.js +425 -0
  31. package/src/security/cors.js +80 -0
  32. package/src/security/csrf.js +125 -0
  33. package/src/security/encryption.js +110 -0
  34. package/src/security/helmet.js +103 -0
  35. package/src/security/index.js +75 -0
  36. package/src/security/rateLimit.js +119 -0
  37. package/src/security/sanitizer.js +113 -0
  38. package/src/security/xss.js +110 -0
  39. package/src/ui/component.js +224 -0
  40. package/src/ui/reactive.js +503 -0
  41. package/src/ui/template.js +448 -0
  42. package/src/utils/cache.js +216 -0
  43. package/src/utils/collection.js +772 -0
  44. package/src/utils/cron.js +213 -0
  45. package/src/utils/date.js +223 -0
  46. package/src/utils/events.js +181 -0
  47. package/src/utils/excel.js +482 -0
  48. package/src/utils/form.js +547 -0
  49. package/src/utils/hash.js +121 -0
  50. package/src/utils/http.js +461 -0
  51. package/src/utils/logger.js +186 -0
  52. package/src/utils/mail.js +347 -0
  53. package/src/utils/paginator.js +179 -0
  54. package/src/utils/pdf.js +417 -0
  55. package/src/utils/queue.js +199 -0
  56. package/src/utils/schema.js +985 -0
  57. package/src/utils/sms.js +243 -0
  58. package/src/utils/storage.js +348 -0
  59. package/src/utils/string.js +236 -0
  60. package/src/utils/validation.js +318 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * VoltJS Model (Active Record ORM)
3
+ *
4
+ * Simple, powerful Active Record pattern for database operations.
5
+ *
6
+ * @example
7
+ * const User = db.model('users', {
8
+ * name: { type: 'string', required: true },
9
+ * email: { type: 'string', unique: true },
10
+ * age: { type: 'integer' },
11
+ * });
12
+ *
13
+ * const user = await User.create({ name: 'John', email: 'john@example.com' });
14
+ * const users = await User.where('age', '>', 18).get();
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ class Model {
20
+ constructor(data = {}, tableName, schema, db) {
21
+ this._tableName = tableName;
22
+ this._schema = schema;
23
+ this._db = db;
24
+ this._original = { ...data };
25
+
26
+ // Assign data as properties
27
+ Object.assign(this, data);
28
+ }
29
+
30
+ /** Create a model class for a table */
31
+ static create(tableName, schema, db) {
32
+ // Create auto-table if in memory mode
33
+ if (db._config.driver === 'memory' && !db._tables.has(tableName)) {
34
+ db._tables.set(tableName, { schema, data: [], autoIncrement: 1 });
35
+ }
36
+
37
+ class BoundModel extends Model {
38
+ constructor(data = {}) {
39
+ super(data, tableName, schema, db);
40
+ }
41
+
42
+ // ===== STATIC QUERY METHODS =====
43
+
44
+ /** Find by ID */
45
+ static async find(id) {
46
+ const results = await db.table(tableName).where('id', '=', id).limit(1).get();
47
+ return results.length > 0 ? new BoundModel(results[0]) : null;
48
+ }
49
+
50
+ /** Find by ID or throw */
51
+ static async findOrFail(id) {
52
+ const result = await BoundModel.find(id);
53
+ if (!result) throw new Error(`${tableName} with id ${id} not found`);
54
+ return result;
55
+ }
56
+
57
+ /** Find by a field */
58
+ static async findBy(field, value) {
59
+ const results = await db.table(tableName).where(field, '=', value).limit(1).get();
60
+ return results.length > 0 ? new BoundModel(results[0]) : null;
61
+ }
62
+
63
+ /** Get all records */
64
+ static async all() {
65
+ const results = await db.table(tableName).get();
66
+ return results.map(r => new BoundModel(r));
67
+ }
68
+
69
+ /** Where clause (starts a query chain) */
70
+ static where(field, op, value) {
71
+ const qb = db.table(tableName).where(field, op, value);
72
+ // Wrap result to return model instances
73
+ const originalGet = qb.get.bind(qb);
74
+ qb.get = async () => {
75
+ const results = await originalGet();
76
+ return results.map(r => new BoundModel(r));
77
+ };
78
+ qb.first = async () => {
79
+ qb.limit(1);
80
+ const results = await originalGet();
81
+ return results.length > 0 ? new BoundModel(results[0]) : null;
82
+ };
83
+ return qb;
84
+ }
85
+
86
+ /** Create a new record */
87
+ static async create(data) {
88
+ // Validate
89
+ BoundModel._validate(data);
90
+
91
+ // Add timestamps
92
+ const now = new Date().toISOString();
93
+ if (schema.created_at !== undefined || schema.createdAt !== undefined) {
94
+ data.created_at = data.created_at || now;
95
+ }
96
+ if (schema.updated_at !== undefined || schema.updatedAt !== undefined) {
97
+ data.updated_at = now;
98
+ }
99
+
100
+ const result = await db.table(tableName).insert(data);
101
+ return new BoundModel({ id: result.insertId, ...data });
102
+ }
103
+
104
+ /** Create multiple records */
105
+ static async createMany(records) {
106
+ return Promise.all(records.map(r => BoundModel.create(r)));
107
+ }
108
+
109
+ /** Update by ID */
110
+ static async updateById(id, data) {
111
+ data.updated_at = new Date().toISOString();
112
+ return db.table(tableName).where('id', '=', id).update(data);
113
+ }
114
+
115
+ /** Delete by ID */
116
+ static async deleteById(id) {
117
+ return db.table(tableName).where('id', '=', id).delete();
118
+ }
119
+
120
+ /** Count records */
121
+ static async count(field, op, value) {
122
+ if (field) {
123
+ const results = await db.table(tableName).where(field, op, value).get();
124
+ return results.length;
125
+ }
126
+ const results = await db.table(tableName).get();
127
+ return results.length;
128
+ }
129
+
130
+ /** Paginate results */
131
+ static async paginate(page = 1, perPage = 20) {
132
+ const offset = (page - 1) * perPage;
133
+ const total = await BoundModel.count();
134
+ const data = await db.table(tableName).limit(perPage).offset(offset).get();
135
+
136
+ return {
137
+ data: data.map(r => new BoundModel(r)),
138
+ total,
139
+ page,
140
+ perPage,
141
+ totalPages: Math.ceil(total / perPage),
142
+ hasNext: page * perPage < total,
143
+ hasPrev: page > 1,
144
+ };
145
+ }
146
+
147
+ /** First or create */
148
+ static async firstOrCreate(searchData, createData = {}) {
149
+ const [field, value] = Object.entries(searchData)[0];
150
+ const existing = await BoundModel.findBy(field, value);
151
+ if (existing) return existing;
152
+ return BoundModel.create({ ...searchData, ...createData });
153
+ }
154
+
155
+ /** Validate data against schema */
156
+ static _validate(data) {
157
+ for (const [field, rules] of Object.entries(schema)) {
158
+ if (field === 'id') continue;
159
+
160
+ const rule = typeof rules === 'string' ? { type: rules } : rules;
161
+
162
+ // Required check
163
+ if (rule.required && (data[field] === undefined || data[field] === null || data[field] === '')) {
164
+ throw new Error(`Validation failed: "${field}" is required`);
165
+ }
166
+
167
+ if (data[field] !== undefined && data[field] !== null) {
168
+ // Type check
169
+ if (rule.type) {
170
+ const type = rule.type.toLowerCase();
171
+ if (type === 'integer' || type === 'int') {
172
+ if (!Number.isInteger(Number(data[field]))) {
173
+ throw new Error(`Validation failed: "${field}" must be an integer`);
174
+ }
175
+ }
176
+ if (type === 'email') {
177
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data[field])) {
178
+ throw new Error(`Validation failed: "${field}" must be a valid email`);
179
+ }
180
+ }
181
+ }
182
+
183
+ // Min/Max
184
+ if (rule.min !== undefined && data[field] < rule.min) {
185
+ throw new Error(`Validation failed: "${field}" must be at least ${rule.min}`);
186
+ }
187
+ if (rule.max !== undefined && data[field] > rule.max) {
188
+ throw new Error(`Validation failed: "${field}" must be at most ${rule.max}`);
189
+ }
190
+
191
+ // MinLength/MaxLength
192
+ if (rule.minLength && String(data[field]).length < rule.minLength) {
193
+ throw new Error(`Validation failed: "${field}" must be at least ${rule.minLength} characters`);
194
+ }
195
+ if (rule.maxLength && String(data[field]).length > rule.maxLength) {
196
+ throw new Error(`Validation failed: "${field}" must be at most ${rule.maxLength} characters`);
197
+ }
198
+
199
+ // Pattern
200
+ if (rule.pattern && !new RegExp(rule.pattern).test(data[field])) {
201
+ throw new Error(`Validation failed: "${field}" does not match required pattern`);
202
+ }
203
+
204
+ // Enum
205
+ if (rule.enum && !rule.enum.includes(data[field])) {
206
+ throw new Error(`Validation failed: "${field}" must be one of: ${rule.enum.join(', ')}`);
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ /** Get table name */
213
+ static get tableName() {
214
+ return tableName;
215
+ }
216
+
217
+ /** Get schema */
218
+ static get modelSchema() {
219
+ return schema;
220
+ }
221
+ }
222
+
223
+ // Instance methods
224
+ BoundModel.prototype.save = async function() {
225
+ if (this.id) {
226
+ const data = this._getChangedFields();
227
+ if (Object.keys(data).length > 0) {
228
+ await BoundModel.updateById(this.id, data);
229
+ this._original = { ...this._original, ...data };
230
+ }
231
+ } else {
232
+ const data = { ...this };
233
+ delete data._tableName;
234
+ delete data._schema;
235
+ delete data._db;
236
+ delete data._original;
237
+ const result = await BoundModel.create(data);
238
+ this.id = result.id;
239
+ }
240
+ return this;
241
+ };
242
+
243
+ BoundModel.prototype.delete = async function() {
244
+ if (this.id) {
245
+ await BoundModel.deleteById(this.id);
246
+ }
247
+ return this;
248
+ };
249
+
250
+ BoundModel.prototype.refresh = async function() {
251
+ if (this.id) {
252
+ const fresh = await BoundModel.find(this.id);
253
+ if (fresh) {
254
+ Object.assign(this, fresh);
255
+ this._original = { ...fresh };
256
+ }
257
+ }
258
+ return this;
259
+ };
260
+
261
+ BoundModel.prototype.toJSON = function() {
262
+ const obj = { ...this };
263
+ delete obj._tableName;
264
+ delete obj._schema;
265
+ delete obj._db;
266
+ delete obj._original;
267
+ return obj;
268
+ };
269
+
270
+ BoundModel.prototype._getChangedFields = function() {
271
+ const changed = {};
272
+ for (const [key, value] of Object.entries(this)) {
273
+ if (key.startsWith('_')) continue;
274
+ if (this._original[key] !== value) {
275
+ changed[key] = value;
276
+ }
277
+ }
278
+ return changed;
279
+ };
280
+
281
+ return BoundModel;
282
+ }
283
+ }
284
+
285
+ module.exports = { Model };
@@ -0,0 +1,394 @@
1
+ /**
2
+ * VoltJS Query Builder
3
+ *
4
+ * Fluent SQL query builder with support for all common operations.
5
+ *
6
+ * @example
7
+ * const users = await db.table('users')
8
+ * .where('age', '>', 18)
9
+ * .where('status', '=', 'active')
10
+ * .orderBy('name', 'ASC')
11
+ * .limit(10)
12
+ * .get();
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ class QueryBuilder {
18
+ constructor(tableName, db) {
19
+ this._table = tableName;
20
+ this._db = db;
21
+ this._wheres = [];
22
+ this._orders = [];
23
+ this._limit = null;
24
+ this._offset = null;
25
+ this._selects = ['*'];
26
+ this._joins = [];
27
+ this._groups = [];
28
+ this._havings = [];
29
+ this._params = [];
30
+ }
31
+
32
+ /** Select specific columns */
33
+ select(...columns) {
34
+ this._selects = columns.flat();
35
+ return this;
36
+ }
37
+
38
+ /** Where clause */
39
+ where(field, op, value) {
40
+ if (value === undefined) {
41
+ // Two-arg form: where('name', 'John') → where('name', '=', 'John')
42
+ value = op;
43
+ op = '=';
44
+ }
45
+ this._wheres.push({ field, op, value, type: 'AND' });
46
+ this._params.push(value);
47
+ return this;
48
+ }
49
+
50
+ /** OR where */
51
+ orWhere(field, op, value) {
52
+ if (value === undefined) { value = op; op = '='; }
53
+ this._wheres.push({ field, op, value, type: 'OR' });
54
+ this._params.push(value);
55
+ return this;
56
+ }
57
+
58
+ /** Where IN */
59
+ whereIn(field, values) {
60
+ this._wheres.push({ field, op: 'IN', value: values, type: 'AND' });
61
+ this._params.push(...values);
62
+ return this;
63
+ }
64
+
65
+ /** Where NOT IN */
66
+ whereNotIn(field, values) {
67
+ this._wheres.push({ field, op: 'NOT IN', value: values, type: 'AND' });
68
+ this._params.push(...values);
69
+ return this;
70
+ }
71
+
72
+ /** Where NULL */
73
+ whereNull(field) {
74
+ this._wheres.push({ field, op: 'IS', value: null, type: 'AND' });
75
+ return this;
76
+ }
77
+
78
+ /** Where NOT NULL */
79
+ whereNotNull(field) {
80
+ this._wheres.push({ field, op: 'IS NOT', value: null, type: 'AND' });
81
+ return this;
82
+ }
83
+
84
+ /** Where BETWEEN */
85
+ whereBetween(field, min, max) {
86
+ this._wheres.push({ field, op: 'BETWEEN', value: [min, max], type: 'AND' });
87
+ this._params.push(min, max);
88
+ return this;
89
+ }
90
+
91
+ /** Where LIKE */
92
+ whereLike(field, pattern) {
93
+ this._wheres.push({ field, op: 'LIKE', value: pattern, type: 'AND' });
94
+ this._params.push(pattern);
95
+ return this;
96
+ }
97
+
98
+ /** Order by */
99
+ orderBy(field, direction = 'ASC') {
100
+ this._orders.push({ field, direction: direction.toUpperCase() });
101
+ return this;
102
+ }
103
+
104
+ /** Limit */
105
+ limit(count) {
106
+ this._limit = count;
107
+ return this;
108
+ }
109
+
110
+ /** Offset */
111
+ offset(count) {
112
+ this._offset = count;
113
+ return this;
114
+ }
115
+
116
+ /** Join */
117
+ join(table, left, op, right) {
118
+ this._joins.push({ type: 'INNER', table, left, op, right });
119
+ return this;
120
+ }
121
+
122
+ /** Left join */
123
+ leftJoin(table, left, op, right) {
124
+ this._joins.push({ type: 'LEFT', table, left, op, right });
125
+ return this;
126
+ }
127
+
128
+ /** Right join */
129
+ rightJoin(table, left, op, right) {
130
+ this._joins.push({ type: 'RIGHT', table, left, op, right });
131
+ return this;
132
+ }
133
+
134
+ /** Group by */
135
+ groupBy(...fields) {
136
+ this._groups.push(...fields);
137
+ return this;
138
+ }
139
+
140
+ /** Having */
141
+ having(field, op, value) {
142
+ this._havings.push({ field, op, value });
143
+ return this;
144
+ }
145
+
146
+ /** Execute SELECT and get results */
147
+ async get() {
148
+ if (this._db._config.driver === 'memory') {
149
+ return this._memoryGet();
150
+ }
151
+
152
+ const sql = this._buildSelect();
153
+ return this._db.query(sql, this._params);
154
+ }
155
+
156
+ /** Get the first result */
157
+ async first() {
158
+ this._limit = 1;
159
+ const results = await this.get();
160
+ return results.length > 0 ? results[0] : null;
161
+ }
162
+
163
+ /** Insert a record */
164
+ async insert(data) {
165
+ if (this._db._config.driver === 'memory') {
166
+ return this._memoryInsert(data);
167
+ }
168
+
169
+ const columns = Object.keys(data);
170
+ const values = Object.values(data);
171
+ const placeholders = columns.map(() => '?').join(', ');
172
+ const sql = `INSERT INTO ${this._table} (${columns.join(', ')}) VALUES (${placeholders})`;
173
+ return this._db.query(sql, values);
174
+ }
175
+
176
+ /** Insert multiple records */
177
+ async insertMany(records) {
178
+ const results = [];
179
+ for (const record of records) {
180
+ results.push(await this.insert(record));
181
+ }
182
+ return results;
183
+ }
184
+
185
+ /** Update records */
186
+ async update(data) {
187
+ if (this._db._config.driver === 'memory') {
188
+ return this._memoryUpdate(data);
189
+ }
190
+
191
+ const setParts = Object.keys(data).map(k => `${k} = ?`);
192
+ const values = [...Object.values(data), ...this._params];
193
+ const sql = `UPDATE ${this._table} SET ${setParts.join(', ')}${this._buildWhere()}`;
194
+ return this._db.query(sql, values);
195
+ }
196
+
197
+ /** Delete records */
198
+ async delete() {
199
+ if (this._db._config.driver === 'memory') {
200
+ return this._memoryDelete();
201
+ }
202
+
203
+ const sql = `DELETE FROM ${this._table}${this._buildWhere()}`;
204
+ return this._db.query(sql, this._params);
205
+ }
206
+
207
+ /** Count matching records */
208
+ async count() {
209
+ if (this._db._config.driver === 'memory') {
210
+ const results = await this._memoryGet();
211
+ return results.length;
212
+ }
213
+
214
+ const sql = `SELECT COUNT(*) as count FROM ${this._table}${this._buildWhere()}`;
215
+ const result = await this._db.query(sql, this._params);
216
+ return result[0]?.count || 0;
217
+ }
218
+
219
+ /** Check if any matching record exists */
220
+ async exists() {
221
+ const count = await this.count();
222
+ return count > 0;
223
+ }
224
+
225
+ /** Get the built SQL (for debugging) */
226
+ toSQL() {
227
+ return { sql: this._buildSelect(), params: this._params };
228
+ }
229
+
230
+ // ===== SQL BUILDERS =====
231
+
232
+ _buildSelect() {
233
+ let sql = `SELECT ${this._selects.join(', ')} FROM ${this._table}`;
234
+ sql += this._buildJoins();
235
+ sql += this._buildWhere();
236
+ sql += this._buildGroupBy();
237
+ sql += this._buildHaving();
238
+ sql += this._buildOrderBy();
239
+ if (this._limit) sql += ` LIMIT ${this._limit}`;
240
+ if (this._offset) sql += ` OFFSET ${this._offset}`;
241
+ return sql;
242
+ }
243
+
244
+ _buildWhere() {
245
+ if (this._wheres.length === 0) return '';
246
+
247
+ const parts = this._wheres.map((w, i) => {
248
+ const prefix = i === 0 ? 'WHERE' : w.type;
249
+ if (w.op === 'IN' || w.op === 'NOT IN') {
250
+ const placeholders = w.value.map(() => '?').join(', ');
251
+ return `${prefix} ${w.field} ${w.op} (${placeholders})`;
252
+ }
253
+ if (w.op === 'BETWEEN') {
254
+ return `${prefix} ${w.field} BETWEEN ? AND ?`;
255
+ }
256
+ if (w.value === null) {
257
+ return `${prefix} ${w.field} ${w.op} NULL`;
258
+ }
259
+ return `${prefix} ${w.field} ${w.op} ?`;
260
+ });
261
+
262
+ return ' ' + parts.join(' ');
263
+ }
264
+
265
+ _buildJoins() {
266
+ return this._joins.map(j => ` ${j.type} JOIN ${j.table} ON ${j.left} ${j.op} ${j.right}`).join('');
267
+ }
268
+
269
+ _buildOrderBy() {
270
+ if (this._orders.length === 0) return '';
271
+ return ' ORDER BY ' + this._orders.map(o => `${o.field} ${o.direction}`).join(', ');
272
+ }
273
+
274
+ _buildGroupBy() {
275
+ if (this._groups.length === 0) return '';
276
+ return ' GROUP BY ' + this._groups.join(', ');
277
+ }
278
+
279
+ _buildHaving() {
280
+ if (this._havings.length === 0) return '';
281
+ return ' HAVING ' + this._havings.map(h => `${h.field} ${h.op} ${h.value}`).join(' AND ');
282
+ }
283
+
284
+ // ===== IN-MEMORY OPERATIONS =====
285
+
286
+ _memoryGet() {
287
+ const table = this._db._tables.get(this._table);
288
+ if (!table) return [];
289
+
290
+ let results = [...table.data];
291
+
292
+ // Apply WHERE conditions
293
+ for (const w of this._wheres) {
294
+ results = results.filter(row => {
295
+ const val = row[w.field];
296
+ switch (w.op) {
297
+ case '=': return val == w.value;
298
+ case '!=': case '<>': return val != w.value;
299
+ case '>': return val > w.value;
300
+ case '<': return val < w.value;
301
+ case '>=': return val >= w.value;
302
+ case '<=': return val <= w.value;
303
+ case 'LIKE':
304
+ const pattern = String(w.value).replace(/%/g, '.*').replace(/_/g, '.');
305
+ return new RegExp(`^${pattern}$`, 'i').test(String(val));
306
+ case 'IN': return Array.isArray(w.value) && w.value.includes(val);
307
+ case 'NOT IN': return Array.isArray(w.value) && !w.value.includes(val);
308
+ case 'IS': return val === null || val === undefined;
309
+ case 'IS NOT': return val !== null && val !== undefined;
310
+ case 'BETWEEN': return val >= w.value[0] && val <= w.value[1];
311
+ default: return true;
312
+ }
313
+ });
314
+ }
315
+
316
+ // Apply ORDER BY
317
+ for (const order of this._orders.reverse()) {
318
+ results.sort((a, b) => {
319
+ if (a[order.field] > b[order.field]) return order.direction === 'ASC' ? 1 : -1;
320
+ if (a[order.field] < b[order.field]) return order.direction === 'ASC' ? -1 : 1;
321
+ return 0;
322
+ });
323
+ }
324
+
325
+ // Apply OFFSET & LIMIT
326
+ if (this._offset) results = results.slice(this._offset);
327
+ if (this._limit) results = results.slice(0, this._limit);
328
+
329
+ // Apply SELECT
330
+ if (this._selects[0] !== '*') {
331
+ results = results.map(row => {
332
+ const filtered = {};
333
+ this._selects.forEach(col => { filtered[col] = row[col]; });
334
+ return filtered;
335
+ });
336
+ }
337
+
338
+ return results;
339
+ }
340
+
341
+ _memoryInsert(data) {
342
+ const table = this._db._tables.get(this._table);
343
+ if (!table) throw new Error(`Table "${this._table}" not found`);
344
+
345
+ const row = { id: table.autoIncrement++, ...data };
346
+ row.created_at = row.created_at || new Date().toISOString();
347
+ row.updated_at = new Date().toISOString();
348
+ table.data.push(row);
349
+
350
+ return { insertId: row.id, affectedRows: 1 };
351
+ }
352
+
353
+ _memoryUpdate(data) {
354
+ const table = this._db._tables.get(this._table);
355
+ if (!table) throw new Error(`Table "${this._table}" not found`);
356
+
357
+ let affected = 0;
358
+ for (const row of table.data) {
359
+ if (this._matchesWheres(row)) {
360
+ Object.assign(row, data);
361
+ row.updated_at = new Date().toISOString();
362
+ affected++;
363
+ }
364
+ }
365
+ return { affectedRows: affected };
366
+ }
367
+
368
+ _memoryDelete() {
369
+ const table = this._db._tables.get(this._table);
370
+ if (!table) throw new Error(`Table "${this._table}" not found`);
371
+
372
+ const before = table.data.length;
373
+ table.data = table.data.filter(row => !this._matchesWheres(row));
374
+ return { affectedRows: before - table.data.length };
375
+ }
376
+
377
+ _matchesWheres(row) {
378
+ if (this._wheres.length === 0) return true;
379
+ return this._wheres.every(w => {
380
+ const val = row[w.field];
381
+ switch (w.op) {
382
+ case '=': return val == w.value;
383
+ case '!=': return val != w.value;
384
+ case '>': return val > w.value;
385
+ case '<': return val < w.value;
386
+ case '>=': return val >= w.value;
387
+ case '<=': return val <= w.value;
388
+ default: return true;
389
+ }
390
+ });
391
+ }
392
+ }
393
+
394
+ module.exports = { QueryBuilder };