js-bao 0.2.11 → 0.3.0-alpha.0

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