js-bao 0.2.10 → 0.2.12

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 (67) hide show
  1. package/README.md +174 -0
  2. package/dist/BaseModel-5YQCROYE.js +17 -0
  3. package/dist/BaseModel-5YQCROYE.js.map +1 -0
  4. package/dist/BaseModel-FCNWDJBH.js +17 -0
  5. package/dist/BaseModel-FCNWDJBH.js.map +1 -0
  6. package/dist/BrowserDatabaseFactory-PXOTK2DQ.js +119 -0
  7. package/dist/BrowserDatabaseFactory-PXOTK2DQ.js.map +1 -0
  8. package/dist/BrowserDatabaseFactory-WD4VX2VZ.js +119 -0
  9. package/dist/BrowserDatabaseFactory-WD4VX2VZ.js.map +1 -0
  10. package/dist/IncludeResolver-RCKQGNPZ.js +385 -0
  11. package/dist/IncludeResolver-RCKQGNPZ.js.map +1 -0
  12. package/dist/IncludeResolver-WGSQDMS7.js +385 -0
  13. package/dist/IncludeResolver-WGSQDMS7.js.map +1 -0
  14. package/dist/NodeDatabaseFactory-J4Z36UF3.js +165 -0
  15. package/dist/NodeDatabaseFactory-J4Z36UF3.js.map +1 -0
  16. package/dist/NodeDatabaseFactory-QIEKAXBM.js +10 -0
  17. package/dist/NodeDatabaseFactory-QIEKAXBM.js.map +1 -0
  18. package/dist/NodeSqliteEngine-HJSAYE4E.js +383 -0
  19. package/dist/NodeSqliteEngine-HJSAYE4E.js.map +1 -0
  20. package/dist/NodeSqliteEngine-I5SLWLME.js +383 -0
  21. package/dist/NodeSqliteEngine-I5SLWLME.js.map +1 -0
  22. package/dist/browser.cjs +3779 -3370
  23. package/dist/browser.d.cts +18 -1
  24. package/dist/browser.d.ts +18 -1
  25. package/dist/browser.js +3750 -3341
  26. package/dist/chunk-3PZWHUZO.js +4153 -0
  27. package/dist/chunk-3PZWHUZO.js.map +1 -0
  28. package/dist/chunk-53MS4MN7.js +373 -0
  29. package/dist/chunk-53MS4MN7.js.map +1 -0
  30. package/dist/chunk-65G2P4GL.js +709 -0
  31. package/dist/chunk-65G2P4GL.js.map +1 -0
  32. package/dist/chunk-6UX3YSCW.js +4151 -0
  33. package/dist/chunk-6UX3YSCW.js.map +1 -0
  34. package/dist/chunk-DANSD6BE.js +709 -0
  35. package/dist/chunk-DANSD6BE.js.map +1 -0
  36. package/dist/chunk-DF3JEQXA.js +373 -0
  37. package/dist/chunk-DF3JEQXA.js.map +1 -0
  38. package/dist/chunk-GO3APTPX.js +61 -0
  39. package/dist/chunk-GO3APTPX.js.map +1 -0
  40. package/dist/chunk-ID4U6IQC.js +53 -0
  41. package/dist/chunk-ID4U6IQC.js.map +1 -0
  42. package/dist/chunk-RQVS3LVL.js +165 -0
  43. package/dist/chunk-RQVS3LVL.js.map +1 -0
  44. package/dist/client.cjs +837 -0
  45. package/dist/client.d.cts +1101 -0
  46. package/dist/client.d.ts +1101 -0
  47. package/dist/client.js +806 -0
  48. package/dist/cloudflare-do.cjs +3637 -0
  49. package/dist/cloudflare-do.d.cts +1366 -0
  50. package/dist/cloudflare-do.d.ts +1366 -0
  51. package/dist/cloudflare-do.js +3614 -0
  52. package/dist/cloudflare.cjs +1048 -0
  53. package/dist/cloudflare.d.cts +1381 -0
  54. package/dist/cloudflare.d.ts +1381 -0
  55. package/dist/cloudflare.js +1017 -0
  56. package/dist/codegen.cjs +259 -18
  57. package/dist/environment-TOTQICSE.js +17 -0
  58. package/dist/environment-TOTQICSE.js.map +1 -0
  59. package/dist/index.cjs +1906 -1493
  60. package/dist/index.d.cts +19 -2
  61. package/dist/index.d.ts +19 -2
  62. package/dist/index.js +1871 -1458
  63. package/dist/node.cjs +4779 -4366
  64. package/dist/node.d.cts +18 -1
  65. package/dist/node.d.ts +18 -1
  66. package/dist/node.js +4602 -4189
  67. package/package.json +41 -12
