js-bao 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/browser.cjs CHANGED
@@ -366,21 +366,36 @@ var init_CursorManager = __esm({
366
366
  return fields;
367
367
  }
368
368
  /**
369
- * Build ORDER BY clause from sort specification
369
+ * Build ORDER BY clause from sort specification.
370
+ *
371
+ * `direction` is the pagination direction (1 = forward, -1 = backward).
372
+ * For backward pagination each field's effective ORDER BY direction is
373
+ * reversed so the cursor's flipped inequality (built by
374
+ * `buildPaginationConditions`) returns the page adjacent to the cursor,
375
+ * not the globally smallest/largest window. Without this,
376
+ * `field < cursor ... ORDER BY field ASC LIMIT n` returns the n smallest
377
+ * rows overall instead of the n rows just below the cursor.
378
+ *
379
+ * The returned `directions` array carries the *declared* per-field sort
380
+ * directions (unaffected by `direction`) — `buildPaginationConditions`
381
+ * needs the declared directions to pick each field's comparison operator.
370
382
  */
371
- static buildOrderClause(sort, fieldFormatter = (field) => field) {
383
+ static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
372
384
  const fields = this.extractSortFields(sort);
373
385
  const clauses = [];
374
386
  const directions = [];
387
+ const sqlDir = (declared) => {
388
+ const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
389
+ return effective === 1 ? "ASC" : "DESC";
390
+ };
375
391
  if (sort && Object.keys(sort).length > 0) {
376
392
  for (const [field, dir] of Object.entries(sort)) {
377
- const sqlDirection = dir === 1 ? "ASC" : "DESC";
378
- clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
393
+ clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
379
394
  directions.push(dir);
380
395
  }
381
396
  }
382
397
  if (!sort || !sort.hasOwnProperty("id")) {
383
- clauses.push(`${fieldFormatter("id")} ASC`);
398
+ clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
384
399
  directions.push(1);
385
400
  } else if (sort && sort.hasOwnProperty("id")) {
386
401
  directions.push(sort["id"]);
@@ -403,18 +418,27 @@ var init_CursorManager = __esm({
403
418
  /**
404
419
  * Generate next and previous cursors from result set
405
420
  */
406
- static generateResultCursors(results, sortFields, _requestedDirection, hasMore, isFirstPage = false) {
421
+ static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
407
422
  if (results.length === 0) {
408
423
  return {};
409
424
  }
410
425
  const cursors = {};
411
426
  if (hasMore && results.length > 0) {
412
427
  const lastResult = results[results.length - 1];
413
- cursors.nextCursor = this.generateCursor(lastResult, sortFields, 1);
428
+ cursors.nextCursor = this.generateCursor(
429
+ lastResult,
430
+ sortFields,
431
+ requestedDirection
432
+ );
414
433
  }
415
434
  if (results.length > 0 && !isFirstPage) {
416
435
  const firstResult = results[0];
417
- cursors.prevCursor = this.generateCursor(firstResult, sortFields, -1);
436
+ const opposite = requestedDirection === 1 ? -1 : 1;
437
+ cursors.prevCursor = this.generateCursor(
438
+ firstResult,
439
+ sortFields,
440
+ opposite
441
+ );
418
442
  }
419
443
  return cursors;
420
444
  }
@@ -547,7 +571,8 @@ var init_DocumentQueryTranslator = __esm({
547
571
  const whereClause = this.translateFilter(filter);
548
572
  const orderClause = CursorManager.buildOrderClause(
549
573
  options?.sort,
550
- (field) => this.getQuotedField(field)
574
+ (field) => this.getQuotedField(field),
575
+ options?.direction || 1
551
576
  );
552
577
  const limitClause = this.buildLimitClause(options);
553
578
  const paginationClause = this.buildPaginationClause(
@@ -1220,6 +1245,69 @@ var init_yjsValues = __esm({
1220
1245
  }
1221
1246
  });
1222
1247
 
1248
+ // src/models/writeLifecycle.ts
1249
+ function ensureStringSetYMap(rm, fieldName) {
1250
+ const nested = rm.get(fieldName);
1251
+ if (nested instanceof Y2.Map) return nested;
1252
+ const fresh = new Y2.Map();
1253
+ if (nested && typeof nested === "object") {
1254
+ for (const [member, marker] of Object.entries(
1255
+ nested
1256
+ )) {
1257
+ if (Boolean(marker)) fresh.set(member, true);
1258
+ }
1259
+ }
1260
+ rm.set(fieldName, fresh);
1261
+ return fresh;
1262
+ }
1263
+ function applyStringSetDelta(target, delta) {
1264
+ if (delta.clear) {
1265
+ for (const member of Array.from(target.keys())) target.delete(member);
1266
+ }
1267
+ if (delta.remove) {
1268
+ for (const member of delta.remove) target.delete(String(member));
1269
+ }
1270
+ if (delta.add) {
1271
+ for (const member of delta.add) {
1272
+ const key = String(member);
1273
+ if (target.get(key) !== true) target.set(key, true);
1274
+ }
1275
+ }
1276
+ }
1277
+ function applyStringSetChange(recordYMap, fieldName, change) {
1278
+ const target = ensureStringSetYMap(recordYMap, fieldName);
1279
+ applyStringSetDelta(target, {
1280
+ add: change.additions,
1281
+ remove: change.removals
1282
+ });
1283
+ }
1284
+ function registerFunctionDefaultGenerator(name, fn) {
1285
+ FUNCTION_DEFAULT_GENERATORS.set(name, fn);
1286
+ }
1287
+ function resolveLiteralOrFunctionDefault(rawDefault) {
1288
+ return typeof rawDefault === "function" ? rawDefault() : rawDefault;
1289
+ }
1290
+ var Y2, import_ulid, KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
1291
+ var init_writeLifecycle = __esm({
1292
+ "src/models/writeLifecycle.ts"() {
1293
+ "use strict";
1294
+ Y2 = __toESM(require("yjs"), 1);
1295
+ import_ulid = require("ulid");
1296
+ KNOWN_FIELD_TYPE_MAP = {
1297
+ string: true,
1298
+ number: true,
1299
+ boolean: true,
1300
+ date: true,
1301
+ id: true,
1302
+ stringset: true
1303
+ };
1304
+ KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
1305
+ FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
1306
+ registerFunctionDefaultGenerator("generate_ulid", () => (0, import_ulid.ulid)());
1307
+ UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
1308
+ }
1309
+ });
1310
+
1223
1311
  // src/models/metaSync.ts
1224
1312
  function inferFieldType(value) {
1225
1313
  if (isStringSetYMap(value)) return "stringset";
@@ -1241,7 +1329,10 @@ function encodeDefault(value) {
1241
1329
  if (value === void 0 || value === null) return void 0;
1242
1330
  if (typeof value === "function") {
1243
1331
  const name = KNOWN_FUNCTION_DEFAULTS.get(value);
1244
- return name ? `$${name}` : "$unknown_function";
1332
+ return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
1333
+ }
1334
+ if (typeof value === "string" && value.startsWith("$")) {
1335
+ return `$${value}`;
1245
1336
  }
1246
1337
  return value;
1247
1338
  }
@@ -1267,7 +1358,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1267
1358
  if (compoundConstraints.length > 0) {
1268
1359
  let constraints = meta.get("_constraints");
1269
1360
  if (!constraints) {
1270
- constraints = new Y2.Map();
1361
+ constraints = new Y3.Map();
1271
1362
  meta.set("_constraints", constraints);
1272
1363
  }
1273
1364
  for (const constraint of compoundConstraints) {
@@ -1278,7 +1369,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1278
1369
  if (relationships && Object.keys(relationships).length > 0) {
1279
1370
  let rels = meta.get("_relationships");
1280
1371
  if (!rels) {
1281
- rels = new Y2.Map();
1372
+ rels = new Y3.Map();
1282
1373
  meta.set("_relationships", rels);
1283
1374
  }
1284
1375
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -1290,7 +1381,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1290
1381
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1291
1382
  let fieldMeta = metaMap.get(fieldName);
1292
1383
  if (!fieldMeta) {
1293
- fieldMeta = new Y2.Map();
1384
+ fieldMeta = new Y3.Map();
1294
1385
  metaMap.set(fieldName, fieldMeta);
1295
1386
  }
1296
1387
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -1307,7 +1398,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1307
1398
  function syncConstraintMeta(constraintsMap, constraint) {
1308
1399
  let cMeta = constraintsMap.get(constraint.name);
1309
1400
  if (!cMeta) {
1310
- cMeta = new Y2.Map();
1401
+ cMeta = new Y3.Map();
1311
1402
  constraintsMap.set(constraint.name, cMeta);
1312
1403
  }
1313
1404
  setIfChanged(cMeta, "type", "unique");
@@ -1317,7 +1408,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
1317
1408
  function syncRelationshipMeta(relsMap, relName, relConfig) {
1318
1409
  let relMeta = relsMap.get(relName);
1319
1410
  if (!relMeta) {
1320
- relMeta = new Y2.Map();
1411
+ relMeta = new Y3.Map();
1321
1412
  relsMap.set(relName, relMeta);
1322
1413
  }
1323
1414
  for (const [key, value] of Object.entries(relConfig)) {
@@ -1332,7 +1423,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
1332
1423
  if (fieldName.startsWith("_")) continue;
1333
1424
  let fieldMeta = meta.get(fieldName);
1334
1425
  if (!fieldMeta) {
1335
- fieldMeta = new Y2.Map();
1426
+ fieldMeta = new Y3.Map();
1336
1427
  meta.set(fieldName, fieldMeta);
1337
1428
  }
1338
1429
  if (!fieldMeta.has("type")) {
@@ -1348,18 +1439,58 @@ function setIfChanged(map, key, value) {
1348
1439
  map.set(key, value);
1349
1440
  }
1350
1441
  }
1351
- var Y2, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1442
+ var Y3, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1352
1443
  var init_metaSync = __esm({
1353
1444
  "src/models/metaSync.ts"() {
1354
1445
  "use strict";
1355
- Y2 = __toESM(require("yjs"), 1);
1446
+ Y3 = __toESM(require("yjs"), 1);
1356
1447
  init_yjsValues();
1448
+ init_writeLifecycle();
1357
1449
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
1358
1450
  _syncedCache = /* @__PURE__ */ new WeakMap();
1359
1451
  }
1360
1452
  });
1361
1453
 
1454
+ // src/utils/uniqueIndex.ts
1455
+ function buildUniqueKey(recordData, fields) {
1456
+ const keyParts = [];
1457
+ for (const field of fields) {
1458
+ const value = recordData instanceof Y4.Map ? recordData.get(field) : recordData[field];
1459
+ if (value === null || value === void 0) {
1460
+ return null;
1461
+ }
1462
+ keyParts.push(String(value));
1463
+ }
1464
+ return keyParts.join("|");
1465
+ }
1466
+ function uniqueIndexMapName(modelName, constraintName) {
1467
+ return `_uniqueIdx_${modelName}_${constraintName}`;
1468
+ }
1469
+ function fieldUniqueConstraintName(modelName, fieldName) {
1470
+ return `${modelName}_${fieldName}_unique`;
1471
+ }
1472
+ var Y4;
1473
+ var init_uniqueIndex = __esm({
1474
+ "src/utils/uniqueIndex.ts"() {
1475
+ "use strict";
1476
+ Y4 = __toESM(require("yjs"), 1);
1477
+ }
1478
+ });
1479
+
1362
1480
  // src/models/relationshipManager.ts
1481
+ function warnIfOrderFieldNullable(modelClass, orderByField, relationshipDescription) {
1482
+ if (ALWAYS_PRESENT_ORDER_FIELDS.has(orderByField)) return;
1483
+ const schema = modelClass.getSchema?.();
1484
+ const fields = schema?.fields;
1485
+ if (!fields || typeof fields.get !== "function") return;
1486
+ const fieldOptions = fields.get(orderByField);
1487
+ if (!fieldOptions) return;
1488
+ if (fieldOptions.required !== true) {
1489
+ Logger.warn(
1490
+ `[RelationshipManager] ${relationshipDescription} orders by '${orderByField}', which is not marked required on ${modelClass.name}. The composite (${orderByField}, id) cursor paginates deterministically, but a row whose '${orderByField}' is actually NULL has no stable cursor position and may drop from the tail. Mark '${orderByField}' as required, or order by a required field such as 'id', to remove the risk. (Full NULL-ordering support is tracked in #1316.)`
1491
+ );
1492
+ }
1493
+ }
1363
1494
  function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1364
1495
  return async function() {
1365
1496
  const relatedId = this[config.relatedIdField];
@@ -1368,6 +1499,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1368
1499
  };
1369
1500
  }
1370
1501
  function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbEngine) {
1502
+ const {
1503
+ relatedIdField,
1504
+ orderByField = "id",
1505
+ orderDirection = "ASC"
1506
+ } = config;
1507
+ warnIfOrderFieldNullable(
1508
+ targetModelClass,
1509
+ orderByField,
1510
+ `hasMany(${targetModelClass.name})`
1511
+ );
1371
1512
  return async function(paginationArgs) {
1372
1513
  const {
1373
1514
  limit,
@@ -1375,71 +1516,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
1375
1516
  beforeCursor,
1376
1517
  direction = "forward"
1377
1518
  } = paginationArgs || {};
1378
- const {
1379
- relatedIdField,
1380
- orderByField = "id",
1381
- orderDirection = "ASC"
1382
- } = config;
1383
1519
  const targetSchema = targetModelClass.getSchema();
1384
1520
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1385
1521
  Logger.error(
1386
1522
  `[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
1387
1523
  );
1388
- return { data: [], nextCursor: null, prevCursor: null };
1524
+ return { data: [], hasMore: false };
1389
1525
  }
1390
- const filter = { [relatedIdField]: this.id };
1526
+ const forward = direction !== "backward";
1527
+ const cursor = forward ? afterCursor : beforeCursor;
1391
1528
  const queryOptions = {
1392
- sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
1529
+ sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
1530
+ direction: forward ? 1 : -1
1393
1531
  };
1394
- if (limit !== void 0) {
1395
- queryOptions.limit = limit + 1;
1396
- }
1397
- if (direction === "forward" && afterCursor) {
1398
- filter[orderByField] = {
1399
- [orderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
1400
- };
1401
- } else if (direction === "backward") {
1402
- if (beforeCursor) {
1403
- filter[orderByField] = {
1404
- [orderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
1405
- };
1406
- }
1407
- queryOptions.sort = { [orderByField]: orderDirection === "ASC" ? -1 : 1 };
1408
- }
1409
- const result = await targetModelClass.query(filter, queryOptions);
1410
- const results = result.data;
1411
- let nextCursor = null;
1412
- let prevCursor = null;
1413
- const isFirstPage = direction === "forward" && !afterCursor;
1414
- if (limit !== void 0) {
1415
- if (direction === "forward") {
1416
- if (results.length > limit) {
1417
- nextCursor = results[limit - 1][orderByField];
1418
- results.pop();
1419
- }
1420
- if (results.length > 0 && !isFirstPage) {
1421
- prevCursor = results[0][orderByField];
1422
- }
1423
- } else {
1424
- if (results.length > limit) {
1425
- prevCursor = results[limit - 1][orderByField];
1426
- results.pop();
1427
- }
1428
- results.reverse();
1429
- if (results.length > 0 && beforeCursor) {
1430
- nextCursor = results[results.length - 1][orderByField];
1431
- }
1432
- if (results.length > 0 && !beforeCursor) {
1433
- if (results.length === limit) {
1434
- prevCursor = results[0][orderByField];
1435
- }
1436
- }
1437
- }
1438
- }
1439
- return { data: results, nextCursor, prevCursor };
1532
+ if (limit !== void 0) queryOptions.limit = limit;
1533
+ if (cursor != null) queryOptions.uniqueStartKey = cursor;
1534
+ return targetModelClass.query(
1535
+ { [relatedIdField]: this.id },
1536
+ queryOptions
1537
+ );
1440
1538
  };
1441
1539
  }
1442
1540
  function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClass, joinModelClass, _dbEngine) {
1541
+ const {
1542
+ joinModelLocalField,
1543
+ joinModelRelatedField,
1544
+ joinModelOrderByField = "id",
1545
+ joinModelOrderDirection = "ASC"
1546
+ } = config;
1547
+ warnIfOrderFieldNullable(
1548
+ joinModelClass,
1549
+ joinModelOrderByField,
1550
+ `hasManyThrough(${targetModelClass.name} via ${joinModelClass.name})`
1551
+ );
1443
1552
  return async function(paginationArgs) {
1444
1553
  const {
1445
1554
  limit,
@@ -1453,87 +1562,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
1453
1562
  Logger.error(
1454
1563
  `[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
1455
1564
  );
1456
- return { data: [], nextCursor: null, prevCursor: null };
1565
+ return { data: [], hasMore: false };
1457
1566
  }
1458
1567
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1459
1568
  Logger.error(
1460
1569
  `[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
1461
1570
  );
1462
- return { data: [], nextCursor: null, prevCursor: null };
1571
+ return { data: [], hasMore: false };
1463
1572
  }
1464
- const {
1465
- joinModelLocalField,
1466
- joinModelRelatedField,
1467
- joinModelOrderByField = "id",
1468
- joinModelOrderDirection = "ASC"
1469
- } = config;
1470
- const joinFilter = { [joinModelLocalField]: this.id };
1573
+ const forward = direction !== "backward";
1574
+ const cursor = forward ? afterCursor : beforeCursor;
1471
1575
  const joinQueryOptions = {
1472
1576
  sort: {
1473
1577
  [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
1474
1578
  },
1475
- projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
1579
+ projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
1580
+ direction: forward ? 1 : -1
1476
1581
  };
1477
- if (limit !== void 0) {
1478
- joinQueryOptions.limit = limit + 1;
1479
- }
1480
- if (direction === "forward" && afterCursor) {
1481
- joinFilter[joinModelOrderByField] = {
1482
- [joinModelOrderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
1483
- };
1484
- } else if (direction === "backward") {
1485
- if (beforeCursor) {
1486
- joinFilter[joinModelOrderByField] = {
1487
- [joinModelOrderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
1488
- };
1489
- }
1490
- joinQueryOptions.sort = {
1491
- [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? -1 : 1
1492
- };
1493
- }
1582
+ if (limit !== void 0) joinQueryOptions.limit = limit;
1583
+ if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
1494
1584
  const joinResult = await joinModelClass.query(
1495
- joinFilter,
1585
+ { [joinModelLocalField]: this.id },
1496
1586
  joinQueryOptions
1497
1587
  );
1498
- const joinResults = joinResult.data;
1499
- let nextCursorFromJoin = null;
1500
- let prevCursorFromJoin = null;
1501
- const isFirstPage = direction === "forward" && !afterCursor;
1502
- if (limit !== void 0) {
1503
- if (direction === "forward") {
1504
- if (joinResults.length > limit) {
1505
- nextCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
1506
- joinResults.pop();
1507
- }
1508
- if (joinResults.length > 0 && !isFirstPage) {
1509
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
1510
- }
1511
- } else {
1512
- if (joinResults.length > limit) {
1513
- prevCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
1514
- joinResults.pop();
1515
- }
1516
- joinResults.reverse();
1517
- if (joinResults.length > 0 && beforeCursor) {
1518
- nextCursorFromJoin = joinResults[joinResults.length - 1][joinModelOrderByField];
1519
- }
1520
- if (joinResults.length > 0 && !beforeCursor) {
1521
- if (joinResults.length === limit) {
1522
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
1523
- }
1524
- }
1525
- }
1588
+ const relatedIds = joinResult.data.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1589
+ let targetData = [];
1590
+ if (relatedIds.length > 0) {
1591
+ const targetResult = await targetModelClass.query({
1592
+ id: { $in: relatedIds }
1593
+ });
1594
+ targetData = targetResult.data;
1526
1595
  }
1527
- const relatedIds = joinResults.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1528
- if (relatedIds.length === 0)
1529
- return { data: [], nextCursor: null, prevCursor: null };
1530
- const targetResult = await targetModelClass.query({
1531
- id: { $in: relatedIds }
1532
- });
1533
1596
  return {
1534
- data: targetResult.data,
1535
- nextCursor: nextCursorFromJoin,
1536
- prevCursor: prevCursorFromJoin
1597
+ data: targetData,
1598
+ nextCursor: joinResult.nextCursor,
1599
+ prevCursor: joinResult.prevCursor,
1600
+ hasMore: joinResult.hasMore
1537
1601
  };
1538
1602
  };
1539
1603
  }
@@ -1657,10 +1721,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
1657
1721
  }, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
1658
1722
  };
1659
1723
  }
1724
+ var ALWAYS_PRESENT_ORDER_FIELDS;
1660
1725
  var init_relationshipManager = __esm({
1661
1726
  "src/models/relationshipManager.ts"() {
1662
1727
  "use strict";
1663
1728
  init_BaseModel();
1729
+ ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
1664
1730
  }
1665
1731
  });
1666
1732
 
@@ -2431,7 +2497,13 @@ var init_IncludeResolver = __esm({
2431
2497
  }
2432
2498
  return null;
2433
2499
  }
2434
- /** Apply projection to a single record. */
2500
+ /**
2501
+ * Apply a projection to a single record. Assumes the projection was already
2502
+ * validated upstream (mixed inclusion+exclusion rejected by the translator):
2503
+ * `isInclusion` treats "any value === 1" as inclusion mode. `id` and `type`
2504
+ * are system-identity fields and are always kept in BOTH modes (mirrors the
2505
+ * DatabaseDO `_applyProjection`); `_meta` stays excludable.
2506
+ */
2435
2507
  _applyProjection(record, projection) {
2436
2508
  const fields = Object.entries(projection);
2437
2509
  if (fields.length === 0) return record;
@@ -2450,7 +2522,7 @@ var init_IncludeResolver = __esm({
2450
2522
  } else {
2451
2523
  const result = { ...record };
2452
2524
  for (const [f, v] of fields) {
2453
- if (v === 0) delete result[f];
2525
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
2454
2526
  }
2455
2527
  return result;
2456
2528
  }
@@ -2470,21 +2542,23 @@ __export(BaseModel_exports, {
2470
2542
  generateULID: () => generateULID
2471
2543
  });
2472
2544
  function generateULID() {
2473
- return (0, import_ulid.ulid)();
2545
+ return (0, import_ulid2.ulid)();
2474
2546
  }
2475
- var Y3, import_ulid, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2547
+ var Y5, import_ulid2, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2476
2548
  var init_BaseModel = __esm({
2477
2549
  "src/models/BaseModel.ts"() {
2478
2550
  "use strict";
2479
- Y3 = __toESM(require("yjs"), 1);
2480
- import_ulid = require("ulid");
2551
+ Y5 = __toESM(require("yjs"), 1);
2552
+ import_ulid2 = require("ulid");
2481
2553
  init_StringSet();
2482
2554
  init_documentTypes();
2483
2555
  init_DocumentQueryTranslator();
2484
2556
  init_CursorManager();
2485
2557
  init_sql();
2486
2558
  init_metaSync();
2559
+ init_writeLifecycle();
2487
2560
  init_yjsValues();
2561
+ init_uniqueIndex();
2488
2562
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2489
2563
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
2490
2564
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2869,7 +2943,9 @@ var init_BaseModel = __esm({
2869
2943
  this.setValue(fieldKey, data[fieldKey]);
2870
2944
  this._constructorProvidedFields.add(fieldKey);
2871
2945
  } else if (fieldOptions.default !== void 0) {
2872
- const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
2946
+ const defaultValue = resolveLiteralOrFunctionDefault(
2947
+ fieldOptions.default
2948
+ );
2873
2949
  this.setValue(fieldKey, defaultValue);
2874
2950
  }
2875
2951
  }
@@ -3161,7 +3237,7 @@ var init_BaseModel = __esm({
3161
3237
  }
3162
3238
  getStringSetFromYjs(fieldName) {
3163
3239
  const yjsData = this.getFromYjs(fieldName);
3164
- if (yjsData instanceof Y3.Map) {
3240
+ if (yjsData instanceof Y5.Map) {
3165
3241
  return Array.from(yjsData.keys());
3166
3242
  }
3167
3243
  if (yjsData && typeof yjsData === "object") {
@@ -3269,7 +3345,7 @@ var init_BaseModel = __esm({
3269
3345
  }
3270
3346
  const extractStringSetValues = (value) => {
3271
3347
  if (!value) return [];
3272
- if (value instanceof Y3.Map) {
3348
+ if (value instanceof Y5.Map) {
3273
3349
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
3274
3350
  }
3275
3351
  if (Array.isArray(value)) {
@@ -3286,7 +3362,7 @@ var init_BaseModel = __esm({
3286
3362
  if (!recordId) continue;
3287
3363
  let itemData;
3288
3364
  const stringSetValuesByField = {};
3289
- if (recordData instanceof Y3.Map) {
3365
+ if (recordData instanceof Y5.Map) {
3290
3366
  itemData = {};
3291
3367
  const unknownFields = [];
3292
3368
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3402,20 +3478,10 @@ var init_BaseModel = __esm({
3402
3478
  Logger.verbose(
3403
3479
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
3404
3480
  );
3405
- const buildUniqueKey = (recordData, fields) => {
3406
- const keyParts = [];
3407
- for (const field of fields) {
3408
- const value = recordData instanceof Y3.Map ? recordData.get(field) : recordData[field];
3409
- if (value === null || value === void 0) {
3410
- return null;
3411
- }
3412
- keyParts.push(String(value));
3413
- }
3414
- return keyParts.join("|");
3415
- };
3481
+ const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
3416
3482
  const extractItemData = (key, recordData) => {
3417
3483
  let itemData;
3418
- if (recordData instanceof Y3.Map) {
3484
+ if (recordData instanceof Y5.Map) {
3419
3485
  itemData = {};
3420
3486
  const unknownFields = [];
3421
3487
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3499,7 +3565,7 @@ var init_BaseModel = __esm({
3499
3565
  const recordsByUniqueKey = /* @__PURE__ */ new Map();
3500
3566
  for (const [recordId, recordData] of allRecords.entries()) {
3501
3567
  if (recordIdsToDiscard.has(recordId)) continue;
3502
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3568
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3503
3569
  if (uniqueKey === null) continue;
3504
3570
  if (!recordsByUniqueKey.has(uniqueKey)) {
3505
3571
  recordsByUniqueKey.set(uniqueKey, []);
@@ -3545,7 +3611,7 @@ var init_BaseModel = __esm({
3545
3611
  if (!recordData || !key) continue;
3546
3612
  const itemData = extractItemData(key, recordData);
3547
3613
  if (!itemData) continue;
3548
- if (change.action === "add" && recordData instanceof Y3.Map) {
3614
+ if (change.action === "add" && recordData instanceof Y5.Map) {
3549
3615
  Logger.verbose(
3550
3616
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
3551
3617
  );
@@ -3643,9 +3709,12 @@ var init_BaseModel = __esm({
3643
3709
  const recordData = documentYMap.get(idToDiscard);
3644
3710
  if (!recordData) continue;
3645
3711
  for (const constraint of schema.resolvedUniqueConstraints) {
3646
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3712
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3647
3713
  if (uniqueKey === null) continue;
3648
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3714
+ const constraintMapName = uniqueIndexMapName(
3715
+ modelName,
3716
+ constraint.name
3717
+ );
3649
3718
  const constraintMap = yDoc.getMap(constraintMapName);
3650
3719
  const currentIndexValue = constraintMap.get(uniqueKey);
3651
3720
  if (currentIndexValue === idToDiscard) {
@@ -3663,11 +3732,14 @@ var init_BaseModel = __esm({
3663
3732
  }, `conflict-resolution-${modelName}-${docId}`);
3664
3733
  yDoc.transact(() => {
3665
3734
  for (const constraint of schema.resolvedUniqueConstraints) {
3666
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3735
+ const constraintMapName = uniqueIndexMapName(
3736
+ modelName,
3737
+ constraint.name
3738
+ );
3667
3739
  const constraintMap = yDoc.getMap(constraintMapName);
3668
3740
  for (const [recordId, recordData] of documentYMap.entries()) {
3669
3741
  if (!recordData) continue;
3670
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3742
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3671
3743
  if (uniqueKey === null) continue;
3672
3744
  const currentIndexValue = constraintMap.get(uniqueKey);
3673
3745
  if (currentIndexValue !== recordId) {
@@ -3690,7 +3762,7 @@ var init_BaseModel = __esm({
3690
3762
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
3691
3763
  );
3692
3764
  for (const [recordId, recordData] of documentYMap.entries()) {
3693
- if (recordData instanceof Y3.Map) {
3765
+ if (recordData instanceof Y5.Map) {
3694
3766
  Logger.verbose(
3695
3767
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
3696
3768
  );
@@ -3916,26 +3988,7 @@ var init_BaseModel = __esm({
3916
3988
  * if the field hasn't been touched since the wire-format change in #561.
3917
3989
  */
3918
3990
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3919
- let nested = recordYMap.get(fieldName);
3920
- if (!(nested instanceof Y3.Map)) {
3921
- const migrated = new Y3.Map();
3922
- if (nested && typeof nested === "object") {
3923
- for (const [member, marker] of Object.entries(
3924
- nested
3925
- )) {
3926
- if (Boolean(marker)) migrated.set(member, true);
3927
- }
3928
- }
3929
- recordYMap.set(fieldName, migrated);
3930
- nested = migrated;
3931
- }
3932
- const target = nested;
3933
- for (const member of change.additions) {
3934
- target.set(member, true);
3935
- }
3936
- for (const member of change.removals) {
3937
- target.delete(member);
3938
- }
3991
+ applyStringSetChange(recordYMap, fieldName, change);
3939
3992
  }
3940
3993
  /**
3941
3994
  * Deep equality check for comparing field values
@@ -4137,6 +4190,34 @@ var init_BaseModel = __esm({
4137
4190
  `[${modelName}.save] getCurrentJSState() returned:`,
4138
4191
  dataToSave
4139
4192
  );
4193
+ const uniqueMissConflicts = /* @__PURE__ */ new Map();
4194
+ for (const constraint of schema.resolvedUniqueConstraints) {
4195
+ const newUniqueKey = this._buildUniqueKey(
4196
+ constraint.fields,
4197
+ dataToSave,
4198
+ modelName,
4199
+ constraint.name
4200
+ );
4201
+ if (newUniqueKey === null) {
4202
+ continue;
4203
+ }
4204
+ const constraintMap = yDoc.getMap(
4205
+ uniqueIndexMapName(modelName, constraint.name)
4206
+ );
4207
+ if (!constraintMap.get(newUniqueKey)) {
4208
+ const constraintKeyValues = constraint.fields.map(
4209
+ (field) => dataToSave[field]
4210
+ );
4211
+ const conflictingRecord = await modelConstructor._resolveByUniqueFallback(
4212
+ constraint,
4213
+ constraintKeyValues,
4214
+ { excludeId: this.id }
4215
+ );
4216
+ if (conflictingRecord) {
4217
+ uniqueMissConflicts.set(constraint.name, conflictingRecord.id);
4218
+ }
4219
+ }
4220
+ }
4140
4221
  await yDoc.transact(async () => {
4141
4222
  if (upsertOnConstraint && upsertOnKey) {
4142
4223
  const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
@@ -4178,7 +4259,7 @@ var init_BaseModel = __esm({
4178
4259
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
4179
4260
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
4180
4261
  if (!recordYMap) {
4181
- recordYMap = new Y3.Map();
4262
+ recordYMap = new Y5.Map();
4182
4263
  Logger.verbose(
4183
4264
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
4184
4265
  );
@@ -4230,7 +4311,10 @@ var init_BaseModel = __esm({
4230
4311
  );
4231
4312
  continue;
4232
4313
  }
4233
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
4314
+ const constraintMapName = uniqueIndexMapName(
4315
+ modelName,
4316
+ constraint.name
4317
+ );
4234
4318
  const constraintMap = yDoc.getMap(constraintMapName);
4235
4319
  Logger.verbose(
4236
4320
  `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
@@ -4259,6 +4343,26 @@ var init_BaseModel = __esm({
4259
4343
  this.id,
4260
4344
  existingRecordIdWithNewKey
4261
4345
  );
4346
+ } else if (!existingRecordIdWithNewKey) {
4347
+ const conflictingRecordId = uniqueMissConflicts.get(constraint.name);
4348
+ if (conflictingRecordId && conflictingRecordId !== this.id) {
4349
+ Logger.verbose(
4350
+ `[${modelName}] Save Check (Item '${this.id}'): index miss for '${constraint.name}', but engine fallback found conflicting index-less record ${conflictingRecordId}.`
4351
+ );
4352
+ throw new UniqueConstraintViolationError(
4353
+ `Unique constraint '${constraint.name}' violated for model '${modelName}' on fields [${constraint.fields.join(
4354
+ ", "
4355
+ )}]. Attempted ID: ${this.id}. Value(s) starting with '${newUniqueKey.substring(
4356
+ 0,
4357
+ 50
4358
+ )}...' already exist for index-less record ID ${conflictingRecordId}.`,
4359
+ modelName,
4360
+ constraint.name,
4361
+ constraint.fields,
4362
+ this.id,
4363
+ conflictingRecordId
4364
+ );
4365
+ }
4262
4366
  }
4263
4367
  }
4264
4368
  if (isUpdate && oldData) {
@@ -4602,34 +4706,46 @@ var init_BaseModel = __esm({
4602
4706
  const modelName = schema.options.name;
4603
4707
  Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
4604
4708
  const hasIncludes = options?.include && options.include.length > 0;
4605
- const translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4709
+ const requestedLimit = options?.limit;
4710
+ const overFetch = typeof requestedLimit === "number" && requestedLimit > 0;
4711
+ let translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4712
+ if (overFetch) {
4713
+ translatorOptions = {
4714
+ ...translatorOptions || {},
4715
+ limit: requestedLimit + 1
4716
+ };
4717
+ }
4606
4718
  const translator = new DocumentQueryTranslator(modelName, schema.fields);
4607
4719
  const translatedQuery = translator.translateFind(filter, translatorOptions);
4608
4720
  Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
4609
4721
  Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
4610
- const results = await _BaseModel.dbInstance.query(
4722
+ let results = await _BaseModel.dbInstance.query(
4611
4723
  translatedQuery.sql,
4612
4724
  translatedQuery.params
4613
4725
  );
4614
4726
  Logger.verbose(`[${modelName}] Query returned ${results.length} results`);
4727
+ let hasMore = false;
4728
+ if (overFetch && results.length > requestedLimit) {
4729
+ hasMore = true;
4730
+ results = results.slice(0, requestedLimit);
4731
+ }
4615
4732
  if (options?.include && options.include.length > 0) {
4616
4733
  const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
4617
4734
  const resolver = new IncludeResolver2(_BaseModel.dbInstance);
4618
4735
  await resolver.resolve(results, options.include, 0, modelName);
4619
4736
  }
4620
- const requestedLimit = options?.limit;
4621
- const hasMore = CursorManager.hasMoreResults(
4622
- requestedLimit,
4623
- results.length
4624
- );
4625
4737
  const isFirstPage = !options?.uniqueStartKey;
4738
+ const direction = options?.direction || 1;
4626
4739
  const cursors = CursorManager.generateResultCursors(
4627
4740
  results,
4628
4741
  translatedQuery.sortFields,
4629
- options?.direction || 1,
4742
+ direction,
4630
4743
  hasMore,
4631
4744
  isFirstPage
4632
4745
  );
4746
+ if (direction === -1) {
4747
+ results.reverse();
4748
+ }
4633
4749
  let transformedData;
4634
4750
  if (options?.projection && Object.keys(options.projection).length > 0) {
4635
4751
  transformedData = results.map((row) => {
@@ -5205,7 +5321,7 @@ var init_BaseModel = __esm({
5205
5321
  );
5206
5322
  }
5207
5323
  for (const [recordId, recordYMap] of documentYMap.entries()) {
5208
- if (recordYMap instanceof Y3.Map) {
5324
+ if (recordYMap instanceof Y5.Map) {
5209
5325
  const instance = new this({ id: recordId });
5210
5326
  instance._metaDocId = docId;
5211
5327
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -5230,6 +5346,50 @@ var init_BaseModel = __esm({
5230
5346
  }
5231
5347
  return allInstances;
5232
5348
  }
5349
+ /**
5350
+ * Resolve a record by its unique-constraint field values via an engine query.
5351
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
5352
+ * entry for a key.
5353
+ *
5354
+ * The index map is a cache over the SQLite projection, not the source of
5355
+ * truth: every record is loaded into the projection on `initializeForDocument`
5356
+ * regardless of index presence, but a record written by a client predating
5357
+ * index maintenance (legacy data) is missing from the index. Such a record
5358
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
5359
+ * check. A query over the constraint fields finds it reliably.
5360
+ *
5361
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
5362
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
5363
+ * whether to heal the missing index entry (write paths) and do their own
5364
+ * logging — this method never mutates the index, so read paths stay
5365
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
5366
+ * constraint field means the record does not participate in the constraint,
5367
+ * so no query runs.
5368
+ */
5369
+ static async _resolveByUniqueFallback(constraint, keyValues, options) {
5370
+ if (!_BaseModel.dbInstance) {
5371
+ return null;
5372
+ }
5373
+ const filter = {};
5374
+ for (let i = 0; i < constraint.fields.length; i++) {
5375
+ const value = keyValues[i];
5376
+ if (value === null || value === void 0) {
5377
+ return null;
5378
+ }
5379
+ filter[constraint.fields[i]] = value;
5380
+ }
5381
+ if (options?.excludeId) {
5382
+ if ("id" in filter) {
5383
+ if (filter.id === options.excludeId) {
5384
+ return null;
5385
+ }
5386
+ } else {
5387
+ filter.id = { $ne: options.excludeId };
5388
+ }
5389
+ }
5390
+ const found = await this.queryOne(filter);
5391
+ return found ?? null;
5392
+ }
5233
5393
  static async findByUnique(constraintName, value) {
5234
5394
  const modelConstructor = this;
5235
5395
  const schema = modelConstructor.getSchema();
@@ -5268,7 +5428,10 @@ var init_BaseModel = __esm({
5268
5428
  const documentInfo = modelConstructor.connectedDocuments.get(docId);
5269
5429
  if (!documentInfo) continue;
5270
5430
  const yDoc = documentInfo.yDoc;
5271
- const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5431
+ const constraintMapName2 = uniqueIndexMapName(
5432
+ currentModelName,
5433
+ constraint.name
5434
+ );
5272
5435
  const constraintMap2 = yDoc.getMap(constraintMapName2);
5273
5436
  const recordId2 = constraintMap2.get(uniqueKeyString);
5274
5437
  if (recordId2) {
@@ -5276,7 +5439,7 @@ var init_BaseModel = __esm({
5276
5439
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
5277
5440
  if (documentYMap) {
5278
5441
  const recordYMap2 = documentYMap.get(recordId2);
5279
- if (recordYMap2 && recordYMap2 instanceof Y3.Map) {
5442
+ if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
5280
5443
  const instance = new modelConstructor({
5281
5444
  id: recordId2
5282
5445
  });
@@ -5290,6 +5453,16 @@ var init_BaseModel = __esm({
5290
5453
  }
5291
5454
  }
5292
5455
  }
5456
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5457
+ constraint,
5458
+ keyValues
5459
+ );
5460
+ if (fallbackRecord) {
5461
+ Logger.verbose(
5462
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5463
+ );
5464
+ return fallbackRecord;
5465
+ }
5293
5466
  Logger.verbose(
5294
5467
  `[${currentModelName}.findByUnique] Record not found in any connected document`
5295
5468
  );
@@ -5303,17 +5476,30 @@ var init_BaseModel = __esm({
5303
5476
  `[${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.`
5304
5477
  );
5305
5478
  }
5306
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5479
+ const constraintMapName = uniqueIndexMapName(
5480
+ currentModelName,
5481
+ constraint.name
5482
+ );
5307
5483
  const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
5308
5484
  const recordId = constraintMap.get(uniqueKeyString);
5309
5485
  if (!recordId) {
5486
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5487
+ constraint,
5488
+ keyValues
5489
+ );
5490
+ if (fallbackRecord) {
5491
+ Logger.verbose(
5492
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5493
+ );
5494
+ return fallbackRecord;
5495
+ }
5310
5496
  return null;
5311
5497
  }
5312
5498
  const recordYMap = legacyDocumentMap.get(recordId);
5313
5499
  if (!recordYMap) {
5314
5500
  return null;
5315
5501
  }
5316
- if (recordYMap instanceof Y3.Map) {
5502
+ if (recordYMap instanceof Y5.Map) {
5317
5503
  const instance = new modelConstructor({
5318
5504
  id: recordId
5319
5505
  });
@@ -5371,7 +5557,10 @@ var init_BaseModel = __esm({
5371
5557
  `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
5372
5558
  );
5373
5559
  }
5374
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5560
+ const constraintMapName = uniqueIndexMapName(
5561
+ currentModelName,
5562
+ constraint.name
5563
+ );
5375
5564
  let existingRecord = null;
5376
5565
  if (connectedDocuments && connectedDocuments.size > 0) {
5377
5566
  for (const docId of connectedDocuments.keys()) {
@@ -5389,6 +5578,33 @@ var init_BaseModel = __esm({
5389
5578
  }
5390
5579
  } else {
5391
5580
  }
5581
+ if (!existingRecord) {
5582
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5583
+ constraint,
5584
+ keyValuesForLookup
5585
+ );
5586
+ if (fallbackRecord) {
5587
+ existingRecord = fallbackRecord;
5588
+ const healDocId = fallbackRecord._metaDocId;
5589
+ const healDocInfo = healDocId ? connectedDocuments.get(healDocId) : void 0;
5590
+ const canHeal = !!healDocInfo && healDocInfo.permissionHint !== "read";
5591
+ if (healDocInfo && canHeal) {
5592
+ healDocInfo.yDoc.transact(() => {
5593
+ const healMap = healDocInfo.yDoc.getMap(constraintMapName);
5594
+ if (healMap.get(uniqueKeyString) !== fallbackRecord.id) {
5595
+ healMap.set(uniqueKeyString, fallbackRecord.id);
5596
+ }
5597
+ }, `heal-unique-index-${currentModelName}-${healDocId}`);
5598
+ Logger.verbose(
5599
+ `[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; healed missing index entry (legacy index-less record).`
5600
+ );
5601
+ } else {
5602
+ Logger.verbose(
5603
+ `[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; skipped index heal (owning document is read-only or not connected).`
5604
+ );
5605
+ }
5606
+ }
5607
+ }
5392
5608
  if (options?.objectMustExist) {
5393
5609
  if (!existingRecord) {
5394
5610
  throw new RecordNotFoundError(
@@ -5545,7 +5761,7 @@ var init_BaseModel = __esm({
5545
5761
  for (const [fieldKey, fieldOptions] of schema.fields) {
5546
5762
  if (fieldOptions?.type !== "stringset") continue;
5547
5763
  const value = recordYMap.get(fieldKey);
5548
- if (value instanceof Y3.Map) {
5764
+ if (value instanceof Y5.Map) {
5549
5765
  this.observeStringSetMapOnce(value);
5550
5766
  }
5551
5767
  }
@@ -5812,7 +6028,144 @@ var import_sql4 = __toESM(require("sql.js"), 1);
5812
6028
  init_BaseModel();
5813
6029
  var import_async_mutex = require("async-mutex");
5814
6030
  init_sql();
6031
+
6032
+ // src/utils/environment.ts
6033
+ function detectEnvironment() {
6034
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
6035
+ return "node";
6036
+ }
6037
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
6038
+ return "browser";
6039
+ }
6040
+ if (typeof self !== "undefined" && typeof self.importScripts === "function") {
6041
+ return "browser";
6042
+ }
6043
+ return "unknown";
6044
+ }
6045
+ function isNode() {
6046
+ return detectEnvironment() === "node";
6047
+ }
6048
+ function isBrowser() {
6049
+ return detectEnvironment() === "browser";
6050
+ }
6051
+ async function isNodeModuleAvailable(moduleName) {
6052
+ if (!isNode()) {
6053
+ return false;
6054
+ }
6055
+ try {
6056
+ await import(
6057
+ /* @vite-ignore */
6058
+ moduleName
6059
+ );
6060
+ return true;
6061
+ } catch {
6062
+ return false;
6063
+ }
6064
+ }
6065
+ var features = {
6066
+ get hasWebWorkers() {
6067
+ return isBrowser() && typeof Worker !== "undefined";
6068
+ },
6069
+ get hasFileSystem() {
6070
+ return isNode();
6071
+ },
6072
+ get hasWebAssembly() {
6073
+ return typeof WebAssembly !== "undefined";
6074
+ },
6075
+ async hasBetterSqlite3() {
6076
+ return await isNodeModuleAvailable("better-sqlite3");
6077
+ }
6078
+ };
6079
+
6080
+ // src/engines/SqljsEngine.ts
6081
+ var import_meta = {};
5815
6082
  var SQL_WASM_URL = "/sql-wasm.wasm";
6083
+ var SQL_WASM_FILENAME = "sql-wasm.wasm";
6084
+ async function resolveNodeWasmPath() {
6085
+ if (!isNode()) {
6086
+ return null;
6087
+ }
6088
+ try {
6089
+ const cjsRequire = typeof require !== "undefined" ? require : void 0;
6090
+ if (cjsRequire) {
6091
+ return cjsRequire.resolve("sql.js/dist/sql-wasm.wasm");
6092
+ }
6093
+ } catch {
6094
+ }
6095
+ try {
6096
+ const metaUrl = import_meta.url;
6097
+ if (metaUrl) {
6098
+ const { createRequire } = await import(
6099
+ /* @vite-ignore */
6100
+ "module"
6101
+ );
6102
+ const req = createRequire(metaUrl);
6103
+ return req.resolve("sql.js/dist/sql-wasm.wasm");
6104
+ }
6105
+ } catch {
6106
+ }
6107
+ return null;
6108
+ }
6109
+ async function preflightNodeWasm(wasmPath) {
6110
+ if (!isNode()) {
6111
+ return;
6112
+ }
6113
+ if (/^[a-z]+:\/\//i.test(wasmPath)) {
6114
+ return;
6115
+ }
6116
+ const { existsSync } = await import(
6117
+ /* @vite-ignore */
6118
+ "fs"
6119
+ );
6120
+ if (!existsSync(wasmPath)) {
6121
+ throw new Error(
6122
+ `[SqljsEngine] sql.js wasm not found at "${wasmPath}". Pass \`locateFile\`/\`wasmURL\` pointing at sql-wasm.wasm, or install \`better-sqlite3\` and use the \`node-sqlite\` engine (see getRecommendedNodeEngine()).`
6123
+ );
6124
+ }
6125
+ }
6126
+ async function loadSqlJsModule(locateFile) {
6127
+ let abortReject;
6128
+ const toError = (reason) => reason instanceof Error ? reason : new Error(
6129
+ `[SqljsEngine] sql.js aborted during initialization: ${String(
6130
+ reason
6131
+ )}`
6132
+ );
6133
+ const nodeProcess = isNode() && typeof process !== "undefined" ? process : void 0;
6134
+ const onUnhandled = (reason) => {
6135
+ const err = toError(reason);
6136
+ if (/Aborted/.test(err.message) || /Aborted/.test(String(reason))) {
6137
+ abortReject?.(err);
6138
+ return;
6139
+ }
6140
+ if (nodeProcess && nodeProcess.listenerCount("unhandledRejection") === 1) {
6141
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6142
+ void Promise.reject(reason);
6143
+ }
6144
+ };
6145
+ if (nodeProcess) {
6146
+ nodeProcess.on("unhandledRejection", onUnhandled);
6147
+ }
6148
+ const abortGuard = new Promise((_resolve, reject) => {
6149
+ abortReject = reject;
6150
+ });
6151
+ abortGuard.catch(() => {
6152
+ });
6153
+ try {
6154
+ const initPromise = (0, import_sql4.default)({
6155
+ locateFile,
6156
+ // Layer 1: emscripten onAbort.
6157
+ onAbort: (reason) => abortReject?.(toError(reason))
6158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6159
+ });
6160
+ return await Promise.race([initPromise, abortGuard]);
6161
+ } finally {
6162
+ if (nodeProcess) {
6163
+ setTimeout(() => {
6164
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6165
+ }, 0);
6166
+ }
6167
+ }
6168
+ }
5816
6169
  var TransactionalSQLJSOperations = class {
5817
6170
  db;
5818
6171
  engine;
@@ -5878,22 +6231,37 @@ var SqljsEngine = class _SqljsEngine {
5878
6231
  }
5879
6232
  async initialize(engineOptions) {
5880
6233
  Logger.info("[SqljsEngine] Initializing SQL.js engine...");
6234
+ const loadingModuleThisCall = !_SqljsEngine.SQL;
5881
6235
  try {
5882
6236
  if (!_SqljsEngine.SQL) {
5883
- const locateFileConfig = engineOptions?.locateFile || ((_file) => engineOptions?.wasmURL || SQL_WASM_URL);
6237
+ let locateFileConfig;
6238
+ if (engineOptions?.locateFile) {
6239
+ locateFileConfig = engineOptions.locateFile;
6240
+ } else if (engineOptions?.wasmURL) {
6241
+ const url = engineOptions.wasmURL;
6242
+ locateFileConfig = (_file) => url;
6243
+ } else {
6244
+ const nodeWasmPath = await resolveNodeWasmPath();
6245
+ const resolved = nodeWasmPath ?? SQL_WASM_URL;
6246
+ locateFileConfig = (_file) => resolved;
6247
+ }
6248
+ const wasmPath = locateFileConfig(SQL_WASM_FILENAME);
5884
6249
  Logger.debug(
5885
6250
  "[SqljsEngine] Attempting to load SQL.js WASM from:",
5886
- locateFileConfig("sql-wasm.wasm")
6251
+ wasmPath
5887
6252
  );
5888
- _SqljsEngine.SQL = await (0, import_sql4.default)({
5889
- locateFile: locateFileConfig
5890
- });
6253
+ await preflightNodeWasm(wasmPath);
6254
+ const SQL = await loadSqlJsModule(locateFileConfig);
6255
+ _SqljsEngine.SQL = SQL;
5891
6256
  Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
5892
6257
  }
5893
6258
  this.db = new _SqljsEngine.SQL.Database();
5894
6259
  Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
5895
6260
  this.updateTableNames();
5896
6261
  } catch (error) {
6262
+ if (loadingModuleThisCall) {
6263
+ _SqljsEngine.SQL = null;
6264
+ }
5897
6265
  Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
5898
6266
  throw error;
5899
6267
  }
@@ -6179,54 +6547,6 @@ var SqljsEngine = class _SqljsEngine {
6179
6547
  }
6180
6548
  };
6181
6549
 
6182
- // src/utils/environment.ts
6183
- function detectEnvironment() {
6184
- if (typeof process !== "undefined" && process.versions && process.versions.node) {
6185
- return "node";
6186
- }
6187
- if (typeof window !== "undefined" && typeof document !== "undefined") {
6188
- return "browser";
6189
- }
6190
- if (typeof self !== "undefined" && typeof self.importScripts === "function") {
6191
- return "browser";
6192
- }
6193
- return "unknown";
6194
- }
6195
- function isNode() {
6196
- return detectEnvironment() === "node";
6197
- }
6198
- function isBrowser() {
6199
- return detectEnvironment() === "browser";
6200
- }
6201
- async function isNodeModuleAvailable(moduleName) {
6202
- if (!isNode()) {
6203
- return false;
6204
- }
6205
- try {
6206
- await import(
6207
- /* @vite-ignore */
6208
- moduleName
6209
- );
6210
- return true;
6211
- } catch {
6212
- return false;
6213
- }
6214
- }
6215
- var features = {
6216
- get hasWebWorkers() {
6217
- return isBrowser() && typeof Worker !== "undefined";
6218
- },
6219
- get hasFileSystem() {
6220
- return isNode();
6221
- },
6222
- get hasWebAssembly() {
6223
- return typeof WebAssembly !== "undefined";
6224
- },
6225
- async hasBetterSqlite3() {
6226
- return await isNodeModuleAvailable("better-sqlite3");
6227
- }
6228
- };
6229
-
6230
6550
  // src/engines/BrowserDatabaseFactory.ts
6231
6551
  init_BaseModel();
6232
6552
  var BrowserDatabaseFactory = class {
@@ -6537,6 +6857,7 @@ init_StringSet();
6537
6857
  // src/models/schema.ts
6538
6858
  init_BaseModel();
6539
6859
  init_ModelRegistry();
6860
+ init_uniqueIndex();
6540
6861
  function defineModelSchema(input) {
6541
6862
  const { name, fields } = input;
6542
6863
  const options = {
@@ -6609,7 +6930,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
6609
6930
  for (const [fieldName, options] of fields.entries()) {
6610
6931
  if (options.unique) {
6611
6932
  resolved.push({
6612
- name: `${modelName}_${fieldName}_unique`,
6933
+ name: fieldUniqueConstraintName(modelName, fieldName),
6613
6934
  fields: [fieldName]
6614
6935
  });
6615
6936
  }
@@ -6673,7 +6994,7 @@ function attachAndRegisterModel(modelClass, schema) {
6673
6994
  }
6674
6995
 
6675
6996
  // src/utils/yDocSchema.ts
6676
- var Y4 = __toESM(require("yjs"), 1);
6997
+ var Y6 = __toESM(require("yjs"), 1);
6677
6998
  init_yjsValues();
6678
6999
  function discoverSchema(yDoc) {
6679
7000
  const models = {};
@@ -6708,7 +7029,7 @@ function discoverModelNames(yDoc) {
6708
7029
  function materializeMap(yDoc, key) {
6709
7030
  try {
6710
7031
  const map = yDoc.getMap(key);
6711
- return map instanceof Y4.Map ? map : null;
7032
+ return map instanceof Y6.Map ? map : null;
6712
7033
  } catch {
6713
7034
  return null;
6714
7035
  }
@@ -6718,11 +7039,11 @@ function readModelMeta(metaMap) {
6718
7039
  let constraints;
6719
7040
  let relationships;
6720
7041
  for (const [key, value] of metaMap.entries()) {
6721
- if (key === "_constraints" && value instanceof Y4.Map) {
7042
+ if (key === "_constraints" && value instanceof Y6.Map) {
6722
7043
  constraints = readConstraints(value);
6723
- } else if (key === "_relationships" && value instanceof Y4.Map) {
7044
+ } else if (key === "_relationships" && value instanceof Y6.Map) {
6724
7045
  relationships = readRelationships(value);
6725
- } else if (value instanceof Y4.Map) {
7046
+ } else if (value instanceof Y6.Map) {
6726
7047
  fields[key] = readFieldMeta(value);
6727
7048
  }
6728
7049
  }
@@ -6757,7 +7078,7 @@ function inferModelFromData(dataMap) {
6757
7078
  const fields = {};
6758
7079
  let sampled = 0;
6759
7080
  for (const [_recordId, recordValue] of dataMap.entries()) {
6760
- if (!(recordValue instanceof Y4.Map)) continue;
7081
+ if (!(recordValue instanceof Y6.Map)) continue;
6761
7082
  if (++sampled > 5) break;
6762
7083
  for (const [fieldName, value] of recordValue.entries()) {
6763
7084
  if (fieldName.startsWith("_")) continue;
@@ -6785,7 +7106,7 @@ function inferTypeFromValue(value) {
6785
7106
  function readConstraints(constraintsMap) {
6786
7107
  const out = {};
6787
7108
  for (const [name, value] of constraintsMap.entries()) {
6788
- if (!(value instanceof Y4.Map)) continue;
7109
+ if (!(value instanceof Y6.Map)) continue;
6789
7110
  let fields = [];
6790
7111
  const rawFields = value.get("fields");
6791
7112
  if (typeof rawFields === "string") {
@@ -6804,7 +7125,7 @@ function readConstraints(constraintsMap) {
6804
7125
  function readRelationships(relsMap) {
6805
7126
  const out = {};
6806
7127
  for (const [name, value] of relsMap.entries()) {
6807
- if (!(value instanceof Y4.Map)) continue;
7128
+ if (!(value instanceof Y6.Map)) continue;
6808
7129
  const rel = {};
6809
7130
  for (const [k, v] of value.entries()) {
6810
7131
  rel[k] = v;