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.js CHANGED
@@ -1,5 +1,11 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
4
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
5
+ }) : x)(function(x) {
6
+ if (typeof require !== "undefined") return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
3
9
  var __esm = (fn, res) => function __init() {
4
10
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
11
  };
@@ -344,21 +350,36 @@ var init_CursorManager = __esm({
344
350
  return fields;
345
351
  }
346
352
  /**
347
- * Build ORDER BY clause from sort specification
353
+ * Build ORDER BY clause from sort specification.
354
+ *
355
+ * `direction` is the pagination direction (1 = forward, -1 = backward).
356
+ * For backward pagination each field's effective ORDER BY direction is
357
+ * reversed so the cursor's flipped inequality (built by
358
+ * `buildPaginationConditions`) returns the page adjacent to the cursor,
359
+ * not the globally smallest/largest window. Without this,
360
+ * `field < cursor ... ORDER BY field ASC LIMIT n` returns the n smallest
361
+ * rows overall instead of the n rows just below the cursor.
362
+ *
363
+ * The returned `directions` array carries the *declared* per-field sort
364
+ * directions (unaffected by `direction`) — `buildPaginationConditions`
365
+ * needs the declared directions to pick each field's comparison operator.
348
366
  */
349
- static buildOrderClause(sort, fieldFormatter = (field) => field) {
367
+ static buildOrderClause(sort, fieldFormatter = (field) => field, direction = 1) {
350
368
  const fields = this.extractSortFields(sort);
351
369
  const clauses = [];
352
370
  const directions = [];
371
+ const sqlDir = (declared) => {
372
+ const effective = direction === -1 ? declared === 1 ? -1 : 1 : declared;
373
+ return effective === 1 ? "ASC" : "DESC";
374
+ };
353
375
  if (sort && Object.keys(sort).length > 0) {
354
376
  for (const [field, dir] of Object.entries(sort)) {
355
- const sqlDirection = dir === 1 ? "ASC" : "DESC";
356
- clauses.push(`${fieldFormatter(field)} ${sqlDirection}`);
377
+ clauses.push(`${fieldFormatter(field)} ${sqlDir(dir)}`);
357
378
  directions.push(dir);
358
379
  }
359
380
  }
360
381
  if (!sort || !sort.hasOwnProperty("id")) {
361
- clauses.push(`${fieldFormatter("id")} ASC`);
382
+ clauses.push(`${fieldFormatter("id")} ${sqlDir(1)}`);
362
383
  directions.push(1);
363
384
  } else if (sort && sort.hasOwnProperty("id")) {
364
385
  directions.push(sort["id"]);
@@ -381,18 +402,27 @@ var init_CursorManager = __esm({
381
402
  /**
382
403
  * Generate next and previous cursors from result set
383
404
  */
384
- static generateResultCursors(results, sortFields, _requestedDirection, hasMore, isFirstPage = false) {
405
+ static generateResultCursors(results, sortFields, requestedDirection, hasMore, isFirstPage = false) {
385
406
  if (results.length === 0) {
386
407
  return {};
387
408
  }
388
409
  const cursors = {};
389
410
  if (hasMore && results.length > 0) {
390
411
  const lastResult = results[results.length - 1];
391
- cursors.nextCursor = this.generateCursor(lastResult, sortFields, 1);
412
+ cursors.nextCursor = this.generateCursor(
413
+ lastResult,
414
+ sortFields,
415
+ requestedDirection
416
+ );
392
417
  }
393
418
  if (results.length > 0 && !isFirstPage) {
394
419
  const firstResult = results[0];
395
- cursors.prevCursor = this.generateCursor(firstResult, sortFields, -1);
420
+ const opposite = requestedDirection === 1 ? -1 : 1;
421
+ cursors.prevCursor = this.generateCursor(
422
+ firstResult,
423
+ sortFields,
424
+ opposite
425
+ );
396
426
  }
397
427
  return cursors;
398
428
  }
@@ -525,7 +555,8 @@ var init_DocumentQueryTranslator = __esm({
525
555
  const whereClause = this.translateFilter(filter);
526
556
  const orderClause = CursorManager.buildOrderClause(
527
557
  options?.sort,
528
- (field) => this.getQuotedField(field)
558
+ (field) => this.getQuotedField(field),
559
+ options?.direction || 1
529
560
  );
530
561
  const limitClause = this.buildLimitClause(options);
531
562
  const paginationClause = this.buildPaginationClause(
@@ -1197,8 +1228,71 @@ var init_yjsValues = __esm({
1197
1228
  }
1198
1229
  });
1199
1230
 
1200
- // src/models/metaSync.ts
1231
+ // src/models/writeLifecycle.ts
1201
1232
  import * as Y2 from "yjs";
1233
+ import { ulid } from "ulid";
1234
+ function ensureStringSetYMap(rm, fieldName) {
1235
+ const nested = rm.get(fieldName);
1236
+ if (nested instanceof Y2.Map) return nested;
1237
+ const fresh = new Y2.Map();
1238
+ if (nested && typeof nested === "object") {
1239
+ for (const [member, marker] of Object.entries(
1240
+ nested
1241
+ )) {
1242
+ if (Boolean(marker)) fresh.set(member, true);
1243
+ }
1244
+ }
1245
+ rm.set(fieldName, fresh);
1246
+ return fresh;
1247
+ }
1248
+ function applyStringSetDelta(target, delta) {
1249
+ if (delta.clear) {
1250
+ for (const member of Array.from(target.keys())) target.delete(member);
1251
+ }
1252
+ if (delta.remove) {
1253
+ for (const member of delta.remove) target.delete(String(member));
1254
+ }
1255
+ if (delta.add) {
1256
+ for (const member of delta.add) {
1257
+ const key = String(member);
1258
+ if (target.get(key) !== true) target.set(key, true);
1259
+ }
1260
+ }
1261
+ }
1262
+ function applyStringSetChange(recordYMap, fieldName, change) {
1263
+ const target = ensureStringSetYMap(recordYMap, fieldName);
1264
+ applyStringSetDelta(target, {
1265
+ add: change.additions,
1266
+ remove: change.removals
1267
+ });
1268
+ }
1269
+ function registerFunctionDefaultGenerator(name, fn) {
1270
+ FUNCTION_DEFAULT_GENERATORS.set(name, fn);
1271
+ }
1272
+ function resolveLiteralOrFunctionDefault(rawDefault) {
1273
+ return typeof rawDefault === "function" ? rawDefault() : rawDefault;
1274
+ }
1275
+ var KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
1276
+ var init_writeLifecycle = __esm({
1277
+ "src/models/writeLifecycle.ts"() {
1278
+ "use strict";
1279
+ KNOWN_FIELD_TYPE_MAP = {
1280
+ string: true,
1281
+ number: true,
1282
+ boolean: true,
1283
+ date: true,
1284
+ id: true,
1285
+ stringset: true
1286
+ };
1287
+ KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
1288
+ FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
1289
+ registerFunctionDefaultGenerator("generate_ulid", () => ulid());
1290
+ UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
1291
+ }
1292
+ });
1293
+
1294
+ // src/models/metaSync.ts
1295
+ import * as Y3 from "yjs";
1202
1296
  function inferFieldType(value) {
1203
1297
  if (isStringSetYMap(value)) return "stringset";
1204
1298
  switch (typeof value) {
@@ -1219,7 +1313,10 @@ function encodeDefault(value) {
1219
1313
  if (value === void 0 || value === null) return void 0;
1220
1314
  if (typeof value === "function") {
1221
1315
  const name = KNOWN_FUNCTION_DEFAULTS.get(value);
1222
- return name ? `$${name}` : "$unknown_function";
1316
+ return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
1317
+ }
1318
+ if (typeof value === "string" && value.startsWith("$")) {
1319
+ return `$${value}`;
1223
1320
  }
1224
1321
  return value;
1225
1322
  }
@@ -1245,7 +1342,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1245
1342
  if (compoundConstraints.length > 0) {
1246
1343
  let constraints = meta.get("_constraints");
1247
1344
  if (!constraints) {
1248
- constraints = new Y2.Map();
1345
+ constraints = new Y3.Map();
1249
1346
  meta.set("_constraints", constraints);
1250
1347
  }
1251
1348
  for (const constraint of compoundConstraints) {
@@ -1256,7 +1353,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1256
1353
  if (relationships && Object.keys(relationships).length > 0) {
1257
1354
  let rels = meta.get("_relationships");
1258
1355
  if (!rels) {
1259
- rels = new Y2.Map();
1356
+ rels = new Y3.Map();
1260
1357
  meta.set("_relationships", rels);
1261
1358
  }
1262
1359
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -1268,7 +1365,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1268
1365
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1269
1366
  let fieldMeta = metaMap.get(fieldName);
1270
1367
  if (!fieldMeta) {
1271
- fieldMeta = new Y2.Map();
1368
+ fieldMeta = new Y3.Map();
1272
1369
  metaMap.set(fieldName, fieldMeta);
1273
1370
  }
1274
1371
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -1285,7 +1382,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1285
1382
  function syncConstraintMeta(constraintsMap, constraint) {
1286
1383
  let cMeta = constraintsMap.get(constraint.name);
1287
1384
  if (!cMeta) {
1288
- cMeta = new Y2.Map();
1385
+ cMeta = new Y3.Map();
1289
1386
  constraintsMap.set(constraint.name, cMeta);
1290
1387
  }
1291
1388
  setIfChanged(cMeta, "type", "unique");
@@ -1295,7 +1392,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
1295
1392
  function syncRelationshipMeta(relsMap, relName, relConfig) {
1296
1393
  let relMeta = relsMap.get(relName);
1297
1394
  if (!relMeta) {
1298
- relMeta = new Y2.Map();
1395
+ relMeta = new Y3.Map();
1299
1396
  relsMap.set(relName, relMeta);
1300
1397
  }
1301
1398
  for (const [key, value] of Object.entries(relConfig)) {
@@ -1310,7 +1407,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
1310
1407
  if (fieldName.startsWith("_")) continue;
1311
1408
  let fieldMeta = meta.get(fieldName);
1312
1409
  if (!fieldMeta) {
1313
- fieldMeta = new Y2.Map();
1410
+ fieldMeta = new Y3.Map();
1314
1411
  meta.set(fieldName, fieldMeta);
1315
1412
  }
1316
1413
  if (!fieldMeta.has("type")) {
@@ -1331,12 +1428,51 @@ var init_metaSync = __esm({
1331
1428
  "src/models/metaSync.ts"() {
1332
1429
  "use strict";
1333
1430
  init_yjsValues();
1431
+ init_writeLifecycle();
1334
1432
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
1335
1433
  _syncedCache = /* @__PURE__ */ new WeakMap();
1336
1434
  }
1337
1435
  });
1338
1436
 
1437
+ // src/utils/uniqueIndex.ts
1438
+ import * as Y4 from "yjs";
1439
+ function buildUniqueKey(recordData, fields) {
1440
+ const keyParts = [];
1441
+ for (const field of fields) {
1442
+ const value = recordData instanceof Y4.Map ? recordData.get(field) : recordData[field];
1443
+ if (value === null || value === void 0) {
1444
+ return null;
1445
+ }
1446
+ keyParts.push(String(value));
1447
+ }
1448
+ return keyParts.join("|");
1449
+ }
1450
+ function uniqueIndexMapName(modelName, constraintName) {
1451
+ return `_uniqueIdx_${modelName}_${constraintName}`;
1452
+ }
1453
+ function fieldUniqueConstraintName(modelName, fieldName) {
1454
+ return `${modelName}_${fieldName}_unique`;
1455
+ }
1456
+ var init_uniqueIndex = __esm({
1457
+ "src/utils/uniqueIndex.ts"() {
1458
+ "use strict";
1459
+ }
1460
+ });
1461
+
1339
1462
  // src/models/relationshipManager.ts
1463
+ function warnIfOrderFieldNullable(modelClass, orderByField, relationshipDescription) {
1464
+ if (ALWAYS_PRESENT_ORDER_FIELDS.has(orderByField)) return;
1465
+ const schema = modelClass.getSchema?.();
1466
+ const fields = schema?.fields;
1467
+ if (!fields || typeof fields.get !== "function") return;
1468
+ const fieldOptions = fields.get(orderByField);
1469
+ if (!fieldOptions) return;
1470
+ if (fieldOptions.required !== true) {
1471
+ Logger.warn(
1472
+ `[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.)`
1473
+ );
1474
+ }
1475
+ }
1340
1476
  function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1341
1477
  return async function() {
1342
1478
  const relatedId = this[config.relatedIdField];
@@ -1345,6 +1481,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1345
1481
  };
1346
1482
  }
1347
1483
  function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbEngine) {
1484
+ const {
1485
+ relatedIdField,
1486
+ orderByField = "id",
1487
+ orderDirection = "ASC"
1488
+ } = config;
1489
+ warnIfOrderFieldNullable(
1490
+ targetModelClass,
1491
+ orderByField,
1492
+ `hasMany(${targetModelClass.name})`
1493
+ );
1348
1494
  return async function(paginationArgs) {
1349
1495
  const {
1350
1496
  limit,
@@ -1352,71 +1498,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
1352
1498
  beforeCursor,
1353
1499
  direction = "forward"
1354
1500
  } = paginationArgs || {};
1355
- const {
1356
- relatedIdField,
1357
- orderByField = "id",
1358
- orderDirection = "ASC"
1359
- } = config;
1360
1501
  const targetSchema = targetModelClass.getSchema();
1361
1502
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1362
1503
  Logger.error(
1363
1504
  `[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
1364
1505
  );
1365
- return { data: [], nextCursor: null, prevCursor: null };
1506
+ return { data: [], hasMore: false };
1366
1507
  }
1367
- const filter = { [relatedIdField]: this.id };
1508
+ const forward = direction !== "backward";
1509
+ const cursor = forward ? afterCursor : beforeCursor;
1368
1510
  const queryOptions = {
1369
- sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
1511
+ sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
1512
+ direction: forward ? 1 : -1
1370
1513
  };
1371
- if (limit !== void 0) {
1372
- queryOptions.limit = limit + 1;
1373
- }
1374
- if (direction === "forward" && afterCursor) {
1375
- filter[orderByField] = {
1376
- [orderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
1377
- };
1378
- } else if (direction === "backward") {
1379
- if (beforeCursor) {
1380
- filter[orderByField] = {
1381
- [orderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
1382
- };
1383
- }
1384
- queryOptions.sort = { [orderByField]: orderDirection === "ASC" ? -1 : 1 };
1385
- }
1386
- const result = await targetModelClass.query(filter, queryOptions);
1387
- const results = result.data;
1388
- let nextCursor = null;
1389
- let prevCursor = null;
1390
- const isFirstPage = direction === "forward" && !afterCursor;
1391
- if (limit !== void 0) {
1392
- if (direction === "forward") {
1393
- if (results.length > limit) {
1394
- nextCursor = results[limit - 1][orderByField];
1395
- results.pop();
1396
- }
1397
- if (results.length > 0 && !isFirstPage) {
1398
- prevCursor = results[0][orderByField];
1399
- }
1400
- } else {
1401
- if (results.length > limit) {
1402
- prevCursor = results[limit - 1][orderByField];
1403
- results.pop();
1404
- }
1405
- results.reverse();
1406
- if (results.length > 0 && beforeCursor) {
1407
- nextCursor = results[results.length - 1][orderByField];
1408
- }
1409
- if (results.length > 0 && !beforeCursor) {
1410
- if (results.length === limit) {
1411
- prevCursor = results[0][orderByField];
1412
- }
1413
- }
1414
- }
1415
- }
1416
- return { data: results, nextCursor, prevCursor };
1514
+ if (limit !== void 0) queryOptions.limit = limit;
1515
+ if (cursor != null) queryOptions.uniqueStartKey = cursor;
1516
+ return targetModelClass.query(
1517
+ { [relatedIdField]: this.id },
1518
+ queryOptions
1519
+ );
1417
1520
  };
1418
1521
  }
1419
1522
  function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClass, joinModelClass, _dbEngine) {
1523
+ const {
1524
+ joinModelLocalField,
1525
+ joinModelRelatedField,
1526
+ joinModelOrderByField = "id",
1527
+ joinModelOrderDirection = "ASC"
1528
+ } = config;
1529
+ warnIfOrderFieldNullable(
1530
+ joinModelClass,
1531
+ joinModelOrderByField,
1532
+ `hasManyThrough(${targetModelClass.name} via ${joinModelClass.name})`
1533
+ );
1420
1534
  return async function(paginationArgs) {
1421
1535
  const {
1422
1536
  limit,
@@ -1430,87 +1544,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
1430
1544
  Logger.error(
1431
1545
  `[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
1432
1546
  );
1433
- return { data: [], nextCursor: null, prevCursor: null };
1547
+ return { data: [], hasMore: false };
1434
1548
  }
1435
1549
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1436
1550
  Logger.error(
1437
1551
  `[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
1438
1552
  );
1439
- return { data: [], nextCursor: null, prevCursor: null };
1553
+ return { data: [], hasMore: false };
1440
1554
  }
1441
- const {
1442
- joinModelLocalField,
1443
- joinModelRelatedField,
1444
- joinModelOrderByField = "id",
1445
- joinModelOrderDirection = "ASC"
1446
- } = config;
1447
- const joinFilter = { [joinModelLocalField]: this.id };
1555
+ const forward = direction !== "backward";
1556
+ const cursor = forward ? afterCursor : beforeCursor;
1448
1557
  const joinQueryOptions = {
1449
1558
  sort: {
1450
1559
  [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
1451
1560
  },
1452
- projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
1561
+ projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
1562
+ direction: forward ? 1 : -1
1453
1563
  };
1454
- if (limit !== void 0) {
1455
- joinQueryOptions.limit = limit + 1;
1456
- }
1457
- if (direction === "forward" && afterCursor) {
1458
- joinFilter[joinModelOrderByField] = {
1459
- [joinModelOrderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
1460
- };
1461
- } else if (direction === "backward") {
1462
- if (beforeCursor) {
1463
- joinFilter[joinModelOrderByField] = {
1464
- [joinModelOrderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
1465
- };
1466
- }
1467
- joinQueryOptions.sort = {
1468
- [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? -1 : 1
1469
- };
1470
- }
1564
+ if (limit !== void 0) joinQueryOptions.limit = limit;
1565
+ if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
1471
1566
  const joinResult = await joinModelClass.query(
1472
- joinFilter,
1567
+ { [joinModelLocalField]: this.id },
1473
1568
  joinQueryOptions
1474
1569
  );
1475
- const joinResults = joinResult.data;
1476
- let nextCursorFromJoin = null;
1477
- let prevCursorFromJoin = null;
1478
- const isFirstPage = direction === "forward" && !afterCursor;
1479
- if (limit !== void 0) {
1480
- if (direction === "forward") {
1481
- if (joinResults.length > limit) {
1482
- nextCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
1483
- joinResults.pop();
1484
- }
1485
- if (joinResults.length > 0 && !isFirstPage) {
1486
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
1487
- }
1488
- } else {
1489
- if (joinResults.length > limit) {
1490
- prevCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
1491
- joinResults.pop();
1492
- }
1493
- joinResults.reverse();
1494
- if (joinResults.length > 0 && beforeCursor) {
1495
- nextCursorFromJoin = joinResults[joinResults.length - 1][joinModelOrderByField];
1496
- }
1497
- if (joinResults.length > 0 && !beforeCursor) {
1498
- if (joinResults.length === limit) {
1499
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
1500
- }
1501
- }
1502
- }
1570
+ const relatedIds = joinResult.data.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1571
+ let targetData = [];
1572
+ if (relatedIds.length > 0) {
1573
+ const targetResult = await targetModelClass.query({
1574
+ id: { $in: relatedIds }
1575
+ });
1576
+ targetData = targetResult.data;
1503
1577
  }
1504
- const relatedIds = joinResults.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1505
- if (relatedIds.length === 0)
1506
- return { data: [], nextCursor: null, prevCursor: null };
1507
- const targetResult = await targetModelClass.query({
1508
- id: { $in: relatedIds }
1509
- });
1510
1578
  return {
1511
- data: targetResult.data,
1512
- nextCursor: nextCursorFromJoin,
1513
- prevCursor: prevCursorFromJoin
1579
+ data: targetData,
1580
+ nextCursor: joinResult.nextCursor,
1581
+ prevCursor: joinResult.prevCursor,
1582
+ hasMore: joinResult.hasMore
1514
1583
  };
1515
1584
  };
1516
1585
  }
@@ -1634,10 +1703,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
1634
1703
  }, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
1635
1704
  };
1636
1705
  }
1706
+ var ALWAYS_PRESENT_ORDER_FIELDS;
1637
1707
  var init_relationshipManager = __esm({
1638
1708
  "src/models/relationshipManager.ts"() {
1639
1709
  "use strict";
1640
1710
  init_BaseModel();
1711
+ ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
1641
1712
  }
1642
1713
  });
1643
1714
 
@@ -2408,7 +2479,13 @@ var init_IncludeResolver = __esm({
2408
2479
  }
2409
2480
  return null;
2410
2481
  }
2411
- /** Apply projection to a single record. */
2482
+ /**
2483
+ * Apply a projection to a single record. Assumes the projection was already
2484
+ * validated upstream (mixed inclusion+exclusion rejected by the translator):
2485
+ * `isInclusion` treats "any value === 1" as inclusion mode. `id` and `type`
2486
+ * are system-identity fields and are always kept in BOTH modes (mirrors the
2487
+ * DatabaseDO `_applyProjection`); `_meta` stays excludable.
2488
+ */
2412
2489
  _applyProjection(record, projection) {
2413
2490
  const fields = Object.entries(projection);
2414
2491
  if (fields.length === 0) return record;
@@ -2427,7 +2504,7 @@ var init_IncludeResolver = __esm({
2427
2504
  } else {
2428
2505
  const result = { ...record };
2429
2506
  for (const [f, v] of fields) {
2430
- if (v === 0) delete result[f];
2507
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
2431
2508
  }
2432
2509
  return result;
2433
2510
  }
@@ -2446,10 +2523,10 @@ __export(BaseModel_exports, {
2446
2523
  UniqueConstraintViolationError: () => UniqueConstraintViolationError,
2447
2524
  generateULID: () => generateULID
2448
2525
  });
2449
- import * as Y3 from "yjs";
2450
- import { ulid } from "ulid";
2526
+ import * as Y5 from "yjs";
2527
+ import { ulid as ulid2 } from "ulid";
2451
2528
  function generateULID() {
2452
- return ulid();
2529
+ return ulid2();
2453
2530
  }
2454
2531
  var LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2455
2532
  var init_BaseModel = __esm({
@@ -2461,7 +2538,9 @@ var init_BaseModel = __esm({
2461
2538
  init_CursorManager();
2462
2539
  init_sql();
2463
2540
  init_metaSync();
2541
+ init_writeLifecycle();
2464
2542
  init_yjsValues();
2543
+ init_uniqueIndex();
2465
2544
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2466
2545
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
2467
2546
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2846,7 +2925,9 @@ var init_BaseModel = __esm({
2846
2925
  this.setValue(fieldKey, data[fieldKey]);
2847
2926
  this._constructorProvidedFields.add(fieldKey);
2848
2927
  } else if (fieldOptions.default !== void 0) {
2849
- const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
2928
+ const defaultValue = resolveLiteralOrFunctionDefault(
2929
+ fieldOptions.default
2930
+ );
2850
2931
  this.setValue(fieldKey, defaultValue);
2851
2932
  }
2852
2933
  }
@@ -3138,7 +3219,7 @@ var init_BaseModel = __esm({
3138
3219
  }
3139
3220
  getStringSetFromYjs(fieldName) {
3140
3221
  const yjsData = this.getFromYjs(fieldName);
3141
- if (yjsData instanceof Y3.Map) {
3222
+ if (yjsData instanceof Y5.Map) {
3142
3223
  return Array.from(yjsData.keys());
3143
3224
  }
3144
3225
  if (yjsData && typeof yjsData === "object") {
@@ -3246,7 +3327,7 @@ var init_BaseModel = __esm({
3246
3327
  }
3247
3328
  const extractStringSetValues = (value) => {
3248
3329
  if (!value) return [];
3249
- if (value instanceof Y3.Map) {
3330
+ if (value instanceof Y5.Map) {
3250
3331
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
3251
3332
  }
3252
3333
  if (Array.isArray(value)) {
@@ -3263,7 +3344,7 @@ var init_BaseModel = __esm({
3263
3344
  if (!recordId) continue;
3264
3345
  let itemData;
3265
3346
  const stringSetValuesByField = {};
3266
- if (recordData instanceof Y3.Map) {
3347
+ if (recordData instanceof Y5.Map) {
3267
3348
  itemData = {};
3268
3349
  const unknownFields = [];
3269
3350
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3379,20 +3460,10 @@ var init_BaseModel = __esm({
3379
3460
  Logger.verbose(
3380
3461
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
3381
3462
  );
3382
- const buildUniqueKey = (recordData, fields) => {
3383
- const keyParts = [];
3384
- for (const field of fields) {
3385
- const value = recordData instanceof Y3.Map ? recordData.get(field) : recordData[field];
3386
- if (value === null || value === void 0) {
3387
- return null;
3388
- }
3389
- keyParts.push(String(value));
3390
- }
3391
- return keyParts.join("|");
3392
- };
3463
+ const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
3393
3464
  const extractItemData = (key, recordData) => {
3394
3465
  let itemData;
3395
- if (recordData instanceof Y3.Map) {
3466
+ if (recordData instanceof Y5.Map) {
3396
3467
  itemData = {};
3397
3468
  const unknownFields = [];
3398
3469
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3476,7 +3547,7 @@ var init_BaseModel = __esm({
3476
3547
  const recordsByUniqueKey = /* @__PURE__ */ new Map();
3477
3548
  for (const [recordId, recordData] of allRecords.entries()) {
3478
3549
  if (recordIdsToDiscard.has(recordId)) continue;
3479
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3550
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3480
3551
  if (uniqueKey === null) continue;
3481
3552
  if (!recordsByUniqueKey.has(uniqueKey)) {
3482
3553
  recordsByUniqueKey.set(uniqueKey, []);
@@ -3522,7 +3593,7 @@ var init_BaseModel = __esm({
3522
3593
  if (!recordData || !key) continue;
3523
3594
  const itemData = extractItemData(key, recordData);
3524
3595
  if (!itemData) continue;
3525
- if (change.action === "add" && recordData instanceof Y3.Map) {
3596
+ if (change.action === "add" && recordData instanceof Y5.Map) {
3526
3597
  Logger.verbose(
3527
3598
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
3528
3599
  );
@@ -3620,9 +3691,12 @@ var init_BaseModel = __esm({
3620
3691
  const recordData = documentYMap.get(idToDiscard);
3621
3692
  if (!recordData) continue;
3622
3693
  for (const constraint of schema.resolvedUniqueConstraints) {
3623
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3694
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3624
3695
  if (uniqueKey === null) continue;
3625
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3696
+ const constraintMapName = uniqueIndexMapName(
3697
+ modelName,
3698
+ constraint.name
3699
+ );
3626
3700
  const constraintMap = yDoc.getMap(constraintMapName);
3627
3701
  const currentIndexValue = constraintMap.get(uniqueKey);
3628
3702
  if (currentIndexValue === idToDiscard) {
@@ -3640,11 +3714,14 @@ var init_BaseModel = __esm({
3640
3714
  }, `conflict-resolution-${modelName}-${docId}`);
3641
3715
  yDoc.transact(() => {
3642
3716
  for (const constraint of schema.resolvedUniqueConstraints) {
3643
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3717
+ const constraintMapName = uniqueIndexMapName(
3718
+ modelName,
3719
+ constraint.name
3720
+ );
3644
3721
  const constraintMap = yDoc.getMap(constraintMapName);
3645
3722
  for (const [recordId, recordData] of documentYMap.entries()) {
3646
3723
  if (!recordData) continue;
3647
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3724
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3648
3725
  if (uniqueKey === null) continue;
3649
3726
  const currentIndexValue = constraintMap.get(uniqueKey);
3650
3727
  if (currentIndexValue !== recordId) {
@@ -3667,7 +3744,7 @@ var init_BaseModel = __esm({
3667
3744
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
3668
3745
  );
3669
3746
  for (const [recordId, recordData] of documentYMap.entries()) {
3670
- if (recordData instanceof Y3.Map) {
3747
+ if (recordData instanceof Y5.Map) {
3671
3748
  Logger.verbose(
3672
3749
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
3673
3750
  );
@@ -3893,26 +3970,7 @@ var init_BaseModel = __esm({
3893
3970
  * if the field hasn't been touched since the wire-format change in #561.
3894
3971
  */
3895
3972
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3896
- let nested = recordYMap.get(fieldName);
3897
- if (!(nested instanceof Y3.Map)) {
3898
- const migrated = new Y3.Map();
3899
- if (nested && typeof nested === "object") {
3900
- for (const [member, marker] of Object.entries(
3901
- nested
3902
- )) {
3903
- if (Boolean(marker)) migrated.set(member, true);
3904
- }
3905
- }
3906
- recordYMap.set(fieldName, migrated);
3907
- nested = migrated;
3908
- }
3909
- const target = nested;
3910
- for (const member of change.additions) {
3911
- target.set(member, true);
3912
- }
3913
- for (const member of change.removals) {
3914
- target.delete(member);
3915
- }
3973
+ applyStringSetChange(recordYMap, fieldName, change);
3916
3974
  }
3917
3975
  /**
3918
3976
  * Deep equality check for comparing field values
@@ -4114,6 +4172,34 @@ var init_BaseModel = __esm({
4114
4172
  `[${modelName}.save] getCurrentJSState() returned:`,
4115
4173
  dataToSave
4116
4174
  );
4175
+ const uniqueMissConflicts = /* @__PURE__ */ new Map();
4176
+ for (const constraint of schema.resolvedUniqueConstraints) {
4177
+ const newUniqueKey = this._buildUniqueKey(
4178
+ constraint.fields,
4179
+ dataToSave,
4180
+ modelName,
4181
+ constraint.name
4182
+ );
4183
+ if (newUniqueKey === null) {
4184
+ continue;
4185
+ }
4186
+ const constraintMap = yDoc.getMap(
4187
+ uniqueIndexMapName(modelName, constraint.name)
4188
+ );
4189
+ if (!constraintMap.get(newUniqueKey)) {
4190
+ const constraintKeyValues = constraint.fields.map(
4191
+ (field) => dataToSave[field]
4192
+ );
4193
+ const conflictingRecord = await modelConstructor._resolveByUniqueFallback(
4194
+ constraint,
4195
+ constraintKeyValues,
4196
+ { excludeId: this.id }
4197
+ );
4198
+ if (conflictingRecord) {
4199
+ uniqueMissConflicts.set(constraint.name, conflictingRecord.id);
4200
+ }
4201
+ }
4202
+ }
4117
4203
  await yDoc.transact(async () => {
4118
4204
  if (upsertOnConstraint && upsertOnKey) {
4119
4205
  const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
@@ -4155,7 +4241,7 @@ var init_BaseModel = __esm({
4155
4241
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
4156
4242
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
4157
4243
  if (!recordYMap) {
4158
- recordYMap = new Y3.Map();
4244
+ recordYMap = new Y5.Map();
4159
4245
  Logger.verbose(
4160
4246
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
4161
4247
  );
@@ -4207,7 +4293,10 @@ var init_BaseModel = __esm({
4207
4293
  );
4208
4294
  continue;
4209
4295
  }
4210
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
4296
+ const constraintMapName = uniqueIndexMapName(
4297
+ modelName,
4298
+ constraint.name
4299
+ );
4211
4300
  const constraintMap = yDoc.getMap(constraintMapName);
4212
4301
  Logger.verbose(
4213
4302
  `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
@@ -4236,6 +4325,26 @@ var init_BaseModel = __esm({
4236
4325
  this.id,
4237
4326
  existingRecordIdWithNewKey
4238
4327
  );
4328
+ } else if (!existingRecordIdWithNewKey) {
4329
+ const conflictingRecordId = uniqueMissConflicts.get(constraint.name);
4330
+ if (conflictingRecordId && conflictingRecordId !== this.id) {
4331
+ Logger.verbose(
4332
+ `[${modelName}] Save Check (Item '${this.id}'): index miss for '${constraint.name}', but engine fallback found conflicting index-less record ${conflictingRecordId}.`
4333
+ );
4334
+ throw new UniqueConstraintViolationError(
4335
+ `Unique constraint '${constraint.name}' violated for model '${modelName}' on fields [${constraint.fields.join(
4336
+ ", "
4337
+ )}]. Attempted ID: ${this.id}. Value(s) starting with '${newUniqueKey.substring(
4338
+ 0,
4339
+ 50
4340
+ )}...' already exist for index-less record ID ${conflictingRecordId}.`,
4341
+ modelName,
4342
+ constraint.name,
4343
+ constraint.fields,
4344
+ this.id,
4345
+ conflictingRecordId
4346
+ );
4347
+ }
4239
4348
  }
4240
4349
  }
4241
4350
  if (isUpdate && oldData) {
@@ -4579,34 +4688,46 @@ var init_BaseModel = __esm({
4579
4688
  const modelName = schema.options.name;
4580
4689
  Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
4581
4690
  const hasIncludes = options?.include && options.include.length > 0;
4582
- const translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4691
+ const requestedLimit = options?.limit;
4692
+ const overFetch = typeof requestedLimit === "number" && requestedLimit > 0;
4693
+ let translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4694
+ if (overFetch) {
4695
+ translatorOptions = {
4696
+ ...translatorOptions || {},
4697
+ limit: requestedLimit + 1
4698
+ };
4699
+ }
4583
4700
  const translator = new DocumentQueryTranslator(modelName, schema.fields);
4584
4701
  const translatedQuery = translator.translateFind(filter, translatorOptions);
4585
4702
  Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
4586
4703
  Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
4587
- const results = await _BaseModel.dbInstance.query(
4704
+ let results = await _BaseModel.dbInstance.query(
4588
4705
  translatedQuery.sql,
4589
4706
  translatedQuery.params
4590
4707
  );
4591
4708
  Logger.verbose(`[${modelName}] Query returned ${results.length} results`);
4709
+ let hasMore = false;
4710
+ if (overFetch && results.length > requestedLimit) {
4711
+ hasMore = true;
4712
+ results = results.slice(0, requestedLimit);
4713
+ }
4592
4714
  if (options?.include && options.include.length > 0) {
4593
4715
  const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
4594
4716
  const resolver = new IncludeResolver2(_BaseModel.dbInstance);
4595
4717
  await resolver.resolve(results, options.include, 0, modelName);
4596
4718
  }
4597
- const requestedLimit = options?.limit;
4598
- const hasMore = CursorManager.hasMoreResults(
4599
- requestedLimit,
4600
- results.length
4601
- );
4602
4719
  const isFirstPage = !options?.uniqueStartKey;
4720
+ const direction = options?.direction || 1;
4603
4721
  const cursors = CursorManager.generateResultCursors(
4604
4722
  results,
4605
4723
  translatedQuery.sortFields,
4606
- options?.direction || 1,
4724
+ direction,
4607
4725
  hasMore,
4608
4726
  isFirstPage
4609
4727
  );
4728
+ if (direction === -1) {
4729
+ results.reverse();
4730
+ }
4610
4731
  let transformedData;
4611
4732
  if (options?.projection && Object.keys(options.projection).length > 0) {
4612
4733
  transformedData = results.map((row) => {
@@ -5182,7 +5303,7 @@ var init_BaseModel = __esm({
5182
5303
  );
5183
5304
  }
5184
5305
  for (const [recordId, recordYMap] of documentYMap.entries()) {
5185
- if (recordYMap instanceof Y3.Map) {
5306
+ if (recordYMap instanceof Y5.Map) {
5186
5307
  const instance = new this({ id: recordId });
5187
5308
  instance._metaDocId = docId;
5188
5309
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -5207,6 +5328,50 @@ var init_BaseModel = __esm({
5207
5328
  }
5208
5329
  return allInstances;
5209
5330
  }
5331
+ /**
5332
+ * Resolve a record by its unique-constraint field values via an engine query.
5333
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
5334
+ * entry for a key.
5335
+ *
5336
+ * The index map is a cache over the SQLite projection, not the source of
5337
+ * truth: every record is loaded into the projection on `initializeForDocument`
5338
+ * regardless of index presence, but a record written by a client predating
5339
+ * index maintenance (legacy data) is missing from the index. Such a record
5340
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
5341
+ * check. A query over the constraint fields finds it reliably.
5342
+ *
5343
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
5344
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
5345
+ * whether to heal the missing index entry (write paths) and do their own
5346
+ * logging — this method never mutates the index, so read paths stay
5347
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
5348
+ * constraint field means the record does not participate in the constraint,
5349
+ * so no query runs.
5350
+ */
5351
+ static async _resolveByUniqueFallback(constraint, keyValues, options) {
5352
+ if (!_BaseModel.dbInstance) {
5353
+ return null;
5354
+ }
5355
+ const filter = {};
5356
+ for (let i = 0; i < constraint.fields.length; i++) {
5357
+ const value = keyValues[i];
5358
+ if (value === null || value === void 0) {
5359
+ return null;
5360
+ }
5361
+ filter[constraint.fields[i]] = value;
5362
+ }
5363
+ if (options?.excludeId) {
5364
+ if ("id" in filter) {
5365
+ if (filter.id === options.excludeId) {
5366
+ return null;
5367
+ }
5368
+ } else {
5369
+ filter.id = { $ne: options.excludeId };
5370
+ }
5371
+ }
5372
+ const found = await this.queryOne(filter);
5373
+ return found ?? null;
5374
+ }
5210
5375
  static async findByUnique(constraintName, value) {
5211
5376
  const modelConstructor = this;
5212
5377
  const schema = modelConstructor.getSchema();
@@ -5245,7 +5410,10 @@ var init_BaseModel = __esm({
5245
5410
  const documentInfo = modelConstructor.connectedDocuments.get(docId);
5246
5411
  if (!documentInfo) continue;
5247
5412
  const yDoc = documentInfo.yDoc;
5248
- const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5413
+ const constraintMapName2 = uniqueIndexMapName(
5414
+ currentModelName,
5415
+ constraint.name
5416
+ );
5249
5417
  const constraintMap2 = yDoc.getMap(constraintMapName2);
5250
5418
  const recordId2 = constraintMap2.get(uniqueKeyString);
5251
5419
  if (recordId2) {
@@ -5253,7 +5421,7 @@ var init_BaseModel = __esm({
5253
5421
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
5254
5422
  if (documentYMap) {
5255
5423
  const recordYMap2 = documentYMap.get(recordId2);
5256
- if (recordYMap2 && recordYMap2 instanceof Y3.Map) {
5424
+ if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
5257
5425
  const instance = new modelConstructor({
5258
5426
  id: recordId2
5259
5427
  });
@@ -5267,6 +5435,16 @@ var init_BaseModel = __esm({
5267
5435
  }
5268
5436
  }
5269
5437
  }
5438
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5439
+ constraint,
5440
+ keyValues
5441
+ );
5442
+ if (fallbackRecord) {
5443
+ Logger.verbose(
5444
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5445
+ );
5446
+ return fallbackRecord;
5447
+ }
5270
5448
  Logger.verbose(
5271
5449
  `[${currentModelName}.findByUnique] Record not found in any connected document`
5272
5450
  );
@@ -5280,17 +5458,30 @@ var init_BaseModel = __esm({
5280
5458
  `[${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.`
5281
5459
  );
5282
5460
  }
5283
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5461
+ const constraintMapName = uniqueIndexMapName(
5462
+ currentModelName,
5463
+ constraint.name
5464
+ );
5284
5465
  const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
5285
5466
  const recordId = constraintMap.get(uniqueKeyString);
5286
5467
  if (!recordId) {
5468
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5469
+ constraint,
5470
+ keyValues
5471
+ );
5472
+ if (fallbackRecord) {
5473
+ Logger.verbose(
5474
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5475
+ );
5476
+ return fallbackRecord;
5477
+ }
5287
5478
  return null;
5288
5479
  }
5289
5480
  const recordYMap = legacyDocumentMap.get(recordId);
5290
5481
  if (!recordYMap) {
5291
5482
  return null;
5292
5483
  }
5293
- if (recordYMap instanceof Y3.Map) {
5484
+ if (recordYMap instanceof Y5.Map) {
5294
5485
  const instance = new modelConstructor({
5295
5486
  id: recordId
5296
5487
  });
@@ -5348,7 +5539,10 @@ var init_BaseModel = __esm({
5348
5539
  `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
5349
5540
  );
5350
5541
  }
5351
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5542
+ const constraintMapName = uniqueIndexMapName(
5543
+ currentModelName,
5544
+ constraint.name
5545
+ );
5352
5546
  let existingRecord = null;
5353
5547
  if (connectedDocuments && connectedDocuments.size > 0) {
5354
5548
  for (const docId of connectedDocuments.keys()) {
@@ -5366,6 +5560,33 @@ var init_BaseModel = __esm({
5366
5560
  }
5367
5561
  } else {
5368
5562
  }
5563
+ if (!existingRecord) {
5564
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5565
+ constraint,
5566
+ keyValuesForLookup
5567
+ );
5568
+ if (fallbackRecord) {
5569
+ existingRecord = fallbackRecord;
5570
+ const healDocId = fallbackRecord._metaDocId;
5571
+ const healDocInfo = healDocId ? connectedDocuments.get(healDocId) : void 0;
5572
+ const canHeal = !!healDocInfo && healDocInfo.permissionHint !== "read";
5573
+ if (healDocInfo && canHeal) {
5574
+ healDocInfo.yDoc.transact(() => {
5575
+ const healMap = healDocInfo.yDoc.getMap(constraintMapName);
5576
+ if (healMap.get(uniqueKeyString) !== fallbackRecord.id) {
5577
+ healMap.set(uniqueKeyString, fallbackRecord.id);
5578
+ }
5579
+ }, `heal-unique-index-${currentModelName}-${healDocId}`);
5580
+ Logger.verbose(
5581
+ `[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; healed missing index entry (legacy index-less record).`
5582
+ );
5583
+ } else {
5584
+ Logger.verbose(
5585
+ `[${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).`
5586
+ );
5587
+ }
5588
+ }
5589
+ }
5369
5590
  if (options?.objectMustExist) {
5370
5591
  if (!existingRecord) {
5371
5592
  throw new RecordNotFoundError(
@@ -5522,7 +5743,7 @@ var init_BaseModel = __esm({
5522
5743
  for (const [fieldKey, fieldOptions] of schema.fields) {
5523
5744
  if (fieldOptions?.type !== "stringset") continue;
5524
5745
  const value = recordYMap.get(fieldKey);
5525
- if (value instanceof Y3.Map) {
5746
+ if (value instanceof Y5.Map) {
5526
5747
  this.observeStringSetMapOnce(value);
5527
5748
  }
5528
5749
  }
@@ -5754,7 +5975,143 @@ init_BaseModel();
5754
5975
  init_sql();
5755
5976
  import initSqlJs from "sql.js";
5756
5977
  import { Mutex } from "async-mutex";
5978
+
5979
+ // src/utils/environment.ts
5980
+ function detectEnvironment() {
5981
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
5982
+ return "node";
5983
+ }
5984
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
5985
+ return "browser";
5986
+ }
5987
+ if (typeof self !== "undefined" && typeof self.importScripts === "function") {
5988
+ return "browser";
5989
+ }
5990
+ return "unknown";
5991
+ }
5992
+ function isNode() {
5993
+ return detectEnvironment() === "node";
5994
+ }
5995
+ function isBrowser() {
5996
+ return detectEnvironment() === "browser";
5997
+ }
5998
+ async function isNodeModuleAvailable(moduleName) {
5999
+ if (!isNode()) {
6000
+ return false;
6001
+ }
6002
+ try {
6003
+ await import(
6004
+ /* @vite-ignore */
6005
+ moduleName
6006
+ );
6007
+ return true;
6008
+ } catch {
6009
+ return false;
6010
+ }
6011
+ }
6012
+ var features = {
6013
+ get hasWebWorkers() {
6014
+ return isBrowser() && typeof Worker !== "undefined";
6015
+ },
6016
+ get hasFileSystem() {
6017
+ return isNode();
6018
+ },
6019
+ get hasWebAssembly() {
6020
+ return typeof WebAssembly !== "undefined";
6021
+ },
6022
+ async hasBetterSqlite3() {
6023
+ return await isNodeModuleAvailable("better-sqlite3");
6024
+ }
6025
+ };
6026
+
6027
+ // src/engines/SqljsEngine.ts
5757
6028
  var SQL_WASM_URL = "/sql-wasm.wasm";
6029
+ var SQL_WASM_FILENAME = "sql-wasm.wasm";
6030
+ async function resolveNodeWasmPath() {
6031
+ if (!isNode()) {
6032
+ return null;
6033
+ }
6034
+ try {
6035
+ const cjsRequire = typeof __require !== "undefined" ? __require : void 0;
6036
+ if (cjsRequire) {
6037
+ return cjsRequire.resolve("sql.js/dist/sql-wasm.wasm");
6038
+ }
6039
+ } catch {
6040
+ }
6041
+ try {
6042
+ const metaUrl = import.meta.url;
6043
+ if (metaUrl) {
6044
+ const { createRequire } = await import(
6045
+ /* @vite-ignore */
6046
+ "module"
6047
+ );
6048
+ const req = createRequire(metaUrl);
6049
+ return req.resolve("sql.js/dist/sql-wasm.wasm");
6050
+ }
6051
+ } catch {
6052
+ }
6053
+ return null;
6054
+ }
6055
+ async function preflightNodeWasm(wasmPath) {
6056
+ if (!isNode()) {
6057
+ return;
6058
+ }
6059
+ if (/^[a-z]+:\/\//i.test(wasmPath)) {
6060
+ return;
6061
+ }
6062
+ const { existsSync } = await import(
6063
+ /* @vite-ignore */
6064
+ "fs"
6065
+ );
6066
+ if (!existsSync(wasmPath)) {
6067
+ throw new Error(
6068
+ `[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()).`
6069
+ );
6070
+ }
6071
+ }
6072
+ async function loadSqlJsModule(locateFile) {
6073
+ let abortReject;
6074
+ const toError = (reason) => reason instanceof Error ? reason : new Error(
6075
+ `[SqljsEngine] sql.js aborted during initialization: ${String(
6076
+ reason
6077
+ )}`
6078
+ );
6079
+ const nodeProcess = isNode() && typeof process !== "undefined" ? process : void 0;
6080
+ const onUnhandled = (reason) => {
6081
+ const err = toError(reason);
6082
+ if (/Aborted/.test(err.message) || /Aborted/.test(String(reason))) {
6083
+ abortReject?.(err);
6084
+ return;
6085
+ }
6086
+ if (nodeProcess && nodeProcess.listenerCount("unhandledRejection") === 1) {
6087
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6088
+ void Promise.reject(reason);
6089
+ }
6090
+ };
6091
+ if (nodeProcess) {
6092
+ nodeProcess.on("unhandledRejection", onUnhandled);
6093
+ }
6094
+ const abortGuard = new Promise((_resolve, reject) => {
6095
+ abortReject = reject;
6096
+ });
6097
+ abortGuard.catch(() => {
6098
+ });
6099
+ try {
6100
+ const initPromise = initSqlJs({
6101
+ locateFile,
6102
+ // Layer 1: emscripten onAbort.
6103
+ onAbort: (reason) => abortReject?.(toError(reason))
6104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6105
+ });
6106
+ return await Promise.race([initPromise, abortGuard]);
6107
+ } finally {
6108
+ if (nodeProcess) {
6109
+ setTimeout(() => {
6110
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6111
+ }, 0);
6112
+ }
6113
+ }
6114
+ }
5758
6115
  var TransactionalSQLJSOperations = class {
5759
6116
  db;
5760
6117
  engine;
@@ -5820,22 +6177,37 @@ var SqljsEngine = class _SqljsEngine {
5820
6177
  }
5821
6178
  async initialize(engineOptions) {
5822
6179
  Logger.info("[SqljsEngine] Initializing SQL.js engine...");
6180
+ const loadingModuleThisCall = !_SqljsEngine.SQL;
5823
6181
  try {
5824
6182
  if (!_SqljsEngine.SQL) {
5825
- const locateFileConfig = engineOptions?.locateFile || ((_file) => engineOptions?.wasmURL || SQL_WASM_URL);
6183
+ let locateFileConfig;
6184
+ if (engineOptions?.locateFile) {
6185
+ locateFileConfig = engineOptions.locateFile;
6186
+ } else if (engineOptions?.wasmURL) {
6187
+ const url = engineOptions.wasmURL;
6188
+ locateFileConfig = (_file) => url;
6189
+ } else {
6190
+ const nodeWasmPath = await resolveNodeWasmPath();
6191
+ const resolved = nodeWasmPath ?? SQL_WASM_URL;
6192
+ locateFileConfig = (_file) => resolved;
6193
+ }
6194
+ const wasmPath = locateFileConfig(SQL_WASM_FILENAME);
5826
6195
  Logger.debug(
5827
6196
  "[SqljsEngine] Attempting to load SQL.js WASM from:",
5828
- locateFileConfig("sql-wasm.wasm")
6197
+ wasmPath
5829
6198
  );
5830
- _SqljsEngine.SQL = await initSqlJs({
5831
- locateFile: locateFileConfig
5832
- });
6199
+ await preflightNodeWasm(wasmPath);
6200
+ const SQL = await loadSqlJsModule(locateFileConfig);
6201
+ _SqljsEngine.SQL = SQL;
5833
6202
  Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
5834
6203
  }
5835
6204
  this.db = new _SqljsEngine.SQL.Database();
5836
6205
  Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
5837
6206
  this.updateTableNames();
5838
6207
  } catch (error) {
6208
+ if (loadingModuleThisCall) {
6209
+ _SqljsEngine.SQL = null;
6210
+ }
5839
6211
  Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
5840
6212
  throw error;
5841
6213
  }
@@ -6121,54 +6493,6 @@ var SqljsEngine = class _SqljsEngine {
6121
6493
  }
6122
6494
  };
6123
6495
 
6124
- // src/utils/environment.ts
6125
- function detectEnvironment() {
6126
- if (typeof process !== "undefined" && process.versions && process.versions.node) {
6127
- return "node";
6128
- }
6129
- if (typeof window !== "undefined" && typeof document !== "undefined") {
6130
- return "browser";
6131
- }
6132
- if (typeof self !== "undefined" && typeof self.importScripts === "function") {
6133
- return "browser";
6134
- }
6135
- return "unknown";
6136
- }
6137
- function isNode() {
6138
- return detectEnvironment() === "node";
6139
- }
6140
- function isBrowser() {
6141
- return detectEnvironment() === "browser";
6142
- }
6143
- async function isNodeModuleAvailable(moduleName) {
6144
- if (!isNode()) {
6145
- return false;
6146
- }
6147
- try {
6148
- await import(
6149
- /* @vite-ignore */
6150
- moduleName
6151
- );
6152
- return true;
6153
- } catch {
6154
- return false;
6155
- }
6156
- }
6157
- var features = {
6158
- get hasWebWorkers() {
6159
- return isBrowser() && typeof Worker !== "undefined";
6160
- },
6161
- get hasFileSystem() {
6162
- return isNode();
6163
- },
6164
- get hasWebAssembly() {
6165
- return typeof WebAssembly !== "undefined";
6166
- },
6167
- async hasBetterSqlite3() {
6168
- return await isNodeModuleAvailable("better-sqlite3");
6169
- }
6170
- };
6171
-
6172
6496
  // src/engines/BrowserDatabaseFactory.ts
6173
6497
  init_BaseModel();
6174
6498
  var BrowserDatabaseFactory = class {
@@ -6479,6 +6803,7 @@ init_StringSet();
6479
6803
  // src/models/schema.ts
6480
6804
  init_BaseModel();
6481
6805
  init_ModelRegistry();
6806
+ init_uniqueIndex();
6482
6807
  function defineModelSchema(input) {
6483
6808
  const { name, fields } = input;
6484
6809
  const options = {
@@ -6551,7 +6876,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
6551
6876
  for (const [fieldName, options] of fields.entries()) {
6552
6877
  if (options.unique) {
6553
6878
  resolved.push({
6554
- name: `${modelName}_${fieldName}_unique`,
6879
+ name: fieldUniqueConstraintName(modelName, fieldName),
6555
6880
  fields: [fieldName]
6556
6881
  });
6557
6882
  }
@@ -6616,7 +6941,7 @@ function attachAndRegisterModel(modelClass, schema) {
6616
6941
 
6617
6942
  // src/utils/yDocSchema.ts
6618
6943
  init_yjsValues();
6619
- import * as Y4 from "yjs";
6944
+ import * as Y6 from "yjs";
6620
6945
  function discoverSchema(yDoc) {
6621
6946
  const models = {};
6622
6947
  const metaNames = /* @__PURE__ */ new Set();
@@ -6650,7 +6975,7 @@ function discoverModelNames(yDoc) {
6650
6975
  function materializeMap(yDoc, key) {
6651
6976
  try {
6652
6977
  const map = yDoc.getMap(key);
6653
- return map instanceof Y4.Map ? map : null;
6978
+ return map instanceof Y6.Map ? map : null;
6654
6979
  } catch {
6655
6980
  return null;
6656
6981
  }
@@ -6660,11 +6985,11 @@ function readModelMeta(metaMap) {
6660
6985
  let constraints;
6661
6986
  let relationships;
6662
6987
  for (const [key, value] of metaMap.entries()) {
6663
- if (key === "_constraints" && value instanceof Y4.Map) {
6988
+ if (key === "_constraints" && value instanceof Y6.Map) {
6664
6989
  constraints = readConstraints(value);
6665
- } else if (key === "_relationships" && value instanceof Y4.Map) {
6990
+ } else if (key === "_relationships" && value instanceof Y6.Map) {
6666
6991
  relationships = readRelationships(value);
6667
- } else if (value instanceof Y4.Map) {
6992
+ } else if (value instanceof Y6.Map) {
6668
6993
  fields[key] = readFieldMeta(value);
6669
6994
  }
6670
6995
  }
@@ -6699,7 +7024,7 @@ function inferModelFromData(dataMap) {
6699
7024
  const fields = {};
6700
7025
  let sampled = 0;
6701
7026
  for (const [_recordId, recordValue] of dataMap.entries()) {
6702
- if (!(recordValue instanceof Y4.Map)) continue;
7027
+ if (!(recordValue instanceof Y6.Map)) continue;
6703
7028
  if (++sampled > 5) break;
6704
7029
  for (const [fieldName, value] of recordValue.entries()) {
6705
7030
  if (fieldName.startsWith("_")) continue;
@@ -6727,7 +7052,7 @@ function inferTypeFromValue(value) {
6727
7052
  function readConstraints(constraintsMap) {
6728
7053
  const out = {};
6729
7054
  for (const [name, value] of constraintsMap.entries()) {
6730
- if (!(value instanceof Y4.Map)) continue;
7055
+ if (!(value instanceof Y6.Map)) continue;
6731
7056
  let fields = [];
6732
7057
  const rawFields = value.get("fields");
6733
7058
  if (typeof rawFields === "string") {
@@ -6746,7 +7071,7 @@ function readConstraints(constraintsMap) {
6746
7071
  function readRelationships(relsMap) {
6747
7072
  const out = {};
6748
7073
  for (const [name, value] of relsMap.entries()) {
6749
- if (!(value instanceof Y4.Map)) continue;
7074
+ if (!(value instanceof Y6.Map)) continue;
6750
7075
  const rel = {};
6751
7076
  for (const [k, v] of value.entries()) {
6752
7077
  rel[k] = v;