@@ -0,0 +1,3637 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/cloudflare-do.ts
21
+ var cloudflare_do_exports = {};
22
+ __export(cloudflare_do_exports, {
23
+ DurableObjectEngine: () => DurableObjectEngine,
24
+ JsonQueryTranslator: () => JsonQueryTranslator,
25
+ JsonSchemaDDL: () => JsonSchemaDDL,
26
+ createDatabaseDO: () => createDatabaseDO,
27
+ createDocumentDO: () => createDocumentDO,
28
+ handleRequest: () => handleRequest
29
+ });
30
+ module.exports = __toCommonJS(cloudflare_do_exports);
31
+
32
+ // src/engines/DatabaseEngine.ts
33
+ var DatabaseEngine = class {
34
+ createTable(_modelName, _schema, _options) {
35
+ throw new Error("Method not implemented.");
36
+ }
37
+ createStringSetJunctionTable(_modelName, _fieldName) {
38
+ throw new Error("Method not implemented.");
39
+ }
40
+ insertStringSetValues(_modelName, _fieldName, _recordId, _values) {
41
+ throw new Error("Method not implemented.");
42
+ }
43
+ removeStringSetValues(_modelName, _fieldName, _recordId, _values) {
44
+ throw new Error("Method not implemented.");
45
+ }
46
+ insert(_modelName, _data) {
47
+ throw new Error("Method not implemented.");
48
+ }
49
+ delete(_modelName, _id) {
50
+ throw new Error("Method not implemented.");
51
+ }
52
+ /**
53
+ * Deletes all records for a specific document from the given model table.
54
+ * This is used when disconnecting a document to remove all its data.
55
+ * @param modelName The name of the model/table
56
+ * @param docId The document ID to filter by
57
+ */
58
+ deleteByDocumentId(_modelName, _docId) {
59
+ throw new Error("Method not implemented.");
60
+ }
61
+ getTableName(_modelName) {
62
+ throw new Error("Method not implemented.");
63
+ }
64
+ // Transaction support
65
+ async withTransaction(_callback) {
66
+ throw new Error("Method not implemented.");
67
+ }
68
+ };
69
+
70
+ // src/utils/sql.ts
71
+ var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
72
+ function isValidIdentifier(name) {
73
+ return IDENTIFIER_PATTERN.test(name);
74
+ }
75
+ function assertValidIdentifier(name, context) {
76
+ if (!isValidIdentifier(name)) {
77
+ throw new Error(
78
+ `${context}: Identifier "${name}" must match ${IDENTIFIER_PATTERN.source}`
79
+ );
80
+ }
81
+ }
82
+ function quoteIdentifier(name) {
83
+ return `"${name.replace(/"/g, '""')}"`;
84
+ }
85
+
86
+ // src/schema/JsonSchemaDDL.ts
87
+ var JsonSchemaDDL = class {
88
+ /**
89
+ * Generate CREATE TABLE statement for the records table
90
+ */
91
+ static createRecordsTable(options) {
92
+ if (options.includeDocIdColumn) {
93
+ return `
94
+ CREATE TABLE IF NOT EXISTS records (
95
+ _id TEXT NOT NULL,
96
+ _type TEXT NOT NULL,
97
+ _data TEXT NOT NULL,
98
+ _meta_doc_id TEXT,
99
+ _meta_permission_hint TEXT,
100
+ PRIMARY KEY (_type, _id)
101
+ )`.trim();
102
+ } else {
103
+ return `
104
+ CREATE TABLE IF NOT EXISTS records (
105
+ _id TEXT NOT NULL,
106
+ _type TEXT NOT NULL,
107
+ _data TEXT NOT NULL,
108
+ PRIMARY KEY (_type, _id)
109
+ )`.trim();
110
+ }
111
+ }
112
+ /**
113
+ * Generate CREATE TABLE statement for the stringset_index table
114
+ */
115
+ static createStringSetIndexTable(options) {
116
+ if (options.includeDocIdColumn) {
117
+ return `
118
+ CREATE TABLE IF NOT EXISTS stringset_index (
119
+ _record_id TEXT NOT NULL,
120
+ _type TEXT NOT NULL,
121
+ field TEXT NOT NULL,
122
+ value TEXT NOT NULL,
123
+ _meta_doc_id TEXT,
124
+ UNIQUE(_type, field, _record_id, value)
125
+ )`.trim();
126
+ } else {
127
+ return `
128
+ CREATE TABLE IF NOT EXISTS stringset_index (
129
+ _record_id TEXT NOT NULL,
130
+ _type TEXT NOT NULL,
131
+ field TEXT NOT NULL,
132
+ value TEXT NOT NULL,
133
+ UNIQUE(_type, field, _record_id, value)
134
+ )`.trim();
135
+ }
136
+ }
137
+ /**
138
+ * Generate base indexes for the schema
139
+ */
140
+ static createBaseIndexes(options) {
141
+ const indexes = [
142
+ // Index on _type for filtering by model
143
+ "CREATE INDEX IF NOT EXISTS idx_records_type ON records(_type)",
144
+ // StringSet indexes for efficient lookups
145
+ "CREATE INDEX IF NOT EXISTS idx_ss_type_field_value ON stringset_index(_type, field, value)",
146
+ "CREATE INDEX IF NOT EXISTS idx_ss_type_field_record ON stringset_index(_type, field, _record_id)"
147
+ ];
148
+ if (options.includeDocIdColumn) {
149
+ indexes.push(
150
+ "CREATE INDEX IF NOT EXISTS idx_records_doc ON records(_meta_doc_id)",
151
+ "CREATE INDEX IF NOT EXISTS idx_ss_doc ON stringset_index(_meta_doc_id)"
152
+ );
153
+ }
154
+ return indexes;
155
+ }
156
+ /**
157
+ * Generate CREATE INDEX statement for a model field
158
+ * Uses json_extract for JSON schema
159
+ */
160
+ static createFieldIndex(modelName, fieldName) {
161
+ assertValidIdentifier(modelName, "createFieldIndex modelName");
162
+ assertValidIdentifier(fieldName, "createFieldIndex fieldName");
163
+ const indexName = `idx_records_${modelName.toLowerCase()}_${fieldName.toLowerCase()}`;
164
+ return `CREATE INDEX IF NOT EXISTS ${indexName} ON records(json_extract(_data, '$.${fieldName}')) WHERE _type = '${modelName}'`;
165
+ }
166
+ /**
167
+ * Generate DROP INDEX statement for a model field
168
+ */
169
+ static dropFieldIndex(modelName, fieldName) {
170
+ assertValidIdentifier(modelName, "dropFieldIndex modelName");
171
+ assertValidIdentifier(fieldName, "dropFieldIndex fieldName");
172
+ const indexName = `idx_records_${modelName.toLowerCase()}_${fieldName.toLowerCase()}`;
173
+ return `DROP INDEX IF EXISTS ${indexName}`;
174
+ }
175
+ /**
176
+ * Generate CREATE TABLE statement for the _indexes table.
177
+ * Stores per-field index registrations (one row per index).
178
+ */
179
+ static createIndexesTable() {
180
+ return `
181
+ CREATE TABLE IF NOT EXISTS _indexes (
182
+ model_name TEXT NOT NULL,
183
+ field_name TEXT NOT NULL,
184
+ field_type TEXT NOT NULL,
185
+ is_unique INTEGER DEFAULT 0,
186
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
187
+ PRIMARY KEY (model_name, field_name)
188
+ )`.trim();
189
+ }
190
+ /**
191
+ * Generate CREATE TABLE statement for the _unique_constraints table.
192
+ * Stores composite (multi-field) unique constraints.
193
+ */
194
+ static createUniqueConstraintsTable() {
195
+ return `
196
+ CREATE TABLE IF NOT EXISTS _unique_constraints (
197
+ model_name TEXT NOT NULL,
198
+ constraint_name TEXT NOT NULL,
199
+ fields_json TEXT NOT NULL,
200
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
201
+ PRIMARY KEY (model_name, constraint_name)
202
+ )`.trim();
203
+ }
204
+ /**
205
+ * Generate CREATE TABLE statement for the _model_fields table.
206
+ * Tracks field names and inferred types as records are written.
207
+ */
208
+ static createModelFieldsTable() {
209
+ return `
210
+ CREATE TABLE IF NOT EXISTS _model_fields (
211
+ model_name TEXT NOT NULL,
212
+ field_name TEXT NOT NULL,
213
+ inferred_type TEXT NOT NULL,
214
+ first_seen_at TEXT DEFAULT CURRENT_TIMESTAMP,
215
+ PRIMARY KEY (model_name, field_name)
216
+ )`.trim();
217
+ }
218
+ /**
219
+ * Generate all DDL statements needed to initialize the schema
220
+ */
221
+ static getAllDDL(options) {
222
+ return [
223
+ this.createRecordsTable(options),
224
+ this.createStringSetIndexTable(options),
225
+ this.createIndexesTable(),
226
+ this.createUniqueConstraintsTable(),
227
+ this.createModelFieldsTable(),
228
+ ...this.createBaseIndexes(options)
229
+ ];
230
+ }
231
+ /**
232
+ * Generate INSERT statement for a record
233
+ */
234
+ static buildInsertSQL(options) {
235
+ if (options.includeDocIdColumn) {
236
+ return `
237
+ INSERT OR REPLACE INTO records (_id, _type, _data, _meta_doc_id, _meta_permission_hint)
238
+ VALUES (?, ?, ?, ?, ?)`.trim();
239
+ } else {
240
+ return `
241
+ INSERT OR REPLACE INTO records (_id, _type, _data)
242
+ VALUES (?, ?, ?)`.trim();
243
+ }
244
+ }
245
+ /**
246
+ * Generate DELETE statement for a record by id and type
247
+ */
248
+ static buildDeleteSQL(options) {
249
+ if (options.includeDocIdColumn) {
250
+ return "DELETE FROM records WHERE _type = ? AND _id = ?";
251
+ } else {
252
+ return "DELETE FROM records WHERE _type = ? AND _id = ?";
253
+ }
254
+ }
255
+ /**
256
+ * Generate DELETE statement for stringset values
257
+ */
258
+ static buildDeleteStringSetSQL() {
259
+ return "DELETE FROM stringset_index WHERE _type = ? AND _record_id = ?";
260
+ }
261
+ /**
262
+ * Generate INSERT statement for stringset values
263
+ */
264
+ static buildInsertStringSetSQL(options) {
265
+ if (options.includeDocIdColumn) {
266
+ return `
267
+ INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value, _meta_doc_id)
268
+ VALUES (?, ?, ?, ?, ?)`.trim();
269
+ } else {
270
+ return `
271
+ INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value)
272
+ VALUES (?, ?, ?, ?)`.trim();
273
+ }
274
+ }
275
+ };
276
+
277
+ // src/engines/JsonSchemaEngine.ts
278
+ var JsonSchemaEngine = class extends DatabaseEngine {
279
+ schemaOptions;
280
+ initialized = false;
281
+ constructor(options) {
282
+ super();
283
+ this.schemaOptions = options;
284
+ }
285
+ /**
286
+ * Initialize the JSON schema tables.
287
+ * Should be called once during engine setup.
288
+ */
289
+ async initializeSchema() {
290
+ if (this.initialized) return;
291
+ const ddlStatements = JsonSchemaDDL.getAllDDL(this.schemaOptions);
292
+ for (const ddl of ddlStatements) {
293
+ await this.execSql(ddl);
294
+ }
295
+ this.initialized = true;
296
+ }
297
+ /**
298
+ * Create table for a model.
299
+ * In JSON schema, this is a no-op since all models share the `records` table.
300
+ * However, we can create indexes for indexed fields.
301
+ */
302
+ async createTable(modelName, schema, _options) {
303
+ await this.initializeSchema();
304
+ for (const [fieldName, fieldOptions] of schema) {
305
+ if (fieldOptions.indexed) {
306
+ const indexDdl = JsonSchemaDDL.createFieldIndex(modelName, fieldName);
307
+ await this.execSql(indexDdl);
308
+ }
309
+ }
310
+ }
311
+ /**
312
+ * Create StringSet junction table.
313
+ * In JSON schema, this is a no-op since we use the shared stringset_index table.
314
+ */
315
+ async createStringSetJunctionTable(_modelName, _fieldName) {
316
+ await this.initializeSchema();
317
+ }
318
+ /**
319
+ * Insert or update a record.
320
+ */
321
+ async insert(modelName, data) {
322
+ await this.initializeSchema();
323
+ const { id, _meta_doc_id, _meta_permission_hint, ...fieldsForJson } = data;
324
+ const dataJson = JSON.stringify(fieldsForJson);
325
+ const sql = JsonSchemaDDL.buildInsertSQL(this.schemaOptions);
326
+ if (this.schemaOptions.includeDocIdColumn) {
327
+ await this.execSql(sql, [
328
+ id,
329
+ modelName,
330
+ dataJson,
331
+ _meta_doc_id || null,
332
+ _meta_permission_hint || null
333
+ ]);
334
+ } else {
335
+ await this.execSql(sql, [id, modelName, dataJson]);
336
+ }
337
+ }
338
+ /**
339
+ * Delete a record by ID.
340
+ */
341
+ async delete(modelName, id) {
342
+ const sql = JsonSchemaDDL.buildDeleteSQL(this.schemaOptions);
343
+ await this.execSql(sql, [modelName, id]);
344
+ const ssDeleteSql = JsonSchemaDDL.buildDeleteStringSetSQL();
345
+ await this.execSql(ssDeleteSql, [modelName, id]);
346
+ }
347
+ /**
348
+ * Delete all records for a specific document.
349
+ * Only applicable in yjs mode with doc ID column.
350
+ */
351
+ async deleteByDocumentId(modelName, docId) {
352
+ if (!this.schemaOptions.includeDocIdColumn) {
353
+ throw new Error(
354
+ "deleteByDocumentId is only supported in yjs mode with doc ID column"
355
+ );
356
+ }
357
+ await this.execSql(
358
+ "DELETE FROM records WHERE _type = ? AND _meta_doc_id = ?",
359
+ [modelName, docId]
360
+ );
361
+ await this.execSql(
362
+ "DELETE FROM stringset_index WHERE _type = ? AND _meta_doc_id = ?",
363
+ [modelName, docId]
364
+ );
365
+ }
366
+ /**
367
+ * Insert StringSet values for a record.
368
+ */
369
+ async insertStringSetValues(modelName, fieldName, recordId, values, docId) {
370
+ if (values.length === 0) return;
371
+ const sql = JsonSchemaDDL.buildInsertStringSetSQL(this.schemaOptions);
372
+ for (const value of values) {
373
+ if (this.schemaOptions.includeDocIdColumn) {
374
+ await this.execSql(sql, [
375
+ recordId,
376
+ modelName,
377
+ fieldName,
378
+ value,
379
+ docId || null
380
+ ]);
381
+ } else {
382
+ await this.execSql(sql, [recordId, modelName, fieldName, value]);
383
+ }
384
+ }
385
+ }
386
+ /**
387
+ * Remove StringSet values for a record.
388
+ */
389
+ async removeStringSetValues(modelName, fieldName, recordId, values) {
390
+ if (values.length === 0) return;
391
+ const placeholders = values.map(() => "?").join(", ");
392
+ const sql = `DELETE FROM stringset_index WHERE _type = ? AND field = ? AND _record_id = ? AND value IN (${placeholders})`;
393
+ await this.execSql(sql, [modelName, fieldName, recordId, ...values]);
394
+ }
395
+ /**
396
+ * Get table name for a model.
397
+ * In JSON schema, all models use the 'records' table.
398
+ */
399
+ getTableName(_modelName) {
400
+ return "records";
401
+ }
402
+ /**
403
+ * Execute a query and return results.
404
+ */
405
+ async query(sql, params) {
406
+ return this.execSql(sql, params);
407
+ }
408
+ /**
409
+ * Get the table schema.
410
+ * Returns information about the records table structure.
411
+ */
412
+ async getTableSchema(_tableName) {
413
+ return this.execSql("PRAGMA table_info(records)");
414
+ }
415
+ /**
416
+ * Get the last error message.
417
+ * Subclasses can override this if needed.
418
+ */
419
+ getLastErrorMessage() {
420
+ return void 0;
421
+ }
422
+ /**
423
+ * Transaction support.
424
+ * Default implementation runs callback without actual transaction.
425
+ * Subclasses should override for proper transaction support.
426
+ */
427
+ async withTransaction(callback) {
428
+ const ops = {
429
+ insert: async (modelName, data) => {
430
+ await this.insert(modelName, data);
431
+ },
432
+ delete: async (modelName, id) => {
433
+ await this.delete(modelName, id);
434
+ },
435
+ query: async (sql, params) => {
436
+ return await this.execSql(sql, params);
437
+ }
438
+ };
439
+ return callback(ops);
440
+ }
441
+ /**
442
+ * Parse a record row from the database.
443
+ * Extracts fields from _data and merges with direct columns.
444
+ */
445
+ parseRecordRow(row) {
446
+ if (!row) return row;
447
+ const { _id, _type, _data, _meta_doc_id, _meta_permission_hint, ...rest } = row;
448
+ let parsedData = {};
449
+ if (_data) {
450
+ try {
451
+ parsedData = JSON.parse(_data);
452
+ } catch (e) {
453
+ console.warn("Failed to parse _data:", e);
454
+ }
455
+ }
456
+ const result = {
457
+ id: _id,
458
+ type: _type,
459
+ ...parsedData,
460
+ ...rest
461
+ // Include any projected fields
462
+ };
463
+ if (_meta_doc_id !== void 0) {
464
+ result._meta_doc_id = _meta_doc_id;
465
+ }
466
+ if (_meta_permission_hint !== void 0) {
467
+ result._meta_permission_hint = _meta_permission_hint;
468
+ }
469
+ return result;
470
+ }
471
+ };
472
+
473
+ // src/engines/cloudflare/DurableObjectEngine.ts
474
+ var DurableObjectEngine = class extends JsonSchemaEngine {
475
+ sql;
476
+ storage;
477
+ constructor(options) {
478
+ super({ includeDocIdColumn: false });
479
+ this.sql = options.sql;
480
+ this.storage = options.storage;
481
+ }
482
+ /**
483
+ * Initialize the engine — creates tables and restores persisted indexes.
484
+ */
485
+ async ensureReady() {
486
+ await this.initializeSchema();
487
+ this._migrateSchema();
488
+ this.loadPersistedIndexes();
489
+ }
490
+ /**
491
+ * Handle schema migrations for existing databases.
492
+ * Adds columns/tables that were added in later versions.
493
+ */
494
+ _migrateSchema() {
495
+ const cols = this.execSqlSync("PRAGMA table_info(_indexes)");
496
+ if (!cols.some((c) => c.name === "is_unique")) {
497
+ this.execSqlSync(
498
+ "ALTER TABLE _indexes ADD COLUMN is_unique INTEGER DEFAULT 0"
499
+ );
500
+ }
501
+ this.execSqlSync(`
502
+ CREATE TABLE IF NOT EXISTS _model_fields (
503
+ model_name TEXT NOT NULL,
504
+ field_name TEXT NOT NULL,
505
+ inferred_type TEXT NOT NULL,
506
+ first_seen_at TEXT DEFAULT CURRENT_TIMESTAMP,
507
+ PRIMARY KEY (model_name, field_name)
508
+ )
509
+ `);
510
+ const recordsCols = this.execSqlSync("PRAGMA table_info(records)");
511
+ const hasOldSchema = recordsCols.some((c) => c.name === "id");
512
+ if (hasOldSchema) {
513
+ this.execSqlSync("ALTER TABLE records RENAME COLUMN id TO _id");
514
+ this.execSqlSync("ALTER TABLE records RENAME COLUMN type TO _type");
515
+ this.execSqlSync("ALTER TABLE records RENAME COLUMN data_json TO _data");
516
+ this.execSqlSync("ALTER TABLE stringset_index RENAME COLUMN record_id TO _record_id");
517
+ this.execSqlSync("ALTER TABLE stringset_index RENAME COLUMN type TO _type");
518
+ this.execSqlSync("DROP INDEX IF EXISTS idx_records_type");
519
+ this.execSqlSync("CREATE INDEX IF NOT EXISTS idx_records_type ON records(_type)");
520
+ this.execSqlSync("DROP INDEX IF EXISTS idx_ss_type_field_value");
521
+ this.execSqlSync("CREATE INDEX IF NOT EXISTS idx_ss_type_field_value ON stringset_index(_type, field, value)");
522
+ this.execSqlSync("DROP INDEX IF EXISTS idx_ss_type_field_record");
523
+ this.execSqlSync("CREATE INDEX IF NOT EXISTS idx_ss_type_field_record ON stringset_index(_type, field, _record_id)");
524
+ try {
525
+ const indexes = this.execSqlSync(
526
+ "SELECT model_name, field_name FROM _indexes"
527
+ );
528
+ for (const row of indexes) {
529
+ const mn = row.model_name;
530
+ const fn = row.field_name;
531
+ const idxName = `idx_records_${mn.toLowerCase()}_${fn.toLowerCase()}`;
532
+ this.execSqlSync(`DROP INDEX IF EXISTS ${idxName}`);
533
+ this.execSqlSync(
534
+ `CREATE INDEX IF NOT EXISTS ${idxName} ON records(json_extract(_data, '$.${fn}')) WHERE _type = '${mn}'`
535
+ );
536
+ }
537
+ } catch (e) {
538
+ console.debug("Could not recreate field indexes during migration:", e);
539
+ }
540
+ try {
541
+ const constraints = this.execSqlSync(
542
+ "SELECT model_name, constraint_name, fields_json FROM _unique_constraints"
543
+ );
544
+ for (const row of constraints) {
545
+ const mn = row.model_name;
546
+ const cn = row.constraint_name;
547
+ const fields = JSON.parse(row.fields_json);
548
+ const idxName = `idx_uc_${mn.toLowerCase()}_${cn.toLowerCase()}`;
549
+ this.execSqlSync(`DROP INDEX IF EXISTS ${idxName}`);
550
+ const fieldExprs = fields.map((f) => `json_extract(_data, '$.${f}')`).join(", ");
551
+ this.execSqlSync(
552
+ `CREATE INDEX IF NOT EXISTS ${idxName} ON records(${fieldExprs}) WHERE _type = '${mn}'`
553
+ );
554
+ }
555
+ } catch (e) {
556
+ console.debug("Could not recreate unique constraint indexes during migration:", e);
557
+ }
558
+ }
559
+ }
560
+ /**
561
+ * Load indexes from the _indexes table and re-ensure they exist.
562
+ */
563
+ loadPersistedIndexes() {
564
+ try {
565
+ const rows = this.execSqlSync(
566
+ "SELECT model_name, field_name FROM _indexes"
567
+ );
568
+ for (const row of rows) {
569
+ const createSql = JsonSchemaDDL.createFieldIndex(
570
+ row.model_name,
571
+ row.field_name
572
+ );
573
+ this.execSqlSync(createSql);
574
+ }
575
+ } catch (e) {
576
+ console.debug("Could not load persisted indexes:", e);
577
+ }
578
+ }
579
+ /**
580
+ * Register an index on a model field.
581
+ * Idempotent — re-registering the same index is a no-op.
582
+ */
583
+ registerIndex(modelName, fieldName, fieldType, unique = false) {
584
+ assertValidIdentifier(modelName, "registerIndex modelName");
585
+ assertValidIdentifier(fieldName, "registerIndex fieldName");
586
+ this.execSqlSync(
587
+ `INSERT OR REPLACE INTO _indexes (model_name, field_name, field_type, is_unique)
588
+ VALUES (?, ?, ?, ?)`,
589
+ [modelName, fieldName, fieldType, unique ? 1 : 0]
590
+ );
591
+ const createSql = JsonSchemaDDL.createFieldIndex(modelName, fieldName);
592
+ this.execSqlSync(createSql);
593
+ }
594
+ /**
595
+ * Drop an index on a model field.
596
+ * Idempotent — dropping a non-existent index is a no-op.
597
+ */
598
+ dropIndex(modelName, fieldName) {
599
+ assertValidIdentifier(modelName, "dropIndex modelName");
600
+ assertValidIdentifier(fieldName, "dropIndex fieldName");
601
+ this.execSqlSync(
602
+ "DELETE FROM _indexes WHERE model_name = ? AND field_name = ?",
603
+ [modelName, fieldName]
604
+ );
605
+ const dropSql = JsonSchemaDDL.dropFieldIndex(modelName, fieldName);
606
+ this.execSqlSync(dropSql);
607
+ }
608
+ /**
609
+ * List all registered indexes, optionally filtered by model.
610
+ */
611
+ listIndexes(modelName) {
612
+ if (modelName) {
613
+ return this.execSqlSync(
614
+ "SELECT model_name, field_name, field_type, is_unique, created_at FROM _indexes WHERE model_name = ?",
615
+ [modelName]
616
+ );
617
+ }
618
+ return this.execSqlSync(
619
+ "SELECT model_name, field_name, field_type, is_unique, created_at FROM _indexes"
620
+ );
621
+ }
622
+ /**
623
+ * Get unique single-field indexes for a model.
624
+ */
625
+ getUniqueIndexes(modelName) {
626
+ return this.execSqlSync(
627
+ "SELECT model_name, field_name, field_type, is_unique, created_at FROM _indexes WHERE model_name = ? AND is_unique = 1",
628
+ [modelName]
629
+ );
630
+ }
631
+ /**
632
+ * Register a composite unique constraint.
633
+ * Idempotent — re-registering the same constraint is a no-op.
634
+ */
635
+ registerUniqueConstraint(modelName, constraintName, fields) {
636
+ assertValidIdentifier(modelName, "registerUniqueConstraint modelName");
637
+ assertValidIdentifier(constraintName, "registerUniqueConstraint constraintName");
638
+ for (const f of fields) {
639
+ assertValidIdentifier(f, "registerUniqueConstraint field");
640
+ }
641
+ this.execSqlSync(
642
+ `INSERT OR REPLACE INTO _unique_constraints (model_name, constraint_name, fields_json)
643
+ VALUES (?, ?, ?)`,
644
+ [modelName, constraintName, JSON.stringify(fields)]
645
+ );
646
+ const fieldExprs = fields.map((f) => `json_extract(_data, '$.${f}')`).join(", ");
647
+ const indexName = `idx_uc_${modelName.toLowerCase()}_${constraintName.toLowerCase()}`;
648
+ this.execSqlSync(
649
+ `CREATE INDEX IF NOT EXISTS ${indexName} ON records(${fieldExprs}) WHERE _type = '${modelName}'`
650
+ );
651
+ }
652
+ /**
653
+ * Drop a composite unique constraint.
654
+ * Idempotent — dropping a non-existent constraint is a no-op.
655
+ */
656
+ dropUniqueConstraint(modelName, constraintName) {
657
+ this.execSqlSync(
658
+ "DELETE FROM _unique_constraints WHERE model_name = ? AND constraint_name = ?",
659
+ [modelName, constraintName]
660
+ );
661
+ const indexName = `idx_uc_${modelName.toLowerCase()}_${constraintName.toLowerCase()}`;
662
+ this.execSqlSync(`DROP INDEX IF EXISTS ${indexName}`);
663
+ }
664
+ /**
665
+ * List composite unique constraints, optionally filtered by model.
666
+ */
667
+ listUniqueConstraints(modelName) {
668
+ const sql = modelName ? "SELECT model_name, constraint_name, fields_json, created_at FROM _unique_constraints WHERE model_name = ?" : "SELECT model_name, constraint_name, fields_json, created_at FROM _unique_constraints";
669
+ const params = modelName ? [modelName] : void 0;
670
+ const rows = this.execSqlSync(sql, params);
671
+ return rows.map((row) => ({
672
+ model_name: row.model_name,
673
+ constraint_name: row.constraint_name,
674
+ fields: JSON.parse(row.fields_json),
675
+ created_at: row.created_at
676
+ }));
677
+ }
678
+ /**
679
+ * Check all unique constraints for a model before saving.
680
+ * Returns null if no violation, or an error message string if violated.
681
+ */
682
+ checkUniqueConstraints(modelName, id, data) {
683
+ const uniqueIndexes = this.getUniqueIndexes(modelName);
684
+ for (const idx of uniqueIndexes) {
685
+ assertValidIdentifier(idx.field_name, "checkUniqueConstraints field_name");
686
+ const value = data[idx.field_name];
687
+ if (value === null || value === void 0) continue;
688
+ const conflicts = this.execSqlSync(
689
+ `SELECT _id FROM records WHERE _type = ? AND json_extract(_data, '$.${idx.field_name}') = ? AND _id != ? LIMIT 1`,
690
+ [modelName, value, id]
691
+ );
692
+ if (conflicts.length > 0) {
693
+ return `Unique constraint violated on field '${idx.field_name}' for model '${modelName}'. Value '${String(value).substring(0, 50)}' already exists on record '${conflicts[0]._id}'.`;
694
+ }
695
+ }
696
+ const composites = this.listUniqueConstraints(modelName);
697
+ for (const constraint of composites) {
698
+ for (const f of constraint.fields) {
699
+ assertValidIdentifier(f, "checkUniqueConstraints composite field");
700
+ }
701
+ const values = constraint.fields.map((f) => data[f]);
702
+ if (values.some((v) => v === null || v === void 0)) continue;
703
+ const conditions = constraint.fields.map((f) => `json_extract(_data, '$.${f}') = ?`).join(" AND ");
704
+ const conflicts = this.execSqlSync(
705
+ `SELECT _id FROM records WHERE _type = ? AND ${conditions} AND _id != ? LIMIT 1`,
706
+ [modelName, ...values, id]
707
+ );
708
+ if (conflicts.length > 0) {
709
+ return `Unique constraint '${constraint.constraint_name}' violated for model '${modelName}' on fields [${constraint.fields.join(", ")}]. Values already exist on record '${conflicts[0]._id}'.`;
710
+ }
711
+ }
712
+ return null;
713
+ }
714
+ /**
715
+ * Check if a record exists (used by hooks to determine isNew).
716
+ */
717
+ recordExists(modelName, id) {
718
+ const rows = this.execSqlSync(
719
+ "SELECT 1 FROM records WHERE _type = ? AND _id = ? LIMIT 1",
720
+ [modelName, id]
721
+ );
722
+ return rows.length > 0;
723
+ }
724
+ /**
725
+ * Execute SQL asynchronously.
726
+ * Wraps the synchronous DO SQLite API.
727
+ */
728
+ async execSql(sql, params) {
729
+ return this.execSqlSync(sql, params);
730
+ }
731
+ /**
732
+ * Execute SQL synchronously.
733
+ * This is the native mode for DO SQLite.
734
+ */
735
+ execSqlSync(sql, params) {
736
+ try {
737
+ const cursor = params ? this.sql.exec(sql, ...params) : this.sql.exec(sql);
738
+ return cursor.toArray();
739
+ } catch (error) {
740
+ console.error("SQL execution error:", error);
741
+ throw error;
742
+ }
743
+ }
744
+ /**
745
+ * Transaction support using DO's transactionSync.
746
+ */
747
+ async withTransaction(callback) {
748
+ const ops = {
749
+ insert: async (modelName, data) => {
750
+ await this.insert(modelName, data);
751
+ },
752
+ delete: async (modelName, id) => {
753
+ await this.delete(modelName, id);
754
+ },
755
+ query: async (sql, params) => {
756
+ return this.execSqlSync(sql, params);
757
+ }
758
+ };
759
+ return callback(ops);
760
+ }
761
+ /**
762
+ * Run operations within a synchronous transaction.
763
+ */
764
+ transactionSync(callback) {
765
+ return this.storage.transactionSync(callback);
766
+ }
767
+ /**
768
+ * Clean up resources.
769
+ */
770
+ async destroy() {
771
+ }
772
+ /**
773
+ * Insert a record with StringSet support.
774
+ */
775
+ async insertWithStringSets(modelName, data, stringSets) {
776
+ this.storage.transactionSync(() => {
777
+ const { id, ...fieldsForJson } = data;
778
+ const jsonFields = { ...fieldsForJson, ...stringSets };
779
+ const dataJson = JSON.stringify(jsonFields);
780
+ this.sql.exec(
781
+ "INSERT OR REPLACE INTO records (_id, _type, _data) VALUES (?, ?, ?)",
782
+ id,
783
+ modelName,
784
+ dataJson
785
+ );
786
+ this.sql.exec(
787
+ "DELETE FROM stringset_index WHERE _type = ? AND _record_id = ?",
788
+ modelName,
789
+ id
790
+ );
791
+ for (const [fieldName, values] of Object.entries(stringSets)) {
792
+ for (const value of values) {
793
+ this.sql.exec(
794
+ "INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
795
+ id,
796
+ modelName,
797
+ fieldName,
798
+ value
799
+ );
800
+ }
801
+ }
802
+ });
803
+ }
804
+ /**
805
+ * Add values to StringSet fields without replacing the entire set.
806
+ * Also updates the arrays stored in data_json.
807
+ */
808
+ addToStringSets(modelName, id, sets) {
809
+ this.storage.transactionSync(() => {
810
+ this.addToStringSetsRaw(modelName, id, sets);
811
+ });
812
+ }
813
+ /**
814
+ * Non-transactional core of addToStringSets.
815
+ * Call this from an outer transaction (e.g. batch) to avoid nested transactions.
816
+ * @internal
817
+ */
818
+ addToStringSetsRaw(modelName, id, sets) {
819
+ for (const [field, values] of Object.entries(sets)) {
820
+ for (const value of values) {
821
+ this.sql.exec(
822
+ "INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
823
+ id,
824
+ modelName,
825
+ field,
826
+ value
827
+ );
828
+ }
829
+ }
830
+ this._syncStringSetsToJson(modelName, id, Object.keys(sets));
831
+ }
832
+ /**
833
+ * Remove values from StringSet fields without replacing the entire set.
834
+ * Also updates the arrays stored in data_json.
835
+ */
836
+ removeFromStringSets(modelName, id, sets) {
837
+ this.storage.transactionSync(() => {
838
+ this.removeFromStringSetsRaw(modelName, id, sets);
839
+ });
840
+ }
841
+ /**
842
+ * Non-transactional core of removeFromStringSets.
843
+ * Call this from an outer transaction (e.g. batch) to avoid nested transactions.
844
+ * @internal
845
+ */
846
+ removeFromStringSetsRaw(modelName, id, sets) {
847
+ for (const [field, values] of Object.entries(sets)) {
848
+ for (const value of values) {
849
+ this.sql.exec(
850
+ "DELETE FROM stringset_index WHERE _record_id = ? AND _type = ? AND field = ? AND value = ?",
851
+ id,
852
+ modelName,
853
+ field,
854
+ value
855
+ );
856
+ }
857
+ }
858
+ this._syncStringSetsToJson(modelName, id, Object.keys(sets));
859
+ }
860
+ /**
861
+ * Sync stringset_index state back into data_json arrays for the given fields.
862
+ * @internal
863
+ */
864
+ _syncStringSetsToJson(modelName, id, fields) {
865
+ const rows = this.execSqlSync(
866
+ "SELECT _data FROM records WHERE _id = ? AND _type = ?",
867
+ [id, modelName]
868
+ );
869
+ if (rows.length === 0) return;
870
+ const data = JSON.parse(rows[0]._data);
871
+ for (const field of fields) {
872
+ const ssRows = this.execSqlSync(
873
+ "SELECT value FROM stringset_index WHERE _record_id = ? AND _type = ? AND field = ? ORDER BY value",
874
+ [id, modelName, field]
875
+ );
876
+ data[field] = ssRows.map((r) => r.value);
877
+ }
878
+ this.sql.exec(
879
+ "UPDATE records SET _data = ? WHERE _id = ? AND _type = ?",
880
+ JSON.stringify(data),
881
+ id,
882
+ modelName
883
+ );
884
+ }
885
+ /**
886
+ * Patch a record: merge provided fields into existing data_json.
887
+ * Only the specified fields are updated; all other fields are preserved.
888
+ * If stringSets are provided, those StringSet fields are fully replaced.
889
+ * Returns false if the record does not exist.
890
+ */
891
+ patchRecord(modelName, id, fields, stringSets) {
892
+ let found = true;
893
+ this.storage.transactionSync(() => {
894
+ found = this.patchRecordRaw(modelName, id, fields, stringSets);
895
+ });
896
+ return found;
897
+ }
898
+ /**
899
+ * Non-transactional core of patchRecord.
900
+ * Call this from an outer transaction (e.g. batch) to avoid nested transactions.
901
+ * @internal
902
+ */
903
+ patchRecordRaw(modelName, id, fields, stringSets) {
904
+ const rows = this.execSqlSync(
905
+ "SELECT _data FROM records WHERE _id = ? AND _type = ?",
906
+ [id, modelName]
907
+ );
908
+ if (rows.length === 0) {
909
+ return false;
910
+ }
911
+ const existing = JSON.parse(rows[0]._data);
912
+ const { id: _stripId, ...patchFields } = fields;
913
+ const merged = { ...existing, ...patchFields };
914
+ if (stringSets) {
915
+ for (const [field, values] of Object.entries(stringSets)) {
916
+ merged[field] = values;
917
+ }
918
+ }
919
+ this.sql.exec(
920
+ "UPDATE records SET _data = ? WHERE _id = ? AND _type = ?",
921
+ JSON.stringify(merged),
922
+ id,
923
+ modelName
924
+ );
925
+ if (stringSets) {
926
+ for (const [fieldName, values] of Object.entries(stringSets)) {
927
+ this.sql.exec(
928
+ "DELETE FROM stringset_index WHERE _record_id = ? AND _type = ? AND field = ?",
929
+ id,
930
+ modelName,
931
+ fieldName
932
+ );
933
+ for (const value of values) {
934
+ this.sql.exec(
935
+ "INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
936
+ id,
937
+ modelName,
938
+ fieldName,
939
+ value
940
+ );
941
+ }
942
+ }
943
+ }
944
+ return true;
945
+ }
946
+ /**
947
+ * Atomically increment/decrement numeric fields on a record.
948
+ * Each key in `fields` is a field name and its value is the delta.
949
+ * Missing fields are initialised to 0 before adding the delta.
950
+ * Returns the new values, or null if the record doesn't exist.
951
+ */
952
+ incrementFields(modelName, id, fields) {
953
+ let result = null;
954
+ this.storage.transactionSync(() => {
955
+ result = this.incrementFieldsRaw(modelName, id, fields);
956
+ });
957
+ return result;
958
+ }
959
+ /**
960
+ * Non-transactional core of incrementFields.
961
+ * Call this from an outer transaction (e.g. batch) to avoid nested transactions.
962
+ * @internal
963
+ */
964
+ incrementFieldsRaw(modelName, id, fields) {
965
+ const rows = this.execSqlSync(
966
+ "SELECT _data FROM records WHERE _id = ? AND _type = ?",
967
+ [id, modelName]
968
+ );
969
+ if (rows.length === 0) return null;
970
+ const data = JSON.parse(rows[0]._data);
971
+ const newValues = {};
972
+ for (const [field, delta] of Object.entries(fields)) {
973
+ const current = typeof data[field] === "number" ? data[field] : 0;
974
+ data[field] = current + delta;
975
+ newValues[field] = data[field];
976
+ }
977
+ this.sql.exec(
978
+ "UPDATE records SET _data = ? WHERE _id = ? AND _type = ?",
979
+ JSON.stringify(data),
980
+ id,
981
+ modelName
982
+ );
983
+ return newValues;
984
+ }
985
+ /**
986
+ * Delete a record and its StringSet values atomically.
987
+ */
988
+ async deleteWithStringSets(modelName, id) {
989
+ this.storage.transactionSync(() => {
990
+ this.sql.exec("DELETE FROM records WHERE _type = ? AND _id = ?", modelName, id);
991
+ this.sql.exec(
992
+ "DELETE FROM stringset_index WHERE _type = ? AND _record_id = ?",
993
+ modelName,
994
+ id
995
+ );
996
+ });
997
+ }
998
+ /**
999
+ * Track field names and inferred types for a model based on written data.
1000
+ * Uses INSERT OR IGNORE so the first-seen type wins.
1001
+ */
1002
+ trackModelFields(modelName, data) {
1003
+ for (const [key, value] of Object.entries(data)) {
1004
+ if (key === "id" || value === null || value === void 0) {
1005
+ continue;
1006
+ }
1007
+ let inferredType;
1008
+ if (typeof value === "number") {
1009
+ inferredType = "number";
1010
+ } else if (typeof value === "boolean") {
1011
+ inferredType = "boolean";
1012
+ } else if (Array.isArray(value)) {
1013
+ inferredType = "array";
1014
+ } else if (typeof value === "object") {
1015
+ inferredType = "object";
1016
+ } else {
1017
+ inferredType = "string";
1018
+ }
1019
+ this.sql.exec(
1020
+ "INSERT OR IGNORE INTO _model_fields (model_name, field_name, inferred_type) VALUES (?, ?, ?)",
1021
+ modelName,
1022
+ key,
1023
+ inferredType
1024
+ );
1025
+ }
1026
+ }
1027
+ /**
1028
+ * Get tracked fields for a model, or all models if no name given.
1029
+ */
1030
+ getModelFields(modelName) {
1031
+ if (modelName) {
1032
+ return this.execSqlSync(
1033
+ "SELECT model_name, field_name, inferred_type, first_seen_at FROM _model_fields WHERE model_name = ? ORDER BY field_name",
1034
+ [modelName]
1035
+ );
1036
+ }
1037
+ return this.execSqlSync(
1038
+ "SELECT model_name, field_name, inferred_type, first_seen_at FROM _model_fields ORDER BY model_name, field_name"
1039
+ );
1040
+ }
1041
+ };
1042
+
1043
+ // src/types/queryTypes.ts
1044
+ var DocumentQueryError = class _DocumentQueryError extends Error {
1045
+ constructor(message) {
1046
+ super(message);
1047
+ this.name = "DocumentQueryError";
1048
+ Object.setPrototypeOf(this, _DocumentQueryError.prototype);
1049
+ }
1050
+ };
1051
+ var InvalidOperatorError = class _InvalidOperatorError extends DocumentQueryError {
1052
+ field;
1053
+ operator;
1054
+ fieldType;
1055
+ constructor(message, field, operator, fieldType) {
1056
+ super(message);
1057
+ this.name = "InvalidOperatorError";
1058
+ this.field = field;
1059
+ this.operator = operator;
1060
+ this.fieldType = fieldType;
1061
+ Object.setPrototypeOf(this, _InvalidOperatorError.prototype);
1062
+ }
1063
+ };
1064
+ var InvalidFieldError = class _InvalidFieldError extends DocumentQueryError {
1065
+ field;
1066
+ modelName;
1067
+ constructor(message, field, modelName) {
1068
+ super(message);
1069
+ this.name = "InvalidFieldError";
1070
+ this.field = field;
1071
+ this.modelName = modelName;
1072
+ Object.setPrototypeOf(this, _InvalidFieldError.prototype);
1073
+ }
1074
+ };
1075
+ var InvalidCursorError = class _InvalidCursorError extends DocumentQueryError {
1076
+ cursor;
1077
+ constructor(message, cursor) {
1078
+ super(message);
1079
+ this.name = "InvalidCursorError";
1080
+ this.cursor = cursor;
1081
+ Object.setPrototypeOf(this, _InvalidCursorError.prototype);
1082
+ }
1083
+ };
1084
+
1085
+ // src/query/CursorManager.ts
1086
+ var base64Encode = (str) => {
1087
+ if (typeof btoa !== "undefined") {
1088
+ return btoa(str);
1089
+ } else if (typeof Buffer !== "undefined") {
1090
+ return Buffer.from(str, "utf-8").toString("base64");
1091
+ } else {
1092
+ throw new Error("No base64 encoding available");
1093
+ }
1094
+ };
1095
+ var base64Decode = (str) => {
1096
+ if (typeof atob !== "undefined") {
1097
+ return atob(str);
1098
+ } else if (typeof Buffer !== "undefined") {
1099
+ return Buffer.from(str, "base64").toString("utf-8");
1100
+ } else {
1101
+ throw new Error("No base64 decoding available");
1102
+ }
1103
+ };
1104
+ var CursorManager = class {
1105
+ /**
1106
+ * Encode cursor data to base64 string
1107
+ */
1108
+ static encodeCursor(cursorData) {
1109
+ try {
1110
+ const jsonString = JSON.stringify(cursorData);
1111
+ return base64Encode(jsonString);
1112
+ } catch (error) {
1113
+ throw new InvalidCursorError(
1114
+ `Failed to encode cursor: ${error instanceof Error ? error.message : "Unknown error"}`,
1115
+ JSON.stringify(cursorData)
1116
+ );
1117
+ }
1118
+ }
1119
+ /**
1120
+ * Decode base64 cursor string to cursor data
1121
+ */
1122
+ static decodeCursor(cursor) {
1123
+ try {
1124
+ const jsonString = base64Decode(cursor);
1125
+ const parsed = JSON.parse(jsonString);
1126
+ if (!parsed || typeof parsed !== "object") {
1127
+ throw new Error("Cursor must be an object");
1128
+ }
1129
+ if (!parsed.values || typeof parsed.values !== "object") {
1130
+ throw new Error("Cursor must have values object");
1131
+ }
1132
+ if (!Array.isArray(parsed.sortFields)) {
1133
+ throw new Error("Cursor must have sortFields array");
1134
+ }
1135
+ if (parsed.direction !== 1 && parsed.direction !== -1) {
1136
+ throw new Error("Cursor direction must be 1 or -1");
1137
+ }
1138
+ return parsed;
1139
+ } catch (error) {
1140
+ throw new InvalidCursorError(
1141
+ `Failed to decode cursor: ${error instanceof Error ? error.message : "Unknown error"}`,
1142
+ cursor
1143
+ );
1144
+ }
1145
+ }
1146
+ /**
1147
+ * Generate cursor from a record and sort specification
1148
+ */
1149
+ static generateCursor(record, sortFields, direction) {
1150
+ const values = {};
1151
+ for (const field of sortFields) {
1152
+ if (record.hasOwnProperty(field)) {
1153
+ values[field] = record[field];
1154
+ } else {
1155
+ throw new Error(
1156
+ `Cannot generate cursor: record missing sort field '${field}'`
1157
+ );
1158
+ }
1159
+ }
1160
+ const cursorData = {
1161
+ values,
1162
+ sortFields,
1163
+ direction
1164
+ };
1165
+ return this.encodeCursor(cursorData);
1166
+ }
1167
+ /**
1168
+ * Build SQL pagination conditions based on cursor
1169
+ * Uses lexicographic ordering for stable pagination with multiple sort fields
1170
+ */
1171
+ static buildPaginationConditions(cursor, currentSortFields, sortDirections, requestedDirection, fieldFormatter = (field) => field) {
1172
+ if (!this.arraysEqual(cursor.sortFields, currentSortFields)) {
1173
+ throw new InvalidCursorError(
1174
+ `Cursor sort fields [${cursor.sortFields.join(
1175
+ ", "
1176
+ )}] don't match query sort fields [${currentSortFields.join(", ")}]`,
1177
+ JSON.stringify(cursor)
1178
+ );
1179
+ }
1180
+ const conditions = [];
1181
+ const params = [];
1182
+ const sortFields = cursor.sortFields;
1183
+ const direction = requestedDirection;
1184
+ for (let i = 0; i < sortFields.length; i++) {
1185
+ const fieldConditions = [];
1186
+ const fieldParams = [];
1187
+ for (let j = 0; j < i; j++) {
1188
+ const field = sortFields[j];
1189
+ const value = cursor.values[field];
1190
+ fieldConditions.push(`${fieldFormatter(field)} = ?`);
1191
+ fieldParams.push(value);
1192
+ }
1193
+ const currentField = sortFields[i];
1194
+ const currentValue = cursor.values[currentField];
1195
+ const fieldSortDir = sortDirections[i] ?? 1;
1196
+ const forwardOp = fieldSortDir === 1 ? ">" : "<";
1197
+ const operator = direction === 1 ? forwardOp : forwardOp === ">" ? "<" : ">";
1198
+ fieldConditions.push(`${fieldFormatter(currentField)} ${operator} ?`);
1199
+ fieldParams.push(currentValue);
1200
+ const levelCondition = fieldConditions.join(" AND ");
1201
+ conditions.push(`(${levelCondition})`);
1202
+ params.push(...fieldParams);
1203
+ }
1204
+ const sql = `(${conditions.join(" OR ")})`;
1205
+ return { sql, params };
1206
+ }
1207
+ /**
1208
+ * Extract sort fields from sort specification, ensuring 'id' is always included for stability
1209
+ */
1210
+ static extractSortFields(sort) {
1211
+ const fields = [];
1212
+ if (sort && Object.keys(sort).length > 0) {
1213
+ fields.push(...Object.keys(sort));
1214
+ }
1215
+ if (!fields.includes("id")) {
1216
+ fields.push("id");
1217
+ }
1218
+ return fields;
1219
+ }
1220
+ /**
1221
+ * Build ORDER BY clause from sort specification
1222
+ */
1223
+ static buildOrderClause(sort, fieldFormatter = (field) => field) {
1224
+ const fields = this.extractSortFields(sort);
1225
+ const clauses = [];
1226
+ const directions = [];
1227
+ if (sort && Object.keys(sort).length > 0) {
1228
+ for (const [field, dir] of Object.entries(sort)) {
1229
+ const sqlDirection = dir === 1 ? "ASC" : "DESC";
1230
+ clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
1231
+ directions.push(dir);
1232
+ }
1233
+ }
1234
+ if (!sort || !sort.hasOwnProperty("id")) {
1235
+ clauses.push(`${fieldFormatter("id")} ASC`);
1236
+ directions.push(1);
1237
+ } else if (sort && sort.hasOwnProperty("id")) {
1238
+ directions.push(sort["id"]);
1239
+ }
1240
+ return {
1241
+ sql: clauses.join(", "),
1242
+ fields,
1243
+ directions
1244
+ };
1245
+ }
1246
+ /**
1247
+ * Determine if there are more results available for pagination
1248
+ */
1249
+ static hasMoreResults(requestedLimit, actualResultCount) {
1250
+ if (!requestedLimit || requestedLimit <= 0) {
1251
+ return false;
1252
+ }
1253
+ return actualResultCount >= requestedLimit;
1254
+ }
1255
+ /**
1256
+ * Generate next and previous cursors from result set
1257
+ */
1258
+ static generateResultCursors(results, sortFields, _requestedDirection, hasMore, isFirstPage = false) {
1259
+ if (results.length === 0) {
1260
+ return {};
1261
+ }
1262
+ const cursors = {};
1263
+ if (hasMore && results.length > 0) {
1264
+ const lastResult = results[results.length - 1];
1265
+ cursors.nextCursor = this.generateCursor(lastResult, sortFields, 1);
1266
+ }
1267
+ if (results.length > 0 && !isFirstPage) {
1268
+ const firstResult = results[0];
1269
+ cursors.prevCursor = this.generateCursor(firstResult, sortFields, -1);
1270
+ }
1271
+ return cursors;
1272
+ }
1273
+ /**
1274
+ * Utility to compare arrays for equality
1275
+ */
1276
+ static arraysEqual(a, b) {
1277
+ if (a.length !== b.length) return false;
1278
+ return a.every((val, index) => val === b[index]);
1279
+ }
1280
+ };
1281
+
1282
+ // src/utils/patterns.ts
1283
+ function escapeLikeLiteral(input) {
1284
+ return input.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
1285
+ }
1286
+ function buildLikePattern(value, mode) {
1287
+ const trimmed = (value ?? "").trim();
1288
+ if (trimmed.length === 0) return null;
1289
+ if (trimmed.length > 1024) {
1290
+ throw new Error("substring value exceeds 1024 characters");
1291
+ }
1292
+ const escaped = escapeLikeLiteral(trimmed);
1293
+ switch (mode) {
1294
+ case "startsWith":
1295
+ return `${escaped}%`;
1296
+ case "endsWith":
1297
+ return `%${escaped}`;
1298
+ case "containsText":
1299
+ return `%${escaped}%`;
1300
+ }
1301
+ }
1302
+ function shouldLogLikeEscapes() {
1303
+ try {
1304
+ if (typeof process !== "undefined" && process.env) {
1305
+ return !!process.env.JS_BAO_DEBUG_LIKE_ESCAPES;
1306
+ }
1307
+ } catch {
1308
+ }
1309
+ return false;
1310
+ }
1311
+
1312
+ // src/query/JsonQueryTranslator.ts
1313
+ var SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
1314
+ var JsonQueryTranslator = class {
1315
+ modelName;
1316
+ schema;
1317
+ options;
1318
+ fieldSqlCache;
1319
+ constructor(modelName, schema, options = { includeDocId: false }) {
1320
+ this.modelName = modelName;
1321
+ this.schema = schema;
1322
+ this.options = options;
1323
+ this.fieldSqlCache = /* @__PURE__ */ new Map();
1324
+ }
1325
+ /**
1326
+ * Get SQL expression for a field.
1327
+ * System fields (id, type) map to internal columns (_id, _type);
1328
+ * others use json_extract(_data, ...).
1329
+ * When no schema is provided, any field is accepted (schemaless mode).
1330
+ */
1331
+ getFieldSql(fieldName) {
1332
+ if (!this.fieldSqlCache.has(fieldName)) {
1333
+ if (SYSTEM_FIELDS.has(fieldName)) {
1334
+ const internalName = `_${fieldName}`;
1335
+ this.fieldSqlCache.set(fieldName, quoteIdentifier(internalName));
1336
+ } else {
1337
+ assertValidIdentifier(fieldName, "query field");
1338
+ this.fieldSqlCache.set(
1339
+ fieldName,
1340
+ `json_extract(_data, '$.${fieldName}')`
1341
+ );
1342
+ }
1343
+ }
1344
+ return this.fieldSqlCache.get(fieldName);
1345
+ }
1346
+ /**
1347
+ * Translate document filter and options to SQL for find operations
1348
+ */
1349
+ translateFind(filter, options) {
1350
+ if (options?.projection) {
1351
+ this.validateProjection(options.projection);
1352
+ }
1353
+ const whereClause = this.translateFilter(filter);
1354
+ const orderClause = CursorManager.buildOrderClause(
1355
+ options?.sort,
1356
+ (field) => this.getFieldSql(field)
1357
+ );
1358
+ const limitClause = this.buildLimitClause(options);
1359
+ const paginationClause = this.buildPaginationClause(
1360
+ options,
1361
+ orderClause.fields,
1362
+ orderClause.directions
1363
+ );
1364
+ const selectClause = this.buildSelectClause(options?.projection);
1365
+ let sql = `SELECT ${selectClause} FROM records`;
1366
+ const params = [];
1367
+ const conditions = [`${quoteIdentifier("_type")} = ?`];
1368
+ params.push(this.modelName);
1369
+ if (whereClause.sql) {
1370
+ conditions.push(whereClause.sql);
1371
+ params.push(...whereClause.params);
1372
+ }
1373
+ if (paginationClause.sql) {
1374
+ conditions.push(paginationClause.sql);
1375
+ params.push(...paginationClause.params);
1376
+ }
1377
+ if (this.options.includeDocId) {
1378
+ const documentClause = this.buildDocumentClause(options?.documents);
1379
+ if (documentClause) {
1380
+ conditions.push(documentClause.sql);
1381
+ params.push(...documentClause.params);
1382
+ }
1383
+ }
1384
+ sql += ` WHERE ${conditions.join(" AND ")}`;
1385
+ if (orderClause.sql) {
1386
+ sql += ` ORDER BY ${orderClause.sql}`;
1387
+ }
1388
+ if (limitClause.sql) {
1389
+ sql += ` LIMIT ${limitClause.sql}`;
1390
+ }
1391
+ return {
1392
+ sql,
1393
+ params,
1394
+ sortFields: orderClause.fields,
1395
+ sortDirections: orderClause.directions
1396
+ };
1397
+ }
1398
+ /**
1399
+ * Translate document filter to SQL for count operations
1400
+ */
1401
+ translateCount(filter, options) {
1402
+ const whereClause = this.translateFilter(filter);
1403
+ const conditions = [`${quoteIdentifier("_type")} = ?`];
1404
+ const params = [this.modelName];
1405
+ if (whereClause.sql) {
1406
+ conditions.push(whereClause.sql);
1407
+ params.push(...whereClause.params);
1408
+ }
1409
+ if (this.options.includeDocId) {
1410
+ const documentClause = this.buildDocumentClause(options?.documents);
1411
+ if (documentClause) {
1412
+ conditions.push(documentClause.sql);
1413
+ params.push(...documentClause.params);
1414
+ }
1415
+ }
1416
+ const sql = `SELECT COUNT(*) as count FROM records WHERE ${conditions.join(
1417
+ " AND "
1418
+ )}`;
1419
+ return { sql, params };
1420
+ }
1421
+ /**
1422
+ * Translate document filter to SQL WHERE clause.
1423
+ * Returns the conditions and params without the type = ? prefix.
1424
+ */
1425
+ translateFilter(filter) {
1426
+ if (!filter || Object.keys(filter).length === 0) {
1427
+ return { sql: "", params: [] };
1428
+ }
1429
+ const conditions = [];
1430
+ const params = [];
1431
+ for (const [key, value] of Object.entries(filter)) {
1432
+ if (key === "$and") {
1433
+ const andClause = this.translateLogicalOperator(
1434
+ "AND",
1435
+ value
1436
+ );
1437
+ if (andClause.sql) {
1438
+ conditions.push(`(${andClause.sql})`);
1439
+ params.push(...andClause.params);
1440
+ }
1441
+ } else if (key === "$or") {
1442
+ const orClause = this.translateLogicalOperator(
1443
+ "OR",
1444
+ value
1445
+ );
1446
+ if (orClause.sql) {
1447
+ conditions.push(`(${orClause.sql})`);
1448
+ params.push(...orClause.params);
1449
+ }
1450
+ } else {
1451
+ const fieldClause = this.translateFieldCondition(key, value);
1452
+ if (fieldClause.sql) {
1453
+ conditions.push(fieldClause.sql);
1454
+ params.push(...fieldClause.params);
1455
+ }
1456
+ }
1457
+ }
1458
+ return {
1459
+ sql: conditions.join(" AND "),
1460
+ params
1461
+ };
1462
+ }
1463
+ /**
1464
+ * Translate logical operators ($and, $or)
1465
+ */
1466
+ translateLogicalOperator(operator, filters) {
1467
+ if (!Array.isArray(filters) || filters.length === 0) {
1468
+ return { sql: "", params: [] };
1469
+ }
1470
+ const clauses = [];
1471
+ const params = [];
1472
+ for (const filter of filters) {
1473
+ const clause = this.translateFilter(filter);
1474
+ if (clause.sql) {
1475
+ clauses.push(clause.sql);
1476
+ params.push(...clause.params);
1477
+ }
1478
+ }
1479
+ if (clauses.length === 0) {
1480
+ return { sql: "", params: [] };
1481
+ }
1482
+ return {
1483
+ sql: clauses.join(` ${operator} `),
1484
+ params
1485
+ };
1486
+ }
1487
+ /**
1488
+ * Translate field condition to SQL
1489
+ */
1490
+ translateFieldCondition(fieldName, condition) {
1491
+ const fieldSql = this.getFieldSql(fieldName);
1492
+ if (condition === null || condition === void 0) {
1493
+ return { sql: `${fieldSql} IS NULL`, params: [] };
1494
+ }
1495
+ if (this.isPrimitiveValue(condition)) {
1496
+ this.validateFieldValue(fieldName, condition);
1497
+ return {
1498
+ sql: `${fieldSql} = ?`,
1499
+ params: [this.convertValueForSQLite(condition)]
1500
+ };
1501
+ }
1502
+ if (typeof condition === "object" && !Array.isArray(condition)) {
1503
+ return this.translateFieldOperators(fieldName, condition);
1504
+ }
1505
+ if (Array.isArray(condition)) {
1506
+ this.validateArrayValues(fieldName, condition);
1507
+ const placeholders = condition.map(() => "?").join(",");
1508
+ const convertedValues = condition.map(
1509
+ (v) => this.convertValueForSQLite(v)
1510
+ );
1511
+ return {
1512
+ sql: `${fieldSql} IN (${placeholders})`,
1513
+ params: convertedValues
1514
+ };
1515
+ }
1516
+ throw new InvalidOperatorError(
1517
+ `Unsupported condition type for field ${fieldName}`,
1518
+ fieldName,
1519
+ "unknown"
1520
+ );
1521
+ }
1522
+ /**
1523
+ * Translate field operators to SQL
1524
+ */
1525
+ translateFieldOperators(fieldName, operators) {
1526
+ const conditions = [];
1527
+ const params = [];
1528
+ const fieldOptions = this.schema?.get(fieldName);
1529
+ const fieldType = fieldOptions?.type;
1530
+ const fieldSql = this.getFieldSql(fieldName);
1531
+ const substringOps = [
1532
+ "$startsWith",
1533
+ "$endsWith",
1534
+ "$containsText"
1535
+ ];
1536
+ const presentSubstringOps = substringOps.filter(
1537
+ (op) => Object.prototype.hasOwnProperty.call(operators, op)
1538
+ );
1539
+ if (presentSubstringOps.length > 1) {
1540
+ throw new InvalidOperatorError(
1541
+ `Only one of $startsWith, $endsWith, $containsText may be used per field`,
1542
+ fieldName,
1543
+ presentSubstringOps[1],
1544
+ fieldType
1545
+ );
1546
+ }
1547
+ if (presentSubstringOps.length === 1) {
1548
+ const op = presentSubstringOps[0];
1549
+ const value = operators[op];
1550
+ if (fieldType && fieldType !== "string" && fieldType !== "stringset") {
1551
+ throw new InvalidOperatorError(
1552
+ `${op} operator is only supported for string and stringset fields, field ${fieldName} is type ${fieldType}`,
1553
+ fieldName,
1554
+ op,
1555
+ fieldType
1556
+ );
1557
+ }
1558
+ if (typeof value !== "string") {
1559
+ throw new InvalidOperatorError(
1560
+ `${op} operator requires a string value for field ${fieldName}`,
1561
+ fieldName,
1562
+ op,
1563
+ fieldType
1564
+ );
1565
+ }
1566
+ let pattern;
1567
+ try {
1568
+ const mode = op === "$startsWith" ? "startsWith" : op === "$endsWith" ? "endsWith" : "containsText";
1569
+ pattern = buildLikePattern(value, mode);
1570
+ } catch (e) {
1571
+ throw new InvalidOperatorError(
1572
+ e.message || "invalid substring value",
1573
+ fieldName,
1574
+ op,
1575
+ fieldType
1576
+ );
1577
+ }
1578
+ if (pattern !== null) {
1579
+ if (shouldLogLikeEscapes()) {
1580
+ try {
1581
+ console.debug(
1582
+ `[Substring] field=${fieldName} op=${op} pattern=${pattern} ci=true`
1583
+ );
1584
+ } catch {
1585
+ }
1586
+ }
1587
+ if (fieldType === "stringset") {
1588
+ const likeSql = `EXISTS (SELECT 1 FROM stringset_index WHERE stringset_index._type = ? AND stringset_index.field = ? AND stringset_index._record_id = records._id AND stringset_index.value LIKE ? ESCAPE '\\' COLLATE NOCASE)`;
1589
+ conditions.push(likeSql);
1590
+ params.push(this.modelName, fieldName, pattern);
1591
+ } else {
1592
+ const likeSql = `${fieldSql} LIKE ? ESCAPE '\\' COLLATE NOCASE`;
1593
+ conditions.push(likeSql);
1594
+ params.push(pattern);
1595
+ }
1596
+ }
1597
+ }
1598
+ for (const [operator, value] of Object.entries(operators)) {
1599
+ if (substringOps.includes(operator)) {
1600
+ continue;
1601
+ }
1602
+ const opClause = this.translateOperator(fieldName, operator, value);
1603
+ if (opClause.sql) {
1604
+ conditions.push(opClause.sql);
1605
+ params.push(...opClause.params);
1606
+ }
1607
+ }
1608
+ return {
1609
+ sql: conditions.join(" AND "),
1610
+ params
1611
+ };
1612
+ }
1613
+ /**
1614
+ * Translate individual operator to SQL
1615
+ */
1616
+ translateOperator(fieldName, operator, value) {
1617
+ const fieldOptions = this.schema?.get(fieldName);
1618
+ const fieldType = fieldOptions?.type;
1619
+ const fieldSql = this.getFieldSql(fieldName);
1620
+ switch (operator) {
1621
+ case "$eq":
1622
+ this.validateFieldValue(fieldName, value);
1623
+ return {
1624
+ sql: `${fieldSql} = ?`,
1625
+ params: [this.convertValueForSQLite(value)]
1626
+ };
1627
+ case "$ne":
1628
+ this.validateFieldValue(fieldName, value);
1629
+ return {
1630
+ sql: `${fieldSql} != ?`,
1631
+ params: [this.convertValueForSQLite(value)]
1632
+ };
1633
+ case "$gt":
1634
+ this.validateOperatorForType(operator, fieldType, [
1635
+ "id",
1636
+ "string",
1637
+ "number",
1638
+ "date"
1639
+ ]);
1640
+ this.validateFieldValue(fieldName, value);
1641
+ return {
1642
+ sql: `${fieldSql} > ?`,
1643
+ params: [this.convertValueForSQLite(value)]
1644
+ };
1645
+ case "$gte":
1646
+ this.validateOperatorForType(operator, fieldType, [
1647
+ "id",
1648
+ "string",
1649
+ "number",
1650
+ "date"
1651
+ ]);
1652
+ this.validateFieldValue(fieldName, value);
1653
+ return {
1654
+ sql: `${fieldSql} >= ?`,
1655
+ params: [this.convertValueForSQLite(value)]
1656
+ };
1657
+ case "$lt":
1658
+ this.validateOperatorForType(operator, fieldType, [
1659
+ "id",
1660
+ "string",
1661
+ "number",
1662
+ "date"
1663
+ ]);
1664
+ this.validateFieldValue(fieldName, value);
1665
+ return {
1666
+ sql: `${fieldSql} < ?`,
1667
+ params: [this.convertValueForSQLite(value)]
1668
+ };
1669
+ case "$lte":
1670
+ this.validateOperatorForType(operator, fieldType, [
1671
+ "id",
1672
+ "string",
1673
+ "number",
1674
+ "date"
1675
+ ]);
1676
+ this.validateFieldValue(fieldName, value);
1677
+ return {
1678
+ sql: `${fieldSql} <= ?`,
1679
+ params: [this.convertValueForSQLite(value)]
1680
+ };
1681
+ case "$in":
1682
+ if (!Array.isArray(value)) {
1683
+ throw new InvalidOperatorError(
1684
+ `$in operator requires an array value for field ${fieldName}`,
1685
+ fieldName,
1686
+ operator,
1687
+ fieldType
1688
+ );
1689
+ }
1690
+ this.validateArrayValues(fieldName, value);
1691
+ if (value.length === 0) {
1692
+ return { sql: "1 = 0", params: [] };
1693
+ }
1694
+ const placeholders = value.map(() => "?").join(",");
1695
+ const convertedValues = value.map((v) => this.convertValueForSQLite(v));
1696
+ return {
1697
+ sql: `${fieldSql} IN (${placeholders})`,
1698
+ params: convertedValues
1699
+ };
1700
+ case "$nin":
1701
+ if (!Array.isArray(value)) {
1702
+ throw new InvalidOperatorError(
1703
+ `$nin operator requires an array value for field ${fieldName}`,
1704
+ fieldName,
1705
+ operator,
1706
+ fieldType
1707
+ );
1708
+ }
1709
+ this.validateArrayValues(fieldName, value);
1710
+ if (value.length === 0) {
1711
+ return { sql: "", params: [] };
1712
+ }
1713
+ const ninPlaceholders = value.map(() => "?").join(",");
1714
+ const convertedNinValues = value.map(
1715
+ (v) => this.convertValueForSQLite(v)
1716
+ );
1717
+ return {
1718
+ sql: `${fieldSql} NOT IN (${ninPlaceholders})`,
1719
+ params: convertedNinValues
1720
+ };
1721
+ case "$exists":
1722
+ if (typeof value !== "boolean") {
1723
+ throw new InvalidOperatorError(
1724
+ `$exists operator requires a boolean value for field ${fieldName}`,
1725
+ fieldName,
1726
+ operator,
1727
+ fieldType
1728
+ );
1729
+ }
1730
+ if (SYSTEM_FIELDS.has(fieldName)) {
1731
+ return value ? { sql: `${fieldSql} IS NOT NULL`, params: [] } : { sql: `${fieldSql} IS NULL`, params: [] };
1732
+ } else {
1733
+ return value ? {
1734
+ sql: `json_type(_data, '$.${fieldName}') IS NOT NULL`,
1735
+ params: []
1736
+ } : {
1737
+ sql: `json_type(_data, '$.${fieldName}') IS NULL`,
1738
+ params: []
1739
+ };
1740
+ }
1741
+ case "$contains":
1742
+ if (fieldType && fieldType !== "stringset") {
1743
+ throw new InvalidOperatorError(
1744
+ `$contains operator is only supported for stringset fields, field ${fieldName} is type ${fieldType}`,
1745
+ fieldName,
1746
+ operator,
1747
+ fieldType
1748
+ );
1749
+ }
1750
+ if (typeof value !== "string") {
1751
+ throw new InvalidOperatorError(
1752
+ `$contains operator requires a string value for field ${fieldName}`,
1753
+ fieldName,
1754
+ operator,
1755
+ fieldType
1756
+ );
1757
+ }
1758
+ return {
1759
+ sql: `EXISTS (SELECT 1 FROM stringset_index WHERE stringset_index._type = ? AND stringset_index.field = ? AND stringset_index._record_id = records._id AND stringset_index.value = ?)`,
1760
+ params: [this.modelName, fieldName, value]
1761
+ };
1762
+ default:
1763
+ throw new InvalidOperatorError(
1764
+ `Unsupported operator: ${operator} for field ${fieldName}`,
1765
+ fieldName,
1766
+ operator,
1767
+ fieldType
1768
+ );
1769
+ }
1770
+ }
1771
+ /**
1772
+ * Build SELECT clause based on projection
1773
+ * For JSON schema, we need to extract fields from data_json
1774
+ */
1775
+ buildSelectClause(projection) {
1776
+ if (!projection || Object.keys(projection).length === 0) {
1777
+ return "_id, _type, _data";
1778
+ }
1779
+ const includeFields = [];
1780
+ const excludeFields = [];
1781
+ let hasIncludes = false;
1782
+ let hasExcludes = false;
1783
+ for (const [field, include] of Object.entries(projection)) {
1784
+ if (include === 1) {
1785
+ includeFields.push(field);
1786
+ hasIncludes = true;
1787
+ } else if (include === 0) {
1788
+ excludeFields.push(field);
1789
+ hasExcludes = true;
1790
+ }
1791
+ }
1792
+ if (hasIncludes && hasExcludes) {
1793
+ throw new InvalidOperatorError(
1794
+ "Cannot mix inclusion and exclusion in projection",
1795
+ "projection",
1796
+ "mixed"
1797
+ );
1798
+ }
1799
+ if (hasIncludes) {
1800
+ if (!includeFields.includes("id")) {
1801
+ includeFields.unshift("id");
1802
+ }
1803
+ const selectParts = ["_id", "_type"];
1804
+ for (const field of includeFields) {
1805
+ if (field === "id" || field === "type") continue;
1806
+ selectParts.push(
1807
+ `json_extract(_data, '$.${field}') AS ${quoteIdentifier(field)}`
1808
+ );
1809
+ }
1810
+ return selectParts.join(", ");
1811
+ } else if (hasExcludes) {
1812
+ return "_id, _type, _data";
1813
+ }
1814
+ return "_id, _type, _data";
1815
+ }
1816
+ /**
1817
+ * Build LIMIT clause
1818
+ */
1819
+ buildLimitClause(options) {
1820
+ if (options?.limit && options.limit > 0) {
1821
+ return { sql: options.limit.toString() };
1822
+ }
1823
+ return { sql: "" };
1824
+ }
1825
+ /**
1826
+ * Build pagination WHERE clause from cursor
1827
+ */
1828
+ buildPaginationClause(options, sortFields, sortDirections) {
1829
+ if (!options?.uniqueStartKey || !sortFields || !sortDirections) {
1830
+ return { sql: "", params: [] };
1831
+ }
1832
+ try {
1833
+ const cursor = CursorManager.decodeCursor(options.uniqueStartKey);
1834
+ const direction = options.direction || 1;
1835
+ return CursorManager.buildPaginationConditions(
1836
+ cursor,
1837
+ sortFields,
1838
+ sortDirections,
1839
+ direction,
1840
+ (field) => this.getFieldSql(field)
1841
+ );
1842
+ } catch (error) {
1843
+ console.warn("Invalid cursor provided, ignoring pagination:", error);
1844
+ return { sql: "", params: [] };
1845
+ }
1846
+ }
1847
+ /**
1848
+ * Validate projection fields — skipped in schemaless mode
1849
+ */
1850
+ validateProjection(projection) {
1851
+ if (!this.schema) return;
1852
+ for (const fieldName of Object.keys(projection)) {
1853
+ if (!this.schema.has(fieldName) && !SYSTEM_FIELDS.has(fieldName)) {
1854
+ throw new InvalidFieldError(
1855
+ `Unknown field: ${fieldName} in model ${this.modelName}`,
1856
+ fieldName,
1857
+ this.modelName
1858
+ );
1859
+ }
1860
+ }
1861
+ }
1862
+ /**
1863
+ * Validate operator is supported for field type
1864
+ */
1865
+ validateOperatorForType(operator, fieldType, allowedTypes) {
1866
+ if (!fieldType) {
1867
+ console.warn(`Field type not specified, allowing operator ${operator}`);
1868
+ return;
1869
+ }
1870
+ if (!allowedTypes.includes(fieldType)) {
1871
+ throw new InvalidOperatorError(
1872
+ `Operator ${operator} is not supported for field type ${fieldType}. Allowed types: ${allowedTypes.join(
1873
+ ", "
1874
+ )}`,
1875
+ "unknown",
1876
+ operator,
1877
+ fieldType
1878
+ );
1879
+ }
1880
+ }
1881
+ /**
1882
+ * Validate field value matches expected type
1883
+ */
1884
+ validateFieldValue(fieldName, value) {
1885
+ if (!this.schema) return;
1886
+ const fieldOptions = this.schema.get(fieldName);
1887
+ if (!fieldOptions || !fieldOptions.type) {
1888
+ return;
1889
+ }
1890
+ const expectedType = fieldOptions.type;
1891
+ const actualType = this.getValueType(value);
1892
+ if (!this.isTypeCompatible(expectedType, actualType, value)) {
1893
+ throw new InvalidOperatorError(
1894
+ `Field ${fieldName} expects ${expectedType}, got ${actualType}`,
1895
+ fieldName,
1896
+ "type_mismatch",
1897
+ expectedType
1898
+ );
1899
+ }
1900
+ }
1901
+ /**
1902
+ * Validate array values for $in/$nin operators
1903
+ */
1904
+ validateArrayValues(fieldName, values) {
1905
+ for (const value of values) {
1906
+ this.validateFieldValue(fieldName, value);
1907
+ }
1908
+ }
1909
+ /**
1910
+ * Check if value is a primitive (not an object with operators)
1911
+ */
1912
+ isPrimitiveValue(value) {
1913
+ if (value === null || value === void 0) {
1914
+ return true;
1915
+ }
1916
+ const type = typeof value;
1917
+ return type === "string" || type === "number" || type === "boolean" || value instanceof Date;
1918
+ }
1919
+ /**
1920
+ * Get the type of a value for validation
1921
+ */
1922
+ getValueType(value) {
1923
+ if (value === null || value === void 0) {
1924
+ return "null";
1925
+ }
1926
+ if (value instanceof Date) {
1927
+ return "date";
1928
+ }
1929
+ return typeof value;
1930
+ }
1931
+ /**
1932
+ * Check if value type is compatible with field type
1933
+ */
1934
+ isTypeCompatible(expectedType, actualType, value) {
1935
+ if (actualType === "null") {
1936
+ return true;
1937
+ }
1938
+ switch (expectedType) {
1939
+ case "id":
1940
+ return actualType === "string";
1941
+ // id fields are strings
1942
+ case "string":
1943
+ return actualType === "string";
1944
+ case "number":
1945
+ return actualType === "number" && !isNaN(value);
1946
+ case "boolean":
1947
+ return actualType === "boolean";
1948
+ case "date":
1949
+ return actualType === "date" || actualType === "string" && !isNaN(Date.parse(value));
1950
+ default:
1951
+ return true;
1952
+ }
1953
+ }
1954
+ /**
1955
+ * Convert value for SQLite compatibility
1956
+ */
1957
+ convertValueForSQLite(value) {
1958
+ if (typeof value === "boolean") {
1959
+ return value ? 1 : 0;
1960
+ }
1961
+ return value;
1962
+ }
1963
+ normalizeDocumentIds(documents) {
1964
+ if (documents == null) {
1965
+ return null;
1966
+ }
1967
+ const docArray = Array.isArray(documents) ? documents : [documents];
1968
+ const normalized = docArray.map((doc) => `${doc}`.trim()).filter((doc) => doc.length > 0);
1969
+ if (normalized.length === 0) {
1970
+ return [];
1971
+ }
1972
+ return Array.from(new Set(normalized));
1973
+ }
1974
+ buildDocumentClause(documents) {
1975
+ const normalized = this.normalizeDocumentIds(documents);
1976
+ if (normalized === null) {
1977
+ return null;
1978
+ }
1979
+ if (normalized.length === 0) {
1980
+ return { sql: "1 = 0", params: [] };
1981
+ }
1982
+ const placeholders = normalized.map(() => "?").join(", ");
1983
+ return {
1984
+ sql: `${quoteIdentifier("_meta_doc_id")} IN (${placeholders})`,
1985
+ params: normalized
1986
+ };
1987
+ }
1988
+ };
1989
+
1990
+ // src/engines/cloudflare/createDocumentDO.ts
1991
+ function createDatabaseDO(config = {}) {
1992
+ const hooks = config.hooks;
1993
+ return class DocumentDO {
1994
+ /** @internal */
1995
+ _doState;
1996
+ /** @internal */
1997
+ _engine;
1998
+ /** @internal */
1999
+ _initialized = false;
2000
+ /** @internal — cache for $contains misuse check: "model:field" keys known to be in data_json */
2001
+ _containsMisuseCache = /* @__PURE__ */ new Set();
2002
+ constructor(state, _env) {
2003
+ this._doState = state;
2004
+ this._engine = new DurableObjectEngine({
2005
+ sql: state.storage.sql,
2006
+ storage: state.storage
2007
+ });
2008
+ }
2009
+ get state() {
2010
+ return this._doState;
2011
+ }
2012
+ get engine() {
2013
+ return this._engine;
2014
+ }
2015
+ /** @internal */
2016
+ async _ensureInitialized() {
2017
+ if (!this._initialized) {
2018
+ await this._engine.ensureReady();
2019
+ this._initialized = true;
2020
+ }
2021
+ }
2022
+ async fetch(request) {
2023
+ try {
2024
+ await this._ensureInitialized();
2025
+ const url = new URL(request.url);
2026
+ const path = url.pathname;
2027
+ const docId = url.searchParams.get("docId") || "";
2028
+ if (request.method === "DELETE" && path === "/destroy") {
2029
+ await this._doState.storage.deleteAll();
2030
+ this._initialized = false;
2031
+ this._engine.initialized = false;
2032
+ return Response.json({
2033
+ deleted: true,
2034
+ deletedAt: (/* @__PURE__ */ new Date()).toISOString()
2035
+ });
2036
+ }
2037
+ if (request.method !== "POST" && request.method !== "GET") {
2038
+ return this._errorResponse("Method not allowed", 405);
2039
+ }
2040
+ switch (path) {
2041
+ case "/query":
2042
+ return this._handleQuery(request, docId);
2043
+ case "/save":
2044
+ return this._handleSave(request, docId);
2045
+ case "/patch":
2046
+ return this._handlePatch(request, docId);
2047
+ case "/delete":
2048
+ return this._handleDelete(request, docId);
2049
+ case "/batch":
2050
+ return this._handleBatch(request, docId);
2051
+ case "/count":
2052
+ return this._handleCount(request, docId);
2053
+ case "/aggregate":
2054
+ return this._handleAggregate(request, docId);
2055
+ case "/increment":
2056
+ return this._handleIncrement(request, docId);
2057
+ case "/stringset/add":
2058
+ return this._handleStringSetAdd(request, docId);
2059
+ case "/stringset/remove":
2060
+ return this._handleStringSetRemove(request, docId);
2061
+ case "/indexes/sync":
2062
+ return this._handleIndexesSync(request);
2063
+ case "/index/register":
2064
+ return this._handleIndexRegister(request);
2065
+ case "/index/drop":
2066
+ return this._handleIndexDrop(request);
2067
+ case "/indexes":
2068
+ return this._handleIndexList(request);
2069
+ case "/unique-constraint/register":
2070
+ return this._handleUniqueConstraintRegister(request);
2071
+ case "/unique-constraint/drop":
2072
+ return this._handleUniqueConstraintDrop(request);
2073
+ case "/unique-constraints":
2074
+ return this._handleUniqueConstraintList(request);
2075
+ case "/health":
2076
+ return this._handleHealth();
2077
+ case "/describe":
2078
+ return this._handleDescribe(request);
2079
+ case "/models":
2080
+ return this._handleModelList();
2081
+ default:
2082
+ return this._errorResponse("Not found", 404);
2083
+ }
2084
+ } catch (error) {
2085
+ console.error("DO fetch error:", error);
2086
+ return this._errorResponse(
2087
+ error instanceof Error ? error.message : "Internal error",
2088
+ 500
2089
+ );
2090
+ }
2091
+ }
2092
+ /** @internal */
2093
+ async _handleQuery(request, docId) {
2094
+ const body = await request.json();
2095
+ const { modelName, options } = body;
2096
+ let filter = body.filter || {};
2097
+ if (hooks?.beforeQuery) {
2098
+ const ctx = {
2099
+ modelName,
2100
+ docId,
2101
+ request,
2102
+ engine: this._engine,
2103
+ filter,
2104
+ options
2105
+ };
2106
+ const result = await hooks.beforeQuery(ctx);
2107
+ if (!result.allow) {
2108
+ return this._errorResponse(result.reason || "Query denied", 403);
2109
+ }
2110
+ if (result.injectFilter) {
2111
+ filter = { $and: [filter, result.injectFilter] };
2112
+ }
2113
+ }
2114
+ const misuseError = this._checkContainsMisuse(modelName, filter);
2115
+ if (misuseError) return misuseError;
2116
+ const translator = new JsonQueryTranslator(modelName, void 0, {
2117
+ includeDocId: false
2118
+ });
2119
+ const { sql, params, sortFields } = translator.translateFind(filter, options);
2120
+ const rawResults = this._engine.execSqlSync(sql, params);
2121
+ let data = rawResults.map((row) => this._parseRow(row));
2122
+ if (hooks?.afterQuery) {
2123
+ const ctx = {
2124
+ modelName,
2125
+ docId,
2126
+ request,
2127
+ engine: this._engine,
2128
+ results: data
2129
+ };
2130
+ const result = await hooks.afterQuery(ctx);
2131
+ data = result.results;
2132
+ }
2133
+ if (options?.include && options.include.length > 0) {
2134
+ data = this._resolveIncludes(data, options.include, 0, modelName);
2135
+ }
2136
+ const limit = options?.limit;
2137
+ const hasMore = CursorManager.hasMoreResults(limit, data.length);
2138
+ const isFirstPage = !options?.uniqueStartKey;
2139
+ const cursors = CursorManager.generateResultCursors(
2140
+ data,
2141
+ sortFields || ["id"],
2142
+ options?.direction || 1,
2143
+ hasMore,
2144
+ isFirstPage
2145
+ );
2146
+ const response = {
2147
+ data,
2148
+ hasMore,
2149
+ ...cursors
2150
+ };
2151
+ return Response.json(response);
2152
+ }
2153
+ /** @internal — Check for reserved field names in record data */
2154
+ _checkReservedFields(data) {
2155
+ for (const key of Object.keys(data)) {
2156
+ if (key === "id") continue;
2157
+ if (key.startsWith("_")) {
2158
+ return `Field '${key}' is reserved (fields starting with '_' are internal)`;
2159
+ }
2160
+ }
2161
+ return null;
2162
+ }
2163
+ /** @internal */
2164
+ async _handleSave(request, docId) {
2165
+ const body = await request.json();
2166
+ const { modelName, id, data, stringSets, ifNotExists, condition } = body;
2167
+ if (!modelName || !id) {
2168
+ return this._errorResponse("modelName and id are required", 400);
2169
+ }
2170
+ if (!data) {
2171
+ return this._errorResponse("data is required", 400);
2172
+ }
2173
+ if (data) {
2174
+ const reservedError = this._checkReservedFields(data);
2175
+ if (reservedError) {
2176
+ return this._errorResponse(reservedError, 400);
2177
+ }
2178
+ }
2179
+ if (condition && !this._checkCondition(modelName, id, condition)) {
2180
+ return this._errorResponse(
2181
+ `Condition not met for ${modelName}/${id}`,
2182
+ 409
2183
+ );
2184
+ }
2185
+ if (ifNotExists && this._engine.recordExists(modelName, id)) {
2186
+ return this._errorResponse(
2187
+ `Record ${modelName}/${id} already exists`,
2188
+ 409
2189
+ );
2190
+ }
2191
+ if (hooks?.beforeSave) {
2192
+ const isNew = !this._engine.recordExists(modelName, id);
2193
+ const ctx = {
2194
+ modelName,
2195
+ docId,
2196
+ request,
2197
+ engine: this._engine,
2198
+ id,
2199
+ data,
2200
+ stringSets,
2201
+ isNew
2202
+ };
2203
+ const result = await hooks.beforeSave(ctx);
2204
+ if (!result.allow) {
2205
+ return this._errorResponse(result.reason || "Save denied", 403);
2206
+ }
2207
+ }
2208
+ const violation = this._engine.checkUniqueConstraints(
2209
+ modelName,
2210
+ id,
2211
+ data
2212
+ );
2213
+ if (violation) {
2214
+ return this._errorResponse(violation, 409);
2215
+ }
2216
+ if (stringSets && Object.keys(stringSets).length > 0) {
2217
+ await this._engine.insertWithStringSets(
2218
+ modelName,
2219
+ { id, ...data },
2220
+ stringSets
2221
+ );
2222
+ } else {
2223
+ await this._engine.insert(modelName, { id, ...data });
2224
+ }
2225
+ this._engine.trackModelFields(modelName, data);
2226
+ const response = { success: true, id };
2227
+ return Response.json(response);
2228
+ }
2229
+ /** @internal — Partial update: merge provided fields into existing record */
2230
+ async _handlePatch(request, docId) {
2231
+ const body = await request.json();
2232
+ const { modelName, id, data, stringSets, condition } = body;
2233
+ if (!modelName || !id || !data) {
2234
+ return this._errorResponse("modelName, id, and data are required", 400);
2235
+ }
2236
+ const reservedError = this._checkReservedFields(data);
2237
+ if (reservedError) {
2238
+ return this._errorResponse(reservedError, 400);
2239
+ }
2240
+ if (!this._engine.recordExists(modelName, id)) {
2241
+ return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
2242
+ }
2243
+ if (condition && !this._checkCondition(modelName, id, condition)) {
2244
+ return this._errorResponse(
2245
+ `Condition not met for ${modelName}/${id}`,
2246
+ 409
2247
+ );
2248
+ }
2249
+ if (hooks?.beforeSave) {
2250
+ const ctx = {
2251
+ modelName,
2252
+ docId,
2253
+ request,
2254
+ engine: this._engine,
2255
+ id,
2256
+ data,
2257
+ stringSets,
2258
+ isNew: false
2259
+ };
2260
+ const result = await hooks.beforeSave(ctx);
2261
+ if (!result.allow) {
2262
+ return this._errorResponse(result.reason || "Save denied", 403);
2263
+ }
2264
+ }
2265
+ const existing = this._engine.execSqlSync(
2266
+ "SELECT _data FROM records WHERE _id = ? AND _type = ?",
2267
+ [id, modelName]
2268
+ );
2269
+ if (existing.length > 0) {
2270
+ const existingData = JSON.parse(existing[0]._data);
2271
+ const mergedData = { ...existingData, ...data };
2272
+ const violation = this._engine.checkUniqueConstraints(
2273
+ modelName,
2274
+ id,
2275
+ mergedData
2276
+ );
2277
+ if (violation) {
2278
+ return this._errorResponse(violation, 409);
2279
+ }
2280
+ }
2281
+ const found = this._engine.patchRecord(modelName, id, data, stringSets);
2282
+ if (!found) {
2283
+ return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
2284
+ }
2285
+ this._engine.trackModelFields(modelName, data);
2286
+ const response = { success: true, id };
2287
+ return Response.json(response);
2288
+ }
2289
+ /** @internal */
2290
+ async _handleDelete(request, docId) {
2291
+ const body = await request.json();
2292
+ const { modelName, id, condition } = body;
2293
+ if (condition && !this._checkCondition(modelName, id, condition)) {
2294
+ return this._errorResponse(
2295
+ `Condition not met for ${modelName}/${id}`,
2296
+ 409
2297
+ );
2298
+ }
2299
+ if (hooks?.beforeDelete) {
2300
+ let record = null;
2301
+ const rows = this._engine.execSqlSync(
2302
+ "SELECT _id, _type, _data FROM records WHERE _id = ? AND _type = ?",
2303
+ [id, modelName]
2304
+ );
2305
+ if (rows.length > 0) {
2306
+ record = this._parseRow(rows[0]);
2307
+ }
2308
+ const ctx = {
2309
+ modelName,
2310
+ docId,
2311
+ request,
2312
+ engine: this._engine,
2313
+ id,
2314
+ record
2315
+ };
2316
+ const result = await hooks.beforeDelete(ctx);
2317
+ if (!result.allow) {
2318
+ return this._errorResponse(result.reason || "Delete denied", 403);
2319
+ }
2320
+ }
2321
+ await this._engine.deleteWithStringSets(modelName, id);
2322
+ const response = { success: true };
2323
+ return Response.json(response);
2324
+ }
2325
+ /** @internal — Execute multiple save/patch/delete operations in a single transaction */
2326
+ async _handleBatch(request, docId) {
2327
+ const body = await request.json();
2328
+ const { operations } = body;
2329
+ if (!operations || !Array.isArray(operations) || operations.length === 0) {
2330
+ return this._errorResponse("operations array is required and must not be empty", 400);
2331
+ }
2332
+ const hookDenials = /* @__PURE__ */ new Map();
2333
+ for (let i = 0; i < operations.length; i++) {
2334
+ const op = operations[i];
2335
+ if (!op.modelName || !op.id || !op.op) {
2336
+ return this._errorResponse(
2337
+ `Operation ${i}: modelName, id, and op are required`,
2338
+ 400
2339
+ );
2340
+ }
2341
+ if (op.op === "save" || op.op === "patch") {
2342
+ if (!op.data) {
2343
+ return this._errorResponse(
2344
+ `Operation ${i}: data is required for ${op.op}`,
2345
+ 400
2346
+ );
2347
+ }
2348
+ const reservedError = this._checkReservedFields(op.data);
2349
+ if (reservedError) {
2350
+ return this._errorResponse(
2351
+ `Operation ${i}: ${reservedError}`,
2352
+ 400
2353
+ );
2354
+ }
2355
+ if (hooks?.beforeSave) {
2356
+ const isNew = op.op === "save" ? !this._engine.recordExists(op.modelName, op.id) : false;
2357
+ const ctx = {
2358
+ modelName: op.modelName,
2359
+ docId,
2360
+ request,
2361
+ engine: this._engine,
2362
+ id: op.id,
2363
+ data: op.data,
2364
+ stringSets: op.stringSets,
2365
+ isNew
2366
+ };
2367
+ const result = await hooks.beforeSave(ctx);
2368
+ if (!result.allow) {
2369
+ hookDenials.set(i, result.reason || "Save denied");
2370
+ }
2371
+ }
2372
+ } else if (op.op === "delete") {
2373
+ if (hooks?.beforeDelete) {
2374
+ let record = null;
2375
+ const rows = this._engine.execSqlSync(
2376
+ "SELECT _id, _type, _data FROM records WHERE _id = ? AND _type = ?",
2377
+ [op.id, op.modelName]
2378
+ );
2379
+ if (rows.length > 0) {
2380
+ record = this._parseRow(rows[0]);
2381
+ }
2382
+ const ctx = {
2383
+ modelName: op.modelName,
2384
+ docId,
2385
+ request,
2386
+ engine: this._engine,
2387
+ id: op.id,
2388
+ record
2389
+ };
2390
+ const result = await hooks.beforeDelete(ctx);
2391
+ if (!result.allow) {
2392
+ hookDenials.set(i, result.reason || "Delete denied");
2393
+ }
2394
+ }
2395
+ } else if (op.op === "increment") {
2396
+ if (!op.fields || typeof op.fields !== "object") {
2397
+ return this._errorResponse(
2398
+ `Operation ${i}: fields is required for increment`,
2399
+ 400
2400
+ );
2401
+ }
2402
+ } else if (op.op === "addToSet" || op.op === "removeFromSet") {
2403
+ if (!op.stringSets || typeof op.stringSets !== "object") {
2404
+ return this._errorResponse(
2405
+ `Operation ${i}: stringSets is required for ${op.op}`,
2406
+ 400
2407
+ );
2408
+ }
2409
+ } else {
2410
+ return this._errorResponse(
2411
+ `Operation ${i}: unknown op '${op.op}'.`,
2412
+ 400
2413
+ );
2414
+ }
2415
+ }
2416
+ const results = [];
2417
+ const fieldsToTrack = [];
2418
+ this._engine.transactionSync(() => {
2419
+ for (let i = 0; i < operations.length; i++) {
2420
+ const op = operations[i];
2421
+ if (hookDenials.has(i)) {
2422
+ results.push({
2423
+ success: false,
2424
+ id: op.id,
2425
+ error: hookDenials.get(i)
2426
+ });
2427
+ continue;
2428
+ }
2429
+ if (op.condition && !this._checkCondition(op.modelName, op.id, op.condition)) {
2430
+ results.push({
2431
+ success: false,
2432
+ id: op.id,
2433
+ error: `Condition not met for ${op.modelName}/${op.id}`
2434
+ });
2435
+ continue;
2436
+ }
2437
+ if (op.op === "save") {
2438
+ if (op.ifNotExists && this._engine.recordExists(op.modelName, op.id)) {
2439
+ results.push({
2440
+ success: false,
2441
+ id: op.id,
2442
+ error: `Record ${op.modelName}/${op.id} already exists`
2443
+ });
2444
+ continue;
2445
+ }
2446
+ if (op.data) {
2447
+ const violation = this._engine.checkUniqueConstraints(
2448
+ op.modelName,
2449
+ op.id,
2450
+ op.data
2451
+ );
2452
+ if (violation) {
2453
+ results.push({
2454
+ success: false,
2455
+ id: op.id,
2456
+ error: violation
2457
+ });
2458
+ continue;
2459
+ }
2460
+ }
2461
+ if (op.stringSets && Object.keys(op.stringSets).length > 0) {
2462
+ const { id: _id, ...fieldsForJson } = op.data;
2463
+ const jsonFields = { ...fieldsForJson, ...op.stringSets };
2464
+ const dataJson = JSON.stringify(jsonFields);
2465
+ this._engine.execSqlSync(
2466
+ "INSERT OR REPLACE INTO records (_id, _type, _data) VALUES (?, ?, ?)",
2467
+ [op.id, op.modelName, dataJson]
2468
+ );
2469
+ this._engine.execSqlSync(
2470
+ "DELETE FROM stringset_index WHERE _type = ? AND _record_id = ?",
2471
+ [op.modelName, op.id]
2472
+ );
2473
+ for (const [fieldName, values] of Object.entries(op.stringSets)) {
2474
+ for (const value of values) {
2475
+ this._engine.execSqlSync(
2476
+ "INSERT OR IGNORE INTO stringset_index (_record_id, _type, field, value) VALUES (?, ?, ?, ?)",
2477
+ [op.id, op.modelName, fieldName, value]
2478
+ );
2479
+ }
2480
+ }
2481
+ } else {
2482
+ const { id: _id, ...fieldsForJson } = op.data;
2483
+ const dataJson = JSON.stringify(fieldsForJson);
2484
+ this._engine.execSqlSync(
2485
+ "INSERT OR REPLACE INTO records (_id, _type, _data) VALUES (?, ?, ?)",
2486
+ [op.id, op.modelName, dataJson]
2487
+ );
2488
+ }
2489
+ if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
2490
+ results.push({ success: true, id: op.id });
2491
+ } else if (op.op === "patch") {
2492
+ if (!this._engine.recordExists(op.modelName, op.id)) {
2493
+ results.push({
2494
+ success: false,
2495
+ id: op.id,
2496
+ error: `Record ${op.modelName}/${op.id} not found`
2497
+ });
2498
+ continue;
2499
+ }
2500
+ if (op.data) {
2501
+ const existing = this._engine.execSqlSync(
2502
+ "SELECT _data FROM records WHERE _id = ? AND _type = ?",
2503
+ [op.id, op.modelName]
2504
+ );
2505
+ if (existing.length > 0) {
2506
+ const existingData = JSON.parse(existing[0]._data);
2507
+ const mergedData = { ...existingData, ...op.data };
2508
+ const violation = this._engine.checkUniqueConstraints(
2509
+ op.modelName,
2510
+ op.id,
2511
+ mergedData
2512
+ );
2513
+ if (violation) {
2514
+ results.push({
2515
+ success: false,
2516
+ id: op.id,
2517
+ error: violation
2518
+ });
2519
+ continue;
2520
+ }
2521
+ }
2522
+ }
2523
+ const found = this._engine.patchRecordRaw(
2524
+ op.modelName,
2525
+ op.id,
2526
+ op.data,
2527
+ op.stringSets
2528
+ );
2529
+ if (!found) {
2530
+ results.push({
2531
+ success: false,
2532
+ id: op.id,
2533
+ error: `Record ${op.modelName}/${op.id} not found`
2534
+ });
2535
+ } else {
2536
+ if (op.data) fieldsToTrack.push({ modelName: op.modelName, data: op.data });
2537
+ results.push({ success: true, id: op.id });
2538
+ }
2539
+ } else if (op.op === "delete") {
2540
+ this._engine.execSqlSync(
2541
+ "DELETE FROM records WHERE _id = ? AND _type = ?",
2542
+ [op.id, op.modelName]
2543
+ );
2544
+ this._engine.execSqlSync(
2545
+ "DELETE FROM stringset_index WHERE _record_id = ? AND _type = ?",
2546
+ [op.id, op.modelName]
2547
+ );
2548
+ results.push({ success: true, id: op.id });
2549
+ } else if (op.op === "increment") {
2550
+ const newValues = this._engine.incrementFieldsRaw(
2551
+ op.modelName,
2552
+ op.id,
2553
+ op.fields
2554
+ );
2555
+ if (!newValues) {
2556
+ results.push({
2557
+ success: false,
2558
+ id: op.id,
2559
+ error: `Record ${op.modelName}/${op.id} not found`
2560
+ });
2561
+ } else {
2562
+ results.push({ success: true, id: op.id, values: newValues });
2563
+ }
2564
+ } else if (op.op === "addToSet") {
2565
+ if (!this._engine.recordExists(op.modelName, op.id)) {
2566
+ results.push({
2567
+ success: false,
2568
+ id: op.id,
2569
+ error: `Record ${op.modelName}/${op.id} not found`
2570
+ });
2571
+ continue;
2572
+ }
2573
+ this._engine.addToStringSetsRaw(
2574
+ op.modelName,
2575
+ op.id,
2576
+ op.stringSets
2577
+ );
2578
+ results.push({ success: true, id: op.id });
2579
+ } else if (op.op === "removeFromSet") {
2580
+ if (!this._engine.recordExists(op.modelName, op.id)) {
2581
+ results.push({
2582
+ success: false,
2583
+ id: op.id,
2584
+ error: `Record ${op.modelName}/${op.id} not found`
2585
+ });
2586
+ continue;
2587
+ }
2588
+ this._engine.removeFromStringSetsRaw(
2589
+ op.modelName,
2590
+ op.id,
2591
+ op.stringSets
2592
+ );
2593
+ results.push({ success: true, id: op.id });
2594
+ }
2595
+ }
2596
+ });
2597
+ for (const entry of fieldsToTrack) {
2598
+ this._engine.trackModelFields(entry.modelName, entry.data);
2599
+ }
2600
+ const response = { results };
2601
+ return Response.json(response);
2602
+ }
2603
+ /** @internal */
2604
+ async _handleCount(request, docId) {
2605
+ const body = await request.json();
2606
+ const { modelName } = body;
2607
+ let filter = body.filter || {};
2608
+ if (hooks?.beforeQuery) {
2609
+ const ctx = {
2610
+ modelName,
2611
+ docId,
2612
+ request,
2613
+ engine: this._engine,
2614
+ filter
2615
+ };
2616
+ const result = await hooks.beforeQuery(ctx);
2617
+ if (!result.allow) {
2618
+ return this._errorResponse(result.reason || "Query denied", 403);
2619
+ }
2620
+ if (result.injectFilter) {
2621
+ filter = { $and: [filter, result.injectFilter] };
2622
+ }
2623
+ }
2624
+ const misuseError = this._checkContainsMisuse(modelName, filter);
2625
+ if (misuseError) return misuseError;
2626
+ const translator = new JsonQueryTranslator(modelName, void 0, {
2627
+ includeDocId: false
2628
+ });
2629
+ const { sql, params } = translator.translateCount(filter);
2630
+ const results = this._engine.execSqlSync(sql, params);
2631
+ const response = { count: results[0]?.count ?? 0 };
2632
+ return Response.json(response);
2633
+ }
2634
+ /** @internal — Atomically increment/decrement numeric fields */
2635
+ async _handleIncrement(request, _docId) {
2636
+ const body = await request.json();
2637
+ const { modelName, id, fields, condition } = body;
2638
+ if (!modelName || !id || !fields) {
2639
+ return this._errorResponse("modelName, id, and fields are required", 400);
2640
+ }
2641
+ if (!this._engine.recordExists(modelName, id)) {
2642
+ return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
2643
+ }
2644
+ if (condition && !this._checkCondition(modelName, id, condition)) {
2645
+ return this._errorResponse(
2646
+ `Condition not met for ${modelName}/${id}`,
2647
+ 409
2648
+ );
2649
+ }
2650
+ const newValues = this._engine.incrementFields(modelName, id, fields);
2651
+ if (!newValues) {
2652
+ return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
2653
+ }
2654
+ const response = { success: true, id, values: newValues };
2655
+ return Response.json(response);
2656
+ }
2657
+ /** @internal — Atomically add values to StringSet fields */
2658
+ async _handleStringSetAdd(request, _docId) {
2659
+ const body = await request.json();
2660
+ const { modelName, id, sets, condition } = body;
2661
+ if (!modelName || !id || !sets) {
2662
+ return this._errorResponse("modelName, id, and sets are required", 400);
2663
+ }
2664
+ if (!this._engine.recordExists(modelName, id)) {
2665
+ return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
2666
+ }
2667
+ if (condition && !this._checkCondition(modelName, id, condition)) {
2668
+ return this._errorResponse(
2669
+ `Condition not met for ${modelName}/${id}`,
2670
+ 409
2671
+ );
2672
+ }
2673
+ this._engine.addToStringSets(modelName, id, sets);
2674
+ const response = { success: true };
2675
+ return Response.json(response);
2676
+ }
2677
+ /** @internal — Atomically remove values from StringSet fields */
2678
+ async _handleStringSetRemove(request, _docId) {
2679
+ const body = await request.json();
2680
+ const { modelName, id, sets, condition } = body;
2681
+ if (!modelName || !id || !sets) {
2682
+ return this._errorResponse("modelName, id, and sets are required", 400);
2683
+ }
2684
+ if (!this._engine.recordExists(modelName, id)) {
2685
+ return this._errorResponse(`Record ${modelName}/${id} not found`, 404);
2686
+ }
2687
+ if (condition && !this._checkCondition(modelName, id, condition)) {
2688
+ return this._errorResponse(
2689
+ `Condition not met for ${modelName}/${id}`,
2690
+ 409
2691
+ );
2692
+ }
2693
+ this._engine.removeFromStringSets(modelName, id, sets);
2694
+ const response = { success: true };
2695
+ return Response.json(response);
2696
+ }
2697
+ async _handleAggregate(request, docId) {
2698
+ const body = await request.json();
2699
+ const { modelName, options: aggOptions } = body;
2700
+ if (!modelName || !aggOptions || !aggOptions.groupBy || !aggOptions.operations) {
2701
+ return this._errorResponse(
2702
+ "modelName, options.groupBy, and options.operations are required",
2703
+ 400
2704
+ );
2705
+ }
2706
+ for (const op of aggOptions.operations) {
2707
+ if (op.type !== "count" && !op.field) {
2708
+ return this._errorResponse(
2709
+ `Operation '${op.type}' requires a field parameter`,
2710
+ 400
2711
+ );
2712
+ }
2713
+ if (op.type === "count" && op.field) {
2714
+ return this._errorResponse(
2715
+ `Operation 'count' should not have a field parameter`,
2716
+ 400
2717
+ );
2718
+ }
2719
+ }
2720
+ let filter = aggOptions.filter || {};
2721
+ if (hooks?.beforeQuery) {
2722
+ const ctx = {
2723
+ modelName,
2724
+ docId,
2725
+ request,
2726
+ engine: this._engine,
2727
+ filter
2728
+ };
2729
+ const result = await hooks.beforeQuery(ctx);
2730
+ if (!result.allow) {
2731
+ return this._errorResponse(result.reason || "Query denied", 403);
2732
+ }
2733
+ if (result.injectFilter) {
2734
+ filter = { $and: [filter, result.injectFilter] };
2735
+ }
2736
+ }
2737
+ const misuseError = this._checkContainsMisuse(modelName, filter);
2738
+ if (misuseError) return misuseError;
2739
+ const translator = new JsonQueryTranslator(modelName, void 0, {
2740
+ includeDocId: false
2741
+ });
2742
+ const regularGroupBy = [];
2743
+ const stringSetMemberships = [];
2744
+ let stringSetFacetField = null;
2745
+ for (const groupBy of aggOptions.groupBy) {
2746
+ if (typeof groupBy === "string") {
2747
+ regularGroupBy.push(groupBy);
2748
+ } else {
2749
+ assertValidIdentifier(groupBy.field, "aggregation groupBy field");
2750
+ stringSetMemberships.push(groupBy);
2751
+ }
2752
+ }
2753
+ const confirmedRegular = [];
2754
+ for (const field of regularGroupBy) {
2755
+ assertValidIdentifier(field, "aggregation groupBy field");
2756
+ const ssCheck = this._engine.execSqlSync(
2757
+ "SELECT 1 FROM stringset_index WHERE _type = ? AND field = ? LIMIT 1",
2758
+ [modelName, field]
2759
+ );
2760
+ if (ssCheck.length > 0) {
2761
+ if (stringSetFacetField !== null) {
2762
+ return this._errorResponse(
2763
+ "Multiple StringSet facet fields not supported in single aggregation",
2764
+ 400
2765
+ );
2766
+ }
2767
+ stringSetFacetField = field;
2768
+ } else {
2769
+ confirmedRegular.push(field);
2770
+ }
2771
+ }
2772
+ try {
2773
+ let sql;
2774
+ let params = [];
2775
+ const aliasMap = [];
2776
+ if (stringSetFacetField && confirmedRegular.length === 0 && stringSetMemberships.length === 0) {
2777
+ const result = this._buildStringSetFacetSql(
2778
+ modelName,
2779
+ stringSetFacetField,
2780
+ aggOptions,
2781
+ filter,
2782
+ translator
2783
+ );
2784
+ sql = result.sql;
2785
+ params = result.params;
2786
+ } else {
2787
+ const result = this._buildRegularAggregationSql(
2788
+ modelName,
2789
+ confirmedRegular,
2790
+ stringSetMemberships,
2791
+ aggOptions,
2792
+ filter,
2793
+ translator
2794
+ );
2795
+ sql = result.sql;
2796
+ params = result.params;
2797
+ aliasMap.push(...result.aliasMap);
2798
+ }
2799
+ const rawResults = this._engine.execSqlSync(sql, params);
2800
+ const processed = this._processAggregationResults(rawResults, aggOptions, aliasMap, stringSetFacetField);
2801
+ const response = { result: processed };
2802
+ return Response.json(response);
2803
+ } catch (err) {
2804
+ return this._errorResponse(
2805
+ err instanceof Error ? err.message : "Aggregation failed",
2806
+ 500
2807
+ );
2808
+ }
2809
+ }
2810
+ /** @internal — Build SQL for StringSet facet aggregation */
2811
+ _buildStringSetFacetSql(modelName, facetField, aggOptions, filter, translator) {
2812
+ const selectParts = ["stringset_index.value AS group_key"];
2813
+ for (const op of aggOptions.operations) {
2814
+ switch (op.type) {
2815
+ case "count":
2816
+ selectParts.push("COUNT(*) AS count");
2817
+ break;
2818
+ case "sum":
2819
+ selectParts.push(`SUM(${translator.getFieldSql(op.field)}) AS sum_${op.field}`);
2820
+ break;
2821
+ case "avg":
2822
+ selectParts.push(`AVG(${translator.getFieldSql(op.field)}) AS avg_${op.field}`);
2823
+ break;
2824
+ case "min":
2825
+ selectParts.push(`MIN(${translator.getFieldSql(op.field)}) AS min_${op.field}`);
2826
+ break;
2827
+ case "max":
2828
+ selectParts.push(`MAX(${translator.getFieldSql(op.field)}) AS max_${op.field}`);
2829
+ break;
2830
+ }
2831
+ }
2832
+ let sql = `SELECT ${selectParts.join(", ")} FROM stringset_index`;
2833
+ sql += ` INNER JOIN records ON stringset_index._record_id = records._id AND records._type = stringset_index._type`;
2834
+ const conditions = [
2835
+ "stringset_index._type = ?",
2836
+ "stringset_index.field = ?"
2837
+ ];
2838
+ const params = [modelName, facetField];
2839
+ if (filter && Object.keys(filter).length > 0) {
2840
+ const whereClause = translator.translateFilter(filter);
2841
+ if (whereClause.sql) {
2842
+ conditions.push(whereClause.sql);
2843
+ params.push(...whereClause.params);
2844
+ }
2845
+ }
2846
+ sql += ` WHERE ${conditions.join(" AND ")}`;
2847
+ sql += " GROUP BY stringset_index.value";
2848
+ if (aggOptions.sort) {
2849
+ const sortExpr = aggOptions.sort.field === "count" ? "COUNT(*)" : aggOptions.sort.field;
2850
+ const dir = aggOptions.sort.direction === 1 ? "ASC" : "DESC";
2851
+ sql += ` ORDER BY ${sortExpr} ${dir}`;
2852
+ }
2853
+ if (aggOptions.limit) {
2854
+ sql += ` LIMIT ${Number(aggOptions.limit)}`;
2855
+ }
2856
+ return { sql, params };
2857
+ }
2858
+ /** @internal — Build SQL for regular field aggregation */
2859
+ _buildRegularAggregationSql(modelName, regularGroupBy, stringSetMemberships, aggOptions, filter, translator) {
2860
+ const selectParts = [];
2861
+ const groupByParts = [];
2862
+ const aliasMap = [];
2863
+ for (const field of regularGroupBy) {
2864
+ const fieldSql = translator.getFieldSql(field);
2865
+ selectParts.push(`${fieldSql} AS "${field}"`);
2866
+ groupByParts.push(fieldSql);
2867
+ }
2868
+ for (let i = 0; i < stringSetMemberships.length; i++) {
2869
+ const m = stringSetMemberships[i];
2870
+ const alias = `ss_${m.field}_${i}`;
2871
+ selectParts.push(
2872
+ `CASE WHEN ${alias}._record_id IS NOT NULL THEN 'true' ELSE 'false' END AS "${alias}"`
2873
+ );
2874
+ groupByParts.push(`CASE WHEN ${alias}._record_id IS NOT NULL THEN 'true' ELSE 'false' END`);
2875
+ aliasMap.push({ alias, field: m.field, contains: m.contains });
2876
+ }
2877
+ for (const op of aggOptions.operations) {
2878
+ switch (op.type) {
2879
+ case "count":
2880
+ selectParts.push("COUNT(*) AS count");
2881
+ break;
2882
+ case "sum":
2883
+ selectParts.push(`SUM(${translator.getFieldSql(op.field)}) AS sum_${op.field}`);
2884
+ break;
2885
+ case "avg":
2886
+ selectParts.push(`AVG(${translator.getFieldSql(op.field)}) AS avg_${op.field}`);
2887
+ break;
2888
+ case "min":
2889
+ selectParts.push(`MIN(${translator.getFieldSql(op.field)}) AS min_${op.field}`);
2890
+ break;
2891
+ case "max":
2892
+ selectParts.push(`MAX(${translator.getFieldSql(op.field)}) AS max_${op.field}`);
2893
+ break;
2894
+ }
2895
+ }
2896
+ let sql = `SELECT ${selectParts.join(", ")} FROM records`;
2897
+ const joinParams = [];
2898
+ for (const entry of aliasMap) {
2899
+ sql += ` LEFT JOIN stringset_index AS ${entry.alias}`;
2900
+ sql += ` ON ${entry.alias}._record_id = records._id`;
2901
+ sql += ` AND ${entry.alias}._type = records._type`;
2902
+ sql += ` AND ${entry.alias}.field = ?`;
2903
+ sql += ` AND ${entry.alias}.value = ?`;
2904
+ joinParams.push(entry.field, entry.contains);
2905
+ }
2906
+ const conditions = ["records._type = ?"];
2907
+ const params = [...joinParams, modelName];
2908
+ if (filter && Object.keys(filter).length > 0) {
2909
+ const whereClause = translator.translateFilter(filter);
2910
+ if (whereClause.sql) {
2911
+ conditions.push(whereClause.sql);
2912
+ params.push(...whereClause.params);
2913
+ }
2914
+ }
2915
+ sql += ` WHERE ${conditions.join(" AND ")}`;
2916
+ if (groupByParts.length > 0) {
2917
+ sql += ` GROUP BY ${groupByParts.join(", ")}`;
2918
+ }
2919
+ if (aggOptions.sort) {
2920
+ const sortField = aggOptions.sort.field;
2921
+ let sortExpr;
2922
+ if (sortField === "count") {
2923
+ sortExpr = "COUNT(*)";
2924
+ } else if (sortField.startsWith("sum_") || sortField.startsWith("avg_") || sortField.startsWith("min_") || sortField.startsWith("max_")) {
2925
+ sortExpr = `"${sortField}"`;
2926
+ } else {
2927
+ const aliasEntry = aliasMap.find((a) => a.alias === sortField);
2928
+ sortExpr = aliasEntry ? `"${sortField}"` : translator.getFieldSql(sortField);
2929
+ }
2930
+ const dir = aggOptions.sort.direction === 1 ? "ASC" : "DESC";
2931
+ sql += ` ORDER BY ${sortExpr} ${dir}`;
2932
+ }
2933
+ if (aggOptions.limit) {
2934
+ sql += ` LIMIT ${Number(aggOptions.limit)}`;
2935
+ }
2936
+ return { sql, params, aliasMap };
2937
+ }
2938
+ /** @internal — Extract operation value(s) from a row. Flattens single-op results. */
2939
+ _extractOpValue(row, operations) {
2940
+ if (operations.length === 1) {
2941
+ const op = operations[0];
2942
+ if (op.type === "count") return row.count;
2943
+ return row[`${op.type}_${op.field}`];
2944
+ }
2945
+ const result = {};
2946
+ for (const op of operations) {
2947
+ if (op.type === "count") {
2948
+ result.count = row.count;
2949
+ } else {
2950
+ result[`${op.type}_${op.field}`] = row[`${op.type}_${op.field}`];
2951
+ }
2952
+ }
2953
+ return result;
2954
+ }
2955
+ /** @internal — Process raw aggregation rows into nested result */
2956
+ _processAggregationResults(results, aggOptions, aliasMap, stringSetFacetField) {
2957
+ if (results.length === 0) return {};
2958
+ if (stringSetFacetField !== null) {
2959
+ const facetResult = {};
2960
+ for (const row of results) {
2961
+ const key = row.group_key;
2962
+ facetResult[key] = this._extractOpValue(row, aggOptions.operations);
2963
+ }
2964
+ return facetResult;
2965
+ }
2966
+ const nestedResult = {};
2967
+ const aliasLookup = /* @__PURE__ */ new Map();
2968
+ for (const entry of aliasMap) {
2969
+ const membershipKey = `${entry.field}::${entry.contains}`;
2970
+ aliasLookup.set(membershipKey, entry.alias);
2971
+ }
2972
+ for (const row of results) {
2973
+ let current = nestedResult;
2974
+ for (let i = 0; i < aggOptions.groupBy.length; i++) {
2975
+ const groupBy = aggOptions.groupBy[i];
2976
+ let key;
2977
+ if (typeof groupBy === "string") {
2978
+ key = String(row[groupBy]);
2979
+ } else {
2980
+ const membershipKey = `${groupBy.field}::${groupBy.contains}`;
2981
+ const alias = aliasLookup.get(membershipKey);
2982
+ key = alias ? row[alias] : "unknown";
2983
+ }
2984
+ if (i === aggOptions.groupBy.length - 1) {
2985
+ current[key] = this._extractOpValue(row, aggOptions.operations);
2986
+ } else {
2987
+ if (!current[key]) current[key] = {};
2988
+ current = current[key];
2989
+ }
2990
+ }
2991
+ }
2992
+ return nestedResult;
2993
+ }
2994
+ /** @internal */
2995
+ /** @internal — Batch sync: compare desired indexes against _indexes table, register missing */
2996
+ async _handleIndexesSync(request) {
2997
+ const body = await request.json();
2998
+ if (!body.models || !Array.isArray(body.models)) {
2999
+ return this._errorResponse("models array is required", 400);
3000
+ }
3001
+ let registered = 0;
3002
+ for (const model of body.models) {
3003
+ if (!model.modelName) continue;
3004
+ const existingIndexes = this._engine.listIndexes(model.modelName);
3005
+ const existingIndexMap = new Map(
3006
+ existingIndexes.map((idx) => [idx.field_name, idx])
3007
+ );
3008
+ const existingConstraints = this._engine.listUniqueConstraints(model.modelName);
3009
+ const existingConstraintSet = new Set(
3010
+ existingConstraints.map((c) => c.constraint_name)
3011
+ );
3012
+ for (const idx of model.indexes || []) {
3013
+ const existing = existingIndexMap.get(idx.fieldName);
3014
+ if (existing) {
3015
+ if (idx.unique && !existing.is_unique) {
3016
+ this._engine.registerIndex(model.modelName, idx.fieldName, idx.fieldType, true);
3017
+ registered++;
3018
+ }
3019
+ continue;
3020
+ }
3021
+ this._engine.registerIndex(model.modelName, idx.fieldName, idx.fieldType, idx.unique);
3022
+ registered++;
3023
+ }
3024
+ for (const uc of model.uniqueConstraints || []) {
3025
+ if (existingConstraintSet.has(uc.name)) continue;
3026
+ this._engine.registerUniqueConstraint(model.modelName, uc.name, uc.fields);
3027
+ registered++;
3028
+ }
3029
+ }
3030
+ const response = { registered };
3031
+ return Response.json(response);
3032
+ }
3033
+ /** @internal */
3034
+ async _handleIndexRegister(request) {
3035
+ const body = await request.json();
3036
+ const { modelName, fieldName, fieldType, unique } = body;
3037
+ if (!modelName || !fieldName || !fieldType) {
3038
+ return this._errorResponse(
3039
+ "modelName, fieldName, and fieldType are required",
3040
+ 400
3041
+ );
3042
+ }
3043
+ this._engine.registerIndex(
3044
+ modelName,
3045
+ fieldName,
3046
+ fieldType,
3047
+ unique || false
3048
+ );
3049
+ const response = {
3050
+ success: true,
3051
+ modelName,
3052
+ fieldName
3053
+ };
3054
+ return Response.json(response);
3055
+ }
3056
+ /** @internal */
3057
+ async _handleIndexDrop(request) {
3058
+ const body = await request.json();
3059
+ const { modelName, fieldName } = body;
3060
+ if (!modelName || !fieldName) {
3061
+ return this._errorResponse(
3062
+ "modelName and fieldName are required",
3063
+ 400
3064
+ );
3065
+ }
3066
+ this._engine.dropIndex(modelName, fieldName);
3067
+ const response = {
3068
+ success: true,
3069
+ modelName,
3070
+ fieldName
3071
+ };
3072
+ return Response.json(response);
3073
+ }
3074
+ /** @internal */
3075
+ _handleIndexList(request) {
3076
+ const url = new URL(request.url);
3077
+ const modelName = url.searchParams.get("modelName") || void 0;
3078
+ const indexes = this._engine.listIndexes(modelName);
3079
+ const response = { indexes };
3080
+ return Response.json(response);
3081
+ }
3082
+ /** @internal */
3083
+ async _handleUniqueConstraintRegister(request) {
3084
+ const body = await request.json();
3085
+ const { modelName, constraintName, fields } = body;
3086
+ if (!modelName || !constraintName || !fields || !Array.isArray(fields) || fields.length < 2) {
3087
+ return this._errorResponse(
3088
+ "modelName, constraintName, and fields (array with 2+ fields) are required",
3089
+ 400
3090
+ );
3091
+ }
3092
+ this._engine.registerUniqueConstraint(modelName, constraintName, fields);
3093
+ const response = {
3094
+ success: true,
3095
+ modelName,
3096
+ constraintName
3097
+ };
3098
+ return Response.json(response);
3099
+ }
3100
+ /** @internal */
3101
+ async _handleUniqueConstraintDrop(request) {
3102
+ const body = await request.json();
3103
+ const { modelName, constraintName } = body;
3104
+ if (!modelName || !constraintName) {
3105
+ return this._errorResponse(
3106
+ "modelName and constraintName are required",
3107
+ 400
3108
+ );
3109
+ }
3110
+ this._engine.dropUniqueConstraint(modelName, constraintName);
3111
+ const response = {
3112
+ success: true,
3113
+ modelName,
3114
+ constraintName
3115
+ };
3116
+ return Response.json(response);
3117
+ }
3118
+ /** @internal */
3119
+ _handleUniqueConstraintList(request) {
3120
+ const url = new URL(request.url);
3121
+ const modelName = url.searchParams.get("modelName") || void 0;
3122
+ const constraints = this._engine.listUniqueConstraints(modelName);
3123
+ const response = { constraints };
3124
+ return Response.json(response);
3125
+ }
3126
+ /** @internal */
3127
+ /**
3128
+ * Extract field names that use $contains from a filter tree.
3129
+ * @internal
3130
+ */
3131
+ _findContainsFields(filter) {
3132
+ const fields = [];
3133
+ if (!filter || typeof filter !== "object") return fields;
3134
+ for (const [key, value] of Object.entries(filter)) {
3135
+ if (key === "$and" || key === "$or") {
3136
+ if (Array.isArray(value)) {
3137
+ for (const sub of value) {
3138
+ fields.push(...this._findContainsFields(sub));
3139
+ }
3140
+ }
3141
+ } else if (value && typeof value === "object" && !Array.isArray(value) && "$contains" in value) {
3142
+ fields.push(key);
3143
+ }
3144
+ }
3145
+ return fields;
3146
+ }
3147
+ /**
3148
+ * Check for $contains misuse with caching.
3149
+ * Returns an error Response if misuse is detected, or null if OK.
3150
+ * @internal
3151
+ */
3152
+ _checkContainsMisuse(modelName, filter) {
3153
+ const containsFields = this._findContainsFields(filter);
3154
+ for (const field of containsFields) {
3155
+ assertValidIdentifier(field, "$contains field");
3156
+ const cacheKey = `${modelName}:${field}`;
3157
+ if (this._containsMisuseCache.has(cacheKey)) {
3158
+ return this._errorResponse(
3159
+ `Field '${field}' was saved as regular data, not a StringSet. To use $contains, save the field via stringSets instead of data.`,
3160
+ 400
3161
+ );
3162
+ }
3163
+ const check = this._engine.execSqlSync(
3164
+ `SELECT 1 FROM records WHERE _type = ? AND json_type(_data, '$.${field}') IS NOT NULL AND json_type(_data, '$.${field}') != 'array' LIMIT 1`,
3165
+ [modelName]
3166
+ );
3167
+ if (check.length > 0) {
3168
+ this._containsMisuseCache.add(cacheKey);
3169
+ return this._errorResponse(
3170
+ `Field '${field}' was saved as regular data, not a StringSet. To use $contains, save the field via stringSets instead of data.`,
3171
+ 400
3172
+ );
3173
+ }
3174
+ }
3175
+ return null;
3176
+ }
3177
+ /**
3178
+ * Check a write condition against the current state of a record.
3179
+ * Returns true if the condition is met (record exists and matches the filter).
3180
+ * Returns false if the record doesn't exist or doesn't match.
3181
+ * @internal
3182
+ */
3183
+ _checkCondition(modelName, id, condition) {
3184
+ const translator = new JsonQueryTranslator(modelName, void 0, {
3185
+ includeDocId: false
3186
+ });
3187
+ const whereClause = translator.translateFilter(condition);
3188
+ let sql = "SELECT 1 FROM records WHERE _id = ? AND _type = ?";
3189
+ const params = [id, modelName];
3190
+ if (whereClause.sql) {
3191
+ sql += ` AND ${whereClause.sql}`;
3192
+ params.push(...whereClause.params);
3193
+ }
3194
+ const rows = this._engine.execSqlSync(sql, params);
3195
+ return rows.length > 0;
3196
+ }
3197
+ _handleHealth() {
3198
+ return Response.json({ status: "ok" });
3199
+ }
3200
+ /** @internal — Return tracked field names and types for a model */
3201
+ _handleDescribe(request) {
3202
+ const url = new URL(request.url);
3203
+ const modelName = url.searchParams.get("modelName") || void 0;
3204
+ if (!modelName) {
3205
+ const body = { error: "modelName query parameter is required" };
3206
+ return Response.json(body, { status: 400 });
3207
+ }
3208
+ const fields = this._engine.getModelFields(modelName);
3209
+ const response = { modelName, fields };
3210
+ return Response.json(response);
3211
+ }
3212
+ /** @internal — List all known model names from records and _model_fields */
3213
+ _handleModelList() {
3214
+ const fromRecords = this._engine.execSqlSync(
3215
+ "SELECT DISTINCT _type FROM records ORDER BY _type"
3216
+ );
3217
+ const fromFields = this._engine.execSqlSync(
3218
+ "SELECT DISTINCT model_name FROM _model_fields ORDER BY model_name"
3219
+ );
3220
+ const fromIndexes = this._engine.execSqlSync(
3221
+ "SELECT DISTINCT model_name FROM _indexes ORDER BY model_name"
3222
+ );
3223
+ const modelSet = /* @__PURE__ */ new Set();
3224
+ for (const row of fromRecords) modelSet.add(row._type);
3225
+ for (const row of fromFields) modelSet.add(row.model_name);
3226
+ for (const row of fromIndexes) modelSet.add(row.model_name);
3227
+ return Response.json({ models: Array.from(modelSet).sort() });
3228
+ }
3229
+ /** @internal */
3230
+ _errorResponse(message, status) {
3231
+ const body = { error: message };
3232
+ return Response.json(body, { status });
3233
+ }
3234
+ /** @internal */
3235
+ _parseRow(row) {
3236
+ const { _id, _type, _data, ...rest } = row;
3237
+ let parsed = {};
3238
+ if (_data) {
3239
+ try {
3240
+ parsed = JSON.parse(_data);
3241
+ } catch (e) {
3242
+ console.warn("Failed to parse _data:", e);
3243
+ }
3244
+ }
3245
+ return { id: _id, type: _type, ...parsed, ...rest };
3246
+ }
3247
+ // ─── Include (Related Data Loading) ──────────────────────────────
3248
+ /** @internal — Validate an IncludeSpec, returning an error message or null */
3249
+ _validateIncludeSpec(spec) {
3250
+ if (!spec.model || typeof spec.model !== "string") {
3251
+ return "include: 'model' is required";
3252
+ }
3253
+ try {
3254
+ assertValidIdentifier(spec.model, "include model");
3255
+ } catch {
3256
+ return `include: invalid model name '${spec.model}'`;
3257
+ }
3258
+ if (spec.type !== "refersTo" && spec.type !== "hasMany" && spec.type !== "refersToMany") {
3259
+ return `include: 'type' must be 'refersTo', 'hasMany', or 'refersToMany', got '${spec.type}'`;
3260
+ }
3261
+ if (spec.type === "refersTo") {
3262
+ if (!spec.sourceField) {
3263
+ return "include refersTo: 'sourceField' is required";
3264
+ }
3265
+ try {
3266
+ assertValidIdentifier(spec.sourceField, "include sourceField");
3267
+ } catch {
3268
+ return `include refersTo: invalid sourceField '${spec.sourceField}'`;
3269
+ }
3270
+ }
3271
+ if (spec.type === "refersToMany") {
3272
+ if (!spec.sourceField) {
3273
+ return "include refersToMany: 'sourceField' is required";
3274
+ }
3275
+ try {
3276
+ assertValidIdentifier(spec.sourceField, "include sourceField");
3277
+ } catch {
3278
+ return `include refersToMany: invalid sourceField '${spec.sourceField}'`;
3279
+ }
3280
+ }
3281
+ if (spec.type === "hasMany") {
3282
+ if (!spec.foreignKey) {
3283
+ return "include hasMany: 'foreignKey' is required";
3284
+ }
3285
+ try {
3286
+ assertValidIdentifier(spec.foreignKey, "include foreignKey");
3287
+ } catch {
3288
+ return `include hasMany: invalid foreignKey '${spec.foreignKey}'`;
3289
+ }
3290
+ if (spec.localField) {
3291
+ try {
3292
+ assertValidIdentifier(spec.localField, "include localField");
3293
+ } catch {
3294
+ return `include hasMany: invalid localField '${spec.localField}'`;
3295
+ }
3296
+ }
3297
+ }
3298
+ if (spec.as) {
3299
+ try {
3300
+ assertValidIdentifier(spec.as, "include as");
3301
+ } catch {
3302
+ return `include: invalid alias '${spec.as}'`;
3303
+ }
3304
+ }
3305
+ if (spec.sort) {
3306
+ for (const field of Object.keys(spec.sort)) {
3307
+ try {
3308
+ assertValidIdentifier(field, "include sort field");
3309
+ } catch {
3310
+ return `include: invalid sort field '${field}'`;
3311
+ }
3312
+ }
3313
+ }
3314
+ return null;
3315
+ }
3316
+ /** @internal — Resolve all includes for a set of records */
3317
+ _resolveIncludes(records, includes, depth, _parentModelName) {
3318
+ if (records.length === 0) return records;
3319
+ if (depth >= 3) return records;
3320
+ for (const spec of includes) {
3321
+ const error = this._validateIncludeSpec(spec);
3322
+ if (error) {
3323
+ console.warn(`[include] Skipping invalid spec: ${error}`);
3324
+ continue;
3325
+ }
3326
+ const resultKey = spec.as || spec.model;
3327
+ for (const rec of records) {
3328
+ if (!rec._related) rec._related = {};
3329
+ }
3330
+ if (spec.type === "refersTo") {
3331
+ this._resolveRefersTo(records, spec, resultKey);
3332
+ } else if (spec.type === "refersToMany") {
3333
+ this._resolveRefersToMany(records, spec, resultKey);
3334
+ } else {
3335
+ this._resolveHasMany(records, spec, resultKey);
3336
+ }
3337
+ if (spec.include && spec.include.length > 0) {
3338
+ const allRelated = [];
3339
+ for (const rec of records) {
3340
+ const val = rec._related[resultKey];
3341
+ if (Array.isArray(val)) {
3342
+ allRelated.push(...val);
3343
+ } else if (val) {
3344
+ allRelated.push(val);
3345
+ }
3346
+ }
3347
+ if (allRelated.length > 0) {
3348
+ this._resolveIncludes(allRelated, spec.include, depth + 1, spec.model);
3349
+ }
3350
+ }
3351
+ }
3352
+ return records;
3353
+ }
3354
+ /** @internal — Resolve a refersTo include (parent FK → single related record) */
3355
+ _resolveRefersTo(records, spec, resultKey) {
3356
+ const sourceField = spec.sourceField;
3357
+ const uniqueValues = [
3358
+ ...new Set(
3359
+ records.map((r) => r[sourceField]).filter((v) => v != null && v !== "")
3360
+ )
3361
+ ];
3362
+ if (uniqueValues.length === 0) {
3363
+ for (const rec of records) {
3364
+ rec._related[resultKey] = null;
3365
+ }
3366
+ return;
3367
+ }
3368
+ const related = this._chunkedIn(
3369
+ spec.model,
3370
+ "id",
3371
+ uniqueValues,
3372
+ spec.filter,
3373
+ { projection: spec.projection }
3374
+ );
3375
+ const lookupMap = /* @__PURE__ */ new Map();
3376
+ for (const r of related) {
3377
+ lookupMap.set(r.id, r);
3378
+ }
3379
+ for (const rec of records) {
3380
+ const fk = rec[sourceField];
3381
+ rec._related[resultKey] = fk && lookupMap.get(fk) || null;
3382
+ }
3383
+ }
3384
+ /** @internal — Resolve a refersToMany include (parent StringSet field → array of related records) */
3385
+ _resolveRefersToMany(records, spec, resultKey) {
3386
+ const sourceField = spec.sourceField;
3387
+ const allTargetIds = /* @__PURE__ */ new Set();
3388
+ for (const rec of records) {
3389
+ const values = rec[sourceField];
3390
+ if (Array.isArray(values)) {
3391
+ for (const v of values) {
3392
+ if (v != null && v !== "") allTargetIds.add(v);
3393
+ }
3394
+ }
3395
+ }
3396
+ if (allTargetIds.size === 0) {
3397
+ for (const rec of records) {
3398
+ rec._related[resultKey] = [];
3399
+ }
3400
+ return;
3401
+ }
3402
+ const targets = this._chunkedIn(
3403
+ spec.model,
3404
+ "id",
3405
+ [...allTargetIds],
3406
+ spec.filter,
3407
+ { projection: spec.projection }
3408
+ );
3409
+ const targetLookup = /* @__PURE__ */ new Map();
3410
+ for (const t of targets) {
3411
+ targetLookup.set(t.id, t);
3412
+ }
3413
+ for (const rec of records) {
3414
+ const values = rec[sourceField];
3415
+ if (Array.isArray(values)) {
3416
+ rec._related[resultKey] = values.map((id) => targetLookup.get(id)).filter(Boolean);
3417
+ } else {
3418
+ rec._related[resultKey] = [];
3419
+ }
3420
+ }
3421
+ }
3422
+ /** @internal — Resolve a hasMany include (target FK → array of related records) */
3423
+ _resolveHasMany(records, spec, resultKey) {
3424
+ const localField = spec.localField || "id";
3425
+ const uniqueValues = [
3426
+ ...new Set(
3427
+ records.map((r) => r[localField]).filter((v) => v != null && v !== "")
3428
+ )
3429
+ ];
3430
+ if (uniqueValues.length === 0) {
3431
+ for (const rec of records) {
3432
+ rec._related[resultKey] = [];
3433
+ }
3434
+ return;
3435
+ }
3436
+ let related;
3437
+ if (spec.limit) {
3438
+ related = this._resolveHasManyWithLimit(spec, uniqueValues);
3439
+ } else {
3440
+ related = this._resolveHasManySimple(spec, uniqueValues);
3441
+ }
3442
+ const foreignKey = spec.foreignKey;
3443
+ const grouped = /* @__PURE__ */ new Map();
3444
+ for (const r of related) {
3445
+ const fk = r[foreignKey];
3446
+ if (!grouped.has(fk)) grouped.set(fk, []);
3447
+ grouped.get(fk).push(r);
3448
+ }
3449
+ if (spec.projection) {
3450
+ for (const [key, list] of grouped) {
3451
+ grouped.set(
3452
+ key,
3453
+ list.map((r) => this._applyProjection(r, spec.projection))
3454
+ );
3455
+ }
3456
+ }
3457
+ for (const rec of records) {
3458
+ rec._related[resultKey] = grouped.get(rec[localField]) || [];
3459
+ }
3460
+ }
3461
+ /** @internal — Simple hasMany without per-parent limit */
3462
+ _resolveHasManySimple(spec, parentValues) {
3463
+ return this._chunkedIn(
3464
+ spec.model,
3465
+ spec.foreignKey,
3466
+ parentValues,
3467
+ spec.filter,
3468
+ { sort: spec.sort }
3469
+ );
3470
+ }
3471
+ /** @internal — hasMany with per-parent limit using ROW_NUMBER() */
3472
+ _resolveHasManyWithLimit(spec, parentValues) {
3473
+ const foreignKey = spec.foreignKey;
3474
+ assertValidIdentifier(foreignKey, "include foreignKey");
3475
+ let sortClause = `"_id" ASC`;
3476
+ if (spec.sort) {
3477
+ const parts = [];
3478
+ for (const [field, dir] of Object.entries(spec.sort)) {
3479
+ assertValidIdentifier(field, "include sort field");
3480
+ const fieldSql = field === "id" ? `"_id"` : field === "type" ? `"_type"` : `json_extract(_data, '$.${field}')`;
3481
+ parts.push(`${fieldSql} ${dir === -1 ? "DESC" : "ASC"}`);
3482
+ }
3483
+ sortClause = parts.join(", ");
3484
+ }
3485
+ const translator = new JsonQueryTranslator(spec.model, void 0, {
3486
+ includeDocId: false
3487
+ });
3488
+ let filterClause = "";
3489
+ let filterParams = [];
3490
+ if (spec.filter) {
3491
+ const translated = translator.translateFilter(spec.filter);
3492
+ if (translated.sql) {
3493
+ filterClause = ` AND ${translated.sql}`;
3494
+ filterParams = translated.params;
3495
+ }
3496
+ }
3497
+ const reservedParams = 1 + filterParams.length + 1;
3498
+ const chunkSize = Math.max(1, 100 - reservedParams);
3499
+ const allResults = [];
3500
+ for (let i = 0; i < parentValues.length; i += chunkSize) {
3501
+ const chunk = parentValues.slice(i, i + chunkSize);
3502
+ const inPlaceholders = chunk.map(() => "?").join(", ");
3503
+ const sql = `
3504
+ SELECT "_id", "_type", _data FROM (
3505
+ SELECT "_id", "_type", _data,
3506
+ ROW_NUMBER() OVER (
3507
+ PARTITION BY json_extract(_data, '$.${foreignKey}')
3508
+ ORDER BY ${sortClause}
3509
+ ) as _rn
3510
+ FROM records
3511
+ WHERE "_type" = ?
3512
+ AND json_extract(_data, '$.${foreignKey}') IN (${inPlaceholders})
3513
+ ${filterClause}
3514
+ ) WHERE _rn <= ?
3515
+ `;
3516
+ const params = [spec.model, ...chunk, ...filterParams, spec.limit];
3517
+ const rows = this._engine.execSqlSync(sql, params);
3518
+ allResults.push(...rows.map((row) => this._parseRow(row)));
3519
+ }
3520
+ return allResults;
3521
+ }
3522
+ /**
3523
+ * @internal — Execute IN queries in chunks to stay under Cloudflare DO's
3524
+ * 100 bound parameter limit per SQL statement.
3525
+ */
3526
+ _chunkedIn(model, field, values, extraFilter, extraOptions) {
3527
+ const translator = new JsonQueryTranslator(model, void 0, {
3528
+ includeDocId: false
3529
+ });
3530
+ let filterParamCount = 0;
3531
+ if (extraFilter) {
3532
+ const translated = translator.translateFilter(extraFilter);
3533
+ filterParamCount = translated.params.length;
3534
+ }
3535
+ const reservedParams = 1 + filterParamCount;
3536
+ const chunkSize = Math.max(1, 100 - reservedParams);
3537
+ const allResults = [];
3538
+ for (let i = 0; i < values.length; i += chunkSize) {
3539
+ const chunk = values.slice(i, i + chunkSize);
3540
+ const filter = {
3541
+ [field]: { $in: chunk },
3542
+ ...extraFilter || {}
3543
+ };
3544
+ const options = {};
3545
+ if (extraOptions?.sort) options.sort = extraOptions.sort;
3546
+ if (extraOptions?.projection) options.projection = extraOptions.projection;
3547
+ const { sql, params } = translator.translateFind(filter, options);
3548
+ const rows = this._engine.execSqlSync(sql, params);
3549
+ allResults.push(...rows.map((row) => this._parseRow(row)));
3550
+ }
3551
+ return allResults;
3552
+ }
3553
+ /** @internal — Apply projection to a single record */
3554
+ _applyProjection(record, projection) {
3555
+ const fields = Object.entries(projection);
3556
+ if (fields.length === 0) return record;
3557
+ const isInclusion = fields.some(([, v]) => v === 1);
3558
+ if (isInclusion) {
3559
+ const result = {};
3560
+ if (record.id !== void 0) result.id = record.id;
3561
+ if (record.type !== void 0) result.type = record.type;
3562
+ if (record._related !== void 0) result._related = record._related;
3563
+ for (const [f, v] of fields) {
3564
+ if (v === 1 && record[f] !== void 0) {
3565
+ result[f] = record[f];
3566
+ }
3567
+ }
3568
+ return result;
3569
+ } else {
3570
+ const result = { ...record };
3571
+ for (const [f, v] of fields) {
3572
+ if (v === 0) delete result[f];
3573
+ }
3574
+ return result;
3575
+ }
3576
+ }
3577
+ };
3578
+ }
3579
+ var createDocumentDO = createDatabaseDO;
3580
+
3581
+ // src/engines/cloudflare/worker.template.ts
3582
+ var CORS_HEADERS = {
3583
+ "Access-Control-Allow-Origin": "*",
3584
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
3585
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
3586
+ };
3587
+ function handleOptions() {
3588
+ return new Response(null, {
3589
+ status: 204,
3590
+ headers: CORS_HEADERS
3591
+ });
3592
+ }
3593
+ function addCorsHeaders(response) {
3594
+ const newHeaders = new Headers(response.headers);
3595
+ for (const [key, value] of Object.entries(CORS_HEADERS)) {
3596
+ newHeaders.set(key, value);
3597
+ }
3598
+ return new Response(response.body, {
3599
+ status: response.status,
3600
+ statusText: response.statusText,
3601
+ headers: newHeaders
3602
+ });
3603
+ }
3604
+ function getDocumentStub(env, docId) {
3605
+ const doId = env.DOCUMENT_DO.idFromName(docId);
3606
+ return env.DOCUMENT_DO.get(doId);
3607
+ }
3608
+ async function handleRequest(request, env) {
3609
+ if (request.method === "OPTIONS") {
3610
+ return handleOptions();
3611
+ }
3612
+ const url = new URL(request.url);
3613
+ let docId = url.searchParams.get("docId");
3614
+ if (!docId) {
3615
+ const pathMatch = url.pathname.match(/^\/docs\/([^\/]+)\/(.+)$/);
3616
+ if (pathMatch) {
3617
+ docId = pathMatch[1];
3618
+ url.pathname = "/" + pathMatch[2];
3619
+ }
3620
+ }
3621
+ if (!docId) {
3622
+ return addCorsHeaders(
3623
+ Response.json(
3624
+ { error: "Missing docId parameter" },
3625
+ { status: 400 }
3626
+ )
3627
+ );
3628
+ }
3629
+ const stub = getDocumentStub(env, docId);
3630
+ const doRequest = new Request(url.toString(), {
3631
+ method: request.method,
3632
+ headers: request.headers,
3633
+ body: request.body
3634
+ });
3635
+ const response = await stub.fetch(doRequest);
3636
+ return addCorsHeaders(response);
3637
+ }