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