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/index.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,17 +1439,44 @@ 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/query/IncludeResolver.ts
1363
1481
  var IncludeResolver_exports = {};
1364
1482
  __export(IncludeResolver_exports, {
@@ -1717,7 +1835,13 @@ var init_IncludeResolver = __esm({
1717
1835
  }
1718
1836
  return null;
1719
1837
  }
1720
- /** Apply projection to a single record. */
1838
+ /**
1839
+ * Apply a projection to a single record. Assumes the projection was already
1840
+ * validated upstream (mixed inclusion+exclusion rejected by the translator):
1841
+ * `isInclusion` treats "any value === 1" as inclusion mode. `id` and `type`
1842
+ * are system-identity fields and are always kept in BOTH modes (mirrors the
1843
+ * DatabaseDO `_applyProjection`); `_meta` stays excludable.
1844
+ */
1721
1845
  _applyProjection(record, projection) {
1722
1846
  const fields = Object.entries(projection);
1723
1847
  if (fields.length === 0) return record;
@@ -1736,7 +1860,7 @@ var init_IncludeResolver = __esm({
1736
1860
  } else {
1737
1861
  const result = { ...record };
1738
1862
  for (const [f, v] of fields) {
1739
- if (v === 0) delete result[f];
1863
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
1740
1864
  }
1741
1865
  return result;
1742
1866
  }
@@ -1756,21 +1880,23 @@ __export(BaseModel_exports, {
1756
1880
  generateULID: () => generateULID
1757
1881
  });
1758
1882
  function generateULID() {
1759
- return (0, import_ulid.ulid)();
1883
+ return (0, import_ulid2.ulid)();
1760
1884
  }
1761
- var Y3, import_ulid, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel;
1885
+ var Y5, import_ulid2, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel;
1762
1886
  var init_BaseModel = __esm({
1763
1887
  "src/models/BaseModel.ts"() {
1764
1888
  "use strict";
1765
- Y3 = __toESM(require("yjs"), 1);
1766
- import_ulid = require("ulid");
1889
+ Y5 = __toESM(require("yjs"), 1);
1890
+ import_ulid2 = require("ulid");
1767
1891
  init_StringSet();
1768
1892
  init_documentTypes();
1769
1893
  init_DocumentQueryTranslator();
1770
1894
  init_CursorManager();
1771
1895
  init_sql();
1772
1896
  init_metaSync();
1897
+ init_writeLifecycle();
1773
1898
  init_yjsValues();
1899
+ init_uniqueIndex();
1774
1900
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
1775
1901
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
1776
1902
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2155,7 +2281,9 @@ var init_BaseModel = __esm({
2155
2281
  this.setValue(fieldKey, data[fieldKey]);
2156
2282
  this._constructorProvidedFields.add(fieldKey);
2157
2283
  } else if (fieldOptions.default !== void 0) {
2158
- const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
2284
+ const defaultValue = resolveLiteralOrFunctionDefault(
2285
+ fieldOptions.default
2286
+ );
2159
2287
  this.setValue(fieldKey, defaultValue);
2160
2288
  }
2161
2289
  }
@@ -2447,7 +2575,7 @@ var init_BaseModel = __esm({
2447
2575
  }
2448
2576
  getStringSetFromYjs(fieldName) {
2449
2577
  const yjsData = this.getFromYjs(fieldName);
2450
- if (yjsData instanceof Y3.Map) {
2578
+ if (yjsData instanceof Y5.Map) {
2451
2579
  return Array.from(yjsData.keys());
2452
2580
  }
2453
2581
  if (yjsData && typeof yjsData === "object") {
@@ -2555,7 +2683,7 @@ var init_BaseModel = __esm({
2555
2683
  }
2556
2684
  const extractStringSetValues = (value) => {
2557
2685
  if (!value) return [];
2558
- if (value instanceof Y3.Map) {
2686
+ if (value instanceof Y5.Map) {
2559
2687
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
2560
2688
  }
2561
2689
  if (Array.isArray(value)) {
@@ -2572,7 +2700,7 @@ var init_BaseModel = __esm({
2572
2700
  if (!recordId) continue;
2573
2701
  let itemData;
2574
2702
  const stringSetValuesByField = {};
2575
- if (recordData instanceof Y3.Map) {
2703
+ if (recordData instanceof Y5.Map) {
2576
2704
  itemData = {};
2577
2705
  const unknownFields = [];
2578
2706
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2688,20 +2816,10 @@ var init_BaseModel = __esm({
2688
2816
  Logger.verbose(
2689
2817
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
2690
2818
  );
2691
- const buildUniqueKey = (recordData, fields) => {
2692
- const keyParts = [];
2693
- for (const field of fields) {
2694
- const value = recordData instanceof Y3.Map ? recordData.get(field) : recordData[field];
2695
- if (value === null || value === void 0) {
2696
- return null;
2697
- }
2698
- keyParts.push(String(value));
2699
- }
2700
- return keyParts.join("|");
2701
- };
2819
+ const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
2702
2820
  const extractItemData = (key, recordData) => {
2703
2821
  let itemData;
2704
- if (recordData instanceof Y3.Map) {
2822
+ if (recordData instanceof Y5.Map) {
2705
2823
  itemData = {};
2706
2824
  const unknownFields = [];
2707
2825
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2785,7 +2903,7 @@ var init_BaseModel = __esm({
2785
2903
  const recordsByUniqueKey = /* @__PURE__ */ new Map();
2786
2904
  for (const [recordId, recordData] of allRecords.entries()) {
2787
2905
  if (recordIdsToDiscard.has(recordId)) continue;
2788
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2906
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2789
2907
  if (uniqueKey === null) continue;
2790
2908
  if (!recordsByUniqueKey.has(uniqueKey)) {
2791
2909
  recordsByUniqueKey.set(uniqueKey, []);
@@ -2831,7 +2949,7 @@ var init_BaseModel = __esm({
2831
2949
  if (!recordData || !key) continue;
2832
2950
  const itemData = extractItemData(key, recordData);
2833
2951
  if (!itemData) continue;
2834
- if (change.action === "add" && recordData instanceof Y3.Map) {
2952
+ if (change.action === "add" && recordData instanceof Y5.Map) {
2835
2953
  Logger.verbose(
2836
2954
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2837
2955
  );
@@ -2929,9 +3047,12 @@ var init_BaseModel = __esm({
2929
3047
  const recordData = documentYMap.get(idToDiscard);
2930
3048
  if (!recordData) continue;
2931
3049
  for (const constraint of schema.resolvedUniqueConstraints) {
2932
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3050
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2933
3051
  if (uniqueKey === null) continue;
2934
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3052
+ const constraintMapName = uniqueIndexMapName(
3053
+ modelName,
3054
+ constraint.name
3055
+ );
2935
3056
  const constraintMap = yDoc.getMap(constraintMapName);
2936
3057
  const currentIndexValue = constraintMap.get(uniqueKey);
2937
3058
  if (currentIndexValue === idToDiscard) {
@@ -2949,11 +3070,14 @@ var init_BaseModel = __esm({
2949
3070
  }, `conflict-resolution-${modelName}-${docId}`);
2950
3071
  yDoc.transact(() => {
2951
3072
  for (const constraint of schema.resolvedUniqueConstraints) {
2952
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3073
+ const constraintMapName = uniqueIndexMapName(
3074
+ modelName,
3075
+ constraint.name
3076
+ );
2953
3077
  const constraintMap = yDoc.getMap(constraintMapName);
2954
3078
  for (const [recordId, recordData] of documentYMap.entries()) {
2955
3079
  if (!recordData) continue;
2956
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3080
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2957
3081
  if (uniqueKey === null) continue;
2958
3082
  const currentIndexValue = constraintMap.get(uniqueKey);
2959
3083
  if (currentIndexValue !== recordId) {
@@ -2976,7 +3100,7 @@ var init_BaseModel = __esm({
2976
3100
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
2977
3101
  );
2978
3102
  for (const [recordId, recordData] of documentYMap.entries()) {
2979
- if (recordData instanceof Y3.Map) {
3103
+ if (recordData instanceof Y5.Map) {
2980
3104
  Logger.verbose(
2981
3105
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
2982
3106
  );
@@ -3202,26 +3326,7 @@ var init_BaseModel = __esm({
3202
3326
  * if the field hasn't been touched since the wire-format change in #561.
3203
3327
  */
3204
3328
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3205
- let nested = recordYMap.get(fieldName);
3206
- if (!(nested instanceof Y3.Map)) {
3207
- const migrated = new Y3.Map();
3208
- if (nested && typeof nested === "object") {
3209
- for (const [member, marker] of Object.entries(
3210
- nested
3211
- )) {
3212
- if (Boolean(marker)) migrated.set(member, true);
3213
- }
3214
- }
3215
- recordYMap.set(fieldName, migrated);
3216
- nested = migrated;
3217
- }
3218
- const target = nested;
3219
- for (const member of change.additions) {
3220
- target.set(member, true);
3221
- }
3222
- for (const member of change.removals) {
3223
- target.delete(member);
3224
- }
3329
+ applyStringSetChange(recordYMap, fieldName, change);
3225
3330
  }
3226
3331
  /**
3227
3332
  * Deep equality check for comparing field values
@@ -3423,6 +3528,34 @@ var init_BaseModel = __esm({
3423
3528
  `[${modelName}.save] getCurrentJSState() returned:`,
3424
3529
  dataToSave
3425
3530
  );
3531
+ const uniqueMissConflicts = /* @__PURE__ */ new Map();
3532
+ for (const constraint of schema.resolvedUniqueConstraints) {
3533
+ const newUniqueKey = this._buildUniqueKey(
3534
+ constraint.fields,
3535
+ dataToSave,
3536
+ modelName,
3537
+ constraint.name
3538
+ );
3539
+ if (newUniqueKey === null) {
3540
+ continue;
3541
+ }
3542
+ const constraintMap = yDoc.getMap(
3543
+ uniqueIndexMapName(modelName, constraint.name)
3544
+ );
3545
+ if (!constraintMap.get(newUniqueKey)) {
3546
+ const constraintKeyValues = constraint.fields.map(
3547
+ (field) => dataToSave[field]
3548
+ );
3549
+ const conflictingRecord = await modelConstructor._resolveByUniqueFallback(
3550
+ constraint,
3551
+ constraintKeyValues,
3552
+ { excludeId: this.id }
3553
+ );
3554
+ if (conflictingRecord) {
3555
+ uniqueMissConflicts.set(constraint.name, conflictingRecord.id);
3556
+ }
3557
+ }
3558
+ }
3426
3559
  await yDoc.transact(async () => {
3427
3560
  if (upsertOnConstraint && upsertOnKey) {
3428
3561
  const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
@@ -3464,7 +3597,7 @@ var init_BaseModel = __esm({
3464
3597
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
3465
3598
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
3466
3599
  if (!recordYMap) {
3467
- recordYMap = new Y3.Map();
3600
+ recordYMap = new Y5.Map();
3468
3601
  Logger.verbose(
3469
3602
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
3470
3603
  );
@@ -3516,7 +3649,10 @@ var init_BaseModel = __esm({
3516
3649
  );
3517
3650
  continue;
3518
3651
  }
3519
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3652
+ const constraintMapName = uniqueIndexMapName(
3653
+ modelName,
3654
+ constraint.name
3655
+ );
3520
3656
  const constraintMap = yDoc.getMap(constraintMapName);
3521
3657
  Logger.verbose(
3522
3658
  `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
@@ -3545,6 +3681,26 @@ var init_BaseModel = __esm({
3545
3681
  this.id,
3546
3682
  existingRecordIdWithNewKey
3547
3683
  );
3684
+ } else if (!existingRecordIdWithNewKey) {
3685
+ const conflictingRecordId = uniqueMissConflicts.get(constraint.name);
3686
+ if (conflictingRecordId && conflictingRecordId !== this.id) {
3687
+ Logger.verbose(
3688
+ `[${modelName}] Save Check (Item '${this.id}'): index miss for '${constraint.name}', but engine fallback found conflicting index-less record ${conflictingRecordId}.`
3689
+ );
3690
+ throw new UniqueConstraintViolationError(
3691
+ `Unique constraint '${constraint.name}' violated for model '${modelName}' on fields [${constraint.fields.join(
3692
+ ", "
3693
+ )}]. Attempted ID: ${this.id}. Value(s) starting with '${newUniqueKey.substring(
3694
+ 0,
3695
+ 50
3696
+ )}...' already exist for index-less record ID ${conflictingRecordId}.`,
3697
+ modelName,
3698
+ constraint.name,
3699
+ constraint.fields,
3700
+ this.id,
3701
+ conflictingRecordId
3702
+ );
3703
+ }
3548
3704
  }
3549
3705
  }
3550
3706
  if (isUpdate && oldData) {
@@ -3888,34 +4044,46 @@ var init_BaseModel = __esm({
3888
4044
  const modelName = schema.options.name;
3889
4045
  Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
3890
4046
  const hasIncludes = options?.include && options.include.length > 0;
3891
- const translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4047
+ const requestedLimit = options?.limit;
4048
+ const overFetch = typeof requestedLimit === "number" && requestedLimit > 0;
4049
+ let translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4050
+ if (overFetch) {
4051
+ translatorOptions = {
4052
+ ...translatorOptions || {},
4053
+ limit: requestedLimit + 1
4054
+ };
4055
+ }
3892
4056
  const translator = new DocumentQueryTranslator(modelName, schema.fields);
3893
4057
  const translatedQuery = translator.translateFind(filter, translatorOptions);
3894
4058
  Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
3895
4059
  Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
3896
- const results = await _BaseModel.dbInstance.query(
4060
+ let results = await _BaseModel.dbInstance.query(
3897
4061
  translatedQuery.sql,
3898
4062
  translatedQuery.params
3899
4063
  );
3900
4064
  Logger.verbose(`[${modelName}] Query returned ${results.length} results`);
4065
+ let hasMore = false;
4066
+ if (overFetch && results.length > requestedLimit) {
4067
+ hasMore = true;
4068
+ results = results.slice(0, requestedLimit);
4069
+ }
3901
4070
  if (options?.include && options.include.length > 0) {
3902
4071
  const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
3903
4072
  const resolver = new IncludeResolver2(_BaseModel.dbInstance);
3904
4073
  await resolver.resolve(results, options.include, 0, modelName);
3905
4074
  }
3906
- const requestedLimit = options?.limit;
3907
- const hasMore = CursorManager.hasMoreResults(
3908
- requestedLimit,
3909
- results.length
3910
- );
3911
4075
  const isFirstPage = !options?.uniqueStartKey;
4076
+ const direction = options?.direction || 1;
3912
4077
  const cursors = CursorManager.generateResultCursors(
3913
4078
  results,
3914
4079
  translatedQuery.sortFields,
3915
- options?.direction || 1,
4080
+ direction,
3916
4081
  hasMore,
3917
4082
  isFirstPage
3918
4083
  );
4084
+ if (direction === -1) {
4085
+ results.reverse();
4086
+ }
3919
4087
  let transformedData;
3920
4088
  if (options?.projection && Object.keys(options.projection).length > 0) {
3921
4089
  transformedData = results.map((row) => {
@@ -4491,7 +4659,7 @@ var init_BaseModel = __esm({
4491
4659
  );
4492
4660
  }
4493
4661
  for (const [recordId, recordYMap] of documentYMap.entries()) {
4494
- if (recordYMap instanceof Y3.Map) {
4662
+ if (recordYMap instanceof Y5.Map) {
4495
4663
  const instance = new this({ id: recordId });
4496
4664
  instance._metaDocId = docId;
4497
4665
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -4516,6 +4684,50 @@ var init_BaseModel = __esm({
4516
4684
  }
4517
4685
  return allInstances;
4518
4686
  }
4687
+ /**
4688
+ * Resolve a record by its unique-constraint field values via an engine query.
4689
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
4690
+ * entry for a key.
4691
+ *
4692
+ * The index map is a cache over the SQLite projection, not the source of
4693
+ * truth: every record is loaded into the projection on `initializeForDocument`
4694
+ * regardless of index presence, but a record written by a client predating
4695
+ * index maintenance (legacy data) is missing from the index. Such a record
4696
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
4697
+ * check. A query over the constraint fields finds it reliably.
4698
+ *
4699
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
4700
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
4701
+ * whether to heal the missing index entry (write paths) and do their own
4702
+ * logging — this method never mutates the index, so read paths stay
4703
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
4704
+ * constraint field means the record does not participate in the constraint,
4705
+ * so no query runs.
4706
+ */
4707
+ static async _resolveByUniqueFallback(constraint, keyValues, options) {
4708
+ if (!_BaseModel.dbInstance) {
4709
+ return null;
4710
+ }
4711
+ const filter = {};
4712
+ for (let i = 0; i < constraint.fields.length; i++) {
4713
+ const value = keyValues[i];
4714
+ if (value === null || value === void 0) {
4715
+ return null;
4716
+ }
4717
+ filter[constraint.fields[i]] = value;
4718
+ }
4719
+ if (options?.excludeId) {
4720
+ if ("id" in filter) {
4721
+ if (filter.id === options.excludeId) {
4722
+ return null;
4723
+ }
4724
+ } else {
4725
+ filter.id = { $ne: options.excludeId };
4726
+ }
4727
+ }
4728
+ const found = await this.queryOne(filter);
4729
+ return found ?? null;
4730
+ }
4519
4731
  static async findByUnique(constraintName, value) {
4520
4732
  const modelConstructor = this;
4521
4733
  const schema = modelConstructor.getSchema();
@@ -4554,7 +4766,10 @@ var init_BaseModel = __esm({
4554
4766
  const documentInfo = modelConstructor.connectedDocuments.get(docId);
4555
4767
  if (!documentInfo) continue;
4556
4768
  const yDoc = documentInfo.yDoc;
4557
- const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4769
+ const constraintMapName2 = uniqueIndexMapName(
4770
+ currentModelName,
4771
+ constraint.name
4772
+ );
4558
4773
  const constraintMap2 = yDoc.getMap(constraintMapName2);
4559
4774
  const recordId2 = constraintMap2.get(uniqueKeyString);
4560
4775
  if (recordId2) {
@@ -4562,7 +4777,7 @@ var init_BaseModel = __esm({
4562
4777
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
4563
4778
  if (documentYMap) {
4564
4779
  const recordYMap2 = documentYMap.get(recordId2);
4565
- if (recordYMap2 && recordYMap2 instanceof Y3.Map) {
4780
+ if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
4566
4781
  const instance = new modelConstructor({
4567
4782
  id: recordId2
4568
4783
  });
@@ -4576,6 +4791,16 @@ var init_BaseModel = __esm({
4576
4791
  }
4577
4792
  }
4578
4793
  }
4794
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
4795
+ constraint,
4796
+ keyValues
4797
+ );
4798
+ if (fallbackRecord) {
4799
+ Logger.verbose(
4800
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
4801
+ );
4802
+ return fallbackRecord;
4803
+ }
4579
4804
  Logger.verbose(
4580
4805
  `[${currentModelName}.findByUnique] Record not found in any connected document`
4581
4806
  );
@@ -4589,17 +4814,30 @@ var init_BaseModel = __esm({
4589
4814
  `[${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.`
4590
4815
  );
4591
4816
  }
4592
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4817
+ const constraintMapName = uniqueIndexMapName(
4818
+ currentModelName,
4819
+ constraint.name
4820
+ );
4593
4821
  const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
4594
4822
  const recordId = constraintMap.get(uniqueKeyString);
4595
4823
  if (!recordId) {
4824
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
4825
+ constraint,
4826
+ keyValues
4827
+ );
4828
+ if (fallbackRecord) {
4829
+ Logger.verbose(
4830
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
4831
+ );
4832
+ return fallbackRecord;
4833
+ }
4596
4834
  return null;
4597
4835
  }
4598
4836
  const recordYMap = legacyDocumentMap.get(recordId);
4599
4837
  if (!recordYMap) {
4600
4838
  return null;
4601
4839
  }
4602
- if (recordYMap instanceof Y3.Map) {
4840
+ if (recordYMap instanceof Y5.Map) {
4603
4841
  const instance = new modelConstructor({
4604
4842
  id: recordId
4605
4843
  });
@@ -4657,7 +4895,10 @@ var init_BaseModel = __esm({
4657
4895
  `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
4658
4896
  );
4659
4897
  }
4660
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4898
+ const constraintMapName = uniqueIndexMapName(
4899
+ currentModelName,
4900
+ constraint.name
4901
+ );
4661
4902
  let existingRecord = null;
4662
4903
  if (connectedDocuments && connectedDocuments.size > 0) {
4663
4904
  for (const docId of connectedDocuments.keys()) {
@@ -4675,6 +4916,33 @@ var init_BaseModel = __esm({
4675
4916
  }
4676
4917
  } else {
4677
4918
  }
4919
+ if (!existingRecord) {
4920
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
4921
+ constraint,
4922
+ keyValuesForLookup
4923
+ );
4924
+ if (fallbackRecord) {
4925
+ existingRecord = fallbackRecord;
4926
+ const healDocId = fallbackRecord._metaDocId;
4927
+ const healDocInfo = healDocId ? connectedDocuments.get(healDocId) : void 0;
4928
+ const canHeal = !!healDocInfo && healDocInfo.permissionHint !== "read";
4929
+ if (healDocInfo && canHeal) {
4930
+ healDocInfo.yDoc.transact(() => {
4931
+ const healMap = healDocInfo.yDoc.getMap(constraintMapName);
4932
+ if (healMap.get(uniqueKeyString) !== fallbackRecord.id) {
4933
+ healMap.set(uniqueKeyString, fallbackRecord.id);
4934
+ }
4935
+ }, `heal-unique-index-${currentModelName}-${healDocId}`);
4936
+ Logger.verbose(
4937
+ `[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; healed missing index entry (legacy index-less record).`
4938
+ );
4939
+ } else {
4940
+ Logger.verbose(
4941
+ `[${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).`
4942
+ );
4943
+ }
4944
+ }
4945
+ }
4678
4946
  if (options?.objectMustExist) {
4679
4947
  if (!existingRecord) {
4680
4948
  throw new RecordNotFoundError(
@@ -4831,7 +5099,7 @@ var init_BaseModel = __esm({
4831
5099
  for (const [fieldKey, fieldOptions] of schema.fields) {
4832
5100
  if (fieldOptions?.type !== "stringset") continue;
4833
5101
  const value = recordYMap.get(fieldKey);
4834
- if (value instanceof Y3.Map) {
5102
+ if (value instanceof Y5.Map) {
4835
5103
  this.observeStringSetMapOnce(value);
4836
5104
  }
4837
5105
  }
@@ -5017,6 +5285,19 @@ var init_BaseModel = __esm({
5017
5285
  });
5018
5286
 
5019
5287
  // src/models/relationshipManager.ts
5288
+ function warnIfOrderFieldNullable(modelClass, orderByField, relationshipDescription) {
5289
+ if (ALWAYS_PRESENT_ORDER_FIELDS.has(orderByField)) return;
5290
+ const schema = modelClass.getSchema?.();
5291
+ const fields = schema?.fields;
5292
+ if (!fields || typeof fields.get !== "function") return;
5293
+ const fieldOptions = fields.get(orderByField);
5294
+ if (!fieldOptions) return;
5295
+ if (fieldOptions.required !== true) {
5296
+ Logger.warn(
5297
+ `[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.)`
5298
+ );
5299
+ }
5300
+ }
5020
5301
  function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
5021
5302
  return async function() {
5022
5303
  const relatedId = this[config.relatedIdField];
@@ -5025,6 +5306,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
5025
5306
  };
5026
5307
  }
5027
5308
  function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbEngine) {
5309
+ const {
5310
+ relatedIdField,
5311
+ orderByField = "id",
5312
+ orderDirection = "ASC"
5313
+ } = config;
5314
+ warnIfOrderFieldNullable(
5315
+ targetModelClass,
5316
+ orderByField,
5317
+ `hasMany(${targetModelClass.name})`
5318
+ );
5028
5319
  return async function(paginationArgs) {
5029
5320
  const {
5030
5321
  limit,
@@ -5032,71 +5323,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
5032
5323
  beforeCursor,
5033
5324
  direction = "forward"
5034
5325
  } = paginationArgs || {};
5035
- const {
5036
- relatedIdField,
5037
- orderByField = "id",
5038
- orderDirection = "ASC"
5039
- } = config;
5040
5326
  const targetSchema = targetModelClass.getSchema();
5041
5327
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
5042
5328
  Logger.error(
5043
5329
  `[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
5044
5330
  );
5045
- return { data: [], nextCursor: null, prevCursor: null };
5331
+ return { data: [], hasMore: false };
5046
5332
  }
5047
- const filter = { [relatedIdField]: this.id };
5333
+ const forward = direction !== "backward";
5334
+ const cursor = forward ? afterCursor : beforeCursor;
5048
5335
  const queryOptions = {
5049
- sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
5336
+ sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
5337
+ direction: forward ? 1 : -1
5050
5338
  };
5051
- if (limit !== void 0) {
5052
- queryOptions.limit = limit + 1;
5053
- }
5054
- if (direction === "forward" && afterCursor) {
5055
- filter[orderByField] = {
5056
- [orderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
5057
- };
5058
- } else if (direction === "backward") {
5059
- if (beforeCursor) {
5060
- filter[orderByField] = {
5061
- [orderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
5062
- };
5063
- }
5064
- queryOptions.sort = { [orderByField]: orderDirection === "ASC" ? -1 : 1 };
5065
- }
5066
- const result = await targetModelClass.query(filter, queryOptions);
5067
- const results = result.data;
5068
- let nextCursor = null;
5069
- let prevCursor = null;
5070
- const isFirstPage = direction === "forward" && !afterCursor;
5071
- if (limit !== void 0) {
5072
- if (direction === "forward") {
5073
- if (results.length > limit) {
5074
- nextCursor = results[limit - 1][orderByField];
5075
- results.pop();
5076
- }
5077
- if (results.length > 0 && !isFirstPage) {
5078
- prevCursor = results[0][orderByField];
5079
- }
5080
- } else {
5081
- if (results.length > limit) {
5082
- prevCursor = results[limit - 1][orderByField];
5083
- results.pop();
5084
- }
5085
- results.reverse();
5086
- if (results.length > 0 && beforeCursor) {
5087
- nextCursor = results[results.length - 1][orderByField];
5088
- }
5089
- if (results.length > 0 && !beforeCursor) {
5090
- if (results.length === limit) {
5091
- prevCursor = results[0][orderByField];
5092
- }
5093
- }
5094
- }
5095
- }
5096
- return { data: results, nextCursor, prevCursor };
5339
+ if (limit !== void 0) queryOptions.limit = limit;
5340
+ if (cursor != null) queryOptions.uniqueStartKey = cursor;
5341
+ return targetModelClass.query(
5342
+ { [relatedIdField]: this.id },
5343
+ queryOptions
5344
+ );
5097
5345
  };
5098
5346
  }
5099
5347
  function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClass, joinModelClass, _dbEngine) {
5348
+ const {
5349
+ joinModelLocalField,
5350
+ joinModelRelatedField,
5351
+ joinModelOrderByField = "id",
5352
+ joinModelOrderDirection = "ASC"
5353
+ } = config;
5354
+ warnIfOrderFieldNullable(
5355
+ joinModelClass,
5356
+ joinModelOrderByField,
5357
+ `hasManyThrough(${targetModelClass.name} via ${joinModelClass.name})`
5358
+ );
5100
5359
  return async function(paginationArgs) {
5101
5360
  const {
5102
5361
  limit,
@@ -5110,87 +5369,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
5110
5369
  Logger.error(
5111
5370
  `[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
5112
5371
  );
5113
- return { data: [], nextCursor: null, prevCursor: null };
5372
+ return { data: [], hasMore: false };
5114
5373
  }
5115
5374
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
5116
5375
  Logger.error(
5117
5376
  `[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
5118
5377
  );
5119
- return { data: [], nextCursor: null, prevCursor: null };
5378
+ return { data: [], hasMore: false };
5120
5379
  }
5121
- const {
5122
- joinModelLocalField,
5123
- joinModelRelatedField,
5124
- joinModelOrderByField = "id",
5125
- joinModelOrderDirection = "ASC"
5126
- } = config;
5127
- const joinFilter = { [joinModelLocalField]: this.id };
5380
+ const forward = direction !== "backward";
5381
+ const cursor = forward ? afterCursor : beforeCursor;
5128
5382
  const joinQueryOptions = {
5129
5383
  sort: {
5130
5384
  [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
5131
5385
  },
5132
- projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
5386
+ projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
5387
+ direction: forward ? 1 : -1
5133
5388
  };
5134
- if (limit !== void 0) {
5135
- joinQueryOptions.limit = limit + 1;
5136
- }
5137
- if (direction === "forward" && afterCursor) {
5138
- joinFilter[joinModelOrderByField] = {
5139
- [joinModelOrderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
5140
- };
5141
- } else if (direction === "backward") {
5142
- if (beforeCursor) {
5143
- joinFilter[joinModelOrderByField] = {
5144
- [joinModelOrderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
5145
- };
5146
- }
5147
- joinQueryOptions.sort = {
5148
- [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? -1 : 1
5149
- };
5150
- }
5389
+ if (limit !== void 0) joinQueryOptions.limit = limit;
5390
+ if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
5151
5391
  const joinResult = await joinModelClass.query(
5152
- joinFilter,
5392
+ { [joinModelLocalField]: this.id },
5153
5393
  joinQueryOptions
5154
5394
  );
5155
- const joinResults = joinResult.data;
5156
- let nextCursorFromJoin = null;
5157
- let prevCursorFromJoin = null;
5158
- const isFirstPage = direction === "forward" && !afterCursor;
5159
- if (limit !== void 0) {
5160
- if (direction === "forward") {
5161
- if (joinResults.length > limit) {
5162
- nextCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
5163
- joinResults.pop();
5164
- }
5165
- if (joinResults.length > 0 && !isFirstPage) {
5166
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
5167
- }
5168
- } else {
5169
- if (joinResults.length > limit) {
5170
- prevCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
5171
- joinResults.pop();
5172
- }
5173
- joinResults.reverse();
5174
- if (joinResults.length > 0 && beforeCursor) {
5175
- nextCursorFromJoin = joinResults[joinResults.length - 1][joinModelOrderByField];
5176
- }
5177
- if (joinResults.length > 0 && !beforeCursor) {
5178
- if (joinResults.length === limit) {
5179
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
5180
- }
5181
- }
5182
- }
5395
+ const relatedIds = joinResult.data.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
5396
+ let targetData = [];
5397
+ if (relatedIds.length > 0) {
5398
+ const targetResult = await targetModelClass.query({
5399
+ id: { $in: relatedIds }
5400
+ });
5401
+ targetData = targetResult.data;
5183
5402
  }
5184
- const relatedIds = joinResults.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
5185
- if (relatedIds.length === 0)
5186
- return { data: [], nextCursor: null, prevCursor: null };
5187
- const targetResult = await targetModelClass.query({
5188
- id: { $in: relatedIds }
5189
- });
5190
5403
  return {
5191
- data: targetResult.data,
5192
- nextCursor: nextCursorFromJoin,
5193
- prevCursor: prevCursorFromJoin
5404
+ data: targetData,
5405
+ nextCursor: joinResult.nextCursor,
5406
+ prevCursor: joinResult.prevCursor,
5407
+ hasMore: joinResult.hasMore
5194
5408
  };
5195
5409
  };
5196
5410
  }
@@ -5314,10 +5528,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
5314
5528
  }, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
5315
5529
  };
5316
5530
  }
5531
+ var ALWAYS_PRESENT_ORDER_FIELDS;
5317
5532
  var init_relationshipManager = __esm({
5318
5533
  "src/models/relationshipManager.ts"() {
5319
5534
  "use strict";
5320
5535
  init_BaseModel();
5536
+ ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
5321
5537
  }
5322
5538
  });
5323
5539
 
@@ -5785,7 +6001,92 @@ var init_environment = __esm({
5785
6001
  });
5786
6002
 
5787
6003
  // src/engines/SqljsEngine.ts
5788
- var import_sql4, import_async_mutex, SQL_WASM_URL, TransactionalSQLJSOperations, SqljsEngine;
6004
+ async function resolveNodeWasmPath() {
6005
+ if (!isNode()) {
6006
+ return null;
6007
+ }
6008
+ try {
6009
+ const cjsRequire = typeof require !== "undefined" ? require : void 0;
6010
+ if (cjsRequire) {
6011
+ return cjsRequire.resolve("sql.js/dist/sql-wasm.wasm");
6012
+ }
6013
+ } catch {
6014
+ }
6015
+ try {
6016
+ const metaUrl = import_meta.url;
6017
+ if (metaUrl) {
6018
+ const { createRequire } = await import(
6019
+ /* @vite-ignore */
6020
+ "module"
6021
+ );
6022
+ const req = createRequire(metaUrl);
6023
+ return req.resolve("sql.js/dist/sql-wasm.wasm");
6024
+ }
6025
+ } catch {
6026
+ }
6027
+ return null;
6028
+ }
6029
+ async function preflightNodeWasm(wasmPath) {
6030
+ if (!isNode()) {
6031
+ return;
6032
+ }
6033
+ if (/^[a-z]+:\/\//i.test(wasmPath)) {
6034
+ return;
6035
+ }
6036
+ const { existsSync } = await import(
6037
+ /* @vite-ignore */
6038
+ "fs"
6039
+ );
6040
+ if (!existsSync(wasmPath)) {
6041
+ throw new Error(
6042
+ `[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()).`
6043
+ );
6044
+ }
6045
+ }
6046
+ async function loadSqlJsModule(locateFile) {
6047
+ let abortReject;
6048
+ const toError = (reason) => reason instanceof Error ? reason : new Error(
6049
+ `[SqljsEngine] sql.js aborted during initialization: ${String(
6050
+ reason
6051
+ )}`
6052
+ );
6053
+ const nodeProcess = isNode() && typeof process !== "undefined" ? process : void 0;
6054
+ const onUnhandled = (reason) => {
6055
+ const err = toError(reason);
6056
+ if (/Aborted/.test(err.message) || /Aborted/.test(String(reason))) {
6057
+ abortReject?.(err);
6058
+ return;
6059
+ }
6060
+ if (nodeProcess && nodeProcess.listenerCount("unhandledRejection") === 1) {
6061
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6062
+ void Promise.reject(reason);
6063
+ }
6064
+ };
6065
+ if (nodeProcess) {
6066
+ nodeProcess.on("unhandledRejection", onUnhandled);
6067
+ }
6068
+ const abortGuard = new Promise((_resolve, reject) => {
6069
+ abortReject = reject;
6070
+ });
6071
+ abortGuard.catch(() => {
6072
+ });
6073
+ try {
6074
+ const initPromise = (0, import_sql4.default)({
6075
+ locateFile,
6076
+ // Layer 1: emscripten onAbort.
6077
+ onAbort: (reason) => abortReject?.(toError(reason))
6078
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6079
+ });
6080
+ return await Promise.race([initPromise, abortGuard]);
6081
+ } finally {
6082
+ if (nodeProcess) {
6083
+ setTimeout(() => {
6084
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6085
+ }, 0);
6086
+ }
6087
+ }
6088
+ }
6089
+ var import_sql4, import_async_mutex, import_meta, SQL_WASM_URL, SQL_WASM_FILENAME, TransactionalSQLJSOperations, SqljsEngine;
5789
6090
  var init_SqljsEngine = __esm({
5790
6091
  "src/engines/SqljsEngine.ts"() {
5791
6092
  "use strict";
@@ -5793,7 +6094,10 @@ var init_SqljsEngine = __esm({
5793
6094
  init_BaseModel();
5794
6095
  import_async_mutex = require("async-mutex");
5795
6096
  init_sql();
6097
+ init_environment();
6098
+ import_meta = {};
5796
6099
  SQL_WASM_URL = "/sql-wasm.wasm";
6100
+ SQL_WASM_FILENAME = "sql-wasm.wasm";
5797
6101
  TransactionalSQLJSOperations = class {
5798
6102
  db;
5799
6103
  engine;
@@ -5859,22 +6163,37 @@ var init_SqljsEngine = __esm({
5859
6163
  }
5860
6164
  async initialize(engineOptions) {
5861
6165
  Logger.info("[SqljsEngine] Initializing SQL.js engine...");
6166
+ const loadingModuleThisCall = !_SqljsEngine.SQL;
5862
6167
  try {
5863
6168
  if (!_SqljsEngine.SQL) {
5864
- const locateFileConfig = engineOptions?.locateFile || ((_file) => engineOptions?.wasmURL || SQL_WASM_URL);
6169
+ let locateFileConfig;
6170
+ if (engineOptions?.locateFile) {
6171
+ locateFileConfig = engineOptions.locateFile;
6172
+ } else if (engineOptions?.wasmURL) {
6173
+ const url = engineOptions.wasmURL;
6174
+ locateFileConfig = (_file) => url;
6175
+ } else {
6176
+ const nodeWasmPath = await resolveNodeWasmPath();
6177
+ const resolved = nodeWasmPath ?? SQL_WASM_URL;
6178
+ locateFileConfig = (_file) => resolved;
6179
+ }
6180
+ const wasmPath = locateFileConfig(SQL_WASM_FILENAME);
5865
6181
  Logger.debug(
5866
6182
  "[SqljsEngine] Attempting to load SQL.js WASM from:",
5867
- locateFileConfig("sql-wasm.wasm")
6183
+ wasmPath
5868
6184
  );
5869
- _SqljsEngine.SQL = await (0, import_sql4.default)({
5870
- locateFile: locateFileConfig
5871
- });
6185
+ await preflightNodeWasm(wasmPath);
6186
+ const SQL = await loadSqlJsModule(locateFileConfig);
6187
+ _SqljsEngine.SQL = SQL;
5872
6188
  Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
5873
6189
  }
5874
6190
  this.db = new _SqljsEngine.SQL.Database();
5875
6191
  Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
5876
6192
  this.updateTableNames();
5877
6193
  } catch (error) {
6194
+ if (loadingModuleThisCall) {
6195
+ _SqljsEngine.SQL = null;
6196
+ }
5878
6197
  Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
5879
6198
  throw error;
5880
6199
  }
@@ -6856,12 +7175,14 @@ __export(index_exports, {
6856
7175
  attachAndRegisterModel: () => attachAndRegisterModel,
6857
7176
  attachSchemaToClass: () => attachSchemaToClass,
6858
7177
  autoRegisterModel: () => autoRegisterModel,
7178
+ buildUniqueKey: () => buildUniqueKey,
6859
7179
  clearMetaSyncCache: () => clearMetaSyncCache,
6860
7180
  createModelClass: () => createModelClass,
6861
7181
  defineModelSchema: () => defineModelSchema,
6862
7182
  discoverModelNames: () => discoverModelNames,
6863
7183
  discoverSchema: () => discoverSchema,
6864
7184
  dumpYDocToPlain: () => dumpYDocToPlain,
7185
+ fieldUniqueConstraintName: () => fieldUniqueConstraintName,
6865
7186
  generateULID: () => generateULID,
6866
7187
  inferFieldType: () => inferFieldType,
6867
7188
  initJsBao: () => initJsBao,
@@ -6872,7 +7193,8 @@ __export(index_exports, {
6872
7193
  schemaToToml: () => schemaToToml,
6873
7194
  summarizePlainYDoc: () => summarizePlainYDoc,
6874
7195
  syncInferredMeta: () => syncInferredMeta,
6875
- syncModelMeta: () => syncModelMeta
7196
+ syncModelMeta: () => syncModelMeta,
7197
+ uniqueIndexMapName: () => uniqueIndexMapName
6876
7198
  });
6877
7199
  module.exports = __toCommonJS(index_exports);
6878
7200
 
@@ -7095,6 +7417,7 @@ init_StringSet();
7095
7417
  // src/models/schema.ts
7096
7418
  init_BaseModel();
7097
7419
  init_ModelRegistry();
7420
+ init_uniqueIndex();
7098
7421
  function defineModelSchema(input) {
7099
7422
  const { name, fields } = input;
7100
7423
  const options = {
@@ -7167,7 +7490,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
7167
7490
  for (const [fieldName, options] of fields.entries()) {
7168
7491
  if (options.unique) {
7169
7492
  resolved.push({
7170
- name: `${modelName}_${fieldName}_unique`,
7493
+ name: fieldUniqueConstraintName(modelName, fieldName),
7171
7494
  fields: [fieldName]
7172
7495
  });
7173
7496
  }
@@ -7494,7 +7817,7 @@ async function loadSchemaFromToml(filePath, options = {}) {
7494
7817
  }
7495
7818
 
7496
7819
  // src/utils/yDocSchema.ts
7497
- var Y4 = __toESM(require("yjs"), 1);
7820
+ var Y6 = __toESM(require("yjs"), 1);
7498
7821
  init_yjsValues();
7499
7822
  function discoverSchema(yDoc) {
7500
7823
  const models = {};
@@ -7529,7 +7852,7 @@ function discoverModelNames(yDoc) {
7529
7852
  function materializeMap(yDoc, key) {
7530
7853
  try {
7531
7854
  const map = yDoc.getMap(key);
7532
- return map instanceof Y4.Map ? map : null;
7855
+ return map instanceof Y6.Map ? map : null;
7533
7856
  } catch {
7534
7857
  return null;
7535
7858
  }
@@ -7539,11 +7862,11 @@ function readModelMeta(metaMap) {
7539
7862
  let constraints;
7540
7863
  let relationships;
7541
7864
  for (const [key, value] of metaMap.entries()) {
7542
- if (key === "_constraints" && value instanceof Y4.Map) {
7865
+ if (key === "_constraints" && value instanceof Y6.Map) {
7543
7866
  constraints = readConstraints(value);
7544
- } else if (key === "_relationships" && value instanceof Y4.Map) {
7867
+ } else if (key === "_relationships" && value instanceof Y6.Map) {
7545
7868
  relationships = readRelationships(value);
7546
- } else if (value instanceof Y4.Map) {
7869
+ } else if (value instanceof Y6.Map) {
7547
7870
  fields[key] = readFieldMeta(value);
7548
7871
  }
7549
7872
  }
@@ -7578,7 +7901,7 @@ function inferModelFromData(dataMap) {
7578
7901
  const fields = {};
7579
7902
  let sampled = 0;
7580
7903
  for (const [_recordId, recordValue] of dataMap.entries()) {
7581
- if (!(recordValue instanceof Y4.Map)) continue;
7904
+ if (!(recordValue instanceof Y6.Map)) continue;
7582
7905
  if (++sampled > 5) break;
7583
7906
  for (const [fieldName, value] of recordValue.entries()) {
7584
7907
  if (fieldName.startsWith("_")) continue;
@@ -7606,7 +7929,7 @@ function inferTypeFromValue(value) {
7606
7929
  function readConstraints(constraintsMap) {
7607
7930
  const out = {};
7608
7931
  for (const [name, value] of constraintsMap.entries()) {
7609
- if (!(value instanceof Y4.Map)) continue;
7932
+ if (!(value instanceof Y6.Map)) continue;
7610
7933
  let fields = [];
7611
7934
  const rawFields = value.get("fields");
7612
7935
  if (typeof rawFields === "string") {
@@ -7625,7 +7948,7 @@ function readConstraints(constraintsMap) {
7625
7948
  function readRelationships(relsMap) {
7626
7949
  const out = {};
7627
7950
  for (const [name, value] of relsMap.entries()) {
7628
- if (!(value instanceof Y4.Map)) continue;
7951
+ if (!(value instanceof Y6.Map)) continue;
7629
7952
  const rel = {};
7630
7953
  for (const [k, v] of value.entries()) {
7631
7954
  rel[k] = v;
@@ -7699,19 +8022,20 @@ function schemaToToml(schema) {
7699
8022
  }
7700
8023
 
7701
8024
  // src/index.ts
8025
+ init_uniqueIndex();
7702
8026
  init_BaseModel();
7703
8027
 
7704
8028
  // src/utils/yDocDump.ts
7705
- var Y5 = __toESM(require("yjs"), 1);
8029
+ var Y7 = __toESM(require("yjs"), 1);
7706
8030
  function toPlain(value) {
7707
- if (value instanceof Y5.Map) {
8031
+ if (value instanceof Y7.Map) {
7708
8032
  const obj = {};
7709
8033
  value.forEach((v, k) => {
7710
8034
  obj[k] = toPlain(v);
7711
8035
  });
7712
8036
  return obj;
7713
8037
  }
7714
- if (value instanceof Y5.Array) {
8038
+ if (value instanceof Y7.Array) {
7715
8039
  return value.toArray().map((item) => toPlain(item));
7716
8040
  }
7717
8041
  return value;
@@ -7720,7 +8044,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7720
8044
  const result = {};
7721
8045
  const includeIndexes = !!options.includeIndexes;
7722
8046
  yDoc.share.forEach((type, name) => {
7723
- if (!(type instanceof Y5.Map)) return;
8047
+ if (!(type instanceof Y7.Map)) return;
7724
8048
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7725
8049
  const records = {};
7726
8050
  type.forEach((recordValue, recordId) => {
@@ -7769,12 +8093,14 @@ function summarizePlainYDoc(dump) {
7769
8093
  attachAndRegisterModel,
7770
8094
  attachSchemaToClass,
7771
8095
  autoRegisterModel,
8096
+ buildUniqueKey,
7772
8097
  clearMetaSyncCache,
7773
8098
  createModelClass,
7774
8099
  defineModelSchema,
7775
8100
  discoverModelNames,
7776
8101
  discoverSchema,
7777
8102
  dumpYDocToPlain,
8103
+ fieldUniqueConstraintName,
7778
8104
  generateULID,
7779
8105
  inferFieldType,
7780
8106
  initJsBao,
@@ -7785,5 +8111,6 @@ function summarizePlainYDoc(dump) {
7785
8111
  schemaToToml,
7786
8112
  summarizePlainYDoc,
7787
8113
  syncInferredMeta,
7788
- syncModelMeta
8114
+ syncModelMeta,
8115
+ uniqueIndexMapName
7789
8116
  });