js-bao 0.2.11 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +174 -0
  2. package/dist/BaseModel-5YQCROYE.js +17 -0
  3. package/dist/BaseModel-5YQCROYE.js.map +1 -0
  4. package/dist/BaseModel-FCNWDJBH.js +17 -0
  5. package/dist/BaseModel-FCNWDJBH.js.map +1 -0
  6. package/dist/BrowserDatabaseFactory-PXOTK2DQ.js +119 -0
  7. package/dist/BrowserDatabaseFactory-PXOTK2DQ.js.map +1 -0
  8. package/dist/BrowserDatabaseFactory-WD4VX2VZ.js +119 -0
  9. package/dist/BrowserDatabaseFactory-WD4VX2VZ.js.map +1 -0
  10. package/dist/IncludeResolver-RCKQGNPZ.js +385 -0
  11. package/dist/IncludeResolver-RCKQGNPZ.js.map +1 -0
  12. package/dist/IncludeResolver-WGSQDMS7.js +385 -0
  13. package/dist/IncludeResolver-WGSQDMS7.js.map +1 -0
  14. package/dist/NodeDatabaseFactory-J4Z36UF3.js +165 -0
  15. package/dist/NodeDatabaseFactory-J4Z36UF3.js.map +1 -0
  16. package/dist/NodeDatabaseFactory-QIEKAXBM.js +10 -0
  17. package/dist/NodeDatabaseFactory-QIEKAXBM.js.map +1 -0
  18. package/dist/NodeSqliteEngine-HJSAYE4E.js +383 -0
  19. package/dist/NodeSqliteEngine-HJSAYE4E.js.map +1 -0
  20. package/dist/NodeSqliteEngine-I5SLWLME.js +383 -0
  21. package/dist/NodeSqliteEngine-I5SLWLME.js.map +1 -0
  22. package/dist/browser.cjs +3779 -3370
  23. package/dist/browser.d.cts +18 -1
  24. package/dist/browser.d.ts +18 -1
  25. package/dist/browser.js +3750 -3341
  26. package/dist/chunk-3PZWHUZO.js +4153 -0
  27. package/dist/chunk-3PZWHUZO.js.map +1 -0
  28. package/dist/chunk-53MS4MN7.js +373 -0
  29. package/dist/chunk-53MS4MN7.js.map +1 -0
  30. package/dist/chunk-65G2P4GL.js +709 -0
  31. package/dist/chunk-65G2P4GL.js.map +1 -0
  32. package/dist/chunk-6UX3YSCW.js +4151 -0
  33. package/dist/chunk-6UX3YSCW.js.map +1 -0
  34. package/dist/chunk-DANSD6BE.js +709 -0
  35. package/dist/chunk-DANSD6BE.js.map +1 -0
  36. package/dist/chunk-DF3JEQXA.js +373 -0
  37. package/dist/chunk-DF3JEQXA.js.map +1 -0
  38. package/dist/chunk-GO3APTPX.js +61 -0
  39. package/dist/chunk-GO3APTPX.js.map +1 -0
  40. package/dist/chunk-ID4U6IQC.js +53 -0
  41. package/dist/chunk-ID4U6IQC.js.map +1 -0
  42. package/dist/chunk-RQVS3LVL.js +165 -0
  43. package/dist/chunk-RQVS3LVL.js.map +1 -0
  44. package/dist/client.cjs +837 -0
  45. package/dist/client.d.cts +1101 -0
  46. package/dist/client.d.ts +1101 -0
  47. package/dist/client.js +806 -0
  48. package/dist/cloudflare-do.cjs +3637 -0
  49. package/dist/cloudflare-do.d.cts +1366 -0
  50. package/dist/cloudflare-do.d.ts +1366 -0
  51. package/dist/cloudflare-do.js +3614 -0
  52. package/dist/cloudflare.cjs +1048 -0
  53. package/dist/cloudflare.d.cts +1381 -0
  54. package/dist/cloudflare.d.ts +1381 -0
  55. package/dist/cloudflare.js +1017 -0
  56. package/dist/codegen.cjs +260 -19
  57. package/dist/environment-TOTQICSE.js +17 -0
  58. package/dist/environment-TOTQICSE.js.map +1 -0
  59. package/dist/index.cjs +1905 -1492
  60. package/dist/index.d.cts +19 -2
  61. package/dist/index.d.ts +19 -2
  62. package/dist/index.js +1870 -1457
  63. package/dist/node.cjs +4779 -4366
  64. package/dist/node.d.cts +18 -1
  65. package/dist/node.d.ts +18 -1
  66. package/dist/node.js +4758 -4345
  67. package/package.json +42 -13
