@datrix/adapter-json 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,2321 @@
1
+ // src/adapter.ts
2
+ import fs2 from "fs/promises";
3
+ import path2 from "path";
4
+ import { validateQueryObject } from "@datrix/core";
5
+
6
+ // src/runner.ts
7
+ import {
8
+ throwInvalidRelationWhereSyntax,
9
+ throwInvalidWhereField
10
+ } from "@datrix/core";
11
+ var JsonQueryRunner = class {
12
+ constructor(table, adapter, schema) {
13
+ this.table = table;
14
+ this.adapter = adapter;
15
+ this.schema = schema;
16
+ }
17
+ schema;
18
+ get tableData() {
19
+ return this.table;
20
+ }
21
+ get tableSchema() {
22
+ return this.schema;
23
+ }
24
+ get adapterRef() {
25
+ return this.adapter;
26
+ }
27
+ async run(query) {
28
+ let result = this.table.data;
29
+ if (query.where) {
30
+ const matchResults = await Promise.all(
31
+ result.map((item) => this.match(item, query.where))
32
+ );
33
+ result = result.filter((_, i) => matchResults[i]);
34
+ } else if (query.type === "select" && query.orderBy && query.orderBy.length > 0) {
35
+ result = [...result];
36
+ }
37
+ if (query.type === "count") {
38
+ return result;
39
+ }
40
+ if (query.select || query.distinct) {
41
+ result = this.project(result, query.select, query.distinct);
42
+ }
43
+ if (query.orderBy && query.orderBy.length > 0) {
44
+ result.sort(
45
+ (a, b) => this.sort(
46
+ a,
47
+ b,
48
+ query.orderBy
49
+ )
50
+ );
51
+ }
52
+ const offset = query.offset ?? 0;
53
+ if (query.limit !== void 0) {
54
+ result = result.slice(offset, offset + query.limit);
55
+ } else if (offset > 0) {
56
+ result = result.slice(offset);
57
+ }
58
+ return result;
59
+ }
60
+ /**
61
+ * Run query without projection (for populate workflow)
62
+ * Applies WHERE, ORDER BY, OFFSET, LIMIT but keeps all fields
63
+ */
64
+ async filterAndSort(query) {
65
+ let result = this.table.data;
66
+ if (query.where) {
67
+ const matchResults = await Promise.all(
68
+ result.map((item) => this.match(item, query.where))
69
+ );
70
+ result = result.filter((_, i) => matchResults[i]);
71
+ } else if (query.orderBy && query.orderBy.length > 0) {
72
+ result = [...result];
73
+ }
74
+ if (query.orderBy && query.orderBy.length > 0) {
75
+ result.sort(
76
+ (a, b) => this.sort(
77
+ a,
78
+ b,
79
+ query.orderBy
80
+ )
81
+ );
82
+ }
83
+ const offset = query.offset ?? 0;
84
+ if (query.limit !== void 0) {
85
+ result = result.slice(offset, offset + query.limit);
86
+ } else if (offset > 0) {
87
+ result = result.slice(offset);
88
+ }
89
+ return result;
90
+ }
91
+ // Exposed for Adapter's RETURNING clause usage
92
+ projectData(data, select, distinct) {
93
+ return this.project(data, select, distinct);
94
+ }
95
+ project(data, select, distinct) {
96
+ let result = data;
97
+ if (select && select !== "*") {
98
+ result = data.map((item) => {
99
+ const projected = {};
100
+ for (const field of select) {
101
+ projected[field] = item[field];
102
+ if (projected[field] === void 0) {
103
+ projected[field] = null;
104
+ }
105
+ }
106
+ return projected;
107
+ });
108
+ }
109
+ if (distinct) {
110
+ const seen = /* @__PURE__ */ new Set();
111
+ result = result.filter((item) => {
112
+ const key = JSON.stringify(item);
113
+ if (seen.has(key)) return false;
114
+ seen.add(key);
115
+ return true;
116
+ });
117
+ }
118
+ return result;
119
+ }
120
+ /**
121
+ * Match WHERE clause against an item
122
+ *
123
+ * **NEW:** Now supports nested relation WHERE queries!
124
+ *
125
+ * @param item - The record to match against
126
+ * @param where - WHERE clause (may contain nested relation conditions)
127
+ * @param overrideSchema - Optional schema to use instead of this.table.schema (for nested relation matching)
128
+ * @returns True if item matches all conditions
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * // Simple WHERE
133
+ * await match(item, { price: { $gt: 10 } })
134
+ *
135
+ * // Nested relation WHERE
136
+ * await match(item, {
137
+ * author: { // Relation field
138
+ * verified: { $eq: true }
139
+ * }
140
+ * })
141
+ * ```
142
+ */
143
+ async match(item, where, overrideSchema) {
144
+ const schema = overrideSchema ?? this.schema;
145
+ for (const [key, value] of Object.entries(where)) {
146
+ if (key === "$and") {
147
+ const results = await Promise.all(
148
+ value.map(
149
+ (cond) => this.match(item, cond, schema)
150
+ )
151
+ );
152
+ if (!results.every((r) => r)) return false;
153
+ continue;
154
+ }
155
+ if (key === "$or") {
156
+ const results = await Promise.all(
157
+ value.map(
158
+ (cond) => this.match(item, cond, schema)
159
+ )
160
+ );
161
+ if (!results.some((r) => r)) return false;
162
+ continue;
163
+ }
164
+ if (key === "$not") {
165
+ if (await this.match(item, value, schema))
166
+ return false;
167
+ continue;
168
+ }
169
+ const fieldDef = schema?.fields?.[key];
170
+ if (fieldDef?.type === "relation") {
171
+ const matched = await this.matchRelation(
172
+ item,
173
+ key,
174
+ value,
175
+ fieldDef
176
+ );
177
+ if (!matched) {
178
+ return false;
179
+ }
180
+ continue;
181
+ }
182
+ if (schema && !schema.fields[key]) {
183
+ throwInvalidWhereField({
184
+ adapter: "json",
185
+ field: key,
186
+ schemaName: schema.name,
187
+ availableFields: Object.keys(schema.fields)
188
+ });
189
+ }
190
+ const itemValue = item[key];
191
+ if (value === null) {
192
+ if (itemValue !== null && itemValue !== void 0) return false;
193
+ } else if (typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) {
194
+ const isOperators = Object.keys(value).some((k) => k.startsWith("$"));
195
+ if (isOperators) {
196
+ if (!this.matchOperators(itemValue, value, key))
197
+ return false;
198
+ } else {
199
+ if (!this.compareValues(itemValue, value, key)) return false;
200
+ }
201
+ } else {
202
+ if (!this.compareValues(itemValue, value, key)) return false;
203
+ }
204
+ }
205
+ return true;
206
+ }
207
+ /**
208
+ * Match nested relation WHERE
209
+ *
210
+ * Loads the related record(s) and recursively matches the nested WHERE clause.
211
+ *
212
+ * @param item - Current record
213
+ * @param relationName - Name of the relation field
214
+ * @param relationWhere - Nested WHERE clause for the relation
215
+ * @param relationField - Relation field definition
216
+ * @returns True if relation matches
217
+ */
218
+ async matchRelation(item, relationName, relationWhere, relationField) {
219
+ const foreignKey = relationField.foreignKey;
220
+ const targetModelName = relationField.model;
221
+ const kind = relationField.kind;
222
+ if (typeof relationWhere === "object" && relationWhere !== null) {
223
+ const keys = Object.keys(relationWhere);
224
+ const logicalOps = /* @__PURE__ */ new Set(["$and", "$or", "$not"]);
225
+ const hasOnlyComparisonOperators = keys.length > 0 && keys.every((k) => k.startsWith("$")) && !keys.some((k) => logicalOps.has(k));
226
+ if (hasOnlyComparisonOperators) {
227
+ throwInvalidRelationWhereSyntax({
228
+ adapter: "json",
229
+ relationName,
230
+ schemaName: this.schema?.name ?? "unknown",
231
+ foreignKey
232
+ });
233
+ }
234
+ }
235
+ if (kind === "belongsTo" || kind === "hasOne") {
236
+ const relatedId = item[foreignKey];
237
+ if (relatedId === null || relatedId === void 0) {
238
+ return false;
239
+ }
240
+ const targetSchema = await this.adapter.getSchemaByModelName(targetModelName);
241
+ if (!targetSchema) {
242
+ return false;
243
+ }
244
+ const relatedRecord = await this.loadRelatedRecord(
245
+ targetModelName,
246
+ relatedId
247
+ );
248
+ if (!relatedRecord) {
249
+ return false;
250
+ }
251
+ return await this.match(relatedRecord, relationWhere, targetSchema);
252
+ }
253
+ if (kind === "hasMany") {
254
+ const sourceId = item["id"];
255
+ if (sourceId === null || sourceId === void 0) {
256
+ return false;
257
+ }
258
+ const targetSchema = await this.adapter.getSchemaByModelName(targetModelName);
259
+ if (!targetSchema) {
260
+ return false;
261
+ }
262
+ const targetTable = targetSchema.tableName ?? targetModelName.toLowerCase();
263
+ const targetTableData = await this.adapter.getCachedTable(targetTable);
264
+ if (!targetTableData) {
265
+ return false;
266
+ }
267
+ const relatedRecords = targetTableData.data.filter(
268
+ (r) => r[foreignKey] === sourceId || r[foreignKey] === Number(sourceId)
269
+ );
270
+ for (const related of relatedRecords) {
271
+ const matches = await this.match(related, relationWhere, targetSchema);
272
+ if (matches) return true;
273
+ }
274
+ return false;
275
+ }
276
+ if (kind === "manyToMany") {
277
+ const junctionTableName = relationField.through;
278
+ if (!junctionTableName) {
279
+ return false;
280
+ }
281
+ const sourceId = item["id"];
282
+ if (sourceId === null || sourceId === void 0) {
283
+ return false;
284
+ }
285
+ const junctionTableData = await this.adapter.getCachedTable(junctionTableName);
286
+ if (!junctionTableData) {
287
+ return false;
288
+ }
289
+ const currentModelName = this.schema?.name ?? "";
290
+ const sourceFK = `${currentModelName}Id`;
291
+ const targetFK = `${targetModelName}Id`;
292
+ const targetIds = junctionTableData.data.filter((row) => {
293
+ const rowSourceId = row[sourceFK];
294
+ return rowSourceId === sourceId || rowSourceId === Number(sourceId);
295
+ }).map((row) => {
296
+ const rawId = row[targetFK];
297
+ return typeof rawId === "string" ? Number(rawId) : rawId;
298
+ }).filter((id) => id !== null && id !== void 0);
299
+ if (targetIds.length === 0) {
300
+ return false;
301
+ }
302
+ const targetSchema = await this.adapter.getSchemaByModelName(targetModelName);
303
+ if (!targetSchema) {
304
+ return false;
305
+ }
306
+ const targetTable = targetSchema.tableName ?? targetModelName.toLowerCase();
307
+ const targetTableData = await this.adapter.getCachedTable(targetTable);
308
+ if (!targetTableData) {
309
+ return false;
310
+ }
311
+ const targetRecords = targetTableData.data.filter((r) => targetIds.includes(r["id"]));
312
+ for (const target of targetRecords) {
313
+ const matches = await this.match(target, relationWhere, targetSchema);
314
+ if (matches) return true;
315
+ }
316
+ return false;
317
+ }
318
+ return false;
319
+ }
320
+ /**
321
+ * Load a related record from adapter's cache
322
+ *
323
+ * Uses getCachedTable which reads from cache or disk if needed.
324
+ *
325
+ * @param modelName - Target model name
326
+ * @param id - Record ID to load
327
+ * @returns Related record or null
328
+ */
329
+ async loadRelatedRecord(modelName, id) {
330
+ try {
331
+ const targetSchema = await this.adapter.getSchemaByModelName(modelName);
332
+ if (!targetSchema) {
333
+ return null;
334
+ }
335
+ const targetTable = targetSchema.tableName ?? modelName.toLowerCase();
336
+ const tableData = await this.adapter.getCachedTable(targetTable);
337
+ if (!tableData) {
338
+ return null;
339
+ }
340
+ const relatedData = tableData.data;
341
+ const record = relatedData.find((r) => r["id"] === id);
342
+ return record ?? null;
343
+ } catch {
344
+ return null;
345
+ }
346
+ }
347
+ compareValues(itemValue, queryValue, fieldName) {
348
+ const schema = this.schema;
349
+ const fieldDef = schema?.fields?.[fieldName];
350
+ if (!fieldDef) {
351
+ return itemValue === queryValue;
352
+ }
353
+ const fieldType = fieldDef.type;
354
+ if (fieldType === "number") {
355
+ const itemNum = Number(itemValue);
356
+ const queryNum = Number(queryValue);
357
+ return !isNaN(itemNum) && !isNaN(queryNum) && itemNum === queryNum;
358
+ }
359
+ if (fieldType === "string") {
360
+ return String(itemValue) === String(queryValue);
361
+ }
362
+ if (fieldType === "boolean") {
363
+ return Boolean(itemValue) === Boolean(queryValue);
364
+ }
365
+ return itemValue === queryValue;
366
+ }
367
+ matchOperators(value, operators, fieldName) {
368
+ for (const [op, opValue] of Object.entries(operators)) {
369
+ switch (op) {
370
+ case "$eq":
371
+ if (!this.compareValues(value, opValue, fieldName)) return false;
372
+ break;
373
+ case "$ne":
374
+ if (this.compareValues(value, opValue, fieldName)) return false;
375
+ break;
376
+ case "$gt": {
377
+ if (value === null || value === void 0) return false;
378
+ const coercedVal = this.coerceForComparison(value, fieldName);
379
+ const coercedOp = this.coerceForComparison(opValue, fieldName);
380
+ if (!(coercedVal > coercedOp)) return false;
381
+ break;
382
+ }
383
+ case "$gte": {
384
+ if (value === null || value === void 0) return false;
385
+ const coercedVal = this.coerceForComparison(value, fieldName);
386
+ const coercedOp = this.coerceForComparison(opValue, fieldName);
387
+ if (!(coercedVal >= coercedOp)) return false;
388
+ break;
389
+ }
390
+ case "$lt": {
391
+ if (value === null || value === void 0) return false;
392
+ const coercedVal = this.coerceForComparison(value, fieldName);
393
+ const coercedOp = this.coerceForComparison(opValue, fieldName);
394
+ if (!(coercedVal < coercedOp)) return false;
395
+ break;
396
+ }
397
+ case "$lte": {
398
+ if (value === null || value === void 0) return false;
399
+ const coercedVal = this.coerceForComparison(value, fieldName);
400
+ const coercedOp = this.coerceForComparison(opValue, fieldName);
401
+ if (!(coercedVal <= coercedOp)) return false;
402
+ break;
403
+ }
404
+ case "$in": {
405
+ const coercedValue = this.coerceForComparison(value, fieldName);
406
+ const coercedArray = opValue.map(
407
+ (v) => this.coerceForComparison(v, fieldName)
408
+ );
409
+ if (!coercedArray.includes(coercedValue)) return false;
410
+ break;
411
+ }
412
+ case "$nin": {
413
+ const coercedValue = this.coerceForComparison(value, fieldName);
414
+ const coercedArray = opValue.map(
415
+ (v) => this.coerceForComparison(v, fieldName)
416
+ );
417
+ if (coercedArray.includes(coercedValue)) return false;
418
+ break;
419
+ }
420
+ case "$exists":
421
+ if (opValue && (value === void 0 || value === null)) return false;
422
+ if (!opValue && value !== void 0 && value !== null) return false;
423
+ break;
424
+ case "$null":
425
+ if (opValue && value !== null && value !== void 0) return false;
426
+ if (!opValue && (value === null || value === void 0)) return false;
427
+ break;
428
+ case "$like":
429
+ case "$ilike": {
430
+ const pattern = opValue.replace(/%/g, ".*").replace(/_/g, ".");
431
+ const flags = op === "$ilike" ? "i" : "";
432
+ const regex = new RegExp(`^${pattern}$`, flags);
433
+ if (!regex.test(String(value ?? ""))) return false;
434
+ break;
435
+ }
436
+ case "$contains":
437
+ if (!String(value ?? "").includes(String(opValue))) return false;
438
+ break;
439
+ case "$notContains":
440
+ if (String(value ?? "").includes(String(opValue))) return false;
441
+ break;
442
+ case "$startsWith":
443
+ if (!String(value ?? "").startsWith(String(opValue))) return false;
444
+ break;
445
+ case "$endsWith":
446
+ if (!String(value ?? "").endsWith(String(opValue))) return false;
447
+ break;
448
+ case "$notNull":
449
+ if (opValue && (value === null || value === void 0)) return false;
450
+ if (!opValue && value !== null && value !== void 0) return false;
451
+ break;
452
+ }
453
+ }
454
+ return true;
455
+ }
456
+ coerceForComparison(value, fieldName) {
457
+ if (value === null || value === void 0) {
458
+ return value;
459
+ }
460
+ const schema = this.schema;
461
+ const fieldDef = schema?.fields?.[fieldName];
462
+ if (!fieldDef) return value;
463
+ const fieldType = fieldDef.type;
464
+ if (fieldType === "number") {
465
+ const num = Number(value);
466
+ return isNaN(num) ? value : num;
467
+ }
468
+ if (fieldType === "string") {
469
+ return String(value);
470
+ }
471
+ return value;
472
+ }
473
+ sort(a, b, orderBy) {
474
+ for (const order of orderBy) {
475
+ const fieldName = order.field;
476
+ const valA = this.coerceForComparison(a[fieldName], fieldName);
477
+ const valB = this.coerceForComparison(b[fieldName], fieldName);
478
+ if (valA === valB) continue;
479
+ const direction = order.direction === "asc" ? 1 : -1;
480
+ if (valA === null || valA === void 0)
481
+ return order.nulls === "first" ? -1 : 1;
482
+ if (valB === null || valB === void 0)
483
+ return order.nulls === "first" ? 1 : -1;
484
+ if (valA < valB) return -1 * direction;
485
+ if (valA > valB) return 1 * direction;
486
+ }
487
+ return 0;
488
+ }
489
+ };
490
+
491
+ // src/lock.ts
492
+ import { throwLockTimeout } from "@datrix/core";
493
+ import fs from "fs/promises";
494
+ import path from "path";
495
+ var SimpleLock = class {
496
+ lockPath;
497
+ lockTimeout;
498
+ // How long to wait to acquire lock
499
+ staleTimeout;
500
+ // How long a lock is valid
501
+ constructor(root, lockTimeout = 5e3, staleTimeout = 3e4) {
502
+ this.lockPath = path.join(root, "db.lock");
503
+ this.lockTimeout = lockTimeout;
504
+ this.staleTimeout = staleTimeout;
505
+ }
506
+ async acquire() {
507
+ const start = Date.now();
508
+ while (true) {
509
+ try {
510
+ await fs.writeFile(this.lockPath, Date.now().toString(), {
511
+ flag: "wx"
512
+ });
513
+ return;
514
+ } catch (error) {
515
+ if (error.code !== "EEXIST") {
516
+ throw error;
517
+ }
518
+ const isStale = await this.checkStale();
519
+ if (isStale) {
520
+ try {
521
+ await fs.unlink(this.lockPath);
522
+ continue;
523
+ } catch (unlinkError) {
524
+ }
525
+ }
526
+ if (Date.now() - start > this.lockTimeout) {
527
+ throwLockTimeout({ adapter: "json", lockTimeout: this.lockTimeout });
528
+ }
529
+ await new Promise((resolve) => setTimeout(resolve, 10));
530
+ }
531
+ }
532
+ }
533
+ async release() {
534
+ try {
535
+ await fs.unlink(this.lockPath);
536
+ } catch (error) {
537
+ if (error.code !== "ENOENT") {
538
+ }
539
+ }
540
+ }
541
+ async checkStale() {
542
+ try {
543
+ const content = await fs.readFile(this.lockPath, "utf-8");
544
+ const timestamp = parseInt(content, 10);
545
+ if (isNaN(timestamp)) return true;
546
+ const age = Date.now() - timestamp;
547
+ return age > this.staleTimeout;
548
+ } catch {
549
+ return false;
550
+ }
551
+ }
552
+ };
553
+
554
+ // src/adapter.ts
555
+ import {
556
+ DatrixAdapterError as DatrixAdapterError2,
557
+ throwNotConnected,
558
+ throwConnectionError,
559
+ throwMigrationError as throwMigrationError2,
560
+ throwTransactionError,
561
+ throwQueryError,
562
+ throwMetaFieldAlreadyExists,
563
+ throwMetaFieldNotFound
564
+ } from "@datrix/core";
565
+
566
+ // src/transaction.ts
567
+ import {
568
+ throwTransactionAlreadyCommitted,
569
+ throwTransactionAlreadyRolledBack,
570
+ throwTransactionSavepointNotSupported,
571
+ throwRawQueryNotSupported
572
+ } from "@datrix/core";
573
+ var JsonTransaction = class {
574
+ constructor(adapter, commitCallback, rollbackCallback) {
575
+ this.adapter = adapter;
576
+ this.commitCallback = commitCallback;
577
+ this.rollbackCallback = rollbackCallback;
578
+ this.id = `tx_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
579
+ }
580
+ id;
581
+ committed = false;
582
+ rolledBack = false;
583
+ /**
584
+ * Check if transaction is still active
585
+ */
586
+ assertActive() {
587
+ if (this.committed) {
588
+ throwTransactionAlreadyCommitted({ adapter: "json" });
589
+ }
590
+ if (this.rolledBack) {
591
+ throwTransactionAlreadyRolledBack({ adapter: "json" });
592
+ }
593
+ }
594
+ /**
595
+ * Execute query within transaction
596
+ *
597
+ * Uses adapter's executeQueryWithOptions with skipLock and skipWrite.
598
+ * Adapter automatically uses transaction cache.
599
+ */
600
+ async executeQuery(query) {
601
+ this.assertActive();
602
+ return this.adapter.executeQueryWithOptions(query, {
603
+ skipLock: true,
604
+ skipWrite: true
605
+ });
606
+ }
607
+ /**
608
+ * Execute raw SQL query (not supported for JSON adapter)
609
+ */
610
+ async executeRawQuery(_sql, _params) {
611
+ throwRawQueryNotSupported({ adapter: "json" });
612
+ }
613
+ // ========================================
614
+ // SchemaOperations implementation
615
+ // ========================================
616
+ /**
617
+ * Create table within transaction
618
+ */
619
+ async createTable(schema) {
620
+ this.assertActive();
621
+ return this.adapter.createTableWithOptions(schema, { skipWrite: true });
622
+ }
623
+ /**
624
+ * Drop table within transaction
625
+ */
626
+ async dropTable(tableName) {
627
+ this.assertActive();
628
+ return this.adapter.dropTableWithOptions(tableName, { skipWrite: true });
629
+ }
630
+ /**
631
+ * Rename table within transaction
632
+ */
633
+ async renameTable(from, to) {
634
+ this.assertActive();
635
+ return this.adapter.renameTableWithOptions(from, to, { skipWrite: true });
636
+ }
637
+ /**
638
+ * Alter table within transaction
639
+ */
640
+ async alterTable(tableName, operations) {
641
+ this.assertActive();
642
+ return this.adapter.alterTableWithOptions(tableName, operations, {
643
+ skipWrite: true
644
+ });
645
+ }
646
+ /**
647
+ * Add index within transaction
648
+ */
649
+ async addIndex(tableName, index) {
650
+ this.assertActive();
651
+ return this.adapter.addIndexWithOptions(tableName, index, {
652
+ skipWrite: true
653
+ });
654
+ }
655
+ /**
656
+ * Drop index within transaction
657
+ */
658
+ async dropIndex(tableName, indexName) {
659
+ this.assertActive();
660
+ return this.adapter.dropIndexWithOptions(tableName, indexName, {
661
+ skipWrite: true
662
+ });
663
+ }
664
+ /**
665
+ * Commit transaction
666
+ *
667
+ * Delegates to adapter's commitTransaction which writes to disk.
668
+ */
669
+ async commit() {
670
+ this.assertActive();
671
+ await this.commitCallback();
672
+ this.committed = true;
673
+ }
674
+ /**
675
+ * Rollback transaction
676
+ *
677
+ * Delegates to adapter's rollbackTransaction which discards changes.
678
+ */
679
+ async rollback() {
680
+ if (this.committed) {
681
+ throwTransactionAlreadyCommitted({ adapter: "json" });
682
+ }
683
+ if (this.rolledBack) {
684
+ return;
685
+ }
686
+ await this.rollbackCallback();
687
+ this.rolledBack = true;
688
+ }
689
+ /**
690
+ * Create savepoint (not yet implemented for JSON adapter)
691
+ */
692
+ async savepoint(_name) {
693
+ throwTransactionSavepointNotSupported({ adapter: "json" });
694
+ }
695
+ /**
696
+ * Rollback to savepoint (not yet implemented for JSON adapter)
697
+ */
698
+ async rollbackTo(_name) {
699
+ throwTransactionSavepointNotSupported({ adapter: "json" });
700
+ }
701
+ /**
702
+ * Release savepoint (not yet implemented for JSON adapter)
703
+ */
704
+ async release(_name) {
705
+ throwTransactionSavepointNotSupported({ adapter: "json" });
706
+ }
707
+ };
708
+
709
+ // src/adapter.ts
710
+ import { FORJA_META_MODEL as FORJA_META_MODEL2, FORJA_META_KEY_PREFIX } from "@datrix/core";
711
+
712
+ // src/table-utils.ts
713
+ import {
714
+ DatrixAdapterError,
715
+ throwForeignKeyConstraint,
716
+ throwMigrationError,
717
+ throwUniqueConstraintField,
718
+ throwUniqueConstraintIndex
719
+ } from "@datrix/core";
720
+ import { FORJA_META_MODEL } from "@datrix/core";
721
+ function validateTableName(tableName) {
722
+ if (tableName.includes("\0")) {
723
+ throwMigrationError({
724
+ adapter: "json",
725
+ message: "Invalid table name: contains null byte",
726
+ table: tableName
727
+ });
728
+ }
729
+ if (tableName.includes("/") || tableName.includes("\\")) {
730
+ throwMigrationError({
731
+ adapter: "json",
732
+ message: "Invalid table name: contains path separators",
733
+ table: tableName
734
+ });
735
+ }
736
+ if (tableName.includes("..")) {
737
+ throwMigrationError({
738
+ adapter: "json",
739
+ message: "Invalid table name: contains parent directory reference",
740
+ table: tableName
741
+ });
742
+ }
743
+ }
744
+ async function createMetaTable(adapter) {
745
+ const metaExists = await adapter.tableExists(FORJA_META_MODEL);
746
+ if (metaExists) {
747
+ return;
748
+ }
749
+ const metaSchema = {
750
+ name: FORJA_META_MODEL,
751
+ tableName: FORJA_META_MODEL,
752
+ fields: {
753
+ id: { type: "number", autoIncrement: true },
754
+ key: { type: "string", required: true, unique: true, maxLength: 255 },
755
+ value: { type: "string", required: true },
756
+ createdAt: { type: "date" },
757
+ updatedAt: { type: "date" }
758
+ }
759
+ };
760
+ await adapter.createTable(metaSchema);
761
+ }
762
+ function applyDefaultValues(schema, data) {
763
+ if (!schema?.fields) return;
764
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
765
+ if (fieldName in data) continue;
766
+ const defaultValue = fieldDef.default;
767
+ if (defaultValue !== void 0) {
768
+ data[fieldName] = defaultValue;
769
+ }
770
+ }
771
+ }
772
+ function checkUniqueConstraints(tableData, schema, newData, excludeId) {
773
+ if (!schema?.fields) return;
774
+ const existingData = tableData.data;
775
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
776
+ if (!fieldDef.unique) continue;
777
+ const value = newData[fieldName];
778
+ if (value === void 0 || value === null) continue;
779
+ const duplicate = existingData.find(
780
+ (row) => row[fieldName] === value && row["id"] !== excludeId
781
+ );
782
+ if (duplicate) {
783
+ throwUniqueConstraintField({
784
+ field: fieldName,
785
+ value,
786
+ adapter: "json",
787
+ table: schema.tableName ?? "unknown"
788
+ });
789
+ }
790
+ }
791
+ if (!schema.indexes) return;
792
+ for (const index of schema.indexes) {
793
+ if (!index.unique) continue;
794
+ const indexValues = index.fields.map((f) => newData[f]);
795
+ if (indexValues.some((v) => v === void 0 || v === null)) continue;
796
+ const duplicate = existingData.find(
797
+ (row) => index.fields.every((f) => row[f] === newData[f]) && row["id"] !== excludeId
798
+ );
799
+ if (duplicate) {
800
+ throwUniqueConstraintIndex({
801
+ fields: index.fields,
802
+ table: schema.tableName ?? "unknown",
803
+ adapter: "json"
804
+ });
805
+ }
806
+ }
807
+ }
808
+ async function checkForeignKeyConstraints(schema, data, adapter) {
809
+ if (!schema?.fields) return;
810
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
811
+ if (fieldDef.type !== "relation") continue;
812
+ const relationField = fieldDef;
813
+ if (relationField.kind !== "belongsTo" && relationField.kind !== "hasOne") {
814
+ continue;
815
+ }
816
+ const foreignKey = relationField.foreignKey ?? `${fieldName}Id`;
817
+ const fkValue = data[foreignKey];
818
+ if (fkValue === void 0 || fkValue === null) continue;
819
+ const targetSchema = await adapter.getSchemaByModelName(
820
+ relationField.model
821
+ );
822
+ if (!targetSchema) continue;
823
+ const targetTable = targetSchema.tableName ?? relationField.model.toLowerCase();
824
+ const targetData = await adapter.getCachedTable(targetTable);
825
+ if (!targetData) continue;
826
+ const exists = targetData.data.some((row) => row["id"] === fkValue);
827
+ if (!exists) {
828
+ throwForeignKeyConstraint({
829
+ foreignKey,
830
+ value: fkValue,
831
+ targetModel: relationField.model,
832
+ table: schema.tableName ?? "unknown",
833
+ adapter: "json"
834
+ });
835
+ }
836
+ }
837
+ }
838
+ async function findFkDependencies(targetTable, adapter) {
839
+ const allTables = await adapter.getTables();
840
+ const deps = [];
841
+ for (const tableName of allTables) {
842
+ const schema = await adapter.getSchemaByTableName(tableName);
843
+ if (!schema?.fields) continue;
844
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
845
+ if (fieldDef.type !== "number") continue;
846
+ const numField = fieldDef;
847
+ const ref = numField.references;
848
+ if (!ref || ref.table !== targetTable) continue;
849
+ const onDelete = ref.onDelete ?? "setNull";
850
+ deps.push({ tableName, fieldName, onDelete });
851
+ }
852
+ }
853
+ return deps;
854
+ }
855
+ async function applyOnDeleteActions(targetTable, idsToDelete, adapter, queryOptions) {
856
+ if (idsToDelete.length === 0) return;
857
+ const deps = await findFkDependencies(targetTable, adapter);
858
+ if (deps.length === 0) return;
859
+ for (const dep of deps) {
860
+ if (dep.onDelete !== "restrict") continue;
861
+ const tableData = await adapter.getCachedTable(dep.tableName);
862
+ if (!tableData) continue;
863
+ const hasReference = tableData.data.some(
864
+ (row) => idsToDelete.includes(row[dep.fieldName])
865
+ );
866
+ if (hasReference) {
867
+ throw new DatrixAdapterError(
868
+ `Cannot delete from '${targetTable}': referenced by '${dep.tableName}.${dep.fieldName}' with ON DELETE RESTRICT`,
869
+ {
870
+ adapter: "json",
871
+ code: "ADAPTER_FOREIGN_KEY_CONSTRAINT",
872
+ operation: "query",
873
+ context: {
874
+ table: targetTable,
875
+ referencedBy: `${dep.tableName}.${dep.fieldName}`
876
+ },
877
+ suggestion: `Remove or update referencing rows in '${dep.tableName}' before deleting from '${targetTable}'`
878
+ }
879
+ );
880
+ }
881
+ }
882
+ for (const dep of deps) {
883
+ if (dep.onDelete !== "setNull") continue;
884
+ await adapter.executeQueryWithOptions(
885
+ {
886
+ type: "update",
887
+ table: dep.tableName,
888
+ where: { [dep.fieldName]: { $in: idsToDelete } },
889
+ data: { [dep.fieldName]: null }
890
+ },
891
+ queryOptions
892
+ );
893
+ }
894
+ for (const dep of deps) {
895
+ if (dep.onDelete !== "cascade") continue;
896
+ const tableData = await adapter.getCachedTable(dep.tableName);
897
+ if (!tableData) continue;
898
+ const childIds = tableData.data.filter((row) => idsToDelete.includes(row[dep.fieldName])).map((row) => row["id"]);
899
+ if (childIds.length === 0) continue;
900
+ await applyOnDeleteActions(dep.tableName, childIds, adapter, queryOptions);
901
+ await adapter.executeQueryWithOptions(
902
+ {
903
+ type: "delete",
904
+ table: dep.tableName,
905
+ where: { id: { $in: childIds } }
906
+ },
907
+ queryOptions
908
+ );
909
+ }
910
+ }
911
+ function applySelectRecursive(rows, select, populate) {
912
+ if (!rows || rows.length === 0) {
913
+ return rows;
914
+ }
915
+ let result = rows;
916
+ if (select && select !== "*") {
917
+ const fieldsToKeep = new Set(select);
918
+ if (populate) {
919
+ for (const relationName of Object.keys(populate)) {
920
+ fieldsToKeep.add(relationName);
921
+ }
922
+ }
923
+ result = rows.map((row) => {
924
+ const projected = {};
925
+ for (const field of fieldsToKeep) {
926
+ if (field in row) {
927
+ projected[field] = row[field];
928
+ }
929
+ }
930
+ return projected;
931
+ });
932
+ }
933
+ if (populate) {
934
+ for (const [relationName, options] of Object.entries(populate)) {
935
+ if (typeof options === "boolean") continue;
936
+ const nestedSelect = options === "*" ? "*" : options.select;
937
+ const nestedPopulate = options === "*" ? void 0 : options.populate;
938
+ for (const row of result) {
939
+ const relationValue = row[relationName];
940
+ if (!relationValue) continue;
941
+ if (Array.isArray(relationValue)) {
942
+ row[relationName] = applySelectRecursive(
943
+ relationValue,
944
+ nestedSelect,
945
+ nestedPopulate
946
+ );
947
+ } else {
948
+ row[relationName] = applySelectRecursive(
949
+ [relationValue],
950
+ nestedSelect,
951
+ nestedPopulate
952
+ )[0];
953
+ }
954
+ }
955
+ }
956
+ }
957
+ return result;
958
+ }
959
+
960
+ // src/export-import/exporter.ts
961
+ var CHUNK_SIZE = 1e3;
962
+ var JsonExporter = class {
963
+ constructor(root, adapter) {
964
+ this.root = root;
965
+ this.adapter = adapter;
966
+ }
967
+ async export(writer) {
968
+ await writer.writeMeta({
969
+ version: 1,
970
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
971
+ });
972
+ const tables = await this.adapter.getTables();
973
+ for (const tableName of tables) {
974
+ const schema = await this.adapter.getTableSchema(tableName);
975
+ if (schema) {
976
+ await writer.writeSchema(schema);
977
+ }
978
+ }
979
+ for (const tableName of tables) {
980
+ await this.exportTable(tableName, writer);
981
+ }
982
+ await writer.finalize();
983
+ }
984
+ async exportTable(tableName, writer) {
985
+ const fs3 = await import("fs/promises");
986
+ const path3 = await import("path");
987
+ const filePath = path3.join(this.root, `${tableName}.json`);
988
+ const content = await fs3.readFile(filePath, "utf-8");
989
+ const tableFile = JSON.parse(content);
990
+ const rows = tableFile.data;
991
+ for (let i = 0; i < rows.length; i += CHUNK_SIZE) {
992
+ await writer.writeChunk(tableName, rows.slice(i, i + CHUNK_SIZE));
993
+ }
994
+ if (rows.length === 0) {
995
+ await writer.writeChunk(tableName, []);
996
+ }
997
+ }
998
+ };
999
+
1000
+ // src/export-import/importer.ts
1001
+ var JsonImporter = class {
1002
+ constructor(root, adapter) {
1003
+ this.root = root;
1004
+ this.adapter = adapter;
1005
+ }
1006
+ async import(reader) {
1007
+ const schemas = await this.collectSchemas(reader);
1008
+ const existingTables = await this.adapter.getTables();
1009
+ for (const tableName of existingTables) {
1010
+ await this.adapter.dropTableWithOptions(tableName, { isImport: true });
1011
+ }
1012
+ for (const schema of schemas.values()) {
1013
+ await this.adapter.createTable(schema, { isImport: true });
1014
+ }
1015
+ const tables = await reader.getTables();
1016
+ for (const tableName of tables) {
1017
+ const rows = [];
1018
+ for await (const chunk of reader.readChunks(tableName)) {
1019
+ rows.push(...chunk);
1020
+ }
1021
+ await this.writeTableFile(tableName, rows);
1022
+ }
1023
+ }
1024
+ async collectSchemas(reader) {
1025
+ const schemas = /* @__PURE__ */ new Map();
1026
+ for await (const schema of reader.readSchemas()) {
1027
+ schemas.set(schema.tableName, schema);
1028
+ }
1029
+ return schemas;
1030
+ }
1031
+ async writeTableFile(tableName, rows) {
1032
+ const fs3 = await import("fs/promises");
1033
+ const path3 = await import("path");
1034
+ const filePath = path3.join(this.root, `${tableName}.json`);
1035
+ const maxId = rows.reduce((max, row) => {
1036
+ const id = typeof row["id"] === "number" ? row["id"] : 0;
1037
+ return id > max ? id : max;
1038
+ }, 0);
1039
+ const tableFile = {
1040
+ meta: {
1041
+ version: 1,
1042
+ lastInsertId: maxId,
1043
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1044
+ name: tableName
1045
+ },
1046
+ data: rows
1047
+ };
1048
+ await fs3.writeFile(filePath, JSON.stringify(tableFile, null, 2), "utf-8");
1049
+ }
1050
+ };
1051
+
1052
+ // src/populate.ts
1053
+ import {
1054
+ throwSchemaNotFound,
1055
+ throwRelationNotFound,
1056
+ throwInvalidRelationType,
1057
+ throwTargetModelNotFound
1058
+ } from "@datrix/core";
1059
+ var JsonPopulator = class {
1060
+ constructor(adapter) {
1061
+ this.adapter = adapter;
1062
+ }
1063
+ async populate(rows, query) {
1064
+ if (!query.populate || rows.length === 0) {
1065
+ return rows;
1066
+ }
1067
+ const _currentSchema = await this.adapter.getSchemaByTableName(query.table);
1068
+ if (!_currentSchema) {
1069
+ throwSchemaNotFound({ adapter: "json", modelName: query.table });
1070
+ }
1071
+ const currentSchema = _currentSchema;
1072
+ const currentModelName = currentSchema.name;
1073
+ const result = [...rows];
1074
+ for (const [relationName, _options] of Object.entries(query.populate)) {
1075
+ const relationField = currentSchema.fields[relationName];
1076
+ if (!relationField) {
1077
+ throwRelationNotFound({
1078
+ adapter: "json",
1079
+ relationName,
1080
+ schemaName: currentSchema.name
1081
+ });
1082
+ }
1083
+ if (relationField.type !== "relation") {
1084
+ throwInvalidRelationType({
1085
+ adapter: "json",
1086
+ relationName,
1087
+ fieldType: relationField.type,
1088
+ schemaName: currentSchema.name
1089
+ });
1090
+ }
1091
+ const relField = relationField;
1092
+ const targetModelName = relField.model;
1093
+ const foreignKey = relField.foreignKey;
1094
+ const kind = relField.kind;
1095
+ const targetSchema = await this.adapter.getSchemaByModelName(targetModelName);
1096
+ if (!targetSchema) {
1097
+ throwTargetModelNotFound({
1098
+ adapter: "json",
1099
+ targetModel: targetModelName,
1100
+ relationName,
1101
+ schemaName: currentSchema.name
1102
+ });
1103
+ }
1104
+ const targetTable = targetSchema.tableName ?? targetModelName.toLowerCase();
1105
+ const tableData = await this.adapter.getCachedTable(targetTable);
1106
+ if (!tableData) continue;
1107
+ const relatedData = tableData.data;
1108
+ const options = typeof _options === "object" && _options !== null && !Array.isArray(_options) ? _options : void 0;
1109
+ if (kind === "belongsTo") {
1110
+ const ids = new Set(
1111
+ result.map((r) => r[foreignKey]).filter((id) => id !== null && id !== void 0)
1112
+ );
1113
+ const relatedMap = /* @__PURE__ */ new Map();
1114
+ if (ids.size > 0) {
1115
+ for (const item of relatedData) {
1116
+ const itemId = item["id"];
1117
+ if (ids.has(itemId)) {
1118
+ relatedMap.set(itemId, item);
1119
+ }
1120
+ }
1121
+ }
1122
+ let filteredMap = relatedMap;
1123
+ if (options?.where) {
1124
+ filteredMap = /* @__PURE__ */ new Map();
1125
+ const filterRunner = new JsonQueryRunner(
1126
+ tableData,
1127
+ this.adapter,
1128
+ targetSchema
1129
+ );
1130
+ for (const [id, item] of relatedMap) {
1131
+ const matched = await filterRunner.filterAndSort({
1132
+ type: "select",
1133
+ table: targetTable,
1134
+ where: options.where,
1135
+ select: "*"
1136
+ });
1137
+ if (matched.some((r) => r["id"] === id)) {
1138
+ filteredMap.set(id, item);
1139
+ }
1140
+ }
1141
+ }
1142
+ for (const row of result) {
1143
+ const fkValue = row[foreignKey];
1144
+ if (fkValue !== null && fkValue !== void 0) {
1145
+ row[relationName] = filteredMap.get(fkValue) ?? null;
1146
+ } else {
1147
+ row[relationName] = null;
1148
+ }
1149
+ }
1150
+ } else if (kind === "hasMany" || kind === "hasOne") {
1151
+ const sourceIds = new Set(
1152
+ result.map((r) => r["id"]).filter((id) => id !== null && id !== void 0)
1153
+ );
1154
+ const grouped = /* @__PURE__ */ new Map();
1155
+ for (const item of relatedData) {
1156
+ const fkValue = item[foreignKey];
1157
+ if (fkValue !== null && fkValue !== void 0 && sourceIds.has(fkValue)) {
1158
+ const group = grouped.get(fkValue) ?? [];
1159
+ group.push(item);
1160
+ grouped.set(fkValue, group);
1161
+ }
1162
+ }
1163
+ const hasSortOrFilter = options?.where || options?.orderBy || options?.limit !== void 0 || options?.offset !== void 0;
1164
+ for (const row of result) {
1165
+ const rowId = row["id"];
1166
+ let group = grouped.get(rowId) ?? [];
1167
+ if (kind === "hasOne") {
1168
+ row[relationName] = group[0] ?? null;
1169
+ } else {
1170
+ if (hasSortOrFilter && group.length > 0) {
1171
+ const groupTable = { ...tableData, data: group };
1172
+ const groupRunner = new JsonQueryRunner(
1173
+ groupTable,
1174
+ this.adapter,
1175
+ targetSchema
1176
+ );
1177
+ group = await groupRunner.filterAndSort({
1178
+ type: "select",
1179
+ table: targetTable,
1180
+ where: options?.where,
1181
+ orderBy: options?.orderBy,
1182
+ limit: options?.limit,
1183
+ offset: options?.offset,
1184
+ select: "*"
1185
+ });
1186
+ }
1187
+ row[relationName] = group;
1188
+ }
1189
+ }
1190
+ } else if (kind === "manyToMany") {
1191
+ const junctionTableName = relField.through;
1192
+ const sourceFK = `${currentModelName}Id`;
1193
+ const targetFK = `${targetModelName}Id`;
1194
+ const junctionData = await this.adapter.getCachedTable(junctionTableName);
1195
+ if (!junctionData) {
1196
+ throw new Error(
1197
+ `Junction table '${junctionTableName}' not found for manyToMany relation '${relationName}' in schema '${currentSchema.name}'`
1198
+ );
1199
+ }
1200
+ const sourceIds = result.map((r) => r["id"]).filter((id) => id !== null && id !== void 0);
1201
+ if (sourceIds.length === 0) continue;
1202
+ const junctionRunner = new JsonQueryRunner(junctionData, this.adapter);
1203
+ const relevantJunctions = await junctionRunner.run({
1204
+ type: "select",
1205
+ table: junctionTableName,
1206
+ where: { [sourceFK]: { $in: sourceIds } },
1207
+ select: "*"
1208
+ });
1209
+ const mapping = /* @__PURE__ */ new Map();
1210
+ for (const junction of relevantJunctions) {
1211
+ const srcId = junction[sourceFK];
1212
+ const tgtId = junction[targetFK];
1213
+ const normalizedSrcId = typeof srcId === "string" ? Number(srcId) : srcId;
1214
+ const normalizedTgtId = typeof tgtId === "string" ? Number(tgtId) : tgtId;
1215
+ const existing = mapping.get(normalizedSrcId) ?? [];
1216
+ existing.push(normalizedTgtId);
1217
+ mapping.set(normalizedSrcId, existing);
1218
+ }
1219
+ const allTargetIds = /* @__PURE__ */ new Set();
1220
+ for (const ids of mapping.values()) {
1221
+ ids.forEach((id) => allTargetIds.add(id));
1222
+ }
1223
+ const targetDataForRunner = await this.adapter.getCachedTable(targetTable);
1224
+ if (!targetDataForRunner) continue;
1225
+ const idFilter = { id: { $in: Array.from(allTargetIds) } };
1226
+ const userWhere = options?.where;
1227
+ const mergedWhere = userWhere ? { $and: [idFilter, userWhere] } : idFilter;
1228
+ const targetRunner = new JsonQueryRunner(
1229
+ targetDataForRunner,
1230
+ this.adapter,
1231
+ targetSchema
1232
+ );
1233
+ const targetRecords = await targetRunner.run({
1234
+ type: "select",
1235
+ table: targetTable,
1236
+ where: mergedWhere,
1237
+ orderBy: options?.orderBy,
1238
+ select: "*"
1239
+ });
1240
+ for (const row of result) {
1241
+ const rowId = row["id"];
1242
+ const normalizedRowId = typeof rowId === "string" ? Number(rowId) : rowId;
1243
+ const targetIds = mapping.get(normalizedRowId) ?? [];
1244
+ let relatedRecords = targetRecords.filter((r) => {
1245
+ const rID = r["id"];
1246
+ const normalizedRID = typeof rID === "string" ? Number(rID) : rID;
1247
+ return targetIds.includes(normalizedRID);
1248
+ });
1249
+ const offset = options?.offset ?? 0;
1250
+ if (options?.limit !== void 0) {
1251
+ relatedRecords = relatedRecords.slice(
1252
+ offset,
1253
+ offset + options.limit
1254
+ );
1255
+ } else if (offset > 0) {
1256
+ relatedRecords = relatedRecords.slice(offset);
1257
+ }
1258
+ row[relationName] = relatedRecords;
1259
+ }
1260
+ }
1261
+ if (typeof _options === "object" && _options !== null && _options.populate) {
1262
+ const nextRows = [];
1263
+ for (const row of result) {
1264
+ const val = row[relationName];
1265
+ if (!val) continue;
1266
+ if (Array.isArray(val)) {
1267
+ nextRows.push(...val);
1268
+ } else {
1269
+ nextRows.push(val);
1270
+ }
1271
+ }
1272
+ if (nextRows.length > 0) {
1273
+ await this.populate(nextRows, {
1274
+ type: "select",
1275
+ table: targetTable,
1276
+ populate: _options.populate,
1277
+ select: "*"
1278
+ });
1279
+ }
1280
+ }
1281
+ }
1282
+ return result;
1283
+ }
1284
+ };
1285
+
1286
+ // src/query-handlers.ts
1287
+ import { throwQueryMissingData } from "@datrix/core";
1288
+ async function handleSelect(ctx) {
1289
+ const { runner, query, adapter } = ctx;
1290
+ let rows;
1291
+ if (query.populate) {
1292
+ rows = await runner.filterAndSort(query);
1293
+ const populator = new JsonPopulator(adapter);
1294
+ rows = await populator.populate(rows, query);
1295
+ rows = applySelectRecursive(rows, query.select, query.populate);
1296
+ } else {
1297
+ rows = await runner.run(query);
1298
+ }
1299
+ return {
1300
+ rows,
1301
+ metadata: { rowCount: rows.length, affectedRows: 0 },
1302
+ shouldWrite: false
1303
+ };
1304
+ }
1305
+ async function handleCount(ctx) {
1306
+ const { runner, query } = ctx;
1307
+ const rows = await runner.run(query);
1308
+ return {
1309
+ rows: [],
1310
+ metadata: { rowCount: 0, affectedRows: 0, count: rows.length },
1311
+ shouldWrite: false,
1312
+ earlyReturn: true
1313
+ };
1314
+ }
1315
+ async function handleInsert(ctx) {
1316
+ const { runner, query } = ctx;
1317
+ const tableData = runner.tableData;
1318
+ const tableSchema = runner.tableSchema;
1319
+ const adapter = runner.adapterRef;
1320
+ if (!query.data || !Array.isArray(query.data)) {
1321
+ throwQueryMissingData({
1322
+ queryType: "insert",
1323
+ table: query.table,
1324
+ adapter: "json"
1325
+ });
1326
+ }
1327
+ const insertedIds = [];
1328
+ const isJunctionTable = tableSchema?._isJunctionTable === true;
1329
+ for (const item of query.data) {
1330
+ const newItem = { ...item };
1331
+ if (isJunctionTable) {
1332
+ const alreadyExists = tableData.data.some(
1333
+ (row) => Object.keys(newItem).every(
1334
+ (key) => key === "id" || row[key] === newItem[key]
1335
+ )
1336
+ );
1337
+ if (alreadyExists) continue;
1338
+ }
1339
+ if (!newItem["id"]) {
1340
+ tableData.meta.lastInsertId = (tableData.meta.lastInsertId ?? 0) + 1;
1341
+ newItem["id"] = tableData.meta.lastInsertId;
1342
+ } else {
1343
+ const manualId = Number(newItem["id"]);
1344
+ if (!isNaN(manualId) && manualId > (tableData.meta.lastInsertId ?? 0)) {
1345
+ tableData.meta.lastInsertId = manualId;
1346
+ }
1347
+ }
1348
+ applyDefaultValues(tableSchema, newItem);
1349
+ await checkForeignKeyConstraints(tableSchema, newItem, adapter);
1350
+ checkUniqueConstraints(tableData, tableSchema, newItem);
1351
+ tableData.data.push(newItem);
1352
+ insertedIds.push(newItem["id"]);
1353
+ }
1354
+ const rows = insertedIds.map((id) => ({ id }));
1355
+ return {
1356
+ rows,
1357
+ metadata: {
1358
+ rowCount: insertedIds.length,
1359
+ affectedRows: insertedIds.length,
1360
+ insertIds: insertedIds
1361
+ },
1362
+ shouldWrite: true
1363
+ };
1364
+ }
1365
+ async function handleUpdate(ctx) {
1366
+ const { runner, query } = ctx;
1367
+ const tableData = runner.tableData;
1368
+ const tableSchema = runner.tableSchema;
1369
+ const adapter = runner.adapterRef;
1370
+ if (!query.data) {
1371
+ throwQueryMissingData({
1372
+ queryType: "update",
1373
+ table: query.table,
1374
+ adapter: "json"
1375
+ });
1376
+ }
1377
+ const updateQuery = {
1378
+ ...query,
1379
+ limit: void 0,
1380
+ offset: void 0,
1381
+ orderBy: void 0
1382
+ };
1383
+ const rowsToUpdate = await runner.filterAndSort(updateQuery);
1384
+ for (const row of rowsToUpdate) {
1385
+ const updatedData = { ...row, ...query.data };
1386
+ await checkForeignKeyConstraints(tableSchema, updatedData, adapter);
1387
+ checkUniqueConstraints(
1388
+ tableData,
1389
+ tableSchema,
1390
+ updatedData,
1391
+ row["id"]
1392
+ );
1393
+ }
1394
+ for (const row of rowsToUpdate) {
1395
+ Object.assign(row, query.data);
1396
+ }
1397
+ const updatedIds = rowsToUpdate.map((r) => r["id"]);
1398
+ const rows = updatedIds.map((id) => ({ id }));
1399
+ return {
1400
+ rows,
1401
+ metadata: { rowCount: updatedIds.length, affectedRows: updatedIds.length },
1402
+ shouldWrite: true
1403
+ };
1404
+ }
1405
+ async function handleDelete(ctx) {
1406
+ const { runner, query, adapter, queryOptions } = ctx;
1407
+ const tableData = runner.tableData;
1408
+ const deleteQuery = {
1409
+ ...query,
1410
+ limit: void 0,
1411
+ offset: void 0,
1412
+ orderBy: void 0
1413
+ };
1414
+ const rowsToDelete = await runner.filterAndSort(deleteQuery);
1415
+ const idsToDelete = rowsToDelete.map((r) => r.id);
1416
+ await applyOnDeleteActions(query.table, idsToDelete, adapter, queryOptions);
1417
+ const idsSet = new Set(idsToDelete);
1418
+ const originalLength = tableData.data.length;
1419
+ tableData.data = tableData.data.filter((d) => !idsSet.has(d["id"]));
1420
+ const deletedIds = rowsToDelete.map((r) => r["id"]);
1421
+ const rows = deletedIds.map((id) => ({ id }));
1422
+ return {
1423
+ rows,
1424
+ metadata: {
1425
+ rowCount: deletedIds.length,
1426
+ affectedRows: originalLength - tableData.data.length
1427
+ },
1428
+ shouldWrite: true
1429
+ };
1430
+ }
1431
+
1432
+ // src/adapter.ts
1433
+ var JsonAdapter = class {
1434
+ name = "json";
1435
+ config;
1436
+ state = "disconnected";
1437
+ cache = /* @__PURE__ */ new Map();
1438
+ lock;
1439
+ cacheEnabled;
1440
+ readLockEnabled;
1441
+ /**
1442
+ * Active transaction cache reference
1443
+ * When a transaction is active, all reads/writes go through this cache first.
1444
+ * Set by beginTransaction, cleared by commit/rollback.
1445
+ */
1446
+ activeTransactionCache = null;
1447
+ /**
1448
+ * Track modified tables during transaction for commit
1449
+ */
1450
+ activeTransactionModifiedTables = null;
1451
+ /**
1452
+ * Tombstone set for tables deleted during transaction.
1453
+ * Prevents fallback to main cache or disk for dropped tables.
1454
+ */
1455
+ activeTransactionDeletedTables = null;
1456
+ constructor(config) {
1457
+ this.config = config;
1458
+ this.lock = new SimpleLock(
1459
+ config.root,
1460
+ config.lockTimeout,
1461
+ config.staleTimeout
1462
+ );
1463
+ this.cacheEnabled = config.cache !== false;
1464
+ this.readLockEnabled = config.readLock === true;
1465
+ }
1466
+ /**
1467
+ * Connect involves ensuring the root directory exists
1468
+ */
1469
+ async connect() {
1470
+ if (this.state === "connected") {
1471
+ return;
1472
+ }
1473
+ this.state = "connecting";
1474
+ try {
1475
+ await fs2.mkdir(this.config.root, { recursive: true });
1476
+ this.state = "connected";
1477
+ if (this.config.standalone) {
1478
+ try {
1479
+ await createMetaTable(this);
1480
+ } catch (error) {
1481
+ this.state = "error";
1482
+ throw error;
1483
+ }
1484
+ }
1485
+ } catch (error) {
1486
+ this.state = "error";
1487
+ const message = error instanceof Error ? error.message : String(error);
1488
+ throwConnectionError({
1489
+ adapter: "json",
1490
+ message: `Failed to access root directory: ${message}`,
1491
+ cause: error instanceof Error ? error : new Error(String(error))
1492
+ });
1493
+ }
1494
+ }
1495
+ async disconnect() {
1496
+ this.state = "disconnected";
1497
+ }
1498
+ isConnected() {
1499
+ return this.state === "connected";
1500
+ }
1501
+ getConnectionState() {
1502
+ return this.state;
1503
+ }
1504
+ /**
1505
+ * Helper to get file path for a table
1506
+ */
1507
+ getTablePath(tableName) {
1508
+ return path2.join(this.config.root, `${tableName}.json`);
1509
+ }
1510
+ /**
1511
+ * Read table with cache support
1512
+ *
1513
+ * Cache lookup order:
1514
+ * 1. Check tombstone (if transaction active and table was dropped)
1515
+ * 2. Transaction cache (if active)
1516
+ * 3. Main cache (with mtime validation)
1517
+ * 4. Disk
1518
+ *
1519
+ * When transaction is active, new reads are cached in transaction cache.
1520
+ * This ensures isolation - transaction sees its own writes.
1521
+ */
1522
+ async readTable(tableName) {
1523
+ const filePath = this.getTablePath(tableName);
1524
+ if (this.activeTransactionDeletedTables?.has(tableName)) {
1525
+ throw new Error(`Table '${tableName}' does not exist`);
1526
+ }
1527
+ if (this.activeTransactionCache) {
1528
+ const txCached = this.activeTransactionCache.get(tableName);
1529
+ if (txCached) {
1530
+ return txCached.data;
1531
+ }
1532
+ }
1533
+ if (this.cacheEnabled) {
1534
+ const stat = await fs2.stat(filePath);
1535
+ const mtime = stat.mtimeMs;
1536
+ const cached = this.cache.get(tableName);
1537
+ if (cached && cached.mtime === mtime) {
1538
+ if (this.activeTransactionCache) {
1539
+ const txData = JSON.parse(JSON.stringify(cached.data));
1540
+ this.activeTransactionCache.set(tableName, { data: txData, mtime });
1541
+ return txData;
1542
+ }
1543
+ return cached.data;
1544
+ }
1545
+ const content2 = await fs2.readFile(filePath, "utf-8");
1546
+ const data = JSON.parse(content2);
1547
+ if (this.activeTransactionCache) {
1548
+ this.activeTransactionCache.set(tableName, { data, mtime });
1549
+ } else {
1550
+ this.cache.set(tableName, { data, mtime });
1551
+ }
1552
+ return data;
1553
+ }
1554
+ const content = await fs2.readFile(filePath, "utf-8");
1555
+ return JSON.parse(content);
1556
+ }
1557
+ /**
1558
+ * Get cached table data (for external use like Populate)
1559
+ */
1560
+ async getCachedTable(tableName) {
1561
+ try {
1562
+ return await this.readTable(tableName);
1563
+ } catch {
1564
+ return null;
1565
+ }
1566
+ }
1567
+ /**
1568
+ * Get schema directly from table file (cache-aware)
1569
+ * This is faster than going through Datrix registry and ensures consistency
1570
+ *
1571
+ * @param tableName - Table name (e.g., "users")
1572
+ * @returns Schema definition or null if not found
1573
+ */
1574
+ async getSchemaByTableName(tableName) {
1575
+ try {
1576
+ return await this.readTableSchema(tableName);
1577
+ } catch {
1578
+ return null;
1579
+ }
1580
+ }
1581
+ /**
1582
+ * Get schema by model name
1583
+ * Requires scanning all tables to find matching schema.name
1584
+ * Prefer getSchemaByTableName when table name is known (faster)
1585
+ *
1586
+ * @param modelName - Model name from schema (e.g., "User")
1587
+ * @returns Schema definition or null if not found
1588
+ */
1589
+ async getSchemaByModelName(modelName) {
1590
+ try {
1591
+ const tablesResult = await this.getTables();
1592
+ for (const tableName of tablesResult) {
1593
+ const schema = await this.getSchemaByTableName(tableName);
1594
+ if (schema?.name === modelName) {
1595
+ return schema;
1596
+ }
1597
+ }
1598
+ return null;
1599
+ } catch {
1600
+ return null;
1601
+ }
1602
+ }
1603
+ /**
1604
+ * Find table name by schema model name
1605
+ *
1606
+ * @param modelName - Model name (e.g., "User")
1607
+ * @returns Table name or null if not found
1608
+ */
1609
+ async findTableNameByModelName(modelName) {
1610
+ const schema = await this.getSchemaByModelName(modelName);
1611
+ return schema?.tableName ?? null;
1612
+ }
1613
+ /**
1614
+ * Read schema for a table from _datrix metadata table.
1615
+ * Transaction-aware: reads from tx cache when inside a transaction.
1616
+ *
1617
+ * @param tableName - Physical table name (e.g. "users")
1618
+ */
1619
+ async readTableSchema(tableName) {
1620
+ const metaFile = await this.readTable(FORJA_META_MODEL2);
1621
+ const metaKey = `${FORJA_META_KEY_PREFIX}${tableName}`;
1622
+ const row = metaFile.data.find(
1623
+ (r) => r["key"] === metaKey
1624
+ );
1625
+ if (!row) {
1626
+ throw new Error(`Schema for '${tableName}' not found in _datrix`);
1627
+ }
1628
+ return JSON.parse(
1629
+ row["value"]
1630
+ );
1631
+ }
1632
+ /**
1633
+ * Upsert schema into _datrix metadata table
1634
+ */
1635
+ async upsertSchemaMeta(schema, skipWrite) {
1636
+ const metaKey = `${FORJA_META_KEY_PREFIX}${schema.tableName ?? schema.name}`;
1637
+ const metaValue = JSON.stringify(schema);
1638
+ const metaFile = await this.readTable(FORJA_META_MODEL2);
1639
+ const existingIndex = metaFile.data.findIndex(
1640
+ (r) => r["key"] === metaKey
1641
+ );
1642
+ if (existingIndex >= 0) {
1643
+ metaFile.data[existingIndex]["value"] = metaValue;
1644
+ } else {
1645
+ const lastInsertId = (metaFile.meta.lastInsertId ?? 0) + 1;
1646
+ metaFile.meta.lastInsertId = lastInsertId;
1647
+ metaFile.data.push({
1648
+ id: lastInsertId,
1649
+ key: metaKey,
1650
+ value: metaValue
1651
+ });
1652
+ }
1653
+ metaFile.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1654
+ if (skipWrite) {
1655
+ this.activeTransactionCache.set(FORJA_META_MODEL2, {
1656
+ data: metaFile,
1657
+ mtime: Date.now()
1658
+ });
1659
+ this.activeTransactionModifiedTables.add(FORJA_META_MODEL2);
1660
+ } else {
1661
+ const filePath = this.getTablePath(FORJA_META_MODEL2);
1662
+ await fs2.writeFile(filePath, JSON.stringify(metaFile, null, 2), "utf-8");
1663
+ await this.updateCache(FORJA_META_MODEL2, metaFile);
1664
+ }
1665
+ }
1666
+ /**
1667
+ * Apply AlterOperations to schema in _datrix and write back
1668
+ */
1669
+ async applyOperationsToMetaSchema(tableName, operations, skipWrite) {
1670
+ const schema = await this.readTableSchema(tableName);
1671
+ const fields = { ...schema.fields };
1672
+ for (const op of operations) {
1673
+ switch (op.type) {
1674
+ case "addColumn":
1675
+ fields[op.column] = op.definition;
1676
+ break;
1677
+ case "dropColumn":
1678
+ delete fields[op.column];
1679
+ break;
1680
+ case "modifyColumn":
1681
+ fields[op.column] = op.newDefinition;
1682
+ break;
1683
+ case "renameColumn": {
1684
+ const fieldDef = fields[op.from];
1685
+ if (fieldDef !== void 0) {
1686
+ fields[op.to] = fieldDef;
1687
+ delete fields[op.from];
1688
+ }
1689
+ for (const [key, def] of Object.entries(fields)) {
1690
+ if (def.type === "relation" && def.foreignKey === op.from) {
1691
+ fields[key] = { ...def, foreignKey: op.to };
1692
+ }
1693
+ }
1694
+ break;
1695
+ }
1696
+ case "addMetaField":
1697
+ if (fields[op.field] !== void 0) {
1698
+ throwMetaFieldAlreadyExists({
1699
+ adapter: "json",
1700
+ field: op.field,
1701
+ table: tableName
1702
+ });
1703
+ }
1704
+ fields[op.field] = op.definition;
1705
+ break;
1706
+ case "dropMetaField":
1707
+ if (fields[op.field] === void 0) {
1708
+ throwMetaFieldNotFound({
1709
+ adapter: "json",
1710
+ field: op.field,
1711
+ table: tableName
1712
+ });
1713
+ }
1714
+ delete fields[op.field];
1715
+ break;
1716
+ case "modifyMetaField":
1717
+ if (fields[op.field] === void 0) {
1718
+ throwMetaFieldNotFound({
1719
+ adapter: "json",
1720
+ field: op.field,
1721
+ table: tableName
1722
+ });
1723
+ }
1724
+ fields[op.field] = op.newDefinition;
1725
+ break;
1726
+ }
1727
+ }
1728
+ const updatedSchema = { ...schema, fields };
1729
+ await this.upsertSchemaMeta(updatedSchema, skipWrite);
1730
+ }
1731
+ /**
1732
+ * Invalidate cache for a specific table
1733
+ */
1734
+ invalidateCache(tableName) {
1735
+ this.cache.delete(tableName);
1736
+ }
1737
+ /**
1738
+ * Update cache after write operation
1739
+ */
1740
+ async updateCache(tableName, data) {
1741
+ if (!this.cacheEnabled) return;
1742
+ const filePath = this.getTablePath(tableName);
1743
+ try {
1744
+ const stat = await fs2.stat(filePath);
1745
+ this.cache.set(tableName, { data, mtime: stat.mtimeMs });
1746
+ } catch {
1747
+ this.invalidateCache(tableName);
1748
+ }
1749
+ }
1750
+ async exportData(writer) {
1751
+ await new JsonExporter(this.config.root, this).export(writer);
1752
+ }
1753
+ async importData(reader) {
1754
+ await new JsonImporter(this.config.root, this).import(reader);
1755
+ }
1756
+ async createTable(schema, options) {
1757
+ return this.createTableWithOptions(schema, void 0, options?.isImport);
1758
+ }
1759
+ /**
1760
+ * Create table with options (for transaction support)
1761
+ */
1762
+ async createTableWithOptions(schema, options, isImport) {
1763
+ const skipWrite = options?.skipWrite ?? false;
1764
+ if (this.config.standalone && !("id" in schema.fields)) {
1765
+ schema = {
1766
+ ...schema,
1767
+ fields: {
1768
+ id: { type: "number", autoIncrement: true },
1769
+ ...schema.fields
1770
+ }
1771
+ };
1772
+ }
1773
+ if (!this.isConnected()) {
1774
+ throwNotConnected({ adapter: "json" });
1775
+ }
1776
+ const tableName = schema.tableName;
1777
+ validateTableName(tableName);
1778
+ if (this.activeTransactionCache?.has(tableName)) {
1779
+ throwMigrationError2({
1780
+ adapter: "json",
1781
+ message: `Table '${schema.name}' already exists`,
1782
+ table: tableName
1783
+ });
1784
+ }
1785
+ const wasDeleted = this.activeTransactionDeletedTables?.has(tableName);
1786
+ if (wasDeleted) {
1787
+ this.activeTransactionDeletedTables.delete(tableName);
1788
+ }
1789
+ if (!wasDeleted) {
1790
+ const filePath = this.getTablePath(tableName);
1791
+ try {
1792
+ await fs2.access(filePath);
1793
+ throwMigrationError2({
1794
+ adapter: "json",
1795
+ message: `Table '${schema.name}' already exists`
1796
+ });
1797
+ } catch (err) {
1798
+ if (err instanceof DatrixAdapterError2) throw err;
1799
+ }
1800
+ }
1801
+ const initialContent = {
1802
+ meta: {
1803
+ version: 1,
1804
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1805
+ name: schema.name
1806
+ },
1807
+ data: []
1808
+ };
1809
+ if (skipWrite) {
1810
+ this.activeTransactionCache.set(tableName, {
1811
+ data: initialContent,
1812
+ mtime: Date.now()
1813
+ });
1814
+ this.activeTransactionModifiedTables.add(tableName);
1815
+ } else {
1816
+ const filePath = this.getTablePath(tableName);
1817
+ await fs2.writeFile(
1818
+ filePath,
1819
+ JSON.stringify(initialContent, null, 2),
1820
+ "utf-8"
1821
+ );
1822
+ await this.updateCache(tableName, initialContent);
1823
+ }
1824
+ if (!isImport) {
1825
+ if (schema.name !== FORJA_META_MODEL2) {
1826
+ const metaExists = await this.tableExists(FORJA_META_MODEL2);
1827
+ if (!metaExists) {
1828
+ throwMigrationError2({
1829
+ adapter: "json",
1830
+ message: `Cannot create table '${schema.name}': '${FORJA_META_MODEL2}' table does not exist yet. Create '${FORJA_META_MODEL2}' first.`
1831
+ });
1832
+ }
1833
+ }
1834
+ await this.upsertSchemaMeta(schema, skipWrite);
1835
+ }
1836
+ }
1837
+ async dropTable(tableName) {
1838
+ return this.dropTableWithOptions(tableName);
1839
+ }
1840
+ /**
1841
+ * Drop table with options (for transaction support)
1842
+ */
1843
+ async dropTableWithOptions(tableName, options) {
1844
+ const skipWrite = options?.skipWrite ?? false;
1845
+ const isImport = options?.isImport ?? false;
1846
+ if (!this.isConnected()) {
1847
+ throwNotConnected({ adapter: "json" });
1848
+ }
1849
+ if (this.activeTransactionDeletedTables?.has(tableName)) {
1850
+ throwMigrationError2({
1851
+ adapter: "json",
1852
+ message: `Table '${tableName}' does not exist`
1853
+ });
1854
+ }
1855
+ const existsInTxCache = this.activeTransactionCache?.has(tableName);
1856
+ const existsInMainCache = this.cache.has(tableName);
1857
+ let existsOnDisk = false;
1858
+ if (!existsInTxCache && !existsInMainCache) {
1859
+ const filePath = this.getTablePath(tableName);
1860
+ try {
1861
+ await fs2.access(filePath);
1862
+ existsOnDisk = true;
1863
+ } catch {
1864
+ }
1865
+ }
1866
+ if (!existsInTxCache && !existsInMainCache && !existsOnDisk) {
1867
+ throwMigrationError2({
1868
+ adapter: "json",
1869
+ message: `Table '${tableName}' does not exist`
1870
+ });
1871
+ }
1872
+ if (skipWrite) {
1873
+ this.activeTransactionDeletedTables.add(tableName);
1874
+ this.activeTransactionCache.delete(tableName);
1875
+ } else {
1876
+ const filePath = this.getTablePath(tableName);
1877
+ await fs2.unlink(filePath);
1878
+ this.invalidateCache(tableName);
1879
+ }
1880
+ if (!isImport && tableName !== FORJA_META_MODEL2) {
1881
+ const metaKey = `${FORJA_META_KEY_PREFIX}${tableName}`;
1882
+ const metaFile = await this.readTable(FORJA_META_MODEL2);
1883
+ metaFile.data = metaFile.data.filter(
1884
+ (r) => r["key"] !== metaKey
1885
+ );
1886
+ metaFile.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1887
+ if (skipWrite) {
1888
+ this.activeTransactionCache.set(FORJA_META_MODEL2, {
1889
+ data: metaFile,
1890
+ mtime: Date.now()
1891
+ });
1892
+ this.activeTransactionModifiedTables.add(FORJA_META_MODEL2);
1893
+ } else {
1894
+ const filePath = this.getTablePath(FORJA_META_MODEL2);
1895
+ await fs2.writeFile(
1896
+ filePath,
1897
+ JSON.stringify(metaFile, null, 2),
1898
+ "utf-8"
1899
+ );
1900
+ await this.updateCache(FORJA_META_MODEL2, metaFile);
1901
+ }
1902
+ }
1903
+ }
1904
+ /**
1905
+ * Execute query (public interface)
1906
+ *
1907
+ * This is the standard DatabaseAdapter interface method.
1908
+ * Internally calls executeQueryWithOptions with default options.
1909
+ */
1910
+ async executeQuery(query) {
1911
+ return this.executeQueryWithOptions(query);
1912
+ }
1913
+ /**
1914
+ * Execute query with options (for transaction support)
1915
+ *
1916
+ * @param query - Query to execute
1917
+ * @param options - Execution options
1918
+ * @param options.skipLock - Skip lock acquisition (transaction already holds lock)
1919
+ * @param options.skipWrite - Skip writing to disk (transaction will write on commit)
1920
+ */
1921
+ async executeQueryWithOptions(query, options) {
1922
+ const skipLock = options?.skipLock ?? false;
1923
+ const skipWrite = options?.skipWrite ?? false;
1924
+ validateQueryObject(query);
1925
+ if (!this.isConnected()) {
1926
+ throwNotConnected({ adapter: "json" });
1927
+ }
1928
+ const isWriteOp = ["insert", "update", "delete"].includes(query.type);
1929
+ const needsLock = !skipLock && (isWriteOp || this.readLockEnabled);
1930
+ let lockAcquired = false;
1931
+ if (needsLock) {
1932
+ try {
1933
+ await this.lock.acquire();
1934
+ lockAcquired = true;
1935
+ } catch (err) {
1936
+ throwQueryError({
1937
+ adapter: "json",
1938
+ message: `Failed to acquire lock: ${err instanceof Error ? err.message : String(err)}`,
1939
+ query,
1940
+ cause: err instanceof Error ? err : new Error(String(err))
1941
+ });
1942
+ }
1943
+ }
1944
+ try {
1945
+ let tableData;
1946
+ try {
1947
+ tableData = await this.readTable(query.table);
1948
+ } catch (err) {
1949
+ if (lockAcquired) await this.lock.release();
1950
+ throwQueryError({
1951
+ adapter: "json",
1952
+ message: `Table '${query.table}' not found`,
1953
+ query,
1954
+ cause: err instanceof Error ? err : new Error(String(err))
1955
+ });
1956
+ }
1957
+ if (!tableData.data || !Array.isArray(tableData.data)) {
1958
+ tableData.data = [];
1959
+ }
1960
+ let tableSchema;
1961
+ try {
1962
+ tableSchema = await this.readTableSchema(query.table);
1963
+ } catch {
1964
+ }
1965
+ const runner = new JsonQueryRunner(tableData, this, tableSchema);
1966
+ let handlerResult;
1967
+ switch (query.type) {
1968
+ case "count":
1969
+ handlerResult = await handleCount({ runner, query });
1970
+ if (handlerResult.earlyReturn) {
1971
+ if (lockAcquired) await this.lock.release();
1972
+ return {
1973
+ rows: [],
1974
+ metadata: handlerResult.metadata
1975
+ };
1976
+ }
1977
+ break;
1978
+ case "select":
1979
+ handlerResult = await handleSelect({ runner, query, adapter: this });
1980
+ break;
1981
+ case "insert":
1982
+ handlerResult = await handleInsert({ runner, query });
1983
+ break;
1984
+ case "update":
1985
+ handlerResult = await handleUpdate({ runner, query });
1986
+ break;
1987
+ case "delete":
1988
+ handlerResult = await handleDelete({
1989
+ runner,
1990
+ query,
1991
+ adapter: this,
1992
+ queryOptions: { skipLock: true, skipWrite }
1993
+ });
1994
+ break;
1995
+ }
1996
+ const rows = handlerResult.rows;
1997
+ const metadata = handlerResult.metadata;
1998
+ const shouldWrite = handlerResult.shouldWrite;
1999
+ if (shouldWrite) {
2000
+ if (skipWrite) {
2001
+ if (this.activeTransactionModifiedTables) {
2002
+ this.activeTransactionModifiedTables.add(query.table);
2003
+ }
2004
+ } else {
2005
+ tableData.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2006
+ const filePath = this.getTablePath(query.table);
2007
+ await fs2.writeFile(
2008
+ filePath,
2009
+ JSON.stringify(tableData, null, 2),
2010
+ "utf-8"
2011
+ );
2012
+ await this.updateCache(query.table, tableData);
2013
+ }
2014
+ }
2015
+ metadata.rowCount = rows.length;
2016
+ if (lockAcquired) await this.lock.release();
2017
+ return {
2018
+ rows,
2019
+ metadata
2020
+ };
2021
+ } catch (error) {
2022
+ if (lockAcquired) await this.lock.release();
2023
+ throw error;
2024
+ }
2025
+ }
2026
+ async executeRawQuery(_sql, _params) {
2027
+ throwQueryError({
2028
+ adapter: "json",
2029
+ message: "executeRawQuery is not supported by JsonAdapter"
2030
+ });
2031
+ }
2032
+ /**
2033
+ * Begin a new transaction
2034
+ *
2035
+ * Acquires lock and creates isolated transaction cache.
2036
+ * All reads/writes within transaction use txCache.
2037
+ */
2038
+ async beginTransaction() {
2039
+ if (!this.isConnected()) {
2040
+ throwNotConnected({ adapter: "json" });
2041
+ }
2042
+ if (this.activeTransactionCache) {
2043
+ throwTransactionError({
2044
+ adapter: "json",
2045
+ message: "A transaction is already active"
2046
+ });
2047
+ }
2048
+ try {
2049
+ await this.lock.acquire();
2050
+ this.activeTransactionCache = /* @__PURE__ */ new Map();
2051
+ this.activeTransactionModifiedTables = /* @__PURE__ */ new Set();
2052
+ this.activeTransactionDeletedTables = /* @__PURE__ */ new Set();
2053
+ const transaction = new JsonTransaction(
2054
+ this,
2055
+ // Commit callback
2056
+ async () => {
2057
+ await this.commitTransaction();
2058
+ },
2059
+ // Rollback callback
2060
+ async () => {
2061
+ await this.rollbackTransaction();
2062
+ }
2063
+ );
2064
+ return transaction;
2065
+ } catch (err) {
2066
+ throwTransactionError({
2067
+ adapter: "json",
2068
+ message: `Failed to begin transaction: ${err instanceof Error ? err.message : String(err)}`,
2069
+ cause: err instanceof Error ? err : new Error(String(err))
2070
+ });
2071
+ }
2072
+ }
2073
+ /**
2074
+ * Commit transaction - write modified tables to disk
2075
+ * @internal Called by JsonTransaction.commit()
2076
+ */
2077
+ async commitTransaction() {
2078
+ if (!this.activeTransactionCache || !this.activeTransactionModifiedTables) {
2079
+ throwTransactionError({
2080
+ adapter: "json",
2081
+ message: "No active transaction to commit"
2082
+ });
2083
+ }
2084
+ try {
2085
+ if (this.activeTransactionDeletedTables) {
2086
+ for (const tableName of this.activeTransactionDeletedTables) {
2087
+ const filePath = this.getTablePath(tableName);
2088
+ try {
2089
+ await fs2.unlink(filePath);
2090
+ } catch {
2091
+ }
2092
+ this.cache.delete(tableName);
2093
+ }
2094
+ }
2095
+ for (const tableName of this.activeTransactionModifiedTables) {
2096
+ if (this.activeTransactionDeletedTables?.has(tableName)) continue;
2097
+ const entry = this.activeTransactionCache.get(tableName);
2098
+ if (entry) {
2099
+ entry.data.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2100
+ const filePath = this.getTablePath(tableName);
2101
+ await fs2.writeFile(
2102
+ filePath,
2103
+ JSON.stringify(entry.data, null, 2),
2104
+ "utf-8"
2105
+ );
2106
+ const stat = await fs2.stat(filePath);
2107
+ entry.mtime = stat.mtimeMs;
2108
+ this.cache.set(tableName, entry);
2109
+ }
2110
+ }
2111
+ } finally {
2112
+ this.activeTransactionCache = null;
2113
+ this.activeTransactionModifiedTables = null;
2114
+ this.activeTransactionDeletedTables = null;
2115
+ await this.lock.release();
2116
+ }
2117
+ }
2118
+ /**
2119
+ * Rollback transaction - discard changes
2120
+ * @internal Called by JsonTransaction.rollback()
2121
+ */
2122
+ async rollbackTransaction() {
2123
+ this.activeTransactionCache = null;
2124
+ this.activeTransactionModifiedTables = null;
2125
+ this.activeTransactionDeletedTables = null;
2126
+ await this.lock.release();
2127
+ }
2128
+ async alterTable(tableName, operations) {
2129
+ return this.alterTableWithOptions(tableName, operations);
2130
+ }
2131
+ /**
2132
+ * Alter table with options (for transaction support)
2133
+ */
2134
+ async alterTableWithOptions(tableName, operations, options) {
2135
+ const skipWrite = options?.skipWrite ?? false;
2136
+ if (!this.isConnected()) {
2137
+ throwNotConnected({ adapter: "json" });
2138
+ }
2139
+ const json = await this.readTable(tableName);
2140
+ for (const op of operations) {
2141
+ switch (op.type) {
2142
+ case "addColumn": {
2143
+ const defaultValue = op.definition.default;
2144
+ for (const row of json.data) {
2145
+ if (!(op.column in row)) {
2146
+ row[op.column] = defaultValue ?? null;
2147
+ }
2148
+ }
2149
+ break;
2150
+ }
2151
+ case "dropColumn": {
2152
+ for (const row of json.data) {
2153
+ delete row[op.column];
2154
+ }
2155
+ break;
2156
+ }
2157
+ case "modifyColumn": {
2158
+ break;
2159
+ }
2160
+ case "renameColumn": {
2161
+ for (const row of json.data) {
2162
+ const r = row;
2163
+ if (op.from in r) {
2164
+ r[op.to] = r[op.from];
2165
+ delete r[op.from];
2166
+ }
2167
+ }
2168
+ break;
2169
+ }
2170
+ }
2171
+ }
2172
+ json.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2173
+ if (skipWrite) {
2174
+ this.activeTransactionCache.set(tableName, {
2175
+ data: json,
2176
+ mtime: Date.now()
2177
+ });
2178
+ this.activeTransactionModifiedTables.add(tableName);
2179
+ } else {
2180
+ const filePath = this.getTablePath(tableName);
2181
+ await fs2.writeFile(filePath, JSON.stringify(json, null, 2), "utf-8");
2182
+ await this.updateCache(tableName, json);
2183
+ }
2184
+ if (tableName !== FORJA_META_MODEL2) {
2185
+ await this.applyOperationsToMetaSchema(tableName, operations, skipWrite);
2186
+ }
2187
+ }
2188
+ async renameTable(from, to) {
2189
+ return this.renameTableWithOptions(from, to);
2190
+ }
2191
+ /**
2192
+ * Rename table with options (for transaction support)
2193
+ */
2194
+ async renameTableWithOptions(from, to, options) {
2195
+ const skipWrite = options?.skipWrite ?? false;
2196
+ if (!this.isConnected()) {
2197
+ throwNotConnected({ adapter: "json" });
2198
+ }
2199
+ validateTableName(to);
2200
+ if (this.activeTransactionDeletedTables?.has(from)) {
2201
+ throwMigrationError2({
2202
+ adapter: "json",
2203
+ message: `Table '${from}' does not exist`
2204
+ });
2205
+ }
2206
+ const targetExistsInTxCache = this.activeTransactionCache?.has(to);
2207
+ const targetExistsInMainCache = this.cache.has(to);
2208
+ let targetExistsOnDisk = false;
2209
+ if (!targetExistsInTxCache && !targetExistsInMainCache) {
2210
+ const toPath = this.getTablePath(to);
2211
+ try {
2212
+ await fs2.access(toPath);
2213
+ targetExistsOnDisk = true;
2214
+ } catch {
2215
+ }
2216
+ }
2217
+ const targetInTombstone = this.activeTransactionDeletedTables?.has(to);
2218
+ if ((targetExistsInTxCache || targetExistsInMainCache || targetExistsOnDisk) && !targetInTombstone) {
2219
+ throwMigrationError2({
2220
+ adapter: "json",
2221
+ message: `Table '${to}' already exists`
2222
+ });
2223
+ }
2224
+ const json = await this.readTable(from);
2225
+ json.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2226
+ if (skipWrite) {
2227
+ this.activeTransactionCache.set(to, {
2228
+ data: json,
2229
+ mtime: Date.now()
2230
+ });
2231
+ this.activeTransactionModifiedTables.add(to);
2232
+ this.activeTransactionDeletedTables.add(from);
2233
+ this.activeTransactionCache.delete(from);
2234
+ this.activeTransactionDeletedTables.delete(to);
2235
+ } else {
2236
+ const fromPath = this.getTablePath(from);
2237
+ const toPath = this.getTablePath(to);
2238
+ await fs2.writeFile(toPath, JSON.stringify(json, null, 2), "utf-8");
2239
+ await fs2.unlink(fromPath);
2240
+ this.invalidateCache(from);
2241
+ await this.updateCache(to, json);
2242
+ }
2243
+ if (from !== FORJA_META_MODEL2 && to !== FORJA_META_MODEL2) {
2244
+ const oldKey = `${FORJA_META_KEY_PREFIX}${from}`;
2245
+ const newKey = `${FORJA_META_KEY_PREFIX}${to}`;
2246
+ const metaFile = await this.readTable(FORJA_META_MODEL2);
2247
+ const row = metaFile.data.find(
2248
+ (r) => r["key"] === oldKey
2249
+ );
2250
+ if (row) {
2251
+ row["key"] = newKey;
2252
+ metaFile.meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2253
+ if (skipWrite) {
2254
+ this.activeTransactionCache.set(FORJA_META_MODEL2, {
2255
+ data: metaFile,
2256
+ mtime: Date.now()
2257
+ });
2258
+ this.activeTransactionModifiedTables.add(FORJA_META_MODEL2);
2259
+ } else {
2260
+ const metaPath = this.getTablePath(FORJA_META_MODEL2);
2261
+ await fs2.writeFile(
2262
+ metaPath,
2263
+ JSON.stringify(metaFile, null, 2),
2264
+ "utf-8"
2265
+ );
2266
+ await this.updateCache(FORJA_META_MODEL2, metaFile);
2267
+ }
2268
+ }
2269
+ }
2270
+ }
2271
+ async addIndex(tableName, index) {
2272
+ return this.addIndexWithOptions(tableName, index);
2273
+ }
2274
+ /**
2275
+ * Add index with options (for transaction support)
2276
+ * Note: JSON adapter doesn't actually create indexes, but we track the operation
2277
+ */
2278
+ async addIndexWithOptions(_tableName, _index, _options) {
2279
+ }
2280
+ async dropIndex(tableName, indexName) {
2281
+ return this.dropIndexWithOptions(tableName, indexName);
2282
+ }
2283
+ /**
2284
+ * Drop index with options (for transaction support)
2285
+ * Note: JSON adapter doesn't actually manage indexes, but we track the operation
2286
+ */
2287
+ async dropIndexWithOptions(_tableName, _indexName, _options) {
2288
+ }
2289
+ async getTables() {
2290
+ if (!this.isConnected()) {
2291
+ throwNotConnected({ adapter: "json" });
2292
+ }
2293
+ const files = await fs2.readdir(this.config.root);
2294
+ const tables = files.filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
2295
+ return tables;
2296
+ }
2297
+ async getTableSchema(tableName) {
2298
+ if (!this.isConnected()) {
2299
+ throwNotConnected({ adapter: "json" });
2300
+ }
2301
+ try {
2302
+ const schema = await this.readTableSchema(tableName);
2303
+ return schema;
2304
+ } catch {
2305
+ return null;
2306
+ }
2307
+ }
2308
+ async tableExists(tableName) {
2309
+ if (!this.isConnected()) return false;
2310
+ try {
2311
+ await fs2.access(this.getTablePath(tableName));
2312
+ return true;
2313
+ } catch {
2314
+ return false;
2315
+ }
2316
+ }
2317
+ };
2318
+ export {
2319
+ JsonAdapter
2320
+ };
2321
+ //# sourceMappingURL=index.mjs.map