@@ -0,0 +1,4153 @@
1
+ // src/models/BaseModel.ts
2
+ import * as Y from "yjs";
3
+ import { ulid } from "ulid";
4
+
5
+ // src/models/StringSet.ts
6
+ var StringSet = class _StringSet {
7
+ _values;
8
+ _model;
9
+ _fieldName;
10
+ constructor(model, fieldName, initialValues = []) {
11
+ this._model = model;
12
+ this._fieldName = fieldName;
13
+ this._values = new Set(initialValues);
14
+ }
15
+ /**
16
+ * Add a string to the set
17
+ */
18
+ add(value) {
19
+ if (typeof value !== "string") {
20
+ throw new Error("StringSet can only contain string values");
21
+ }
22
+ if (!this._values.has(value)) {
23
+ this._values.add(value);
24
+ this._model.markStringSetChange(this._fieldName, "add", value);
25
+ }
26
+ }
27
+ /**
28
+ * Remove a string from the set
29
+ */
30
+ remove(value) {
31
+ if (this._values.has(value)) {
32
+ this._values.delete(value);
33
+ this._model.markStringSetChange(this._fieldName, "remove", value);
34
+ }
35
+ }
36
+ /**
37
+ * Check if the set contains a string
38
+ */
39
+ has(value) {
40
+ return this._values.has(value);
41
+ }
42
+ /**
43
+ * Clear all strings from the set
44
+ */
45
+ clear() {
46
+ if (this._values.size > 0) {
47
+ this._values.clear();
48
+ this._model.markStringSetChange(this._fieldName, "clear");
49
+ }
50
+ }
51
+ /**
52
+ * Get the number of strings in the set
53
+ */
54
+ get size() {
55
+ return this._values.size;
56
+ }
57
+ /**
58
+ * Get an iterator of all values
59
+ */
60
+ values() {
61
+ return this._values.values();
62
+ }
63
+ /**
64
+ * Make the StringSet iterable
65
+ */
66
+ [Symbol.iterator]() {
67
+ return this._values[Symbol.iterator]();
68
+ }
69
+ /**
70
+ * Convert to array
71
+ */
72
+ toArray() {
73
+ return Array.from(this._values);
74
+ }
75
+ /**
76
+ * Union with another StringSet
77
+ */
78
+ union(other) {
79
+ const result = new _StringSet(this._model, this._fieldName, this.toArray());
80
+ for (const value of other) {
81
+ result._values.add(value);
82
+ }
83
+ return result;
84
+ }
85
+ /**
86
+ * Intersection with another StringSet
87
+ */
88
+ intersection(other) {
89
+ const result = new _StringSet(this._model, this._fieldName);
90
+ for (const value of this._values) {
91
+ if (other.has(value)) {
92
+ result._values.add(value);
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+ /**
98
+ * Difference with another StringSet (values in this set but not in other)
99
+ */
100
+ difference(other) {
101
+ const result = new _StringSet(this._model, this._fieldName);
102
+ for (const value of this._values) {
103
+ if (!other.has(value)) {
104
+ result._values.add(value);
105
+ }
106
+ }
107
+ return result;
108
+ }
109
+ /**
110
+ * Get the current state including pending changes
111
+ * This is used internally by the model to determine the current view
112
+ */
113
+ _getCurrentState() {
114
+ return new Set(this._values);
115
+ }
116
+ /**
117
+ * Update the internal state (used when loading from Yjs or applying changes)
118
+ */
119
+ _updateInternalState(values) {
120
+ this._values = new Set(values);
121
+ }
122
+ };
123
+
124
+ // src/types/documentTypes.ts
125
+ var DocumentClosedError = class extends Error {
126
+ code = "ERR_DOC_CLOSED";
127
+ constructor(message) {
128
+ super(message);
129
+ this.name = "DocumentClosedError";
130
+ }
131
+ };
132
+ var DocumentResolutionError = class extends Error {
133
+ code = "ERR_DOC_UNRESOLVED";
134
+ constructor(message) {
135
+ super(message);
136
+ this.name = "DocumentResolutionError";
137
+ }
138
+ };
139
+
140
+ // src/types/queryTypes.ts
141
+ var DocumentQueryError = class _DocumentQueryError extends Error {
142
+ constructor(message) {
143
+ super(message);
144
+ this.name = "DocumentQueryError";
145
+ Object.setPrototypeOf(this, _DocumentQueryError.prototype);
146
+ }
147
+ };
148
+ var InvalidOperatorError = class _InvalidOperatorError extends DocumentQueryError {
149
+ field;
150
+ operator;
151
+ fieldType;
152
+ constructor(message, field, operator, fieldType) {
153
+ super(message);
154
+ this.name = "InvalidOperatorError";
155
+ this.field = field;
156
+ this.operator = operator;
157
+ this.fieldType = fieldType;
158
+ Object.setPrototypeOf(this, _InvalidOperatorError.prototype);
159
+ }
160
+ };
161
+ var InvalidFieldError = class _InvalidFieldError extends DocumentQueryError {
162
+ field;
163
+ modelName;
164
+ constructor(message, field, modelName) {
165
+ super(message);
166
+ this.name = "InvalidFieldError";
167
+ this.field = field;
168
+ this.modelName = modelName;
169
+ Object.setPrototypeOf(this, _InvalidFieldError.prototype);
170
+ }
171
+ };
172
+ var InvalidCursorError = class _InvalidCursorError extends DocumentQueryError {
173
+ cursor;
174
+ constructor(message, cursor) {
175
+ super(message);
176
+ this.name = "InvalidCursorError";
177
+ this.cursor = cursor;
178
+ Object.setPrototypeOf(this, _InvalidCursorError.prototype);
179
+ }
180
+ };
181
+
182
+ // src/query/CursorManager.ts
183
+ var base64Encode = (str) => {
184
+ if (typeof btoa !== "undefined") {
185
+ return btoa(str);
186
+ } else if (typeof Buffer !== "undefined") {
187
+ return Buffer.from(str, "utf-8").toString("base64");
188
+ } else {
189
+ throw new Error("No base64 encoding available");
190
+ }
191
+ };
192
+ var base64Decode = (str) => {
193
+ if (typeof atob !== "undefined") {
194
+ return atob(str);
195
+ } else if (typeof Buffer !== "undefined") {
196
+ return Buffer.from(str, "base64").toString("utf-8");
197
+ } else {
198
+ throw new Error("No base64 decoding available");
199
+ }
200
+ };
201
+ var CursorManager = class {
202
+ /**
203
+ * Encode cursor data to base64 string
204
+ */
205
+ static encodeCursor(cursorData) {
206
+ try {
207
+ const jsonString = JSON.stringify(cursorData);
208
+ return base64Encode(jsonString);
209
+ } catch (error) {
210
+ throw new InvalidCursorError(
211
+ `Failed to encode cursor: ${error instanceof Error ? error.message : "Unknown error"}`,
212
+ JSON.stringify(cursorData)
213
+ );
214
+ }
215
+ }
216
+ /**
217
+ * Decode base64 cursor string to cursor data
218
+ */
219
+ static decodeCursor(cursor) {
220
+ try {
221
+ const jsonString = base64Decode(cursor);
222
+ const parsed = JSON.parse(jsonString);
223
+ if (!parsed || typeof parsed !== "object") {
224
+ throw new Error("Cursor must be an object");
225
+ }
226
+ if (!parsed.values || typeof parsed.values !== "object") {
227
+ throw new Error("Cursor must have values object");
228
+ }
229
+ if (!Array.isArray(parsed.sortFields)) {
230
+ throw new Error("Cursor must have sortFields array");
231
+ }
232
+ if (parsed.direction !== 1 && parsed.direction !== -1) {
233
+ throw new Error("Cursor direction must be 1 or -1");
234
+ }
235
+ return parsed;
236
+ } catch (error) {
237
+ throw new InvalidCursorError(
238
+ `Failed to decode cursor: ${error instanceof Error ? error.message : "Unknown error"}`,
239
+ cursor
240
+ );
241
+ }
242
+ }
243
+ /**
244
+ * Generate cursor from a record and sort specification
245
+ */
246
+ static generateCursor(record, sortFields, direction) {
247
+ const values = {};
248
+ for (const field of sortFields) {
249
+ if (record.hasOwnProperty(field)) {
250
+ values[field] = record[field];
251
+ } else {
252
+ throw new Error(
253
+ `Cannot generate cursor: record missing sort field '${field}'`
254
+ );
255
+ }
256
+ }
257
+ const cursorData = {
258
+ values,
259
+ sortFields,
260
+ direction
261
+ };
262
+ return this.encodeCursor(cursorData);
263
+ }
264
+ /**
265
+ * Build SQL pagination conditions based on cursor
266
+ * Uses lexicographic ordering for stable pagination with multiple sort fields
267
+ */
268
+ static buildPaginationConditions(cursor, currentSortFields, sortDirections, requestedDirection, fieldFormatter = (field) => field) {
269
+ if (!this.arraysEqual(cursor.sortFields, currentSortFields)) {
270
+ throw new InvalidCursorError(
271
+ `Cursor sort fields [${cursor.sortFields.join(
272
+ ", "
273
+ )}] don't match query sort fields [${currentSortFields.join(", ")}]`,
274
+ JSON.stringify(cursor)
275
+ );
276
+ }
277
+ const conditions = [];
278
+ const params = [];
279
+ const sortFields = cursor.sortFields;
280
+ const direction = requestedDirection;
281
+ for (let i = 0; i < sortFields.length; i++) {
282
+ const fieldConditions = [];
283
+ const fieldParams = [];
284
+ for (let j = 0; j < i; j++) {
285
+ const field = sortFields[j];
286
+ const value = cursor.values[field];
287
+ fieldConditions.push(`${fieldFormatter(field)} = ?`);
288
+ fieldParams.push(value);
289
+ }
290
+ const currentField = sortFields[i];
291
+ const currentValue = cursor.values[currentField];
292
+ const fieldSortDir = sortDirections[i] ?? 1;
293
+ const forwardOp = fieldSortDir === 1 ? ">" : "<";
294
+ const operator = direction === 1 ? forwardOp : forwardOp === ">" ? "<" : ">";
295
+ fieldConditions.push(`${fieldFormatter(currentField)} ${operator} ?`);
296
+ fieldParams.push(currentValue);
297
+ const levelCondition = fieldConditions.join(" AND ");
298
+ conditions.push(`(${levelCondition})`);
299
+ params.push(...fieldParams);
300
+ }
301
+ const sql = `(${conditions.join(" OR ")})`;
302
+ return { sql, params };
303
+ }
304
+ /**
305
+ * Extract sort fields from sort specification, ensuring 'id' is always included for stability
306
+ */
307
+ static extractSortFields(sort) {
308
+ const fields = [];
309
+ if (sort && Object.keys(sort).length > 0) {
310
+ fields.push(...Object.keys(sort));
311
+ }
312
+ if (!fields.includes("id")) {
313
+ fields.push("id");
314
+ }
315
+ return fields;
316
+ }
317
+ /**
318
+ * Build ORDER BY clause from sort specification
319
+ */
320
+ static buildOrderClause(sort, fieldFormatter = (field) => field) {
321
+ const fields = this.extractSortFields(sort);
322
+ const clauses = [];
323
+ const directions = [];
324
+ if (sort && Object.keys(sort).length > 0) {
325
+ for (const [field, dir] of Object.entries(sort)) {
326
+ const sqlDirection = dir === 1 ? "ASC" : "DESC";
327
+ clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
328
+ directions.push(dir);
329
+ }
330
+ }
331
+ if (!sort || !sort.hasOwnProperty("id")) {
332
+ clauses.push(`${fieldFormatter("id")} ASC`);
333
+ directions.push(1);
334
+ } else if (sort && sort.hasOwnProperty("id")) {
335
+ directions.push(sort["id"]);
336
+ }
337
+ return {
338
+ sql: clauses.join(", "),
339
+ fields,
340
+ directions
341
+ };
342
+ }
343
+ /**
344
+ * Determine if there are more results available for pagination
345
+ */
346
+ static hasMoreResults(requestedLimit, actualResultCount) {
347
+ if (!requestedLimit || requestedLimit <= 0) {
348
+ return false;
349
+ }
350
+ return actualResultCount >= requestedLimit;
351
+ }
352
+ /**
353
+ * Generate next and previous cursors from result set
354
+ */
355
+ static generateResultCursors(results, sortFields, _requestedDirection, hasMore, isFirstPage = false) {
356
+ if (results.length === 0) {
357
+ return {};
358
+ }
359
+ const cursors = {};
360
+ if (hasMore && results.length > 0) {
361
+ const lastResult = results[results.length - 1];
362
+ cursors.nextCursor = this.generateCursor(lastResult, sortFields, 1);
363
+ }
364
+ if (results.length > 0 && !isFirstPage) {
365
+ const firstResult = results[0];
366
+ cursors.prevCursor = this.generateCursor(firstResult, sortFields, -1);
367
+ }
368
+ return cursors;
369
+ }
370
+ /**
371
+ * Utility to compare arrays for equality
372
+ */
373
+ static arraysEqual(a, b) {
374
+ if (a.length !== b.length) return false;
375
+ return a.every((val, index) => val === b[index]);
376
+ }
377
+ };
378
+
379
+ // src/utils/sql.ts
380
+ var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
381
+ function isValidIdentifier(name) {
382
+ return IDENTIFIER_PATTERN.test(name);
383
+ }
384
+ function assertValidIdentifier(name, context) {
385
+ if (!isValidIdentifier(name)) {
386
+ throw new Error(
387
+ `${context}: Identifier "${name}" must match ${IDENTIFIER_PATTERN.source}`
388
+ );
389
+ }
390
+ }
391
+ function quoteIdentifier(name) {
392
+ return `"${name.replace(/"/g, '""')}"`;
393
+ }
394
+ function deterministicIdentifierHash(value) {
395
+ let hash = 0;
396
+ for (let i = 0; i < value.length; i++) {
397
+ hash = (hash << 5) - hash + value.charCodeAt(i);
398
+ hash |= 0;
399
+ }
400
+ const positiveHash = hash === 0 ? 0 : Math.abs(hash);
401
+ return positiveHash.toString(36);
402
+ }
403
+ function buildSafeAlias(prefix, rawDescriptor) {
404
+ const suffix = deterministicIdentifierHash(rawDescriptor);
405
+ const candidate = `${prefix}_${suffix}`;
406
+ assertValidIdentifier(candidate, "Alias generation");
407
+ return candidate;
408
+ }
409
+
410
+ // src/utils/patterns.ts
411
+ function escapeLikeLiteral(input) {
412
+ return input.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
413
+ }
414
+ function buildLikePattern(value, mode) {
415
+ const trimmed = (value ?? "").trim();
416
+ if (trimmed.length === 0) return null;
417
+ if (trimmed.length > 1024) {
418
+ throw new Error("substring value exceeds 1024 characters");
419
+ }
420
+ const escaped = escapeLikeLiteral(trimmed);
421
+ switch (mode) {
422
+ case "startsWith":
423
+ return `${escaped}%`;
424
+ case "endsWith":
425
+ return `%${escaped}`;
426
+ case "containsText":
427
+ return `%${escaped}%`;
428
+ }
429
+ }
430
+ function shouldLogLikeEscapes() {
431
+ try {
432
+ if (typeof process !== "undefined" && process.env) {
433
+ return !!process.env.JS_BAO_DEBUG_LIKE_ESCAPES;
434
+ }
435
+ } catch {
436
+ }
437
+ return false;
438
+ }
439
+
440
+ // src/query/DocumentQueryTranslator.ts
441
+ var SYSTEM_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
442
+ var DocumentQueryTranslator = class {
443
+ modelName;
444
+ schema;
445
+ tableName;
446
+ quotedTableName;
447
+ fieldSqlCache;
448
+ constructor(modelName, schema) {
449
+ this.modelName = modelName;
450
+ this.schema = schema;
451
+ this.tableName = `model_${modelName.toLowerCase()}`;
452
+ this.quotedTableName = quoteIdentifier(this.tableName);
453
+ this.fieldSqlCache = /* @__PURE__ */ new Map();
454
+ }
455
+ getQuotedField(fieldName) {
456
+ if (!this.fieldSqlCache.has(fieldName)) {
457
+ if (!this.schema.has(fieldName) && !SYSTEM_FIELDS.has(fieldName)) {
458
+ throw new InvalidFieldError(
459
+ `Unknown field: ${fieldName} in model ${this.modelName}`,
460
+ fieldName,
461
+ this.modelName
462
+ );
463
+ }
464
+ this.fieldSqlCache.set(fieldName, quoteIdentifier(fieldName));
465
+ }
466
+ return this.fieldSqlCache.get(fieldName);
467
+ }
468
+ /**
469
+ * Translate document filter and options to SQL for find operations
470
+ */
471
+ translateFind(filter, options) {
472
+ if (options?.projection) {
473
+ this.validateProjection(options.projection);
474
+ }
475
+ const whereClause = this.translateFilter(filter);
476
+ const orderClause = CursorManager.buildOrderClause(
477
+ options?.sort,
478
+ (field) => this.getQuotedField(field)
479
+ );
480
+ const limitClause = this.buildLimitClause(options);
481
+ const paginationClause = this.buildPaginationClause(
482
+ options,
483
+ orderClause.fields,
484
+ orderClause.directions
485
+ );
486
+ const selectClause = this.buildSelectClause(options?.projection);
487
+ let sql = `SELECT ${selectClause} FROM ${this.quotedTableName}`;
488
+ const params = [];
489
+ const conditions = [];
490
+ if (whereClause.sql) {
491
+ conditions.push(whereClause.sql);
492
+ params.push(...whereClause.params);
493
+ }
494
+ if (paginationClause.sql) {
495
+ conditions.push(paginationClause.sql);
496
+ params.push(...paginationClause.params);
497
+ }
498
+ const documentClause = this.buildDocumentClause(options?.documents);
499
+ if (documentClause) {
500
+ conditions.push(documentClause.sql);
501
+ params.push(...documentClause.params);
502
+ }
503
+ if (conditions.length > 0) {
504
+ sql += ` WHERE ${conditions.join(" AND ")}`;
505
+ }
506
+ if (orderClause.sql) {
507
+ sql += ` ORDER BY ${orderClause.sql}`;
508
+ }
509
+ if (limitClause.sql) {
510
+ sql += ` LIMIT ${limitClause.sql}`;
511
+ }
512
+ return {
513
+ sql,
514
+ params,
515
+ sortFields: orderClause.fields,
516
+ sortDirections: orderClause.directions
517
+ };
518
+ }
519
+ /**
520
+ * Translate document filter to SQL for count operations
521
+ */
522
+ translateCount(filter, options) {
523
+ const whereClause = this.translateFilter(filter);
524
+ const documentClause = this.buildDocumentClause(options?.documents);
525
+ const conditions = [];
526
+ const params = [];
527
+ if (whereClause.sql) {
528
+ conditions.push(whereClause.sql);
529
+ params.push(...whereClause.params);
530
+ }
531
+ if (documentClause) {
532
+ conditions.push(documentClause.sql);
533
+ params.push(...documentClause.params);
534
+ }
535
+ let sql = `SELECT COUNT(*) as count FROM ${this.quotedTableName}`;
536
+ if (conditions.length > 0) {
537
+ sql += ` WHERE ${conditions.join(" AND ")}`;
538
+ }
539
+ return {
540
+ sql,
541
+ params
542
+ };
543
+ }
544
+ /**
545
+ * Translate document filter to SQL WHERE clause
546
+ */
547
+ translateFilter(filter) {
548
+ if (!filter || Object.keys(filter).length === 0) {
549
+ return { sql: "", params: [] };
550
+ }
551
+ const conditions = [];
552
+ const params = [];
553
+ for (const [key, value] of Object.entries(filter)) {
554
+ if (key === "$and") {
555
+ const andClause = this.translateLogicalOperator(
556
+ "AND",
557
+ value
558
+ );
559
+ if (andClause.sql) {
560
+ conditions.push(`(${andClause.sql})`);
561
+ params.push(...andClause.params);
562
+ }
563
+ } else if (key === "$or") {
564
+ const orClause = this.translateLogicalOperator(
565
+ "OR",
566
+ value
567
+ );
568
+ if (orClause.sql) {
569
+ conditions.push(`(${orClause.sql})`);
570
+ params.push(...orClause.params);
571
+ }
572
+ } else {
573
+ const fieldClause = this.translateFieldCondition(key, value);
574
+ if (fieldClause.sql) {
575
+ conditions.push(fieldClause.sql);
576
+ params.push(...fieldClause.params);
577
+ }
578
+ }
579
+ }
580
+ return {
581
+ sql: conditions.join(" AND "),
582
+ params
583
+ };
584
+ }
585
+ /**
586
+ * Translate logical operators ($and, $or)
587
+ */
588
+ translateLogicalOperator(operator, filters) {
589
+ if (!Array.isArray(filters) || filters.length === 0) {
590
+ return { sql: "", params: [] };
591
+ }
592
+ const clauses = [];
593
+ const params = [];
594
+ for (const filter of filters) {
595
+ const clause = this.translateFilter(filter);
596
+ if (clause.sql) {
597
+ clauses.push(clause.sql);
598
+ params.push(...clause.params);
599
+ }
600
+ }
601
+ if (clauses.length === 0) {
602
+ return { sql: "", params: [] };
603
+ }
604
+ return {
605
+ sql: clauses.join(` ${operator} `),
606
+ params
607
+ };
608
+ }
609
+ /**
610
+ * Translate field condition to SQL
611
+ */
612
+ translateFieldCondition(fieldName, condition) {
613
+ const column = this.getQuotedField(fieldName);
614
+ if (condition === null || condition === void 0) {
615
+ return { sql: `${column} IS NULL`, params: [] };
616
+ }
617
+ if (this.isPrimitiveValue(condition)) {
618
+ this.validateFieldValue(fieldName, condition);
619
+ return {
620
+ sql: `${column} = ?`,
621
+ params: [this.convertValueForSQLite(condition)]
622
+ };
623
+ }
624
+ if (typeof condition === "object" && !Array.isArray(condition)) {
625
+ return this.translateFieldOperators(fieldName, condition);
626
+ }
627
+ if (Array.isArray(condition)) {
628
+ this.validateArrayValues(fieldName, condition);
629
+ const placeholders = condition.map(() => "?").join(",");
630
+ const convertedValues = condition.map(
631
+ (v) => this.convertValueForSQLite(v)
632
+ );
633
+ return {
634
+ sql: `${column} IN (${placeholders})`,
635
+ params: convertedValues
636
+ };
637
+ }
638
+ throw new InvalidOperatorError(
639
+ `Unsupported condition type for field ${fieldName}`,
640
+ fieldName,
641
+ "unknown"
642
+ );
643
+ }
644
+ /**
645
+ * Translate field operators to SQL
646
+ */
647
+ translateFieldOperators(fieldName, operators) {
648
+ const conditions = [];
649
+ const params = [];
650
+ const fieldOptions = this.schema.get(fieldName);
651
+ const fieldType = fieldOptions?.type;
652
+ const column = this.getQuotedField(fieldName);
653
+ const substringOps = ["$startsWith", "$endsWith", "$containsText"];
654
+ const presentSubstringOps = substringOps.filter(
655
+ (op) => Object.prototype.hasOwnProperty.call(operators, op)
656
+ );
657
+ if (presentSubstringOps.length > 1) {
658
+ throw new InvalidOperatorError(
659
+ `Only one of $startsWith, $endsWith, $containsText may be used per field`,
660
+ fieldName,
661
+ presentSubstringOps[1],
662
+ fieldType
663
+ );
664
+ }
665
+ if (presentSubstringOps.length === 1) {
666
+ const op = presentSubstringOps[0];
667
+ const value = operators[op];
668
+ if (fieldType !== "string" && fieldType !== "stringset") {
669
+ throw new InvalidOperatorError(
670
+ `${op} operator is only supported for string and stringset fields, field ${fieldName} is type ${fieldType}`,
671
+ fieldName,
672
+ op,
673
+ fieldType
674
+ );
675
+ }
676
+ if (typeof value !== "string") {
677
+ throw new InvalidOperatorError(
678
+ `${op} operator requires a string value for field ${fieldName}`,
679
+ fieldName,
680
+ op,
681
+ fieldType
682
+ );
683
+ }
684
+ let pattern;
685
+ try {
686
+ const mode = op === "$startsWith" ? "startsWith" : op === "$endsWith" ? "endsWith" : "containsText";
687
+ pattern = buildLikePattern(value, mode);
688
+ } catch (e) {
689
+ throw new InvalidOperatorError(
690
+ e.message || "invalid substring value",
691
+ fieldName,
692
+ op,
693
+ fieldType
694
+ );
695
+ }
696
+ if (pattern !== null) {
697
+ if (shouldLogLikeEscapes()) {
698
+ try {
699
+ console.debug(
700
+ `[Substring] field=${fieldName} op=${op} pattern=${pattern} ci=true`
701
+ );
702
+ } catch {
703
+ }
704
+ }
705
+ if (fieldType === "string") {
706
+ const likeSql = `${column} LIKE ? ESCAPE '\\' COLLATE NOCASE`;
707
+ conditions.push(likeSql);
708
+ params.push(pattern);
709
+ } else {
710
+ const junctionTableName = `${this.tableName}_${fieldName}`;
711
+ const foreignKeyColumn = `${this.modelName.toLowerCase()}_id`;
712
+ const quotedJunctionTable = quoteIdentifier(junctionTableName);
713
+ const quotedForeignKey = quoteIdentifier(foreignKeyColumn);
714
+ const quotedValueColumn = quoteIdentifier("value");
715
+ const quotedPrimaryKey = quoteIdentifier("id");
716
+ const likeSql = `EXISTS (SELECT 1 FROM ${quotedJunctionTable} WHERE ${quotedJunctionTable}.${quotedForeignKey} = ${this.quotedTableName}.${quotedPrimaryKey} AND ${quotedJunctionTable}.${quotedValueColumn} LIKE ? ESCAPE '\\' COLLATE NOCASE)`;
717
+ conditions.push(likeSql);
718
+ params.push(pattern);
719
+ }
720
+ }
721
+ }
722
+ for (const [operator, value] of Object.entries(operators)) {
723
+ if (substringOps.includes(operator)) {
724
+ continue;
725
+ }
726
+ const opClause = this.translateOperator(fieldName, operator, value);
727
+ if (opClause.sql) {
728
+ conditions.push(opClause.sql);
729
+ params.push(...opClause.params);
730
+ }
731
+ }
732
+ return {
733
+ sql: conditions.join(" AND "),
734
+ params
735
+ };
736
+ }
737
+ /**
738
+ * Translate individual operator to SQL
739
+ */
740
+ translateOperator(fieldName, operator, value) {
741
+ const fieldOptions = this.schema.get(fieldName);
742
+ const fieldType = fieldOptions?.type;
743
+ const column = this.getQuotedField(fieldName);
744
+ switch (operator) {
745
+ case "$eq":
746
+ this.validateFieldValue(fieldName, value);
747
+ return {
748
+ sql: `${column} = ?`,
749
+ params: [this.convertValueForSQLite(value)]
750
+ };
751
+ case "$ne":
752
+ this.validateFieldValue(fieldName, value);
753
+ return {
754
+ sql: `${column} != ?`,
755
+ params: [this.convertValueForSQLite(value)]
756
+ };
757
+ case "$gt":
758
+ this.validateOperatorForType(operator, fieldType, [
759
+ "id",
760
+ "string",
761
+ "number",
762
+ "date"
763
+ ]);
764
+ this.validateFieldValue(fieldName, value);
765
+ return {
766
+ sql: `${column} > ?`,
767
+ params: [this.convertValueForSQLite(value)]
768
+ };
769
+ case "$gte":
770
+ this.validateOperatorForType(operator, fieldType, [
771
+ "id",
772
+ "string",
773
+ "number",
774
+ "date"
775
+ ]);
776
+ this.validateFieldValue(fieldName, value);
777
+ return {
778
+ sql: `${column} >= ?`,
779
+ params: [this.convertValueForSQLite(value)]
780
+ };
781
+ case "$lt":
782
+ this.validateOperatorForType(operator, fieldType, [
783
+ "id",
784
+ "string",
785
+ "number",
786
+ "date"
787
+ ]);
788
+ this.validateFieldValue(fieldName, value);
789
+ return {
790
+ sql: `${column} < ?`,
791
+ params: [this.convertValueForSQLite(value)]
792
+ };
793
+ case "$lte":
794
+ this.validateOperatorForType(operator, fieldType, [
795
+ "id",
796
+ "string",
797
+ "number",
798
+ "date"
799
+ ]);
800
+ this.validateFieldValue(fieldName, value);
801
+ return {
802
+ sql: `${column} <= ?`,
803
+ params: [this.convertValueForSQLite(value)]
804
+ };
805
+ case "$in":
806
+ if (!Array.isArray(value)) {
807
+ throw new InvalidOperatorError(
808
+ `$in operator requires an array value for field ${fieldName}`,
809
+ fieldName,
810
+ operator,
811
+ fieldType
812
+ );
813
+ }
814
+ this.validateArrayValues(fieldName, value);
815
+ if (value.length === 0) {
816
+ return { sql: "1 = 0", params: [] };
817
+ }
818
+ const placeholders = value.map(() => "?").join(",");
819
+ const convertedValues = value.map((v) => this.convertValueForSQLite(v));
820
+ return {
821
+ sql: `${column} IN (${placeholders})`,
822
+ params: convertedValues
823
+ };
824
+ case "$nin":
825
+ if (!Array.isArray(value)) {
826
+ throw new InvalidOperatorError(
827
+ `$nin operator requires an array value for field ${fieldName}`,
828
+ fieldName,
829
+ operator,
830
+ fieldType
831
+ );
832
+ }
833
+ this.validateArrayValues(fieldName, value);
834
+ if (value.length === 0) {
835
+ return { sql: "", params: [] };
836
+ }
837
+ const ninPlaceholders = value.map(() => "?").join(",");
838
+ const convertedNinValues = value.map(
839
+ (v) => this.convertValueForSQLite(v)
840
+ );
841
+ return {
842
+ sql: `${column} NOT IN (${ninPlaceholders})`,
843
+ params: convertedNinValues
844
+ };
845
+ case "$exists":
846
+ if (typeof value !== "boolean") {
847
+ throw new InvalidOperatorError(
848
+ `$exists operator requires a boolean value for field ${fieldName}`,
849
+ fieldName,
850
+ operator,
851
+ fieldType
852
+ );
853
+ }
854
+ return value ? { sql: `${column} IS NOT NULL`, params: [] } : { sql: `${column} IS NULL`, params: [] };
855
+ case "$contains":
856
+ if (fieldType !== "stringset") {
857
+ throw new InvalidOperatorError(
858
+ `$contains operator is only supported for stringset fields, field ${fieldName} is type ${fieldType}`,
859
+ fieldName,
860
+ operator,
861
+ fieldType
862
+ );
863
+ }
864
+ if (typeof value !== "string") {
865
+ throw new InvalidOperatorError(
866
+ `$contains operator requires a string value for stringset field ${fieldName}`,
867
+ fieldName,
868
+ operator,
869
+ fieldType
870
+ );
871
+ }
872
+ const junctionTableName = `${this.tableName}_${fieldName}`;
873
+ const foreignKeyColumn = `${this.modelName.toLowerCase()}_id`;
874
+ const quotedJunctionTable = quoteIdentifier(junctionTableName);
875
+ const quotedForeignKey = quoteIdentifier(foreignKeyColumn);
876
+ const quotedValueColumn = quoteIdentifier("value");
877
+ const quotedPrimaryKey = quoteIdentifier("id");
878
+ return {
879
+ sql: `EXISTS (SELECT 1 FROM ${quotedJunctionTable} WHERE ${quotedJunctionTable}.${quotedForeignKey} = ${this.quotedTableName}.${quotedPrimaryKey} AND ${quotedJunctionTable}.${quotedValueColumn} = ?)`,
880
+ params: [value]
881
+ };
882
+ default:
883
+ throw new InvalidOperatorError(
884
+ `Unsupported operator: ${operator} for field ${fieldName}`,
885
+ fieldName,
886
+ operator,
887
+ fieldType
888
+ );
889
+ }
890
+ }
891
+ /**
892
+ * Build SELECT clause based on projection
893
+ */
894
+ buildSelectClause(projection) {
895
+ if (!projection || Object.keys(projection).length === 0) {
896
+ return "*";
897
+ }
898
+ const includeFields = [];
899
+ const excludeFields = [];
900
+ let hasIncludes = false;
901
+ let hasExcludes = false;
902
+ for (const [field, include] of Object.entries(projection)) {
903
+ if (include === 1) {
904
+ includeFields.push(field);
905
+ hasIncludes = true;
906
+ } else if (include === 0) {
907
+ excludeFields.push(field);
908
+ hasExcludes = true;
909
+ }
910
+ }
911
+ if (hasIncludes && hasExcludes) {
912
+ throw new InvalidOperatorError(
913
+ "Cannot mix inclusion and exclusion in projection",
914
+ "projection",
915
+ "mixed"
916
+ );
917
+ }
918
+ if (hasIncludes) {
919
+ if (!includeFields.includes("id")) {
920
+ includeFields.unshift("id");
921
+ }
922
+ const quoted = includeFields.map((field) => this.getQuotedField(field));
923
+ return quoted.join(", ");
924
+ } else if (hasExcludes) {
925
+ const allFields = Array.from(this.schema.keys());
926
+ const selectedFields = allFields.filter(
927
+ (field) => !excludeFields.includes(field)
928
+ );
929
+ const quoted = selectedFields.map((field) => this.getQuotedField(field));
930
+ return quoted.join(", ");
931
+ }
932
+ return "*";
933
+ }
934
+ /**
935
+ * Build LIMIT clause
936
+ */
937
+ buildLimitClause(options) {
938
+ if (options?.limit && options.limit > 0) {
939
+ return { sql: options.limit.toString() };
940
+ }
941
+ return { sql: "" };
942
+ }
943
+ /**
944
+ * Build pagination WHERE clause from cursor
945
+ */
946
+ buildPaginationClause(options, sortFields, sortDirections) {
947
+ if (!options?.uniqueStartKey || !sortFields || !sortDirections) {
948
+ return { sql: "", params: [] };
949
+ }
950
+ try {
951
+ const cursor = CursorManager.decodeCursor(options.uniqueStartKey);
952
+ const direction = options.direction || 1;
953
+ return CursorManager.buildPaginationConditions(
954
+ cursor,
955
+ sortFields,
956
+ sortDirections,
957
+ direction,
958
+ (field) => this.getQuotedField(field)
959
+ );
960
+ } catch (error) {
961
+ console.warn("Invalid cursor provided, ignoring pagination:", error);
962
+ return { sql: "", params: [] };
963
+ }
964
+ }
965
+ /**
966
+ * Validate that field exists in schema
967
+ */
968
+ validateField(fieldName) {
969
+ if (!this.schema.has(fieldName) && !SYSTEM_FIELDS.has(fieldName)) {
970
+ throw new InvalidFieldError(
971
+ `Unknown field: ${fieldName} in model ${this.modelName}`,
972
+ fieldName,
973
+ this.modelName
974
+ );
975
+ }
976
+ }
977
+ /**
978
+ * Validate projection fields
979
+ */
980
+ validateProjection(projection) {
981
+ for (const fieldName of Object.keys(projection)) {
982
+ this.validateField(fieldName);
983
+ }
984
+ }
985
+ /**
986
+ * Validate operator is supported for field type
987
+ */
988
+ validateOperatorForType(operator, fieldType, allowedTypes) {
989
+ if (!fieldType) {
990
+ console.warn(`Field type not specified, allowing operator ${operator}`);
991
+ return;
992
+ }
993
+ if (!allowedTypes.includes(fieldType)) {
994
+ throw new InvalidOperatorError(
995
+ `Operator ${operator} is not supported for field type ${fieldType}. Allowed types: ${allowedTypes.join(
996
+ ", "
997
+ )}`,
998
+ "unknown",
999
+ operator,
1000
+ fieldType
1001
+ );
1002
+ }
1003
+ }
1004
+ /**
1005
+ * Validate field value matches expected type
1006
+ */
1007
+ validateFieldValue(fieldName, value) {
1008
+ const fieldOptions = this.schema.get(fieldName);
1009
+ if (!fieldOptions || !fieldOptions.type) {
1010
+ return;
1011
+ }
1012
+ const expectedType = fieldOptions.type;
1013
+ const actualType = this.getValueType(value);
1014
+ if (!this.isTypeCompatible(expectedType, actualType, value)) {
1015
+ throw new InvalidOperatorError(
1016
+ `Field ${fieldName} expects ${expectedType}, got ${actualType}`,
1017
+ fieldName,
1018
+ "type_mismatch",
1019
+ expectedType
1020
+ );
1021
+ }
1022
+ }
1023
+ /**
1024
+ * Validate array values for $in/$nin operators
1025
+ */
1026
+ validateArrayValues(fieldName, values) {
1027
+ for (const value of values) {
1028
+ this.validateFieldValue(fieldName, value);
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Check if value is a primitive (not an object with operators)
1033
+ */
1034
+ isPrimitiveValue(value) {
1035
+ if (value === null || value === void 0) {
1036
+ return true;
1037
+ }
1038
+ const type = typeof value;
1039
+ return type === "string" || type === "number" || type === "boolean" || value instanceof Date;
1040
+ }
1041
+ /**
1042
+ * Get the type of a value for validation
1043
+ */
1044
+ getValueType(value) {
1045
+ if (value === null || value === void 0) {
1046
+ return "null";
1047
+ }
1048
+ if (value instanceof Date) {
1049
+ return "date";
1050
+ }
1051
+ return typeof value;
1052
+ }
1053
+ /**
1054
+ * Check if value type is compatible with field type
1055
+ */
1056
+ isTypeCompatible(expectedType, actualType, value) {
1057
+ if (actualType === "null") {
1058
+ return true;
1059
+ }
1060
+ switch (expectedType) {
1061
+ case "id":
1062
+ return actualType === "string";
1063
+ // id fields are strings
1064
+ case "string":
1065
+ return actualType === "string";
1066
+ case "number":
1067
+ return actualType === "number" && !isNaN(value);
1068
+ case "boolean":
1069
+ return actualType === "boolean";
1070
+ case "date":
1071
+ return actualType === "date" || actualType === "string" && !isNaN(Date.parse(value));
1072
+ default:
1073
+ return true;
1074
+ }
1075
+ }
1076
+ /**
1077
+ * Convert value for SQLite compatibility
1078
+ */
1079
+ convertValueForSQLite(value) {
1080
+ if (typeof value === "boolean") {
1081
+ return value ? 1 : 0;
1082
+ }
1083
+ return value;
1084
+ }
1085
+ normalizeDocumentIds(documents) {
1086
+ if (documents == null) {
1087
+ return null;
1088
+ }
1089
+ const docArray = Array.isArray(documents) ? documents : [documents];
1090
+ const normalized = docArray.map((doc) => `${doc}`.trim()).filter((doc) => doc.length > 0);
1091
+ if (normalized.length === 0) {
1092
+ return [];
1093
+ }
1094
+ return Array.from(new Set(normalized));
1095
+ }
1096
+ buildDocumentClause(documents) {
1097
+ const normalized = this.normalizeDocumentIds(documents);
1098
+ if (normalized === null) {
1099
+ return null;
1100
+ }
1101
+ if (normalized.length === 0) {
1102
+ return { sql: "1 = 0", params: [] };
1103
+ }
1104
+ const placeholders = normalized.map(() => "?").join(", ");
1105
+ return {
1106
+ sql: `${quoteIdentifier("_meta_doc_id")} IN (${placeholders})`,
1107
+ params: normalized
1108
+ };
1109
+ }
1110
+ };
1111
+
1112
+ // src/models/BaseModel.ts
1113
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
1114
+ LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
1115
+ LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
1116
+ LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
1117
+ LogLevel2[LogLevel2["INFO"] = 3] = "INFO";
1118
+ LogLevel2[LogLevel2["DEBUG"] = 4] = "DEBUG";
1119
+ LogLevel2[LogLevel2["VERBOSE"] = 5] = "VERBOSE";
1120
+ return LogLevel2;
1121
+ })(LogLevel || {});
1122
+ var Logger = class {
1123
+ static _logLevel = 1 /* ERROR */;
1124
+ static _logCallback = null;
1125
+ static setLogLevel(level) {
1126
+ this._logLevel = level;
1127
+ }
1128
+ static getLogLevel() {
1129
+ return this._logLevel;
1130
+ }
1131
+ static setLogCallback(callback) {
1132
+ this._logCallback = callback;
1133
+ }
1134
+ static error(message, ...args) {
1135
+ if (this._logLevel >= 1 /* ERROR */) {
1136
+ const fullMessage = args.length > 0 ? `${message} ${args.map(
1137
+ (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
1138
+ ).join(" ")}` : message;
1139
+ console.error(`[ERROR] ${message}`, ...args);
1140
+ if (this._logCallback) {
1141
+ this._logCallback(fullMessage, 1 /* ERROR */);
1142
+ }
1143
+ }
1144
+ }
1145
+ static warn(message, ...args) {
1146
+ if (this._logLevel >= 2 /* WARN */) {
1147
+ const fullMessage = args.length > 0 ? `${message} ${args.map(
1148
+ (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
1149
+ ).join(" ")}` : message;
1150
+ console.warn(`[WARN] ${message}`, ...args);
1151
+ if (this._logCallback) {
1152
+ this._logCallback(fullMessage, 2 /* WARN */);
1153
+ }
1154
+ }
1155
+ }
1156
+ static info(message, ...args) {
1157
+ if (this._logLevel >= 3 /* INFO */) {
1158
+ const fullMessage = args.length > 0 ? `${message} ${args.map(
1159
+ (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
1160
+ ).join(" ")}` : message;
1161
+ console.log(`[INFO] ${message}`, ...args);
1162
+ if (this._logCallback) {
1163
+ this._logCallback(fullMessage, 3 /* INFO */);
1164
+ }
1165
+ }
1166
+ }
1167
+ static debug(message, ...args) {
1168
+ if (this._logLevel >= 4 /* DEBUG */) {
1169
+ const fullMessage = args.length > 0 ? `${message} ${args.map(
1170
+ (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
1171
+ ).join(" ")}` : message;
1172
+ console.log(`[DEBUG] ${message}`, ...args);
1173
+ if (this._logCallback) {
1174
+ this._logCallback(fullMessage, 4 /* DEBUG */);
1175
+ }
1176
+ }
1177
+ }
1178
+ static verbose(message, ...args) {
1179
+ if (this._logLevel >= 5 /* VERBOSE */) {
1180
+ const fullMessage = args.length > 0 ? `${message} ${args.map(
1181
+ (arg) => typeof arg === "object" ? JSON.stringify(arg) : String(arg)
1182
+ ).join(" ")}` : message;
1183
+ console.log(`[VERBOSE] ${message}`, ...args);
1184
+ if (this._logCallback) {
1185
+ this._logCallback(fullMessage, 5 /* VERBOSE */);
1186
+ }
1187
+ }
1188
+ }
1189
+ };
1190
+ var UniqueConstraintViolationError = class _UniqueConstraintViolationError extends Error {
1191
+ modelName;
1192
+ constraintName;
1193
+ fields;
1194
+ recordIdAttempted;
1195
+ // ID of the record that failed to save/update
1196
+ conflictingRecordId;
1197
+ // ID of the record that already has the unique value
1198
+ constructor(message, modelName, constraintName, fields, recordIdAttempted, conflictingRecordId) {
1199
+ super(message);
1200
+ this.name = "UniqueConstraintViolationError";
1201
+ this.modelName = modelName;
1202
+ this.constraintName = constraintName;
1203
+ this.fields = fields;
1204
+ this.recordIdAttempted = recordIdAttempted;
1205
+ this.conflictingRecordId = conflictingRecordId;
1206
+ Object.setPrototypeOf(this, _UniqueConstraintViolationError.prototype);
1207
+ }
1208
+ };
1209
+ var RecordNotFoundError = class _RecordNotFoundError extends Error {
1210
+ constructor(message) {
1211
+ super(message);
1212
+ this.name = "RecordNotFoundError";
1213
+ Object.setPrototypeOf(this, _RecordNotFoundError.prototype);
1214
+ }
1215
+ };
1216
+ function generateULID() {
1217
+ return ulid();
1218
+ }
1219
+ var SCHEMA_ACCESSORS_KEY = /* @__PURE__ */ Symbol("jsBaoSchemaAccessors");
1220
+ var BaseModel = class _BaseModel {
1221
+ static modelName;
1222
+ static listenersMap = /* @__PURE__ */ new Map();
1223
+ // Default document resolution mappings (session-scoped)
1224
+ static modelNameToDefaultDocId = /* @__PURE__ */ new Map();
1225
+ static globalDefaultDocId;
1226
+ static defaultDocChangedListeners = /* @__PURE__ */ new Set();
1227
+ static modelDocMappingChangedListeners = /* @__PURE__ */ new Set();
1228
+ // These were for tracking initialization state, which is now more centralized in ModelRegistry
1229
+ // and the main initializeORM flow. We'll rely on the Model's static `initialize` method.
1230
+ // private static initializedModels: Set<string> = new Set();
1231
+ // private static initializationPromises: Map<string, Promise<void>> = new Map();
1232
+ // Default document constants for legacy mode compatibility
1233
+ static DEFAULT_LEGACY_DOC_ID = "__legacy_default__";
1234
+ // private static readonly DEFAULT_PERMISSION: DocumentPermissionHint = "read-write";
1235
+ // dbInstance will be set per model via its static initialize, but query methods need access.
1236
+ // This will be a single instance shared across all models once initialized.
1237
+ static dbInstance = null;
1238
+ // Multi-document management (now used for both legacy and multi-document modes)
1239
+ static connectedDocuments = /* @__PURE__ */ new Map();
1240
+ static documentYMaps = /* @__PURE__ */ new Map();
1241
+ // Maps docId to YMap for that document
1242
+ // Copy-on-write state management
1243
+ _localChanges = null;
1244
+ _isDirty = false;
1245
+ _isLoadingFromYjs = false;
1246
+ // Flag to distinguish loading vs user changes
1247
+ // StringSet field caching
1248
+ _stringSetFields = /* @__PURE__ */ new Map();
1249
+ // Multi-document instance metadata
1250
+ _metaDocId = null;
1251
+ _metaPermissionHint = null;
1252
+ /**
1253
+ * Returns the document id this instance is associated with, or null if not resolved yet.
1254
+ */
1255
+ getDocumentId() {
1256
+ return this._metaDocId;
1257
+ }
1258
+ // id is now a plain property. Subclasses will define it with @Field.
1259
+ id;
1260
+ type;
1261
+ // This should be the modelName from ModelOptions
1262
+ // Static logging methods
1263
+ static setLogLevel(level) {
1264
+ Logger.setLogLevel(level);
1265
+ }
1266
+ static getLogLevel() {
1267
+ return Logger.getLogLevel();
1268
+ }
1269
+ // ---- Default doc resolution: static APIs ----
1270
+ static setModelDefaultDocumentId(modelName, docId) {
1271
+ const previous = this.modelNameToDefaultDocId.get(modelName);
1272
+ this.modelNameToDefaultDocId.set(modelName, docId);
1273
+ for (const listener of this.modelDocMappingChangedListeners) {
1274
+ try {
1275
+ listener({ modelName, previous, current: docId });
1276
+ } catch {
1277
+ }
1278
+ }
1279
+ }
1280
+ static removeModelDefaultDocumentId(modelName) {
1281
+ const previous = this.modelNameToDefaultDocId.get(modelName);
1282
+ this.modelNameToDefaultDocId.delete(modelName);
1283
+ for (const listener of this.modelDocMappingChangedListeners) {
1284
+ try {
1285
+ listener({ modelName, previous, current: void 0 });
1286
+ } catch {
1287
+ }
1288
+ }
1289
+ }
1290
+ static clearModelDefaultDocumentIds() {
1291
+ const entries = Array.from(this.modelNameToDefaultDocId.entries());
1292
+ this.modelNameToDefaultDocId.clear();
1293
+ for (const [modelName, previous] of entries) {
1294
+ for (const listener of this.modelDocMappingChangedListeners) {
1295
+ try {
1296
+ listener({ modelName, previous, current: void 0 });
1297
+ } catch {
1298
+ }
1299
+ }
1300
+ }
1301
+ }
1302
+ static setGlobalDefaultDocumentId(docId) {
1303
+ const previous = this.globalDefaultDocId;
1304
+ this.globalDefaultDocId = docId;
1305
+ for (const listener of this.defaultDocChangedListeners) {
1306
+ try {
1307
+ listener({ previous, current: docId });
1308
+ } catch {
1309
+ }
1310
+ }
1311
+ }
1312
+ static clearGlobalDefaultDocumentId() {
1313
+ const previous = this.globalDefaultDocId;
1314
+ this.globalDefaultDocId = void 0;
1315
+ for (const listener of this.defaultDocChangedListeners) {
1316
+ try {
1317
+ listener({ previous, current: void 0 });
1318
+ } catch {
1319
+ }
1320
+ }
1321
+ }
1322
+ static getModelDefaultDocumentMapping() {
1323
+ return Object.fromEntries(this.modelNameToDefaultDocId.entries());
1324
+ }
1325
+ static getDocumentIdForModel(modelName) {
1326
+ return this.modelNameToDefaultDocId.get(modelName);
1327
+ }
1328
+ static getGlobalDefaultDocumentId() {
1329
+ return this.globalDefaultDocId;
1330
+ }
1331
+ static onDefaultDocChanged(listener) {
1332
+ this.defaultDocChangedListeners.add(listener);
1333
+ return () => this.defaultDocChangedListeners.delete(listener);
1334
+ }
1335
+ static onModelDocMappingChanged(listener) {
1336
+ this.modelDocMappingChangedListeners.add(listener);
1337
+ return () => this.modelDocMappingChangedListeners.delete(listener);
1338
+ }
1339
+ // Helper used by DocumentManager to clear mappings for a given docId when disconnected
1340
+ static _clearMappingsForDocId(docId) {
1341
+ const toRemove = [];
1342
+ for (const [modelName, mappedDocId] of this.modelNameToDefaultDocId) {
1343
+ if (mappedDocId === docId) {
1344
+ toRemove.push(modelName);
1345
+ }
1346
+ }
1347
+ for (const modelName of toRemove) {
1348
+ this.removeModelDefaultDocumentId(modelName);
1349
+ }
1350
+ if (this.globalDefaultDocId === docId) {
1351
+ this.clearGlobalDefaultDocumentId();
1352
+ }
1353
+ }
1354
+ static attachFieldAccessorsSet(modelClass) {
1355
+ const existing = modelClass[SCHEMA_ACCESSORS_KEY] ?? /* @__PURE__ */ new Set();
1356
+ modelClass[SCHEMA_ACCESSORS_KEY] = existing;
1357
+ return existing;
1358
+ }
1359
+ static attachFieldAccessors(modelClass, fields) {
1360
+ const attached = _BaseModel.attachFieldAccessorsSet(modelClass);
1361
+ const prototype = modelClass.prototype;
1362
+ for (const fieldName of fields.keys()) {
1363
+ if (fieldName === "id") continue;
1364
+ if (attached.has(fieldName)) continue;
1365
+ const existingDescriptor = Object.getOwnPropertyDescriptor(
1366
+ prototype,
1367
+ fieldName
1368
+ );
1369
+ if (existingDescriptor && !existingDescriptor.configurable) {
1370
+ continue;
1371
+ }
1372
+ Object.defineProperty(prototype, fieldName, {
1373
+ configurable: true,
1374
+ enumerable: true,
1375
+ get() {
1376
+ return this.getValue(fieldName);
1377
+ },
1378
+ set(value) {
1379
+ this.setValue(fieldName, value);
1380
+ }
1381
+ });
1382
+ attached.add(fieldName);
1383
+ }
1384
+ }
1385
+ constructor(data = {}) {
1386
+ if (new.target === _BaseModel) {
1387
+ throw new Error("BaseModel cannot be instantiated directly.");
1388
+ }
1389
+ const modelConstructor = this.constructor;
1390
+ const schema = modelConstructor.getSchema();
1391
+ const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
1392
+ if (!schema || !schema.fields) {
1393
+ throw new Error(
1394
+ `[${modelConstructor.name}] Schema not loaded. Ensure the model is properly initialized.`
1395
+ );
1396
+ }
1397
+ if (verboseEnabled) {
1398
+ Logger.verbose(
1399
+ `BaseModel Constructor - ${modelConstructor.name}: Initializing with data:`,
1400
+ data
1401
+ );
1402
+ Logger.verbose(
1403
+ `BaseModel Constructor - ${modelConstructor.name}: Schema fields received:`,
1404
+ schema.fields
1405
+ );
1406
+ }
1407
+ const idFieldOptions = schema.fields.get("id");
1408
+ if (data.id) {
1409
+ this.id = data.id;
1410
+ } else if (idFieldOptions?.autoAssign) {
1411
+ this.id = generateULID();
1412
+ } else if (idFieldOptions?.default) {
1413
+ this.id = typeof idFieldOptions.default === "function" ? idFieldOptions.default() : idFieldOptions.default;
1414
+ } else {
1415
+ throw new Error(
1416
+ `[${modelConstructor.name}] No ID provided and no autoAssign or default configured for id field`
1417
+ );
1418
+ }
1419
+ const isLoadingExistingRecord = data.id && Object.keys(data).length === 1;
1420
+ if (verboseEnabled) {
1421
+ Logger.verbose(
1422
+ `[BaseModel Constructor - ${modelConstructor.name}] Is loading existing record: ${isLoadingExistingRecord}, ID: ${this.id}`
1423
+ );
1424
+ Logger.verbose(
1425
+ `[BaseModel Constructor - ${modelConstructor.name}] Connected documents:`,
1426
+ Array.from(modelConstructor.connectedDocuments.keys())
1427
+ );
1428
+ }
1429
+ if (isLoadingExistingRecord) {
1430
+ let recordYMap = void 0;
1431
+ let foundDocId = void 0;
1432
+ for (const [docId] of modelConstructor.connectedDocuments) {
1433
+ if (verboseEnabled) {
1434
+ Logger.verbose(
1435
+ `[BaseModel Constructor - ${modelConstructor.name}] Searching in document: ${docId}`
1436
+ );
1437
+ }
1438
+ const documentYMap = modelConstructor.documentYMaps.get(
1439
+ `${docId}_${schema.options.name}`
1440
+ );
1441
+ if (verboseEnabled) {
1442
+ Logger.verbose(
1443
+ `[BaseModel Constructor - ${modelConstructor.name}] Document YMap found: ${!!documentYMap}`
1444
+ );
1445
+ }
1446
+ if (documentYMap) {
1447
+ const foundRecord = documentYMap.get(this.id);
1448
+ if (verboseEnabled) {
1449
+ Logger.verbose(
1450
+ `[BaseModel Constructor - ${modelConstructor.name}] Record found in document ${docId}: ${!!foundRecord}`
1451
+ );
1452
+ }
1453
+ if (foundRecord) {
1454
+ recordYMap = foundRecord;
1455
+ foundDocId = docId;
1456
+ break;
1457
+ }
1458
+ }
1459
+ }
1460
+ if (recordYMap && foundDocId) {
1461
+ this._isLoadingFromYjs = true;
1462
+ this._isDirty = false;
1463
+ this._localChanges = null;
1464
+ this._metaDocId = foundDocId;
1465
+ const connectedDoc = modelConstructor.connectedDocuments.get(foundDocId);
1466
+ if (connectedDoc) {
1467
+ this._metaPermissionHint = connectedDoc.permissionHint;
1468
+ }
1469
+ if (schema.options && schema.options.name) {
1470
+ this.type = schema.options.name;
1471
+ }
1472
+ if (verboseEnabled) {
1473
+ Logger.verbose(
1474
+ `[BaseModel Constructor - ${modelConstructor.name}] ID INITIALIZED TO:`,
1475
+ this.id
1476
+ );
1477
+ }
1478
+ return;
1479
+ }
1480
+ }
1481
+ this._isLoadingFromYjs = false;
1482
+ if (data && Object.keys(data).length > 0) {
1483
+ for (const [
1484
+ fieldKey,
1485
+ fieldOptions
1486
+ ] of schema.fields.entries()) {
1487
+ if (fieldKey === "id") continue;
1488
+ if (data.hasOwnProperty(fieldKey)) {
1489
+ this.setValue(fieldKey, data[fieldKey]);
1490
+ } else if (fieldOptions.default !== void 0) {
1491
+ const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
1492
+ this.setValue(fieldKey, defaultValue);
1493
+ }
1494
+ }
1495
+ }
1496
+ if (schema.options && schema.options.name) {
1497
+ this.type = schema.options.name;
1498
+ } else {
1499
+ Logger.warn(
1500
+ `[${modelConstructor.name}] Schema missing options.name, 'type' field may not be set correctly.`
1501
+ );
1502
+ }
1503
+ if (!this.id) {
1504
+ Logger.error(
1505
+ `[BaseModel Constructor - ${modelConstructor.name}] ID is STILL UNDEFINED. Current instance state:`,
1506
+ JSON.stringify(this)
1507
+ );
1508
+ throw new Error(
1509
+ `[${modelConstructor.name}] ID is missing after initialization. Ensure 'id' field is defined in the model's schema with autoAssign:true or a default value, or an id is provided during instantiation.`
1510
+ );
1511
+ }
1512
+ Logger.verbose(
1513
+ `[BaseModel Constructor - ${modelConstructor.name}] ID INITIALIZED TO:`,
1514
+ this.id
1515
+ );
1516
+ return;
1517
+ }
1518
+ // Copy-on-write helper methods
1519
+ ensureLocalChanges() {
1520
+ if (this._localChanges === null) {
1521
+ this._localChanges = {};
1522
+ }
1523
+ return this._localChanges;
1524
+ }
1525
+ hasLocalChange(fieldKey) {
1526
+ return this._localChanges !== null && fieldKey in this._localChanges;
1527
+ }
1528
+ getFromYjs(fieldKey) {
1529
+ const modelConstructor = this.constructor;
1530
+ const schema = modelConstructor.getSchema();
1531
+ const modelName = schema?.options?.name;
1532
+ const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
1533
+ if (verboseEnabled) {
1534
+ Logger.verbose(
1535
+ `[getFromYjs] Called for field '${fieldKey}' on model ${modelName}, instance ID: ${this.id}`
1536
+ );
1537
+ Logger.verbose(`[getFromYjs] this._metaDocId: ${this._metaDocId}`);
1538
+ Logger.verbose(
1539
+ `[getFromYjs] documentYMaps available: ${!!modelConstructor.documentYMaps}`
1540
+ );
1541
+ Logger.verbose(
1542
+ `[getFromYjs] connectedDocuments available: ${!!modelConstructor.connectedDocuments}`
1543
+ );
1544
+ }
1545
+ let recordYMap = void 0;
1546
+ const docId = this._metaDocId;
1547
+ if (verboseEnabled) {
1548
+ Logger.verbose(`[getFromYjs] Using document ID: ${docId}`);
1549
+ }
1550
+ const documentYMapKey = `${docId}_${modelName}`;
1551
+ if (verboseEnabled) {
1552
+ Logger.verbose(
1553
+ `[getFromYjs] Looking for documentYMap with key: ${documentYMapKey}`
1554
+ );
1555
+ }
1556
+ const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
1557
+ if (verboseEnabled) {
1558
+ Logger.verbose(`[getFromYjs] DocumentYMap found: ${!!documentYMap}`);
1559
+ }
1560
+ if (documentYMap) {
1561
+ if (verboseEnabled) {
1562
+ Logger.verbose(
1563
+ `[getFromYjs] DocumentYMap keys: [${Array.from(
1564
+ documentYMap.keys()
1565
+ ).join(", ")}]`
1566
+ );
1567
+ }
1568
+ recordYMap = documentYMap.get(this.id);
1569
+ if (verboseEnabled) {
1570
+ Logger.verbose(
1571
+ `[getFromYjs] RecordYMap found for ID '${this.id}': ${!!recordYMap}`
1572
+ );
1573
+ }
1574
+ if (recordYMap) {
1575
+ if (verboseEnabled) {
1576
+ Logger.verbose(
1577
+ `[getFromYjs] RecordYMap keys: [${Array.from(
1578
+ recordYMap.keys()
1579
+ ).join(", ")}]`
1580
+ );
1581
+ }
1582
+ const fieldValue2 = recordYMap.get(fieldKey);
1583
+ if (verboseEnabled) {
1584
+ Logger.verbose(
1585
+ `[getFromYjs] Field '${fieldKey}' value: ${fieldValue2}`
1586
+ );
1587
+ }
1588
+ }
1589
+ }
1590
+ if (!recordYMap) {
1591
+ if (verboseEnabled) {
1592
+ Logger.verbose(
1593
+ `[getFromYjs] No recordYMap found, falling back to schema defaults`
1594
+ );
1595
+ }
1596
+ const fieldOptions = schema?.fields?.get(fieldKey);
1597
+ if (fieldOptions?.default !== void 0) {
1598
+ const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
1599
+ if (verboseEnabled) {
1600
+ Logger.verbose(
1601
+ `[getFromYjs] Returning schema default: ${defaultValue}`
1602
+ );
1603
+ }
1604
+ return defaultValue;
1605
+ }
1606
+ if (verboseEnabled) {
1607
+ Logger.verbose(`[getFromYjs] No schema default, returning undefined`);
1608
+ }
1609
+ return void 0;
1610
+ }
1611
+ const fieldValue = recordYMap.get(fieldKey);
1612
+ if (verboseEnabled) {
1613
+ Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
1614
+ }
1615
+ return fieldValue;
1616
+ }
1617
+ getValue(fieldKey) {
1618
+ const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
1619
+ if (verboseEnabled) {
1620
+ Logger.verbose(
1621
+ `[getValue] Called for field '${fieldKey}' on instance ID: ${this.id}`
1622
+ );
1623
+ }
1624
+ const schema = this.constructor.getSchema();
1625
+ const fieldOptions = schema?.fields?.get(fieldKey);
1626
+ if (fieldOptions?.type === "stringset") {
1627
+ if (verboseEnabled) {
1628
+ Logger.verbose(
1629
+ `[getValue] Field '${fieldKey}' is StringSet type, returning StringSet instance`
1630
+ );
1631
+ }
1632
+ return this.getOrCreateStringSet(fieldKey);
1633
+ }
1634
+ if (this.hasLocalChange(fieldKey)) {
1635
+ if (verboseEnabled) {
1636
+ Logger.verbose(
1637
+ `[getValue] Field '${fieldKey}' has local changes: ${this._localChanges[fieldKey]}`
1638
+ );
1639
+ }
1640
+ return this._localChanges[fieldKey];
1641
+ }
1642
+ if (verboseEnabled) {
1643
+ Logger.verbose(
1644
+ `[getValue] Field '${fieldKey}' no local changes, calling getFromYjs`
1645
+ );
1646
+ }
1647
+ const result = this.getFromYjs(fieldKey);
1648
+ if (verboseEnabled) {
1649
+ Logger.verbose(
1650
+ `[getValue] Field '${fieldKey}' returning from getFromYjs: ${result}`
1651
+ );
1652
+ }
1653
+ return result;
1654
+ }
1655
+ setValue(fieldKey, value) {
1656
+ const schema = this.constructor.getSchema();
1657
+ const fieldOptions = schema?.fields?.get(fieldKey);
1658
+ if (fieldOptions?.type === "stringset") {
1659
+ throw new Error(
1660
+ `Cannot directly assign to StringSet field '${fieldKey}'. Use the StringSet methods (add, remove, clear) instead.`
1661
+ );
1662
+ }
1663
+ if (!this._isLoadingFromYjs) {
1664
+ }
1665
+ const changes = this.ensureLocalChanges();
1666
+ changes[fieldKey] = value;
1667
+ this._isDirty = true;
1668
+ }
1669
+ get isDirty() {
1670
+ return this._isDirty;
1671
+ }
1672
+ get hasUnsavedChanges() {
1673
+ return this._localChanges !== null && Object.keys(this._localChanges).length > 0;
1674
+ }
1675
+ clearLocalChanges() {
1676
+ this._localChanges = null;
1677
+ this._isDirty = false;
1678
+ }
1679
+ discardChanges() {
1680
+ this.clearLocalChanges();
1681
+ }
1682
+ // Validation methods
1683
+ validateFieldValue(fieldKey, value) {
1684
+ const schema = this.constructor.getSchema();
1685
+ const fieldOptions = schema.fields.get(fieldKey);
1686
+ if (!fieldOptions) {
1687
+ throw new Error(`Unknown field: ${fieldKey}`);
1688
+ }
1689
+ if (fieldOptions.required && (value === null || value === void 0)) {
1690
+ throw new Error(`Field ${fieldKey} is required`);
1691
+ }
1692
+ }
1693
+ validateBeforeSave() {
1694
+ const schema = this.constructor.getSchema();
1695
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
1696
+ const currentValue = this.getValue(fieldKey);
1697
+ if (fieldOptions.required && (currentValue === null || currentValue === void 0)) {
1698
+ throw new Error(`Field ${fieldKey} is required before save`);
1699
+ }
1700
+ if (fieldOptions.type === "stringset" && currentValue instanceof StringSet) {
1701
+ if (fieldOptions.maxCount && currentValue.size > fieldOptions.maxCount) {
1702
+ throw new Error(
1703
+ `StringSet field '${fieldKey}' exceeds maximum count of ${fieldOptions.maxCount}. Current count: ${currentValue.size}`
1704
+ );
1705
+ }
1706
+ if (fieldOptions.maxLength) {
1707
+ for (const value of currentValue) {
1708
+ if (value.length > fieldOptions.maxLength) {
1709
+ throw new Error(
1710
+ `StringSet field '${fieldKey}' contains a string that exceeds maximum length of ${fieldOptions.maxLength}: "${value}" (length: ${value.length})`
1711
+ );
1712
+ }
1713
+ }
1714
+ }
1715
+ }
1716
+ }
1717
+ }
1718
+ // State inspection helpers
1719
+ getChangedFields() {
1720
+ return this._localChanges ? Object.keys(this._localChanges) : [];
1721
+ }
1722
+ getOriginalValue(fieldKey) {
1723
+ return this.getFromYjs(fieldKey);
1724
+ }
1725
+ getCurrentValue(fieldKey) {
1726
+ return this.getValue(fieldKey);
1727
+ }
1728
+ hasFieldChanged(fieldKey) {
1729
+ return this.hasLocalChange(fieldKey);
1730
+ }
1731
+ // StringSet support methods
1732
+ markStringSetChange(fieldName, operation, value) {
1733
+ const changes = this.ensureLocalChanges();
1734
+ if (!changes[fieldName]) {
1735
+ changes[fieldName] = {
1736
+ type: "stringset",
1737
+ additions: /* @__PURE__ */ new Set(),
1738
+ removals: /* @__PURE__ */ new Set()
1739
+ };
1740
+ }
1741
+ const stringSetChanges = changes[fieldName];
1742
+ if (operation === "clear") {
1743
+ const currentValues = this.getStringSetCurrentValues(fieldName);
1744
+ stringSetChanges.additions.clear();
1745
+ stringSetChanges.removals = new Set(currentValues);
1746
+ } else if (operation === "add" && value) {
1747
+ stringSetChanges.removals.delete(value);
1748
+ stringSetChanges.additions.add(value);
1749
+ } else if (operation === "remove" && value) {
1750
+ stringSetChanges.additions.delete(value);
1751
+ stringSetChanges.removals.add(value);
1752
+ }
1753
+ this._isDirty = true;
1754
+ }
1755
+ getStringSetCurrentValues(fieldName) {
1756
+ const yjsData = this.getFromYjs(fieldName);
1757
+ if (yjsData && typeof yjsData === "object") {
1758
+ return Object.keys(yjsData);
1759
+ }
1760
+ return [];
1761
+ }
1762
+ getStringSetFromYjs(fieldName) {
1763
+ const yjsData = this.getFromYjs(fieldName);
1764
+ if (yjsData && typeof yjsData === "object") {
1765
+ return Object.keys(yjsData);
1766
+ }
1767
+ return [];
1768
+ }
1769
+ getOrCreateStringSet(fieldName) {
1770
+ if (!this._stringSetFields.has(fieldName)) {
1771
+ const initialValues = this.getStringSetFromYjs(fieldName);
1772
+ const pendingChanges = this._localChanges?.[fieldName];
1773
+ let currentValues = new Set(initialValues);
1774
+ if (pendingChanges && pendingChanges.type === "stringset") {
1775
+ for (const addition of pendingChanges.additions) {
1776
+ currentValues.add(addition);
1777
+ }
1778
+ for (const removal of pendingChanges.removals) {
1779
+ currentValues.delete(removal);
1780
+ }
1781
+ }
1782
+ const stringSet = new StringSet(
1783
+ this,
1784
+ fieldName,
1785
+ Array.from(currentValues)
1786
+ );
1787
+ this._stringSetFields.set(fieldName, stringSet);
1788
+ }
1789
+ return this._stringSetFields.get(fieldName);
1790
+ }
1791
+ // Legacy initialize method - removed (explicit document required)
1792
+ static async initialize(_yDoc, _db) {
1793
+ throw new Error(
1794
+ `[${this.name}] initialize(yDoc, db) is no longer supported. Use initializeForDocument(yDoc, db, docId, permission)`
1795
+ );
1796
+ }
1797
+ // New method for multi-document initialization
1798
+ static async initializeForDocument(yDoc, db, docId, permissionHint) {
1799
+ const schema = this.getSchema?.();
1800
+ const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
1801
+ if (!schema || !schema.options || !schema.options.name || !schema.resolvedUniqueConstraints) {
1802
+ throw new Error(
1803
+ `[${this.name}] Model schema is not registered, missing options, or missing resolvedUniqueConstraints. Did you forget to use the @Model decorator or has the schema structure changed?`
1804
+ );
1805
+ }
1806
+ const modelName = schema.options.name;
1807
+ if (verboseEnabled) {
1808
+ Logger.verbose(
1809
+ `[${this.name}] Initializing model ${modelName} for document ${docId} (permission: ${permissionHint})...`
1810
+ );
1811
+ }
1812
+ if (!_BaseModel.dbInstance) {
1813
+ _BaseModel.dbInstance = db;
1814
+ }
1815
+ this.connectedDocuments.set(docId, {
1816
+ yDoc,
1817
+ permissionHint
1818
+ });
1819
+ const documentYMap = yDoc.getMap(modelName);
1820
+ this.documentYMaps.set(
1821
+ `${docId}_${modelName}`,
1822
+ documentYMap
1823
+ );
1824
+ const stringSetFieldNames = [];
1825
+ schema.fields.forEach((fieldOptions, fieldName) => {
1826
+ if (fieldOptions?.type === "stringset") {
1827
+ stringSetFieldNames.push(fieldName);
1828
+ }
1829
+ });
1830
+ const documentRecordCount = typeof documentYMap.size === "number" ? documentYMap.size : Array.from(documentYMap.keys()).length;
1831
+ const shouldReindexJunctionTables = stringSetFieldNames.length > 0 && documentRecordCount > 0;
1832
+ if (verboseEnabled) {
1833
+ Logger.verbose(
1834
+ `[${this.name}] Document YMap initialized for ${modelName}/${docId} with keys:`,
1835
+ Array.from(documentYMap.keys())
1836
+ );
1837
+ }
1838
+ if (verboseEnabled) {
1839
+ Logger.verbose(
1840
+ `[${this.name}] Ensuring DB table exists for ${modelName}...`
1841
+ );
1842
+ }
1843
+ await db.createTable(modelName, schema.fields, schema.options);
1844
+ if (stringSetFieldNames.length > 0) {
1845
+ Logger.info(
1846
+ `[${this.name}] Junction table reindex check for ${modelName}/${docId}: found ${stringSetFieldNames.length} StringSet field(s) [${stringSetFieldNames.join(
1847
+ ", "
1848
+ )}] with ${documentRecordCount} record(s) in document. ${shouldReindexJunctionTables ? "Re-indexing junction tables from document data." : "No records to re-index yet; ensuring tables exist."}`
1849
+ );
1850
+ for (const fieldName of stringSetFieldNames) {
1851
+ await db.createStringSetJunctionTable(modelName, fieldName);
1852
+ }
1853
+ }
1854
+ try {
1855
+ await db.deleteByDocumentId(modelName, docId);
1856
+ } catch (error) {
1857
+ Logger.error(
1858
+ `[${this.name}] Error clearing existing data for document ${docId} during initialization:`,
1859
+ error
1860
+ );
1861
+ }
1862
+ if (verboseEnabled) {
1863
+ Logger.verbose(
1864
+ `[${this.name}] Loading initial data from document ${docId} YMap for ${modelName}...`
1865
+ );
1866
+ }
1867
+ const extractStringSetValues = (value) => {
1868
+ if (!value) return [];
1869
+ if (value instanceof Y.Map) {
1870
+ return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
1871
+ }
1872
+ if (Array.isArray(value)) {
1873
+ return value.filter((v) => typeof v === "string");
1874
+ }
1875
+ if (typeof value === "object") {
1876
+ return Object.entries(value).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
1877
+ }
1878
+ return [];
1879
+ };
1880
+ const junctionInsertCounts = {};
1881
+ await db.withTransaction(async (transactionalOps) => {
1882
+ for (const [recordId, recordData] of documentYMap.entries()) {
1883
+ if (!recordId) continue;
1884
+ let itemData;
1885
+ const stringSetValuesByField = {};
1886
+ if (recordData instanceof Y.Map) {
1887
+ itemData = {};
1888
+ const unknownFields = [];
1889
+ for (const [fieldKey, value] of recordData.entries()) {
1890
+ const fieldOptions = schema.fields.get(fieldKey);
1891
+ if (!fieldOptions) {
1892
+ unknownFields.push(fieldKey);
1893
+ continue;
1894
+ }
1895
+ if (fieldOptions.type === "stringset") {
1896
+ stringSetValuesByField[fieldKey] = extractStringSetValues(value);
1897
+ continue;
1898
+ }
1899
+ if (value !== void 0) {
1900
+ itemData[fieldKey] = value;
1901
+ }
1902
+ }
1903
+ if (unknownFields.length > 0) {
1904
+ Logger.warn(
1905
+ `[${this.name}] Ignoring unknown fields [${unknownFields.join(
1906
+ ", "
1907
+ )}] when loading record ${recordId} from document ${docId}`
1908
+ );
1909
+ }
1910
+ } else {
1911
+ const legacyData = recordData;
1912
+ const filteredData = {};
1913
+ const unknownFields = [];
1914
+ for (const [fieldKey, value] of Object.entries(legacyData)) {
1915
+ const fieldOptions = schema.fields.get(fieldKey);
1916
+ if (!fieldOptions) {
1917
+ unknownFields.push(fieldKey);
1918
+ continue;
1919
+ }
1920
+ if (fieldOptions.type === "stringset") {
1921
+ stringSetValuesByField[fieldKey] = extractStringSetValues(value);
1922
+ continue;
1923
+ }
1924
+ if (value !== void 0) {
1925
+ filteredData[fieldKey] = value;
1926
+ }
1927
+ }
1928
+ if (unknownFields.length > 0) {
1929
+ Logger.warn(
1930
+ `[${this.name}] Ignoring unknown fields [${unknownFields.join(
1931
+ ", "
1932
+ )}] when loading legacy record ${recordId} from document ${docId}`
1933
+ );
1934
+ }
1935
+ itemData = filteredData;
1936
+ Logger.warn(
1937
+ `[${this.name}] Found legacy plain object during document initialization for ${recordId}`
1938
+ );
1939
+ }
1940
+ if (!itemData.id) continue;
1941
+ try {
1942
+ await transactionalOps.insert(modelName, {
1943
+ ...itemData,
1944
+ type: modelName,
1945
+ _meta_doc_id: docId,
1946
+ _meta_permission_hint: permissionHint
1947
+ });
1948
+ for (const [fieldName, values] of Object.entries(
1949
+ stringSetValuesByField
1950
+ )) {
1951
+ if (!values || values.length === 0) continue;
1952
+ try {
1953
+ await db.insertStringSetValues(
1954
+ modelName,
1955
+ fieldName,
1956
+ recordId,
1957
+ values
1958
+ );
1959
+ junctionInsertCounts[fieldName] = (junctionInsertCounts[fieldName] || 0) + values.length;
1960
+ } catch (stringSetError) {
1961
+ Logger.error(
1962
+ `[${this.name}] Error indexing StringSet field ${fieldName} for record ${recordId} in document ${docId}:`,
1963
+ stringSetError
1964
+ );
1965
+ throw stringSetError;
1966
+ }
1967
+ }
1968
+ } catch (error) {
1969
+ if (error instanceof Error && error.message.includes("UNIQUE constraint")) {
1970
+ console.warn(
1971
+ `[${this.name}] Warning: Unique constraint conflict when loading ${recordId} from document ${docId}. This record already exists from another document. Data will still be indexed with document ID for disambiguation.`
1972
+ );
1973
+ } else {
1974
+ Logger.error(
1975
+ `[${this.name}] Error inserting item into db (document ${docId} load for ${modelName}):`,
1976
+ error,
1977
+ itemData
1978
+ );
1979
+ }
1980
+ }
1981
+ }
1982
+ });
1983
+ if (stringSetFieldNames.length > 0) {
1984
+ const totalInserted = Object.values(junctionInsertCounts).reduce(
1985
+ (sum, count) => sum + count,
1986
+ 0
1987
+ );
1988
+ const perFieldSummary = Object.entries(junctionInsertCounts).map(([fieldName, count]) => `${fieldName}: ${count}`).join("; ");
1989
+ if (shouldReindexJunctionTables) {
1990
+ Logger.info(
1991
+ `[${this.name}] Junction table reindex summary for ${modelName}/${docId}: ${totalInserted} total value(s) processed${perFieldSummary ? ` (${perFieldSummary})` : ""}.`
1992
+ );
1993
+ } else {
1994
+ Logger.info(
1995
+ `[${this.name}] Junction table reindex summary for ${modelName}/${docId}: no StringSet values to index yet.`
1996
+ );
1997
+ }
1998
+ }
1999
+ Logger.verbose(
2000
+ `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
2001
+ );
2002
+ const buildUniqueKey = (recordData, fields) => {
2003
+ const keyParts = [];
2004
+ for (const field of fields) {
2005
+ const value = recordData instanceof Y.Map ? recordData.get(field) : recordData[field];
2006
+ if (value === null || value === void 0) {
2007
+ return null;
2008
+ }
2009
+ keyParts.push(String(value));
2010
+ }
2011
+ return keyParts.join("|");
2012
+ };
2013
+ const extractItemData = (key, recordData) => {
2014
+ let itemData;
2015
+ if (recordData instanceof Y.Map) {
2016
+ itemData = {};
2017
+ const unknownFields = [];
2018
+ for (const [fieldKey, value] of recordData.entries()) {
2019
+ const fieldOptions = schema.fields.get(fieldKey);
2020
+ if (!fieldOptions) {
2021
+ unknownFields.push(fieldKey);
2022
+ continue;
2023
+ }
2024
+ if (fieldOptions.type === "stringset") {
2025
+ continue;
2026
+ }
2027
+ if (value !== void 0) {
2028
+ itemData[fieldKey] = value;
2029
+ }
2030
+ }
2031
+ if (unknownFields.length > 0) {
2032
+ Logger.warn(
2033
+ `[${this.name}] Ignoring unknown fields [${unknownFields.join(
2034
+ ", "
2035
+ )}] when syncing record ${key} from document ${docId}`
2036
+ );
2037
+ }
2038
+ } else {
2039
+ const unknownFields = [];
2040
+ const filteredData = {};
2041
+ for (const [fieldKey, value] of Object.entries(recordData)) {
2042
+ const fieldOptions = schema.fields.get(fieldKey);
2043
+ if (!fieldOptions) {
2044
+ unknownFields.push(fieldKey);
2045
+ continue;
2046
+ }
2047
+ if (fieldOptions.type === "stringset") {
2048
+ continue;
2049
+ }
2050
+ if (value !== void 0) {
2051
+ filteredData[fieldKey] = value;
2052
+ }
2053
+ }
2054
+ if (unknownFields.length > 0) {
2055
+ Logger.warn(
2056
+ `[${this.name}] Ignoring unknown fields [${unknownFields.join(
2057
+ ", "
2058
+ )}] when syncing legacy record ${key} from document ${docId}`
2059
+ );
2060
+ }
2061
+ itemData = filteredData;
2062
+ }
2063
+ if (!itemData.id) return null;
2064
+ return itemData;
2065
+ };
2066
+ const resolveConflictsForBatch = (candidateRecords) => {
2067
+ if (schema.resolvedUniqueConstraints.length === 0) {
2068
+ return /* @__PURE__ */ new Set();
2069
+ }
2070
+ const recordIdsToDiscard = /* @__PURE__ */ new Set();
2071
+ const recordIdsToKeep = /* @__PURE__ */ new Set();
2072
+ const allRecords = /* @__PURE__ */ new Map();
2073
+ for (const [recordId, recordData] of documentYMap.entries()) {
2074
+ if (recordData && !candidateRecords.has(recordId)) {
2075
+ allRecords.set(recordId, recordData);
2076
+ }
2077
+ }
2078
+ for (const [recordId, recordData] of candidateRecords.entries()) {
2079
+ allRecords.set(recordId, recordData);
2080
+ }
2081
+ for (const constraint of schema.resolvedUniqueConstraints) {
2082
+ const recordsByUniqueKey = /* @__PURE__ */ new Map();
2083
+ for (const [recordId, recordData] of allRecords.entries()) {
2084
+ if (recordIdsToDiscard.has(recordId)) continue;
2085
+ const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2086
+ if (uniqueKey === null) continue;
2087
+ if (!recordsByUniqueKey.has(uniqueKey)) {
2088
+ recordsByUniqueKey.set(uniqueKey, []);
2089
+ }
2090
+ recordsByUniqueKey.get(uniqueKey).push(recordId);
2091
+ }
2092
+ for (const [uniqueKey, recordIds] of recordsByUniqueKey.entries()) {
2093
+ if (recordIds.length <= 1) continue;
2094
+ Logger.warn(
2095
+ `[${this.name}] CRDT conflict detected for unique constraint '${constraint.name}' on key '${uniqueKey.substring(0, 50)}${uniqueKey.length > 50 ? "..." : ""}': ${recordIds.length} records found.`
2096
+ );
2097
+ recordIds.sort();
2098
+ const idToKeep = recordIds[recordIds.length - 1];
2099
+ recordIdsToKeep.add(idToKeep);
2100
+ for (let i = 0; i < recordIds.length - 1; i++) {
2101
+ recordIdsToDiscard.add(recordIds[i]);
2102
+ }
2103
+ }
2104
+ }
2105
+ for (const idToKeep of recordIdsToKeep) {
2106
+ recordIdsToDiscard.delete(idToKeep);
2107
+ }
2108
+ return recordIdsToDiscard;
2109
+ };
2110
+ documentYMap.observe(async (event) => {
2111
+ Logger.verbose(
2112
+ `[${this.name}] Document YMap change detected for ${modelName}/${docId}:`,
2113
+ event
2114
+ );
2115
+ const currentDbInstance = _BaseModel.dbInstance;
2116
+ if (!currentDbInstance) {
2117
+ Logger.error(
2118
+ `[${this.name}] DB instance not available for document YMap observer on ${modelName}/${docId}.`
2119
+ );
2120
+ return;
2121
+ }
2122
+ const isRemoteChange = !event.transaction.local;
2123
+ const remoteAdds = /* @__PURE__ */ new Map();
2124
+ const localAddsAndUpdates = [];
2125
+ for (const [key, change] of event.changes.keys.entries()) {
2126
+ const recordData = documentYMap.get(key);
2127
+ if (change.action === "add" || change.action === "update") {
2128
+ if (!recordData || !key) continue;
2129
+ const itemData = extractItemData(key, recordData);
2130
+ if (!itemData) continue;
2131
+ if (change.action === "add" && recordData instanceof Y.Map) {
2132
+ Logger.verbose(
2133
+ `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2134
+ );
2135
+ this.setupNestedYMapObserverForDocument(
2136
+ key,
2137
+ recordData,
2138
+ docId,
2139
+ permissionHint
2140
+ );
2141
+ }
2142
+ if (isRemoteChange && change.action === "add") {
2143
+ remoteAdds.set(key, { recordData, itemData });
2144
+ } else {
2145
+ localAddsAndUpdates.push({ key, action: change.action, recordData, itemData });
2146
+ }
2147
+ } else if (change.action === "delete") {
2148
+ Logger.verbose(
2149
+ `[${this.name}] Deleting item from db (${modelName}/${docId}):`,
2150
+ key
2151
+ );
2152
+ try {
2153
+ await currentDbInstance.delete(modelName, key);
2154
+ } catch (error) {
2155
+ Logger.error(
2156
+ `[${this.name}] Error deleting item from db (${modelName}/${docId}):`,
2157
+ error,
2158
+ key
2159
+ );
2160
+ }
2161
+ }
2162
+ }
2163
+ for (const { itemData } of localAddsAndUpdates) {
2164
+ try {
2165
+ Logger.verbose(
2166
+ `[${this.name}] Syncing local item to db from document ${docId} (${modelName}):`,
2167
+ itemData
2168
+ );
2169
+ await currentDbInstance.insert(modelName, {
2170
+ ...itemData,
2171
+ type: modelName,
2172
+ _meta_doc_id: docId,
2173
+ _meta_permission_hint: permissionHint
2174
+ });
2175
+ } catch (error) {
2176
+ Logger.error(
2177
+ `[${this.name}] Error syncing local item to db from document ${docId} (${modelName}):`,
2178
+ error,
2179
+ itemData
2180
+ );
2181
+ }
2182
+ }
2183
+ if (remoteAdds.size > 0) {
2184
+ Logger.verbose(
2185
+ `[${this.name}] Processing ${remoteAdds.size} remote add(s) with conflict resolution for ${modelName}/${docId}`
2186
+ );
2187
+ const candidateRecords = /* @__PURE__ */ new Map();
2188
+ for (const [key, { recordData }] of remoteAdds.entries()) {
2189
+ candidateRecords.set(key, recordData);
2190
+ }
2191
+ const idsToDiscard = resolveConflictsForBatch(candidateRecords);
2192
+ if (idsToDiscard.size > 0) {
2193
+ Logger.info(
2194
+ `[${this.name}] Discarding ${idsToDiscard.size} duplicate record(s) from remote sync for ${modelName}/${docId}: ${Array.from(idsToDiscard).join(", ")}`
2195
+ );
2196
+ }
2197
+ for (const [key, { itemData }] of remoteAdds.entries()) {
2198
+ if (idsToDiscard.has(key)) {
2199
+ Logger.verbose(
2200
+ `[${this.name}] Skipping SQLite insert for discarded duplicate: ${key}`
2201
+ );
2202
+ continue;
2203
+ }
2204
+ try {
2205
+ Logger.verbose(
2206
+ `[${this.name}] Syncing remote item to db from document ${docId} (${modelName}):`,
2207
+ itemData
2208
+ );
2209
+ await currentDbInstance.insert(modelName, {
2210
+ ...itemData,
2211
+ type: modelName,
2212
+ _meta_doc_id: docId,
2213
+ _meta_permission_hint: permissionHint
2214
+ });
2215
+ } catch (error) {
2216
+ Logger.error(
2217
+ `[${this.name}] Error syncing remote item to db from document ${docId} (${modelName}):`,
2218
+ error,
2219
+ itemData
2220
+ );
2221
+ }
2222
+ }
2223
+ if (idsToDiscard.size > 0) {
2224
+ yDoc.transact(() => {
2225
+ for (const idToDiscard of idsToDiscard) {
2226
+ const recordData = documentYMap.get(idToDiscard);
2227
+ if (!recordData) continue;
2228
+ for (const constraint of schema.resolvedUniqueConstraints) {
2229
+ const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2230
+ if (uniqueKey === null) continue;
2231
+ const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
2232
+ const constraintMap = yDoc.getMap(constraintMapName);
2233
+ const currentIndexValue = constraintMap.get(uniqueKey);
2234
+ if (currentIndexValue === idToDiscard) {
2235
+ constraintMap.delete(uniqueKey);
2236
+ Logger.verbose(
2237
+ `[${this.name}] Removed discarded record ${idToDiscard} from unique index ${constraintMapName}`
2238
+ );
2239
+ }
2240
+ }
2241
+ documentYMap.delete(idToDiscard);
2242
+ Logger.verbose(
2243
+ `[${this.name}] Removed discarded record ${idToDiscard} from Y.Doc`
2244
+ );
2245
+ }
2246
+ }, `conflict-resolution-${modelName}-${docId}`);
2247
+ yDoc.transact(() => {
2248
+ for (const constraint of schema.resolvedUniqueConstraints) {
2249
+ const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
2250
+ const constraintMap = yDoc.getMap(constraintMapName);
2251
+ for (const [recordId, recordData] of documentYMap.entries()) {
2252
+ if (!recordData) continue;
2253
+ const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2254
+ if (uniqueKey === null) continue;
2255
+ const currentIndexValue = constraintMap.get(uniqueKey);
2256
+ if (currentIndexValue !== recordId) {
2257
+ constraintMap.set(uniqueKey, recordId);
2258
+ Logger.verbose(
2259
+ `[${this.name}] Updated unique index ${constraintMapName}['${uniqueKey.substring(0, 30)}...'] to point to ${recordId}`
2260
+ );
2261
+ }
2262
+ }
2263
+ }
2264
+ }, `update-indexes-${modelName}-${docId}`);
2265
+ }
2266
+ }
2267
+ Logger.verbose(
2268
+ `[${this.name}] Document YMap observation transaction for ${modelName}/${docId} completed.`
2269
+ );
2270
+ this.notifyListeners();
2271
+ });
2272
+ Logger.verbose(
2273
+ `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
2274
+ );
2275
+ for (const [recordId, recordData] of documentYMap.entries()) {
2276
+ if (recordData instanceof Y.Map) {
2277
+ Logger.verbose(
2278
+ `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
2279
+ );
2280
+ this.setupNestedYMapObserverForDocument(
2281
+ recordId,
2282
+ recordData,
2283
+ docId,
2284
+ permissionHint
2285
+ );
2286
+ }
2287
+ }
2288
+ Logger.verbose(
2289
+ `[${this.name}] Model ${modelName} initialization complete for document ${docId}.`
2290
+ );
2291
+ }
2292
+ // New method to clean up data for a disconnected document
2293
+ static async cleanupDocumentData(docId) {
2294
+ const modelConstructor = this;
2295
+ const schema = modelConstructor.getSchema?.();
2296
+ if (!schema || !schema.options?.name) {
2297
+ Logger.warn(
2298
+ `[${this.name}] Cannot cleanup document data: Schema not found for model ${this.name}`
2299
+ );
2300
+ return;
2301
+ }
2302
+ const modelName = schema.options.name;
2303
+ Logger.verbose(
2304
+ `[${this.name}] Cleaning up data for document ${docId} from model ${modelName}...`
2305
+ );
2306
+ modelConstructor.connectedDocuments.delete(docId);
2307
+ modelConstructor.documentYMaps.delete(`${docId}_${modelName}`);
2308
+ Logger.verbose(
2309
+ `[${this.name}] Document ${docId} cleanup complete for model ${modelName}.`
2310
+ );
2311
+ }
2312
+ static subscribe(callback) {
2313
+ const schema = this.getSchema?.();
2314
+ const modelTypeKey = schema?.options?.name || this.name.toLowerCase();
2315
+ if (!_BaseModel.listenersMap.has(modelTypeKey)) {
2316
+ _BaseModel.listenersMap.set(modelTypeKey, /* @__PURE__ */ new Set());
2317
+ }
2318
+ const listeners = _BaseModel.listenersMap.get(modelTypeKey);
2319
+ listeners.add(callback);
2320
+ return () => {
2321
+ listeners.delete(callback);
2322
+ if (listeners.size === 0) {
2323
+ _BaseModel.listenersMap.delete(modelTypeKey);
2324
+ }
2325
+ };
2326
+ }
2327
+ static notifyListeners() {
2328
+ const schema = this.getSchema?.();
2329
+ const modelTypeKey = schema?.options?.name || this.name.toLowerCase();
2330
+ const listeners = _BaseModel.listenersMap.get(modelTypeKey);
2331
+ if (!listeners) return;
2332
+ Logger.verbose(
2333
+ `[${this.name}] Notifying ${listeners.size} listeners for ${modelTypeKey}`
2334
+ );
2335
+ listeners.forEach((callback) => {
2336
+ try {
2337
+ callback();
2338
+ } catch (e) {
2339
+ Logger.error("Error in listener callback:", e);
2340
+ }
2341
+ });
2342
+ }
2343
+ /**
2344
+ * Legacy migration method - no longer needed in the new multidoc architecture.
2345
+ * Data migration is now handled during document initialization.
2346
+ */
2347
+ static async migrateToNestedYMaps() {
2348
+ const schema = this.getSchema?.();
2349
+ if (!schema || !schema.options?.name) {
2350
+ throw new Error(
2351
+ `[${this.name}] Cannot migrate: Schema not properly initialized`
2352
+ );
2353
+ }
2354
+ const modelName = schema.options.name;
2355
+ Logger.verbose(
2356
+ `[${modelName}] Migration method called but not needed in multidoc architecture`
2357
+ );
2358
+ }
2359
+ /**
2360
+ * Utility to diff current instance data against YJS nested map data
2361
+ * Returns object with added, modified, and removed fields
2362
+ */
2363
+ _diffWithYjsData() {
2364
+ const debugEnabled = Logger.getLogLevel() >= 4 /* DEBUG */;
2365
+ Logger.debug(
2366
+ `[_diffWithYjsData] Starting diff calculation for ID: ${this.id}`
2367
+ );
2368
+ Logger.debug(
2369
+ `[_diffWithYjsData] hasUnsavedChanges: ${this.hasUnsavedChanges}`
2370
+ );
2371
+ Logger.debug(`[_diffWithYjsData] _localChanges:`, this._localChanges);
2372
+ if (!this.hasUnsavedChanges) {
2373
+ Logger.debug(
2374
+ `[_diffWithYjsData] No unsaved changes, returning empty diff`
2375
+ );
2376
+ return { added: {}, modified: {}, removed: [] };
2377
+ }
2378
+ const modelConstructor = this.constructor;
2379
+ const schema = modelConstructor.getSchema();
2380
+ const modelName = schema.options.name;
2381
+ const docId = this._metaDocId;
2382
+ Logger.debug(`[_diffWithYjsData] modelName: ${modelName}, docId: ${docId}`);
2383
+ if (!docId) {
2384
+ throw new Error(
2385
+ `[${modelName}] Cannot diff with Y.js data: model instance has no document ID`
2386
+ );
2387
+ }
2388
+ if (!modelConstructor.documentYMaps) {
2389
+ throw new Error(
2390
+ `[${modelName}] Multi-document system not initialized. documentYMaps is undefined.`
2391
+ );
2392
+ }
2393
+ const documentYMap = modelConstructor.documentYMaps.get(
2394
+ `${docId}_${modelName}`
2395
+ );
2396
+ Logger.debug(`[_diffWithYjsData] documentYMap found: ${!!documentYMap}`);
2397
+ if (!documentYMap) {
2398
+ throw new Error(
2399
+ `[${modelName}] YMap not found for document '${docId}'. This should not happen.`
2400
+ );
2401
+ }
2402
+ const recordYMap = documentYMap.get(this.id);
2403
+ Logger.debug(`[_diffWithYjsData] recordYMap found: ${!!recordYMap}`);
2404
+ const added = {};
2405
+ const modified = {};
2406
+ const removed = [];
2407
+ if (!recordYMap) {
2408
+ Logger.debug(
2409
+ `[_diffWithYjsData] No existing recordYMap, treating all local changes as 'added'`
2410
+ );
2411
+ if (this._localChanges) {
2412
+ for (const [key, value] of Object.entries(this._localChanges)) {
2413
+ Logger.debug(`[_diffWithYjsData] Adding field '${key}': ${value}`);
2414
+ if (value && value.type === "stringset") {
2415
+ const stringSetData = {};
2416
+ for (const addition of value.additions) {
2417
+ stringSetData[addition] = true;
2418
+ }
2419
+ added[key] = stringSetData;
2420
+ } else {
2421
+ added[key] = value;
2422
+ }
2423
+ }
2424
+ }
2425
+ Logger.debug(`[_diffWithYjsData] Final diff for new record:`, {
2426
+ added,
2427
+ modified: {},
2428
+ removed: []
2429
+ });
2430
+ return { added, modified, removed: [] };
2431
+ }
2432
+ Logger.debug(
2433
+ `[_diffWithYjsData] Existing record found, comparing local changes with Y.js data`
2434
+ );
2435
+ if (debugEnabled) {
2436
+ Logger.debug(
2437
+ `[_diffWithYjsData] Existing recordYMap keys:`,
2438
+ Array.from(recordYMap.keys())
2439
+ );
2440
+ }
2441
+ if (this._localChanges) {
2442
+ for (const [key, localValue] of Object.entries(this._localChanges)) {
2443
+ Logger.debug(
2444
+ `[_diffWithYjsData] Processing field '${key}' with local value: ${localValue}`
2445
+ );
2446
+ if (localValue && localValue.type === "stringset") {
2447
+ const currentYjsData = recordYMap.get(key) || {};
2448
+ const newStringSetData = {
2449
+ ...currentYjsData
2450
+ };
2451
+ for (const addition of localValue.additions) {
2452
+ newStringSetData[addition] = true;
2453
+ }
2454
+ for (const removal of localValue.removals) {
2455
+ delete newStringSetData[removal];
2456
+ }
2457
+ const yjsValue = recordYMap.get(key);
2458
+ if (yjsValue === void 0) {
2459
+ added[key] = newStringSetData;
2460
+ } else if (!this._deepEqual(yjsValue, newStringSetData)) {
2461
+ modified[key] = newStringSetData;
2462
+ }
2463
+ } else {
2464
+ const yjsValue = recordYMap.get(key);
2465
+ Logger.debug(
2466
+ `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
2467
+ );
2468
+ if (yjsValue === void 0) {
2469
+ Logger.debug(
2470
+ `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
2471
+ );
2472
+ added[key] = localValue;
2473
+ } else if (!this._deepEqual(yjsValue, localValue)) {
2474
+ Logger.debug(
2475
+ `[_diffWithYjsData] Field '${key}' different in Y.js, adding to 'modified'`
2476
+ );
2477
+ modified[key] = localValue;
2478
+ } else {
2479
+ Logger.debug(`[_diffWithYjsData] Field '${key}' unchanged`);
2480
+ }
2481
+ }
2482
+ }
2483
+ }
2484
+ Logger.debug(`[_diffWithYjsData] Final diff result:`, {
2485
+ added,
2486
+ modified,
2487
+ removed
2488
+ });
2489
+ Logger.verbose(`[${modelConstructor.name}] Diff for ${this.id}:`, {
2490
+ added: Object.keys(added),
2491
+ modified: Object.keys(modified),
2492
+ removed
2493
+ });
2494
+ return { added, modified, removed };
2495
+ }
2496
+ /**
2497
+ * Deep equality check for comparing field values
2498
+ */
2499
+ _deepEqual(a, b) {
2500
+ if (a === b) return true;
2501
+ if (a == null || b == null) return a === b;
2502
+ if (typeof a !== typeof b) return false;
2503
+ if (typeof a !== "object") return false;
2504
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
2505
+ if (Array.isArray(a)) {
2506
+ if (a.length !== b.length) return false;
2507
+ for (let i = 0; i < a.length; i++) {
2508
+ if (!this._deepEqual(a[i], b[i])) return false;
2509
+ }
2510
+ return true;
2511
+ }
2512
+ const keysA = Object.keys(a);
2513
+ const keysB = Object.keys(b);
2514
+ if (keysA.length !== keysB.length) return false;
2515
+ for (const key of keysA) {
2516
+ if (!keysB.includes(key)) return false;
2517
+ if (!this._deepEqual(a[key], b[key])) return false;
2518
+ }
2519
+ return true;
2520
+ }
2521
+ static _buildKeyFromValues(fields, keyValues, modelName, constraintName) {
2522
+ if (fields.length === 0) return null;
2523
+ if (fields.length !== keyValues.length) {
2524
+ Logger.warn(
2525
+ `[${modelName}] Constraint '${constraintName}': Mismatch between number of fields (${fields.length}) and values (${keyValues.length}). Cannot build key.`
2526
+ );
2527
+ return null;
2528
+ }
2529
+ if (keyValues.some((v) => v === null || v === void 0)) {
2530
+ return null;
2531
+ }
2532
+ if (fields.length === 1) {
2533
+ return String(keyValues[0]);
2534
+ } else {
2535
+ return JSON.stringify(keyValues);
2536
+ }
2537
+ }
2538
+ _buildUniqueKey(fields, data, modelName, constraintName) {
2539
+ const values = fields.map((f) => data[f]);
2540
+ return this.constructor._buildKeyFromValues(
2541
+ fields,
2542
+ values,
2543
+ modelName,
2544
+ constraintName
2545
+ );
2546
+ }
2547
+ async save(options) {
2548
+ Logger.verbose(
2549
+ `[BaseModel.save()] Method entered. ID: ${this.id}, Model: ${this.constructor.modelName || this.constructor.name}. Timestamp: ${Date.now()}`
2550
+ );
2551
+ Logger.verbose(`[BaseModel.save()] Current 'this' object:`, this);
2552
+ Logger.verbose(`[BaseModel.save()] 'this.id' specifically:`, this.id);
2553
+ Logger.verbose(`[BaseModel.save()] Options:`, options);
2554
+ if (!this._isDirty) {
2555
+ Logger.verbose(
2556
+ `[${this.constructor.name}] No changes to save for ${this.id}`
2557
+ );
2558
+ return;
2559
+ }
2560
+ const modelConstructor = this.constructor;
2561
+ const schema = modelConstructor.getSchema();
2562
+ if (!schema || !schema.options || !schema.fields || !schema.resolvedUniqueConstraints) {
2563
+ throw new Error(
2564
+ `Schema (or resolvedUniqueConstraints) not found or invalid for model ${modelConstructor.name} in save.`
2565
+ );
2566
+ }
2567
+ const modelName = schema.options.name;
2568
+ let targetDocId = null;
2569
+ let targetYDoc = null;
2570
+ let targetYMap = null;
2571
+ let permissionHint = null;
2572
+ const explicitDocId = options?.targetDocument || null;
2573
+ const instanceRememberedDocId = this._metaDocId;
2574
+ const modelDefaultDocId = this.constructor.getDocumentIdForModel(modelName);
2575
+ const globalDefaultDocId = this.constructor.getGlobalDefaultDocumentId();
2576
+ const precedence = [
2577
+ { name: "explicit", value: explicitDocId },
2578
+ { name: "remembered", value: instanceRememberedDocId },
2579
+ { name: "modelDefault", value: modelDefaultDocId },
2580
+ { name: "globalDefault", value: globalDefaultDocId }
2581
+ ];
2582
+ for (const source of precedence) {
2583
+ if (source.value) {
2584
+ targetDocId = source.value;
2585
+ const documentInfo2 = this.constructor.connectedDocuments.get(targetDocId);
2586
+ if (!documentInfo2) {
2587
+ throw new DocumentClosedError(
2588
+ `[${modelName}] Document '${targetDocId}' from '${source.name}' is not connected. Please connect it or provide an open document id.`
2589
+ );
2590
+ }
2591
+ break;
2592
+ }
2593
+ }
2594
+ if (!targetDocId) {
2595
+ throw new DocumentResolutionError(
2596
+ `[${modelName}] Unable to resolve a target document id (no explicit, no remembered, no model default, no global default).`
2597
+ );
2598
+ }
2599
+ const documentInfo = modelConstructor.connectedDocuments.get(targetDocId);
2600
+ if (!documentInfo) {
2601
+ throw new DocumentClosedError(
2602
+ `[${modelName}] Document '${targetDocId}' is not connected. Please connect the document first.`
2603
+ );
2604
+ }
2605
+ targetYDoc = documentInfo.yDoc;
2606
+ permissionHint = documentInfo.permissionHint;
2607
+ targetYMap = modelConstructor.documentYMaps.get(`${targetDocId}_${modelName}`) || null;
2608
+ if (!targetYMap) {
2609
+ throw new Error(
2610
+ `[${modelName}] YMap not found for document '${targetDocId}'. This should not happen.`
2611
+ );
2612
+ }
2613
+ if (permissionHint === "read" && !options?.forceWrite) {
2614
+ throw new Error(
2615
+ `[${modelName}] Cannot save to document '${targetDocId}' with read-only permission. Use forceWrite option to override.`
2616
+ );
2617
+ }
2618
+ if (!targetYDoc) {
2619
+ throw new Error(
2620
+ `[${modelName}] Target Y.Doc not found for document '${targetDocId}'. Ensure document connection has completed.`
2621
+ );
2622
+ }
2623
+ const yDoc = targetYDoc;
2624
+ if (!this.id) {
2625
+ Logger.error(
2626
+ "[BaseModel.save()] ID is undefined before saving! Instance dump:",
2627
+ JSON.stringify(this)
2628
+ );
2629
+ throw new Error("Cannot save item without an id. Ensure id is set.");
2630
+ }
2631
+ this.validateBeforeSave();
2632
+ Logger.debug(`[${modelName}.save] About to get current JS state`);
2633
+ Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
2634
+ Logger.debug(`[${modelName}.save] _isDirty:`, this._isDirty);
2635
+ const dataToSave = this.getCurrentJSState();
2636
+ Logger.debug(
2637
+ `[${modelName}.save] getCurrentJSState() returned:`,
2638
+ dataToSave
2639
+ );
2640
+ await yDoc.transact(async () => {
2641
+ if (!targetYMap) {
2642
+ throw new Error(
2643
+ `[${modelName}] Target YMap not found for save operation`
2644
+ );
2645
+ }
2646
+ let recordYMap = targetYMap.get(this.id);
2647
+ const isUpdate = !!recordYMap;
2648
+ Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
2649
+ Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
2650
+ if (!recordYMap) {
2651
+ recordYMap = new Y.Map();
2652
+ Logger.verbose(
2653
+ `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
2654
+ );
2655
+ targetYMap.set(this.id, recordYMap);
2656
+ recordYMap.set("id", this.id);
2657
+ }
2658
+ if (!isUpdate || this._metaDocId && this._metaDocId !== targetDocId) {
2659
+ this._metaDocId = targetDocId;
2660
+ this._metaPermissionHint = permissionHint;
2661
+ Logger.debug(`[${modelName}.save] Set _metaDocId to: ${targetDocId}`);
2662
+ }
2663
+ let oldData = null;
2664
+ if (isUpdate) {
2665
+ oldData = {};
2666
+ for (const [key, value] of recordYMap.entries()) {
2667
+ oldData[key] = value;
2668
+ }
2669
+ Logger.verbose(
2670
+ `[${modelName}] Retrieved old data for update:`,
2671
+ oldData
2672
+ );
2673
+ Logger.debug(`[${modelName}.save] oldData:`, oldData);
2674
+ }
2675
+ Logger.debug(`[${modelName}.save] About to calculate diff`);
2676
+ const diff = this._diffWithYjsData();
2677
+ Logger.debug(`[${modelName}.save] Diff result:`, diff);
2678
+ const hasChanges = Object.keys(diff.added).length > 0 || Object.keys(diff.modified).length > 0 || diff.removed.length > 0;
2679
+ Logger.debug(`[${modelName}.save] hasChanges: ${hasChanges}`);
2680
+ if (!hasChanges && isUpdate) {
2681
+ Logger.verbose(
2682
+ `[${modelName}] No changes detected for ${this.id}, skipping save`
2683
+ );
2684
+ Logger.debug(`[${modelName}.save] No changes detected, skipping save`);
2685
+ return;
2686
+ }
2687
+ for (const constraint of schema.resolvedUniqueConstraints) {
2688
+ const newUniqueKey = this._buildUniqueKey(
2689
+ constraint.fields,
2690
+ dataToSave,
2691
+ modelName,
2692
+ constraint.name
2693
+ );
2694
+ Logger.verbose(
2695
+ `[${modelName}] Save Check (Item '${this.id}'): Constraint '${constraint.name}', Built Key: '${newUniqueKey ? newUniqueKey.substring(0, 50) : null}'`
2696
+ );
2697
+ if (newUniqueKey === null) {
2698
+ Logger.verbose(
2699
+ `[${modelName}] Save Check (Item '${this.id}'): Constraint '${constraint.name}' skipped due to null key.`
2700
+ );
2701
+ continue;
2702
+ }
2703
+ const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
2704
+ const constraintMap = yDoc.getMap(constraintMapName);
2705
+ Logger.verbose(
2706
+ `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
2707
+ 0,
2708
+ 50
2709
+ )}'`
2710
+ );
2711
+ const existingRecordIdWithNewKey = constraintMap.get(newUniqueKey);
2712
+ Logger.verbose(
2713
+ `[${modelName}] Save Check (Item '${this.id}'): Found Existing ID: '${existingRecordIdWithNewKey}' for Key '${newUniqueKey.substring(
2714
+ 0,
2715
+ 50
2716
+ )}'`
2717
+ );
2718
+ if (existingRecordIdWithNewKey && existingRecordIdWithNewKey !== this.id) {
2719
+ throw new UniqueConstraintViolationError(
2720
+ `Unique constraint '${constraint.name}' violated for model '${modelName}' on fields [${constraint.fields.join(
2721
+ ", "
2722
+ )}]. Attempted ID: ${this.id}. Value(s) starting with '${newUniqueKey.substring(
2723
+ 0,
2724
+ 50
2725
+ )}...' already exist for record ID ${existingRecordIdWithNewKey}.`,
2726
+ modelName,
2727
+ constraint.name,
2728
+ constraint.fields,
2729
+ this.id,
2730
+ existingRecordIdWithNewKey
2731
+ );
2732
+ }
2733
+ }
2734
+ if (isUpdate && oldData) {
2735
+ for (const constraint of schema.resolvedUniqueConstraints) {
2736
+ const oldUniqueKey = this._buildUniqueKey(
2737
+ constraint.fields,
2738
+ oldData,
2739
+ modelName,
2740
+ constraint.name
2741
+ );
2742
+ const newUniqueKey = this._buildUniqueKey(
2743
+ constraint.fields,
2744
+ dataToSave,
2745
+ modelName,
2746
+ constraint.name
2747
+ );
2748
+ if (oldUniqueKey !== null && oldUniqueKey !== newUniqueKey) {
2749
+ const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
2750
+ const constraintMap = yDoc.getMap(constraintMapName);
2751
+ Logger.verbose(
2752
+ `[${modelName}] Save (update): Deleting old unique key '${oldUniqueKey.substring(
2753
+ 0,
2754
+ 50
2755
+ )}...' for constraint '${constraint.name}', item ${this.id}`
2756
+ );
2757
+ constraintMap.delete(oldUniqueKey);
2758
+ }
2759
+ }
2760
+ }
2761
+ for (const constraint of schema.resolvedUniqueConstraints) {
2762
+ const newUniqueKey = this._buildUniqueKey(
2763
+ constraint.fields,
2764
+ dataToSave,
2765
+ modelName,
2766
+ constraint.name
2767
+ );
2768
+ if (newUniqueKey !== null) {
2769
+ const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
2770
+ const constraintMap = yDoc.getMap(constraintMapName);
2771
+ Logger.verbose(
2772
+ `[${modelName}] Save Set (Item '${this.id}'): SET into '${constraintMapName}' with Key '${newUniqueKey.substring(
2773
+ 0,
2774
+ 50
2775
+ )}', Value (Item ID) '${this.id}'`
2776
+ );
2777
+ constraintMap.set(newUniqueKey, this.id);
2778
+ }
2779
+ }
2780
+ Logger.debug(
2781
+ `[${modelName}.save] Applying field-level changes for ${this.id}`
2782
+ );
2783
+ Logger.debug(`[${modelName}.save] Diff result:`, diff);
2784
+ for (const [key, value] of Object.entries(diff.added)) {
2785
+ Logger.debug(`[${modelName}.save] Adding field '${key}':`, value);
2786
+ recordYMap.set(key, value);
2787
+ }
2788
+ for (const [key, value] of Object.entries(diff.modified)) {
2789
+ Logger.debug(`[${modelName}.save] Modifying field '${key}':`, value);
2790
+ recordYMap.set(key, value);
2791
+ }
2792
+ for (const key of diff.removed) {
2793
+ Logger.debug(`[${modelName}.save] Removing field '${key}'`);
2794
+ recordYMap.delete(key);
2795
+ }
2796
+ Logger.debug(
2797
+ `[${modelName}.save] After applying changes, recordYMap contents:`,
2798
+ Object.fromEntries(recordYMap.entries())
2799
+ );
2800
+ if (!isUpdate) {
2801
+ if (targetDocId && permissionHint) {
2802
+ modelConstructor.setupNestedYMapObserverForDocument(
2803
+ this.id,
2804
+ recordYMap,
2805
+ targetDocId,
2806
+ permissionHint
2807
+ );
2808
+ } else {
2809
+ modelConstructor.setupNestedYMapObserver(
2810
+ this.id,
2811
+ recordYMap
2812
+ );
2813
+ }
2814
+ } else {
2815
+ if (!recordYMap.has("id")) {
2816
+ Logger.verbose(
2817
+ `[${modelName}] Adding id field '${this.id}' to existing nested YMap`
2818
+ );
2819
+ recordYMap.set("id", this.id);
2820
+ }
2821
+ }
2822
+ Logger.verbose(`[${modelName}] Save completed for ${this.id}`);
2823
+ if (_BaseModel.dbInstance) {
2824
+ Logger.verbose(
2825
+ `[${modelName}] Syncing StringSet changes to database for ${this.id}`
2826
+ );
2827
+ for (const [fieldName, localValue] of Object.entries(
2828
+ this._localChanges || {}
2829
+ )) {
2830
+ if (localValue && localValue.type === "stringset") {
2831
+ const fieldOptions = schema.fields.get(fieldName);
2832
+ if (fieldOptions?.type === "stringset") {
2833
+ if (localValue.additions.size > 0) {
2834
+ await _BaseModel.dbInstance.insertStringSetValues(
2835
+ modelName,
2836
+ fieldName,
2837
+ this.id,
2838
+ Array.from(localValue.additions)
2839
+ );
2840
+ }
2841
+ if (localValue.removals.size > 0) {
2842
+ await _BaseModel.dbInstance.removeStringSetValues(
2843
+ modelName,
2844
+ fieldName,
2845
+ this.id,
2846
+ Array.from(localValue.removals)
2847
+ );
2848
+ }
2849
+ }
2850
+ }
2851
+ }
2852
+ Logger.verbose(
2853
+ `[${modelName}] StringSet database sync completed for ${this.id}`
2854
+ );
2855
+ }
2856
+ }, `save-record-${modelName}-${this.id}`);
2857
+ Logger.debug(
2858
+ `[${modelName}.save] About to clear local changes for ${this.id}`
2859
+ );
2860
+ Logger.debug(
2861
+ `[${modelName}.save] _localChanges before clear:`,
2862
+ this._localChanges
2863
+ );
2864
+ this.clearLocalChanges();
2865
+ Logger.debug(
2866
+ `[${modelName}.save] _localChanges after clear:`,
2867
+ this._localChanges
2868
+ );
2869
+ if (Logger.getLogLevel() >= 5 /* VERBOSE */) {
2870
+ Logger.verbose(`[${modelName}.save] Successfully saved ${this.id}`);
2871
+ }
2872
+ }
2873
+ async delete() {
2874
+ const modelConstructor = this.constructor;
2875
+ const schema = modelConstructor.getSchema();
2876
+ if (!schema || !schema.options || !schema.resolvedUniqueConstraints) {
2877
+ throw new Error(
2878
+ `Schema (or resolvedUniqueConstraints) not found or invalid for model ${modelConstructor.name} in delete.`
2879
+ );
2880
+ }
2881
+ const modelName = schema.options.name;
2882
+ if (!this.id) {
2883
+ throw new Error("Cannot delete item without an id.");
2884
+ }
2885
+ let sourceYDoc = null;
2886
+ let sourceYMap = null;
2887
+ let docId = void 0;
2888
+ docId = this._metaDocId ?? void 0;
2889
+ const documentInfo = docId ? modelConstructor.connectedDocuments.get(docId) : void 0;
2890
+ if (!documentInfo) {
2891
+ throw new Error(
2892
+ `[${modelName}] Document '${docId}' is not connected. Cannot delete from disconnected document.`
2893
+ );
2894
+ }
2895
+ sourceYDoc = documentInfo.yDoc;
2896
+ sourceYMap = modelConstructor.documentYMaps.get(`${docId}_${modelName}`) || null;
2897
+ if (!sourceYMap) {
2898
+ throw new Error(
2899
+ `[${modelName}] YMap not found for document '${docId}'. This should not happen.`
2900
+ );
2901
+ }
2902
+ const recordYMap = sourceYMap.get(this.id);
2903
+ if (!recordYMap) {
2904
+ Logger.warn(
2905
+ `[${modelName}] Delete: Item ${this.id} not found in YMap${docId ? ` for document ${docId}` : ""}. Cannot remove from unique constraint maps. Attempting main map deletion only.`
2906
+ );
2907
+ sourceYDoc.transact(() => {
2908
+ sourceYMap.delete(this.id);
2909
+ }, `delete-ghost-record-${modelName}-${this.id}`);
2910
+ return;
2911
+ }
2912
+ const dataToDelete = {};
2913
+ for (const [key, value] of recordYMap.entries()) {
2914
+ dataToDelete[key] = value;
2915
+ }
2916
+ await sourceYDoc.transact(async () => {
2917
+ for (const constraint of schema.resolvedUniqueConstraints) {
2918
+ const uniqueKey = this._buildUniqueKey(
2919
+ constraint.fields,
2920
+ dataToDelete,
2921
+ modelName,
2922
+ constraint.name
2923
+ );
2924
+ if (uniqueKey !== null) {
2925
+ const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
2926
+ const constraintMap = sourceYDoc.getMap(constraintMapName);
2927
+ Logger.verbose(
2928
+ `[${modelName}] Delete: Removing unique key '${uniqueKey.substring(
2929
+ 0,
2930
+ 50
2931
+ )}...' for constraint '${constraint.name}', item ${this.id}${docId ? ` from document ${docId}` : ""}`
2932
+ );
2933
+ constraintMap.delete(uniqueKey);
2934
+ }
2935
+ }
2936
+ Logger.verbose(
2937
+ `[${modelName}] Deleting nested YMap from main YMap: ${this.id}${docId ? ` from document ${docId}` : ""}`
2938
+ );
2939
+ sourceYMap.delete(this.id);
2940
+ }, `delete-record-${modelName}-${this.id}`);
2941
+ }
2942
+ // Get current state combining local changes with Yjs data
2943
+ getCurrentJSState() {
2944
+ const schema = this.constructor.getSchema();
2945
+ if (!schema || !schema.fields) {
2946
+ throw new Error(
2947
+ `Schema not found or invalid for model ${this.constructor.name} in getCurrentJSState.`
2948
+ );
2949
+ }
2950
+ const result = {};
2951
+ Logger.debug(
2952
+ `[getCurrentJSState] Starting for model ${this.constructor.name}, ID: ${this.id}`
2953
+ );
2954
+ for (const fieldKey of schema.fields.keys()) {
2955
+ const value = this.getValue(fieldKey);
2956
+ if (value !== void 0) {
2957
+ const fieldOptions = schema.fields.get(fieldKey);
2958
+ if (fieldOptions?.type === "stringset" && value instanceof StringSet) {
2959
+ result[fieldKey] = value.toArray();
2960
+ } else {
2961
+ result[fieldKey] = value;
2962
+ }
2963
+ }
2964
+ }
2965
+ if (schema.options?.name) {
2966
+ result.type = schema.options.name;
2967
+ }
2968
+ if (this.id !== void 0) {
2969
+ result.id = this.id;
2970
+ }
2971
+ if (this._metaDocId !== null) {
2972
+ result._meta_doc_id = this._metaDocId;
2973
+ }
2974
+ if (this._metaPermissionHint !== null) {
2975
+ result._meta_permission_hint = this._metaPermissionHint;
2976
+ }
2977
+ return result;
2978
+ }
2979
+ // Keep toJSON for backward compatibility, but delegate to getCurrentJSState
2980
+ toJSON() {
2981
+ return this.getCurrentJSState();
2982
+ }
2983
+ static async find(id) {
2984
+ const schema = this.getSchema?.();
2985
+ if (!schema || !schema.options.name)
2986
+ throw new Error("Model not properly initialized for find");
2987
+ const modelName = schema.options.name;
2988
+ Logger.verbose(`[${modelName}] Finding item by id:`, id);
2989
+ const modelConstructor = this;
2990
+ const debugEnabled = Logger.getLogLevel() >= 4 /* DEBUG */;
2991
+ Logger.debug(`[${modelName}.find] Using multi-document system`);
2992
+ if (debugEnabled) {
2993
+ Logger.debug(
2994
+ `[${modelName}.find] Connected documents: [${Array.from(
2995
+ modelConstructor.connectedDocuments.keys()
2996
+ ).join(", ")}]`
2997
+ );
2998
+ }
2999
+ for (const [docId] of modelConstructor.connectedDocuments) {
3000
+ Logger.debug(`[${modelName}.find] Searching in document: ${docId}`);
3001
+ const documentYMapKey = `${docId}_${modelName}`;
3002
+ Logger.debug(
3003
+ `[${modelName}.find] Looking for documentYMap with key: ${documentYMapKey}`
3004
+ );
3005
+ const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
3006
+ Logger.debug(`[${modelName}.find] DocumentYMap found: ${!!documentYMap}`);
3007
+ if (documentYMap) {
3008
+ if (debugEnabled) {
3009
+ Logger.debug(
3010
+ `[${modelName}.find] DocumentYMap keys: [${Array.from(
3011
+ documentYMap.keys()
3012
+ ).join(", ")}]`
3013
+ );
3014
+ }
3015
+ const recordYMap = documentYMap.get(id);
3016
+ Logger.debug(
3017
+ `[${modelName}.find] RecordYMap found for ID '${id}': ${!!recordYMap}`
3018
+ );
3019
+ if (recordYMap) {
3020
+ if (debugEnabled) {
3021
+ Logger.debug(
3022
+ `[${modelName}.find] RecordYMap keys: [${Array.from(
3023
+ recordYMap.keys()
3024
+ ).join(", ")}]`
3025
+ );
3026
+ }
3027
+ Logger.debug(`[${modelName}.find] Creating instance with ID: ${id}`);
3028
+ const instance = new this({ id });
3029
+ Logger.debug(`[${modelName}.find] Setting _metaDocId to: ${docId}`);
3030
+ instance._metaDocId = docId;
3031
+ const connectedDoc = modelConstructor.connectedDocuments.get(docId);
3032
+ if (connectedDoc) {
3033
+ Logger.debug(
3034
+ `[${modelName}.find] Setting _metaPermissionHint to: ${connectedDoc.permissionHint}`
3035
+ );
3036
+ instance._metaPermissionHint = connectedDoc.permissionHint;
3037
+ }
3038
+ Logger.debug(
3039
+ `[${modelName}.find] Returning instance with _metaDocId: ${instance._metaDocId}`
3040
+ );
3041
+ return instance;
3042
+ }
3043
+ }
3044
+ }
3045
+ return null;
3046
+ }
3047
+ /**
3048
+ * Document-style query API - returns paginated results
3049
+ */
3050
+ static async query(filter = {}, options) {
3051
+ if (!_BaseModel.dbInstance) {
3052
+ const modelNameForError = this.modelName || this.name || "BaseModel";
3053
+ throw new Error(
3054
+ `[${modelNameForError}] Database not initialized for query. Connect at least one document via initJsBao(...).connectDocument or call initializeForDocument(yDoc, db, docId, permissionHint) before running queries.`
3055
+ );
3056
+ }
3057
+ const schema = this.getSchema?.();
3058
+ if (!schema || !schema.options?.name) {
3059
+ throw new Error("Model not properly initialized for query");
3060
+ }
3061
+ const modelName = schema.options.name;
3062
+ Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
3063
+ const hasIncludes = options?.include && options.include.length > 0;
3064
+ const translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
3065
+ const translator = new DocumentQueryTranslator(modelName, schema.fields);
3066
+ const translatedQuery = translator.translateFind(filter, translatorOptions);
3067
+ Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
3068
+ Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
3069
+ const results = await _BaseModel.dbInstance.query(
3070
+ translatedQuery.sql,
3071
+ translatedQuery.params
3072
+ );
3073
+ Logger.verbose(`[${modelName}] Query returned ${results.length} results`);
3074
+ if (options?.include && options.include.length > 0) {
3075
+ const { IncludeResolver } = await import("./IncludeResolver-RCKQGNPZ.js");
3076
+ const resolver = new IncludeResolver(_BaseModel.dbInstance);
3077
+ await resolver.resolve(results, options.include, 0, modelName);
3078
+ }
3079
+ const requestedLimit = options?.limit;
3080
+ const hasMore = CursorManager.hasMoreResults(
3081
+ requestedLimit,
3082
+ results.length
3083
+ );
3084
+ const isFirstPage = !options?.uniqueStartKey;
3085
+ const cursors = CursorManager.generateResultCursors(
3086
+ results,
3087
+ translatedQuery.sortFields,
3088
+ options?.direction || 1,
3089
+ hasMore,
3090
+ isFirstPage
3091
+ );
3092
+ let transformedData;
3093
+ if (options?.projection && Object.keys(options.projection).length > 0) {
3094
+ transformedData = results.map((row) => {
3095
+ const projected = {};
3096
+ if (row.hasOwnProperty("id")) {
3097
+ projected.id = row.id;
3098
+ }
3099
+ for (const [field, include] of Object.entries(options.projection)) {
3100
+ if (include === 1 && row.hasOwnProperty(field)) {
3101
+ projected[field] = row[field];
3102
+ }
3103
+ }
3104
+ if (row._related) projected._related = row._related;
3105
+ return projected;
3106
+ });
3107
+ } else {
3108
+ const modelConstructor = this;
3109
+ const documentYMaps = modelConstructor.documentYMaps;
3110
+ const connectedDocuments = modelConstructor.connectedDocuments;
3111
+ transformedData = results.map((data) => {
3112
+ let foundRecordYMap;
3113
+ let foundDocId;
3114
+ for (const docId of connectedDocuments.keys()) {
3115
+ const documentYMapKey = `${docId}_${modelName}`;
3116
+ const documentYMap = documentYMaps.get(documentYMapKey);
3117
+ if (documentYMap) {
3118
+ const recordYMap = documentYMap.get(data.id);
3119
+ if (recordYMap) {
3120
+ foundRecordYMap = recordYMap;
3121
+ foundDocId = docId;
3122
+ break;
3123
+ }
3124
+ }
3125
+ }
3126
+ if (!foundRecordYMap || !foundDocId) {
3127
+ Logger.warn(
3128
+ `[${modelName}] Query result item ID ${data.id} not found in any connected document YMap. DB might be out of sync.`
3129
+ );
3130
+ return null;
3131
+ }
3132
+ const instance = new this({ id: data.id });
3133
+ if (instance && typeof instance === "object" && instance instanceof _BaseModel) {
3134
+ instance._metaDocId = foundDocId;
3135
+ instance._metaPermissionHint = "read-write";
3136
+ if (data._related) instance._related = data._related;
3137
+ }
3138
+ return instance;
3139
+ }).filter(Boolean);
3140
+ }
3141
+ return {
3142
+ data: transformedData,
3143
+ nextCursor: cursors.nextCursor,
3144
+ prevCursor: cursors.prevCursor,
3145
+ hasMore
3146
+ };
3147
+ }
3148
+ /**
3149
+ * Main aggregation API - performs grouping, faceting, and statistical operations
3150
+ * @param options Aggregation configuration with groupBy, operations, filter, limit, and sort
3151
+ * @returns Nested object structure with aggregation results
3152
+ *
3153
+ * @example
3154
+ * // Simple facet count
3155
+ * const tagCounts = await Model.aggregate({
3156
+ * groupBy: ['tags'],
3157
+ * operations: [{ type: 'count' }]
3158
+ * });
3159
+ * // Result: { red: 15, blue: 8, green: 12 }
3160
+ *
3161
+ * @example
3162
+ * // Multi-dimensional grouping with multiple operations
3163
+ * const categoryStats = await Model.aggregate({
3164
+ * groupBy: ['category', 'status'],
3165
+ * operations: [
3166
+ * { type: 'count' },
3167
+ * { type: 'sum', field: 'amount' },
3168
+ * { type: 'avg', field: 'score' }
3169
+ * ],
3170
+ * filter: { active: true },
3171
+ * sort: { field: 'count', direction: 'desc' },
3172
+ * limit: 10
3173
+ * });
3174
+ *
3175
+ * @example
3176
+ * // StringSet membership grouping
3177
+ * const urgentCounts = await Model.aggregate({
3178
+ * groupBy: [{ field: 'tags', contains: 'urgent' }],
3179
+ * operations: [{ type: 'count' }]
3180
+ * });
3181
+ * // Result: { true: 5, false: 23 }
3182
+ */
3183
+ static async aggregate(options) {
3184
+ if (!_BaseModel.dbInstance) {
3185
+ const modelNameForError = this.modelName || this.name || "BaseModel";
3186
+ throw new Error(
3187
+ `[${modelNameForError}] Database not initialized for aggregation. Connect at least one document via initJsBao(...).connectDocument or call initializeForDocument(yDoc, db, docId, permissionHint) before running aggregations.`
3188
+ );
3189
+ }
3190
+ const schema = this.getSchema?.();
3191
+ if (!schema || !schema.options?.name) {
3192
+ throw new Error("Model not properly initialized for aggregation");
3193
+ }
3194
+ const modelName = schema.options.name;
3195
+ Logger.verbose(`[${modelName}] Executing structured aggregation:`, options);
3196
+ for (const operation of options.operations) {
3197
+ if (operation.type !== "count" && !operation.field) {
3198
+ throw new Error(
3199
+ `Operation '${operation.type}' requires a field parameter`
3200
+ );
3201
+ }
3202
+ if (operation.type === "count" && operation.field) {
3203
+ throw new Error(`Operation 'count' should not have a field parameter`);
3204
+ }
3205
+ }
3206
+ for (const groupBy of options.groupBy) {
3207
+ if (typeof groupBy === "string") {
3208
+ if (!schema.fields.has(groupBy)) {
3209
+ throw new Error(`Unknown field '${groupBy}' in groupBy`);
3210
+ }
3211
+ } else {
3212
+ if (!schema.fields.has(groupBy.field)) {
3213
+ throw new Error(
3214
+ `Unknown field '${groupBy.field}' in StringSet membership groupBy`
3215
+ );
3216
+ }
3217
+ const fieldOptions = schema.fields.get(groupBy.field);
3218
+ if (fieldOptions?.type !== "stringset") {
3219
+ throw new Error(
3220
+ `Field '${groupBy.field}' is not a StringSet field but used in membership groupBy`
3221
+ );
3222
+ }
3223
+ }
3224
+ }
3225
+ const aggregationQuery = this.buildAggregationQuery(
3226
+ options,
3227
+ schema,
3228
+ modelName
3229
+ );
3230
+ Logger.verbose(
3231
+ `[${modelName}] Generated aggregation SQL:`,
3232
+ aggregationQuery.sql
3233
+ );
3234
+ Logger.verbose(
3235
+ `[${modelName}] Aggregation params:`,
3236
+ aggregationQuery.params
3237
+ );
3238
+ const results = await _BaseModel.dbInstance.query(
3239
+ aggregationQuery.sql,
3240
+ aggregationQuery.params
3241
+ );
3242
+ Logger.verbose(
3243
+ `[${modelName}] Aggregation returned ${results.length} results`
3244
+ );
3245
+ const processedResult = this.processAggregationResults(
3246
+ results,
3247
+ options,
3248
+ aggregationQuery.aliasMetadata
3249
+ );
3250
+ if (aggregationQuery.aliasMetadata && aggregationQuery.aliasMetadata.length > 0) {
3251
+ const debugMap = {};
3252
+ for (const detail of aggregationQuery.aliasMetadata) {
3253
+ debugMap[detail.alias] = {
3254
+ field: detail.field,
3255
+ contains: detail.contains,
3256
+ originalAlias: detail.originalAlias
3257
+ };
3258
+ }
3259
+ Object.defineProperty(processedResult, "aliasDebugMap", {
3260
+ value: debugMap,
3261
+ enumerable: false,
3262
+ configurable: false,
3263
+ writable: false
3264
+ });
3265
+ }
3266
+ return processedResult;
3267
+ }
3268
+ /**
3269
+ * Build SQL query for structured aggregation
3270
+ */
3271
+ static buildAggregationQuery(options, schema, modelName) {
3272
+ const translator = new DocumentQueryTranslator(modelName, schema.fields);
3273
+ const regularGroupBy = [];
3274
+ const stringSetFacets = [];
3275
+ const stringSetMemberships = [];
3276
+ for (const groupBy of options.groupBy) {
3277
+ if (typeof groupBy === "string") {
3278
+ const fieldOptions = schema.fields.get(groupBy);
3279
+ if (fieldOptions?.type === "stringset") {
3280
+ stringSetFacets.push(groupBy);
3281
+ } else {
3282
+ regularGroupBy.push(groupBy);
3283
+ }
3284
+ } else {
3285
+ stringSetMemberships.push(groupBy);
3286
+ }
3287
+ }
3288
+ if (stringSetFacets.length > 0 && regularGroupBy.length === 0 && stringSetMemberships.length === 0) {
3289
+ return this.buildStringSetFacetQuery(
3290
+ stringSetFacets,
3291
+ options,
3292
+ translator,
3293
+ modelName
3294
+ );
3295
+ } else if (stringSetMemberships.length > 0 || regularGroupBy.length > 0) {
3296
+ return this.buildRegularAggregationQuery(
3297
+ regularGroupBy,
3298
+ stringSetMemberships,
3299
+ options,
3300
+ translator,
3301
+ modelName
3302
+ );
3303
+ } else {
3304
+ throw new Error("Invalid aggregation configuration");
3305
+ }
3306
+ }
3307
+ /**
3308
+ * Get the proper database table name (should match database engine naming)
3309
+ */
3310
+ static getDatabaseTableName(modelName) {
3311
+ return `model_${modelName.toLowerCase()}`;
3312
+ }
3313
+ /**
3314
+ * Get the proper database junction table name for StringSet fields
3315
+ */
3316
+ static getDatabaseJunctionTableName(modelName, fieldName) {
3317
+ return `${this.getDatabaseTableName(modelName)}_${fieldName}`;
3318
+ }
3319
+ static buildMembershipKey(field, contains) {
3320
+ return `${field}::${contains}`;
3321
+ }
3322
+ /**
3323
+ * Build query for StringSet facet counts
3324
+ */
3325
+ static buildStringSetFacetQuery(stringSetFields, options, translator, modelName) {
3326
+ if (stringSetFields.length > 1) {
3327
+ throw new Error(
3328
+ "Multiple StringSet facet fields not supported in single aggregation"
3329
+ );
3330
+ }
3331
+ const fieldName = stringSetFields[0];
3332
+ const junctionTable = this.getDatabaseJunctionTableName(
3333
+ modelName,
3334
+ fieldName
3335
+ );
3336
+ const quotedJunctionTable = quoteIdentifier(junctionTable);
3337
+ const mainTable = this.getDatabaseTableName(modelName);
3338
+ const quotedMainTable = quoteIdentifier(mainTable);
3339
+ const recordIdColumn = `${modelName.toLowerCase()}_id`;
3340
+ const quotedRecordIdColumn = quoteIdentifier(recordIdColumn);
3341
+ const quotedValueColumn = quoteIdentifier("value");
3342
+ const groupKeyAlias = quoteIdentifier("group_key");
3343
+ let sql = `
3344
+ SELECT
3345
+ ${quotedJunctionTable}.${quotedValueColumn} AS ${groupKeyAlias},
3346
+ COUNT(*) as count
3347
+ FROM ${quotedJunctionTable}
3348
+ INNER JOIN ${quotedMainTable} ON ${quotedJunctionTable}.${quotedRecordIdColumn} = ${quotedMainTable}.${quoteIdentifier(
3349
+ "id"
3350
+ )}
3351
+ `;
3352
+ let params = [];
3353
+ if (options.filter && Object.keys(options.filter).length > 0) {
3354
+ const whereClause = translator.translateFind(options.filter, {});
3355
+ const whereMatch = whereClause.sql.match(
3356
+ /WHERE\s+(.+?)(?:\s+ORDER\s+BY|\s+LIMIT|\s*$)/i
3357
+ );
3358
+ if (whereMatch && whereMatch[1]) {
3359
+ sql += ` WHERE ${whereMatch[1]}`;
3360
+ params = whereClause.params;
3361
+ }
3362
+ }
3363
+ sql += ` GROUP BY ${quotedJunctionTable}.${quotedValueColumn}`;
3364
+ if (options.sort) {
3365
+ const sortField = options.sort.field === "count" ? "COUNT(*)" : quoteIdentifier(options.sort.field);
3366
+ const sqlDirection = options.sort.direction === 1 ? "ASC" : "DESC";
3367
+ sql += ` ORDER BY ${sortField} ${sqlDirection}`;
3368
+ }
3369
+ if (options.limit) {
3370
+ sql += ` LIMIT ${options.limit}`;
3371
+ }
3372
+ return { sql, params };
3373
+ }
3374
+ /**
3375
+ * Build query for regular field aggregation
3376
+ */
3377
+ static buildRegularAggregationQuery(regularGroupBy, stringSetMemberships, options, translator, modelName) {
3378
+ const mainTable = this.getDatabaseTableName(modelName);
3379
+ const quotedMainTable = quoteIdentifier(mainTable);
3380
+ const recordIdColumn = `${modelName.toLowerCase()}_id`;
3381
+ const quotedRecordIdColumn = quoteIdentifier(recordIdColumn);
3382
+ const quotedIdColumn = quoteIdentifier("id");
3383
+ const quotedValueColumn = quoteIdentifier("value");
3384
+ const selectParts = [];
3385
+ const aliasMetadata = [];
3386
+ const usedAliases = /* @__PURE__ */ new Set();
3387
+ for (const field of regularGroupBy) {
3388
+ selectParts.push(`${quotedMainTable}.${quoteIdentifier(field)}`);
3389
+ }
3390
+ for (const membership of stringSetMemberships) {
3391
+ const membershipKey = this.buildMembershipKey(
3392
+ membership.field,
3393
+ membership.contains
3394
+ );
3395
+ const originalAlias = `has_${membership.field}_${membership.contains}`;
3396
+ let aliasPrefix = `has_${membership.field}`;
3397
+ let alias = buildSafeAlias(aliasPrefix, membershipKey);
3398
+ let counter = 1;
3399
+ while (usedAliases.has(alias)) {
3400
+ alias = buildSafeAlias(`${aliasPrefix}_${counter++}`, membershipKey);
3401
+ }
3402
+ usedAliases.add(alias);
3403
+ const quotedAlias = quoteIdentifier(alias);
3404
+ selectParts.push(
3405
+ `CASE WHEN ${quotedAlias}.${quotedRecordIdColumn} IS NOT NULL THEN 'true' ELSE 'false' END AS ${quotedAlias}`
3406
+ );
3407
+ aliasMetadata.push({
3408
+ alias,
3409
+ membershipKey,
3410
+ field: membership.field,
3411
+ contains: membership.contains,
3412
+ originalAlias
3413
+ });
3414
+ }
3415
+ for (const operation of options.operations) {
3416
+ switch (operation.type) {
3417
+ case "count":
3418
+ selectParts.push("COUNT(*) AS count");
3419
+ break;
3420
+ case "sum": {
3421
+ const fieldName = operation.field;
3422
+ selectParts.push(
3423
+ `SUM(${quotedMainTable}.${quoteIdentifier(
3424
+ fieldName
3425
+ )}) AS ${quoteIdentifier(`sum_${fieldName}`)}`
3426
+ );
3427
+ break;
3428
+ }
3429
+ case "avg": {
3430
+ const fieldName = operation.field;
3431
+ selectParts.push(
3432
+ `AVG(${quotedMainTable}.${quoteIdentifier(
3433
+ fieldName
3434
+ )}) AS ${quoteIdentifier(`avg_${fieldName}`)}`
3435
+ );
3436
+ break;
3437
+ }
3438
+ case "min": {
3439
+ const fieldName = operation.field;
3440
+ selectParts.push(
3441
+ `MIN(${quotedMainTable}.${quoteIdentifier(
3442
+ fieldName
3443
+ )}) AS ${quoteIdentifier(`min_${fieldName}`)}`
3444
+ );
3445
+ break;
3446
+ }
3447
+ case "max": {
3448
+ const fieldName = operation.field;
3449
+ selectParts.push(
3450
+ `MAX(${quotedMainTable}.${quoteIdentifier(
3451
+ fieldName
3452
+ )}) AS ${quoteIdentifier(`max_${fieldName}`)}`
3453
+ );
3454
+ break;
3455
+ }
3456
+ }
3457
+ }
3458
+ let sql = `SELECT ${selectParts.join(", ")} FROM ${quotedMainTable}`;
3459
+ aliasMetadata.forEach((detail, index) => {
3460
+ const membership = stringSetMemberships[index];
3461
+ const junctionTable = this.getDatabaseJunctionTableName(
3462
+ modelName,
3463
+ membership.field
3464
+ );
3465
+ const quotedJunctionTable = quoteIdentifier(junctionTable);
3466
+ const quotedAlias = quoteIdentifier(detail.alias);
3467
+ sql += ` LEFT JOIN ${quotedJunctionTable} AS ${quotedAlias} ON ${quotedAlias}.${quotedRecordIdColumn} = ${quotedMainTable}.${quotedIdColumn} AND ${quotedAlias}.${quotedValueColumn} = ?`;
3468
+ });
3469
+ let params = [];
3470
+ for (const membership of stringSetMemberships) {
3471
+ params.push(membership.contains);
3472
+ }
3473
+ if (options.filter && Object.keys(options.filter).length > 0) {
3474
+ const whereClause = translator.translateFind(options.filter, {});
3475
+ const whereMatch = whereClause.sql.match(
3476
+ /WHERE\s+(.+?)(?:\s+ORDER\s+BY|\s+LIMIT|\s*$)/i
3477
+ );
3478
+ if (whereMatch && whereMatch[1]) {
3479
+ sql += ` WHERE ${whereMatch[1]}`;
3480
+ params = params.concat(whereClause.params);
3481
+ }
3482
+ }
3483
+ const groupByParts = [];
3484
+ for (const field of regularGroupBy) {
3485
+ groupByParts.push(`${quotedMainTable}.${quoteIdentifier(field)}`);
3486
+ }
3487
+ aliasMetadata.forEach((detail) => {
3488
+ const quotedAlias = quoteIdentifier(detail.alias);
3489
+ groupByParts.push(`${quotedAlias}.${quotedRecordIdColumn}`);
3490
+ });
3491
+ if (groupByParts.length > 0) {
3492
+ sql += ` GROUP BY ${groupByParts.join(", ")}`;
3493
+ }
3494
+ if (options.sort) {
3495
+ const requestedField = options.sort.field;
3496
+ let sortExpression;
3497
+ if (requestedField === "count") {
3498
+ sortExpression = "COUNT(*)";
3499
+ } else {
3500
+ const membershipDetail = aliasMetadata.find(
3501
+ (detail) => detail.alias === requestedField || detail.originalAlias === requestedField || detail.membershipKey === requestedField
3502
+ );
3503
+ if (membershipDetail) {
3504
+ sortExpression = quoteIdentifier(membershipDetail.alias);
3505
+ } else if (requestedField.startsWith("sum_") || requestedField.startsWith("avg_") || requestedField.startsWith("min_") || requestedField.startsWith("max_")) {
3506
+ sortExpression = quoteIdentifier(requestedField);
3507
+ } else {
3508
+ sortExpression = `${quotedMainTable}.${quoteIdentifier(
3509
+ requestedField
3510
+ )}`;
3511
+ }
3512
+ }
3513
+ const sqlDirection = options.sort.direction === 1 ? "ASC" : "DESC";
3514
+ sql += ` ORDER BY ${sortExpression} ${sqlDirection}`;
3515
+ }
3516
+ if (options.limit) {
3517
+ sql += ` LIMIT ${options.limit}`;
3518
+ }
3519
+ return { sql, params, aliasMetadata };
3520
+ }
3521
+ /**
3522
+ * Process aggregation results into nested structure
3523
+ */
3524
+ static processAggregationResults(results, options, aliasMetadata) {
3525
+ if (results.length === 0) {
3526
+ return {};
3527
+ }
3528
+ const membershipAliasLookup = /* @__PURE__ */ new Map();
3529
+ if (aliasMetadata) {
3530
+ for (const detail of aliasMetadata) {
3531
+ membershipAliasLookup.set(detail.membershipKey, detail.alias);
3532
+ membershipAliasLookup.set(detail.originalAlias, detail.alias);
3533
+ }
3534
+ }
3535
+ if (options.groupBy.length === 1 && typeof options.groupBy[0] === "string" && results[0].hasOwnProperty("group_key")) {
3536
+ const facetResult = {};
3537
+ for (const row of results) {
3538
+ const key = row.group_key;
3539
+ facetResult[key] = {};
3540
+ for (const operation of options.operations) {
3541
+ if (operation.type === "count") {
3542
+ facetResult[key].count = row.count;
3543
+ } else {
3544
+ const operationKey = `${operation.type}_${operation.field}`;
3545
+ facetResult[key][operationKey] = row[operationKey];
3546
+ }
3547
+ }
3548
+ if (options.operations.length === 1 && options.operations[0].type === "count") {
3549
+ facetResult[key] = row.count;
3550
+ }
3551
+ }
3552
+ return facetResult;
3553
+ }
3554
+ const nestedResult = {};
3555
+ for (const row of results) {
3556
+ let current = nestedResult;
3557
+ for (let i = 0; i < options.groupBy.length; i++) {
3558
+ const groupBy = options.groupBy[i];
3559
+ let key;
3560
+ if (typeof groupBy === "string") {
3561
+ key = String(row[groupBy]);
3562
+ } else {
3563
+ const membershipKey = this.buildMembershipKey(
3564
+ groupBy.field,
3565
+ groupBy.contains
3566
+ );
3567
+ const resolvedAlias = membershipAliasLookup.get(membershipKey) || membershipAliasLookup.get(
3568
+ `has_${groupBy.field}_${groupBy.contains}`
3569
+ );
3570
+ if (!resolvedAlias) {
3571
+ throw new Error(
3572
+ `Missing alias metadata for StringSet membership ${groupBy.field}:${groupBy.contains}`
3573
+ );
3574
+ }
3575
+ key = row[resolvedAlias];
3576
+ }
3577
+ if (i === options.groupBy.length - 1) {
3578
+ current[key] = {};
3579
+ for (const operation of options.operations) {
3580
+ if (operation.type === "count") {
3581
+ current[key].count = row.count;
3582
+ } else {
3583
+ const operationKey = `${operation.type}_${operation.field}`;
3584
+ current[key][operationKey] = row[operationKey];
3585
+ }
3586
+ }
3587
+ if (options.operations.length === 1 && options.operations[0].type === "count") {
3588
+ current[key] = row.count;
3589
+ }
3590
+ } else {
3591
+ if (!current[key]) {
3592
+ current[key] = {};
3593
+ }
3594
+ current = current[key];
3595
+ }
3596
+ }
3597
+ }
3598
+ return nestedResult;
3599
+ }
3600
+ /**
3601
+ * Document-style query API - returns single result or null
3602
+ */
3603
+ static async queryOne(filter = {}, options) {
3604
+ const result = await this.query(filter, { ...options, limit: 1 });
3605
+ return result.data.length > 0 ? result.data[0] : null;
3606
+ }
3607
+ /**
3608
+ * Document-style count API
3609
+ */
3610
+ static async count(filter = {}, options) {
3611
+ if (!_BaseModel.dbInstance) {
3612
+ const modelNameForError = this.modelName || this.name || "BaseModel";
3613
+ throw new Error(
3614
+ `[${modelNameForError}] Database not initialized for count. Connect at least one document via initJsBao(...).connectDocument or call initializeForDocument(yDoc, db, docId, permissionHint) before running counts.`
3615
+ );
3616
+ }
3617
+ const schema = this.getSchema?.();
3618
+ if (!schema || !schema.options?.name) {
3619
+ throw new Error("Model not properly initialized for count");
3620
+ }
3621
+ const modelName = schema.options.name;
3622
+ Logger.verbose(`[${modelName}] Executing count:`, { filter, options });
3623
+ const translator = new DocumentQueryTranslator(modelName, schema.fields);
3624
+ const translatedQuery = translator.translateCount(filter, options);
3625
+ Logger.verbose(`[${modelName}] Translated count SQL:`, translatedQuery.sql);
3626
+ Logger.verbose(`[${modelName}] Count SQL params:`, translatedQuery.params);
3627
+ const results = await _BaseModel.dbInstance.query(
3628
+ translatedQuery.sql,
3629
+ translatedQuery.params
3630
+ );
3631
+ const count = results[0]?.count || 0;
3632
+ Logger.verbose(`[${modelName}] Count returned:`, count);
3633
+ return count;
3634
+ }
3635
+ static async findAll() {
3636
+ const schema = this.getSchema?.();
3637
+ if (!schema || !schema.options.name)
3638
+ throw new Error("Model not properly initialized for findAll");
3639
+ const modelName = schema.options.name;
3640
+ const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
3641
+ if (verboseEnabled) {
3642
+ Logger.verbose(`[${modelName}] Finding all items`);
3643
+ }
3644
+ const modelConstructor = this;
3645
+ const allInstances = [];
3646
+ if (modelConstructor.documentYMaps && modelConstructor.connectedDocuments && modelConstructor.connectedDocuments.size > 0) {
3647
+ if (verboseEnabled) {
3648
+ Logger.verbose(`[${modelName}.findAll] Using multi-document system`);
3649
+ }
3650
+ for (const [docId] of modelConstructor.connectedDocuments) {
3651
+ if (verboseEnabled) {
3652
+ Logger.verbose(
3653
+ `[${modelName}.findAll] Searching in document: ${docId}`
3654
+ );
3655
+ }
3656
+ const documentYMapKey = `${docId}_${modelName}`;
3657
+ const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
3658
+ if (documentYMap) {
3659
+ if (verboseEnabled) {
3660
+ Logger.verbose(
3661
+ `[${modelName}.findAll] DocumentYMap found for ${docId}, keys: [${Array.from(
3662
+ documentYMap.keys()
3663
+ ).join(", ")}]`
3664
+ );
3665
+ }
3666
+ for (const [recordId, recordYMap] of documentYMap.entries()) {
3667
+ if (recordYMap instanceof Y.Map) {
3668
+ const instance = new this({ id: recordId });
3669
+ instance._metaDocId = docId;
3670
+ const connectedDoc = modelConstructor.connectedDocuments.get(docId);
3671
+ if (connectedDoc) {
3672
+ instance._metaPermissionHint = connectedDoc.permissionHint;
3673
+ }
3674
+ allInstances.push(instance);
3675
+ } else {
3676
+ Logger.warn(
3677
+ `[${modelName}] Found legacy plain object in document ${docId}, consider migrating data`
3678
+ );
3679
+ allInstances.push(new this(recordYMap));
3680
+ }
3681
+ }
3682
+ }
3683
+ }
3684
+ }
3685
+ if (verboseEnabled) {
3686
+ Logger.verbose(
3687
+ `[${modelName}.findAll] Found ${allInstances.length} total instances`
3688
+ );
3689
+ }
3690
+ return allInstances;
3691
+ }
3692
+ static async findByUnique(constraintName, value) {
3693
+ const modelConstructor = this;
3694
+ const schema = modelConstructor.getSchema();
3695
+ const currentModelName = modelConstructor.modelName || schema?.options?.name || "BaseModel";
3696
+ if (!schema || !schema.options || !schema.resolvedUniqueConstraints) {
3697
+ throw new Error(
3698
+ `[${currentModelName}] Schema or unique constraints not loaded for findByUnique.`
3699
+ );
3700
+ }
3701
+ const constraint = schema.resolvedUniqueConstraints.find(
3702
+ (c) => c.name === constraintName
3703
+ );
3704
+ if (!constraint) {
3705
+ throw new Error(
3706
+ `[${currentModelName}] Unique constraint named '${constraintName}' not found.`
3707
+ );
3708
+ }
3709
+ const keyValues = Array.isArray(value) ? value : [value];
3710
+ const uniqueKeyString = modelConstructor._buildKeyFromValues(
3711
+ constraint.fields,
3712
+ keyValues,
3713
+ currentModelName,
3714
+ constraint.name
3715
+ );
3716
+ if (uniqueKeyString === null) {
3717
+ Logger.verbose(
3718
+ `[${currentModelName}] findByUnique for '${constraintName}': unique key could not be built. Returning null.`
3719
+ );
3720
+ return null;
3721
+ }
3722
+ if (modelConstructor.documentYMaps && modelConstructor.connectedDocuments && modelConstructor.connectedDocuments.size > 0) {
3723
+ Logger.verbose(
3724
+ `[${currentModelName}.findByUnique] Using multi-document system`
3725
+ );
3726
+ for (const [docId] of modelConstructor.connectedDocuments) {
3727
+ const documentInfo = modelConstructor.connectedDocuments.get(docId);
3728
+ if (!documentInfo) continue;
3729
+ const yDoc = documentInfo.yDoc;
3730
+ const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
3731
+ const constraintMap2 = yDoc.getMap(constraintMapName2);
3732
+ const recordId2 = constraintMap2.get(uniqueKeyString);
3733
+ if (recordId2) {
3734
+ const documentYMapKey = `${docId}_${currentModelName}`;
3735
+ const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
3736
+ if (documentYMap) {
3737
+ const recordYMap2 = documentYMap.get(recordId2);
3738
+ if (recordYMap2 && recordYMap2 instanceof Y.Map) {
3739
+ const instance = new modelConstructor({
3740
+ id: recordId2
3741
+ });
3742
+ instance._metaDocId = docId;
3743
+ instance._metaPermissionHint = documentInfo.permissionHint;
3744
+ Logger.verbose(
3745
+ `[${currentModelName}.findByUnique] Found record '${recordId2}' in document '${docId}'`
3746
+ );
3747
+ return instance;
3748
+ }
3749
+ }
3750
+ }
3751
+ }
3752
+ Logger.verbose(
3753
+ `[${currentModelName}.findByUnique] Record not found in any connected document`
3754
+ );
3755
+ return null;
3756
+ }
3757
+ const legacyDocId = _BaseModel.DEFAULT_LEGACY_DOC_ID;
3758
+ const legacyDocumentMap = modelConstructor.documentYMaps?.get(legacyDocId);
3759
+ const legacyDocInfo = modelConstructor.connectedDocuments?.get(legacyDocId);
3760
+ if (!legacyDocumentMap || !legacyDocInfo) {
3761
+ throw new Error(
3762
+ `[${currentModelName}] No Y.Doc is connected for this model. Connect a document via initJsBao(...).connectDocument or call initializeForDocument(yDoc, db, docId, permissionHint) before using legacy findByUnique.`
3763
+ );
3764
+ }
3765
+ const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
3766
+ const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
3767
+ const recordId = constraintMap.get(uniqueKeyString);
3768
+ if (!recordId) {
3769
+ return null;
3770
+ }
3771
+ const recordYMap = legacyDocumentMap.get(recordId);
3772
+ if (!recordYMap) {
3773
+ return null;
3774
+ }
3775
+ if (recordYMap instanceof Y.Map) {
3776
+ const instance = new modelConstructor({
3777
+ id: recordId
3778
+ });
3779
+ return instance;
3780
+ } else {
3781
+ return new modelConstructor(recordYMap);
3782
+ }
3783
+ }
3784
+ static async upsertByUnique(constraintName, uniqueLookupValue, dataToUpsert, options) {
3785
+ const modelConstructor = this;
3786
+ const schema = modelConstructor.getSchema();
3787
+ const currentModelName = modelConstructor.modelName || schema?.options?.name || "BaseModel";
3788
+ if (!schema || !schema.options || !schema.resolvedUniqueConstraints) {
3789
+ throw new Error(
3790
+ `[${currentModelName}] Schema or unique constraints not loaded for upsertByUnique.`
3791
+ );
3792
+ }
3793
+ const connectedDocuments = modelConstructor.connectedDocuments;
3794
+ if (!connectedDocuments || connectedDocuments.size === 0) {
3795
+ throw new Error(
3796
+ `[${currentModelName}] No documents are connected. Connect at least one document via initJsBao(...).connectDocument or call initializeForDocument(yDoc, db, docId, permissionHint) before calling upsertByUnique.`
3797
+ );
3798
+ }
3799
+ const constraint = schema.resolvedUniqueConstraints.find(
3800
+ (c) => c.name === constraintName
3801
+ );
3802
+ if (!constraint) {
3803
+ throw new Error(
3804
+ `[${currentModelName}] Unique constraint named '${constraintName}' not found for upsert.`
3805
+ );
3806
+ }
3807
+ const keyValuesForLookup = Array.isArray(uniqueLookupValue) ? uniqueLookupValue : [uniqueLookupValue];
3808
+ constraint.fields.forEach((field, index) => {
3809
+ if (!dataToUpsert.hasOwnProperty(field)) {
3810
+ throw new Error(
3811
+ `[${currentModelName}] upsertByUnique: dataToUpsert is missing required unique field '${field}' from constraint '${constraintName}'.`
3812
+ );
3813
+ }
3814
+ const dataValue = dataToUpsert[field];
3815
+ const lookupValue = keyValuesForLookup[index];
3816
+ if (dataValue !== lookupValue) {
3817
+ throw new Error(
3818
+ `[${currentModelName}] upsertByUnique: Mismatch between dataToUpsert.'${field}' (value: ${dataValue}) and uniqueLookupValue for constraint '${constraintName}' (value: ${lookupValue}).`
3819
+ );
3820
+ }
3821
+ });
3822
+ const uniqueKeyString = modelConstructor._buildKeyFromValues(
3823
+ constraint.fields,
3824
+ keyValuesForLookup,
3825
+ currentModelName,
3826
+ constraint.name
3827
+ );
3828
+ if (uniqueKeyString === null) {
3829
+ throw new Error(
3830
+ `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
3831
+ );
3832
+ }
3833
+ const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
3834
+ let existingRecord = null;
3835
+ if (connectedDocuments && connectedDocuments.size > 0) {
3836
+ for (const docId of connectedDocuments.keys()) {
3837
+ const docInfo = connectedDocuments.get(docId);
3838
+ if (docInfo) {
3839
+ const constraintMap = docInfo.yDoc.getMap(constraintMapName);
3840
+ const recordId = constraintMap.get(uniqueKeyString);
3841
+ if (recordId) {
3842
+ existingRecord = await modelConstructor.find(
3843
+ recordId
3844
+ );
3845
+ break;
3846
+ }
3847
+ }
3848
+ }
3849
+ } else {
3850
+ }
3851
+ if (options?.objectMustExist) {
3852
+ if (!existingRecord) {
3853
+ throw new RecordNotFoundError(
3854
+ `[${currentModelName}] upsertByUnique (objectMustExist): Record with constraint '${constraintName}' and value(s) starting with '${uniqueKeyString.substring(
3855
+ 0,
3856
+ 50
3857
+ )}...' not found.`
3858
+ );
3859
+ }
3860
+ Logger.verbose(
3861
+ `[${currentModelName}] upsertByUnique (objectMustExist): Updating existing record ${existingRecord.id}`
3862
+ );
3863
+ Object.assign(existingRecord, dataToUpsert);
3864
+ if (options?.targetDocument) {
3865
+ await existingRecord.save({ targetDocument: options.targetDocument });
3866
+ } else {
3867
+ await existingRecord.save();
3868
+ }
3869
+ return existingRecord;
3870
+ }
3871
+ if (options?.objectMustNotExist) {
3872
+ if (existingRecord) {
3873
+ throw new UniqueConstraintViolationError(
3874
+ `[${currentModelName}] upsertByUnique (objectMustNotExist): Record with constraint '${constraintName}' and value(s) starting with '${uniqueKeyString.substring(
3875
+ 0,
3876
+ 50
3877
+ )}...' already exists with ID ${existingRecord.id}.`,
3878
+ currentModelName,
3879
+ constraintName,
3880
+ constraint.fields,
3881
+ dataToUpsert.id,
3882
+ existingRecord.id
3883
+ );
3884
+ }
3885
+ Logger.verbose(
3886
+ `[${currentModelName}] upsertByUnique (objectMustNotExist): Creating new record.`
3887
+ );
3888
+ const newInstance = new modelConstructor(
3889
+ dataToUpsert
3890
+ );
3891
+ if (!newInstance.id) {
3892
+ throw new Error(
3893
+ `[${currentModelName}] upsertByUnique: ID must be provided in dataToUpsert or auto-generated by constructor for new records.`
3894
+ );
3895
+ }
3896
+ if (!options?.targetDocument) {
3897
+ throw new Error(
3898
+ `[${currentModelName}] upsertByUnique: targetDocument is required when creating new records.`
3899
+ );
3900
+ }
3901
+ await newInstance.save({ targetDocument: options.targetDocument });
3902
+ return newInstance;
3903
+ }
3904
+ if (existingRecord) {
3905
+ Logger.verbose(
3906
+ `[${currentModelName}] upsertByUnique (default): Updating existing record ${existingRecord.id}`
3907
+ );
3908
+ Object.assign(existingRecord, dataToUpsert);
3909
+ if (options?.targetDocument) {
3910
+ await existingRecord.save({ targetDocument: options.targetDocument });
3911
+ } else {
3912
+ await existingRecord.save();
3913
+ }
3914
+ return existingRecord;
3915
+ } else {
3916
+ Logger.verbose(
3917
+ `[${currentModelName}] upsertByUnique (default): Creating new record.`
3918
+ );
3919
+ const newInstance = new modelConstructor(
3920
+ dataToUpsert
3921
+ );
3922
+ if (!newInstance.id) {
3923
+ throw new Error(
3924
+ `[${currentModelName}] upsertByUnique: ID must be provided in dataToUpsert or auto-generated by constructor for new records.`
3925
+ );
3926
+ }
3927
+ if (!options?.targetDocument) {
3928
+ throw new Error(
3929
+ `[${currentModelName}] upsertByUnique: targetDocument is required when creating new records.`
3930
+ );
3931
+ }
3932
+ await newInstance.save({ targetDocument: options.targetDocument });
3933
+ return newInstance;
3934
+ }
3935
+ }
3936
+ /**
3937
+ * Execute a callback with automatic transaction handling for all modified models
3938
+ */
3939
+ static async withTransaction(callback) {
3940
+ const modifiedModels = /* @__PURE__ */ new Set();
3941
+ const originalSetValue = _BaseModel.prototype.setValue;
3942
+ _BaseModel.prototype.setValue = function(fieldKey, value) {
3943
+ modifiedModels.add(this);
3944
+ return originalSetValue.call(this, fieldKey, value);
3945
+ };
3946
+ try {
3947
+ const result = await callback();
3948
+ if (modifiedModels.size > 0) {
3949
+ Logger.verbose(
3950
+ `[BaseModel] Transaction saving ${modifiedModels.size} modified models`
3951
+ );
3952
+ let yDoc = null;
3953
+ const firstModel = Array.from(modifiedModels)[0];
3954
+ const modelConstructor = firstModel.constructor;
3955
+ if (modelConstructor.connectedDocuments && modelConstructor.connectedDocuments.size > 0) {
3956
+ yDoc = Array.from(modelConstructor.connectedDocuments.values())[0].yDoc;
3957
+ }
3958
+ if (yDoc) {
3959
+ await yDoc.transact(async () => {
3960
+ for (const model of modifiedModels) {
3961
+ if (model.isDirty) {
3962
+ await model.save();
3963
+ }
3964
+ }
3965
+ }, "batch-model-transaction");
3966
+ } else {
3967
+ for (const model of modifiedModels) {
3968
+ if (model.isDirty) {
3969
+ await model.save();
3970
+ }
3971
+ }
3972
+ }
3973
+ }
3974
+ return result;
3975
+ } finally {
3976
+ _BaseModel.prototype.setValue = originalSetValue;
3977
+ }
3978
+ }
3979
+ /**
3980
+ * Sets up deep observation on a nested YMap to sync field-level changes to the database
3981
+ */
3982
+ static setupNestedYMapObserver(recordId, recordYMap) {
3983
+ const modelConstructor = this;
3984
+ const schema = modelConstructor.getSchema();
3985
+ const modelName = schema?.options?.name;
3986
+ if (!modelName) {
3987
+ Logger.error(
3988
+ `[${modelConstructor.name}] Cannot setup nested YMap observer: model name not found`
3989
+ );
3990
+ return;
3991
+ }
3992
+ Logger.verbose(
3993
+ `[${modelName}] Setting up nested YMap observer for record ${recordId}`
3994
+ );
3995
+ recordYMap.observe(async (event) => {
3996
+ Logger.verbose(
3997
+ `[${modelName}] Nested YMap change detected for record ${recordId}:`,
3998
+ event
3999
+ );
4000
+ const currentDbInstance = _BaseModel.dbInstance;
4001
+ if (!currentDbInstance) {
4002
+ Logger.error(
4003
+ `[${modelName}] DB instance not available for nested YMap observer on record ${recordId}`
4004
+ );
4005
+ return;
4006
+ }
4007
+ const updatedData = {};
4008
+ const unknownFields = [];
4009
+ for (const [key, value] of recordYMap.entries()) {
4010
+ const fieldOptions = schema?.fields?.get(key);
4011
+ if (!fieldOptions) {
4012
+ unknownFields.push(key);
4013
+ continue;
4014
+ }
4015
+ if (fieldOptions.type === "stringset") {
4016
+ continue;
4017
+ }
4018
+ if (value !== void 0) {
4019
+ updatedData[key] = value;
4020
+ }
4021
+ }
4022
+ if (unknownFields.length > 0) {
4023
+ Logger.warn(
4024
+ `[${modelName}] Ignoring unknown fields [${unknownFields.join(
4025
+ ", "
4026
+ )}] when syncing nested record ${recordId}`
4027
+ );
4028
+ }
4029
+ if (!updatedData.id) {
4030
+ Logger.warn(
4031
+ `[${modelName}] Nested YMap change for ${recordId} missing id field, skipping DB sync`
4032
+ );
4033
+ return;
4034
+ }
4035
+ try {
4036
+ Logger.verbose(
4037
+ `[${modelName}] Syncing nested YMap changes to database for record ${recordId}:`,
4038
+ updatedData
4039
+ );
4040
+ await currentDbInstance.insert(modelName, {
4041
+ ...updatedData,
4042
+ type: modelName
4043
+ });
4044
+ Logger.verbose(
4045
+ `[${modelName}] Database sync completed for nested YMap changes on record ${recordId}`
4046
+ );
4047
+ } catch (error) {
4048
+ Logger.error(
4049
+ `[${modelName}] Error syncing nested YMap changes to database for record ${recordId}:`,
4050
+ error,
4051
+ updatedData
4052
+ );
4053
+ }
4054
+ modelConstructor.notifyListeners();
4055
+ });
4056
+ }
4057
+ /**
4058
+ * Sets up deep observation on a nested YMap for a specific document to sync field-level changes to the database
4059
+ */
4060
+ static setupNestedYMapObserverForDocument(recordId, recordYMap, docId, permissionHint) {
4061
+ const modelConstructor = this;
4062
+ const schema = modelConstructor.getSchema();
4063
+ const modelName = schema?.options?.name;
4064
+ if (!modelName) {
4065
+ Logger.error(
4066
+ `[${modelConstructor.name}] Cannot setup nested YMap observer: model name not found`
4067
+ );
4068
+ return;
4069
+ }
4070
+ Logger.verbose(
4071
+ `[${modelName}] Setting up nested YMap observer for record ${recordId} in document ${docId}`
4072
+ );
4073
+ recordYMap.observe(async (event) => {
4074
+ Logger.verbose(
4075
+ `[${modelName}] Nested YMap change detected for record ${recordId} in document ${docId}:`,
4076
+ event
4077
+ );
4078
+ const currentDbInstance = _BaseModel.dbInstance;
4079
+ if (!currentDbInstance) {
4080
+ Logger.error(
4081
+ `[${modelName}] DB instance not available for nested YMap observer on record ${recordId} in document ${docId}`
4082
+ );
4083
+ return;
4084
+ }
4085
+ const updatedData = {};
4086
+ const unknownFields = [];
4087
+ for (const [key, value] of recordYMap.entries()) {
4088
+ const fieldOptions = schema?.fields?.get(key);
4089
+ if (!fieldOptions) {
4090
+ unknownFields.push(key);
4091
+ continue;
4092
+ }
4093
+ if (fieldOptions.type === "stringset") {
4094
+ continue;
4095
+ }
4096
+ if (value !== void 0) {
4097
+ updatedData[key] = value;
4098
+ }
4099
+ }
4100
+ if (unknownFields.length > 0) {
4101
+ Logger.warn(
4102
+ `[${modelName}] Ignoring unknown fields [${unknownFields.join(
4103
+ ", "
4104
+ )}] when syncing nested record ${recordId} in document ${docId}`
4105
+ );
4106
+ }
4107
+ if (!updatedData.id) {
4108
+ Logger.warn(
4109
+ `[${modelName}] Nested YMap change for ${recordId} in document ${docId} missing id field, skipping DB sync`
4110
+ );
4111
+ return;
4112
+ }
4113
+ try {
4114
+ Logger.verbose(
4115
+ `[${modelName}] Syncing nested YMap changes to database for record ${recordId} in document ${docId}:`,
4116
+ updatedData
4117
+ );
4118
+ await currentDbInstance.insert(modelName, {
4119
+ ...updatedData,
4120
+ type: modelName,
4121
+ _meta_doc_id: docId,
4122
+ _meta_permission_hint: permissionHint
4123
+ });
4124
+ Logger.verbose(
4125
+ `[${modelName}] Database sync completed for nested YMap changes on record ${recordId} in document ${docId}`
4126
+ );
4127
+ } catch (error) {
4128
+ Logger.error(
4129
+ `[${modelName}] Error syncing nested YMap changes to database for record ${recordId} in document ${docId}:`,
4130
+ error,
4131
+ updatedData
4132
+ );
4133
+ }
4134
+ modelConstructor.notifyListeners();
4135
+ });
4136
+ }
4137
+ };
4138
+
4139
+ export {
4140
+ StringSet,
4141
+ DocumentClosedError,
4142
+ DocumentResolutionError,
4143
+ assertValidIdentifier,
4144
+ quoteIdentifier,
4145
+ DocumentQueryTranslator,
4146
+ LogLevel,
4147
+ Logger,
4148
+ UniqueConstraintViolationError,
4149
+ RecordNotFoundError,
4150
+ generateULID,
4151
+ BaseModel
4152
+ };
4153
+ //# sourceMappingURL=chunk-3PZWHUZO.js.map