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.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,11 +1428,37 @@ 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/query/IncludeResolver.ts
1340
1463
  var IncludeResolver_exports = {};
1341
1464
  __export(IncludeResolver_exports, {
@@ -1694,7 +1817,13 @@ var init_IncludeResolver = __esm({
1694
1817
  }
1695
1818
  return null;
1696
1819
  }
1697
- /** Apply projection to a single record. */
1820
+ /**
1821
+ * Apply a projection to a single record. Assumes the projection was already
1822
+ * validated upstream (mixed inclusion+exclusion rejected by the translator):
1823
+ * `isInclusion` treats "any value === 1" as inclusion mode. `id` and `type`
1824
+ * are system-identity fields and are always kept in BOTH modes (mirrors the
1825
+ * DatabaseDO `_applyProjection`); `_meta` stays excludable.
1826
+ */
1698
1827
  _applyProjection(record, projection) {
1699
1828
  const fields = Object.entries(projection);
1700
1829
  if (fields.length === 0) return record;
@@ -1713,7 +1842,7 @@ var init_IncludeResolver = __esm({
1713
1842
  } else {
1714
1843
  const result = { ...record };
1715
1844
  for (const [f, v] of fields) {
1716
- if (v === 0) delete result[f];
1845
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
1717
1846
  }
1718
1847
  return result;
1719
1848
  }
@@ -1732,10 +1861,10 @@ __export(BaseModel_exports, {
1732
1861
  UniqueConstraintViolationError: () => UniqueConstraintViolationError,
1733
1862
  generateULID: () => generateULID
1734
1863
  });
1735
- import * as Y3 from "yjs";
1736
- import { ulid } from "ulid";
1864
+ import * as Y5 from "yjs";
1865
+ import { ulid as ulid2 } from "ulid";
1737
1866
  function generateULID() {
1738
- return ulid();
1867
+ return ulid2();
1739
1868
  }
1740
1869
  var LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel;
1741
1870
  var init_BaseModel = __esm({
@@ -1747,7 +1876,9 @@ var init_BaseModel = __esm({
1747
1876
  init_CursorManager();
1748
1877
  init_sql();
1749
1878
  init_metaSync();
1879
+ init_writeLifecycle();
1750
1880
  init_yjsValues();
1881
+ init_uniqueIndex();
1751
1882
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
1752
1883
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
1753
1884
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2132,7 +2263,9 @@ var init_BaseModel = __esm({
2132
2263
  this.setValue(fieldKey, data[fieldKey]);
2133
2264
  this._constructorProvidedFields.add(fieldKey);
2134
2265
  } else if (fieldOptions.default !== void 0) {
2135
- const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
2266
+ const defaultValue = resolveLiteralOrFunctionDefault(
2267
+ fieldOptions.default
2268
+ );
2136
2269
  this.setValue(fieldKey, defaultValue);
2137
2270
  }
2138
2271
  }
@@ -2424,7 +2557,7 @@ var init_BaseModel = __esm({
2424
2557
  }
2425
2558
  getStringSetFromYjs(fieldName) {
2426
2559
  const yjsData = this.getFromYjs(fieldName);
2427
- if (yjsData instanceof Y3.Map) {
2560
+ if (yjsData instanceof Y5.Map) {
2428
2561
  return Array.from(yjsData.keys());
2429
2562
  }
2430
2563
  if (yjsData && typeof yjsData === "object") {
@@ -2532,7 +2665,7 @@ var init_BaseModel = __esm({
2532
2665
  }
2533
2666
  const extractStringSetValues = (value) => {
2534
2667
  if (!value) return [];
2535
- if (value instanceof Y3.Map) {
2668
+ if (value instanceof Y5.Map) {
2536
2669
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
2537
2670
  }
2538
2671
  if (Array.isArray(value)) {
@@ -2549,7 +2682,7 @@ var init_BaseModel = __esm({
2549
2682
  if (!recordId) continue;
2550
2683
  let itemData;
2551
2684
  const stringSetValuesByField = {};
2552
- if (recordData instanceof Y3.Map) {
2685
+ if (recordData instanceof Y5.Map) {
2553
2686
  itemData = {};
2554
2687
  const unknownFields = [];
2555
2688
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2665,20 +2798,10 @@ var init_BaseModel = __esm({
2665
2798
  Logger.verbose(
2666
2799
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
2667
2800
  );
2668
- const buildUniqueKey = (recordData, fields) => {
2669
- const keyParts = [];
2670
- for (const field of fields) {
2671
- const value = recordData instanceof Y3.Map ? recordData.get(field) : recordData[field];
2672
- if (value === null || value === void 0) {
2673
- return null;
2674
- }
2675
- keyParts.push(String(value));
2676
- }
2677
- return keyParts.join("|");
2678
- };
2801
+ const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
2679
2802
  const extractItemData = (key, recordData) => {
2680
2803
  let itemData;
2681
- if (recordData instanceof Y3.Map) {
2804
+ if (recordData instanceof Y5.Map) {
2682
2805
  itemData = {};
2683
2806
  const unknownFields = [];
2684
2807
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2762,7 +2885,7 @@ var init_BaseModel = __esm({
2762
2885
  const recordsByUniqueKey = /* @__PURE__ */ new Map();
2763
2886
  for (const [recordId, recordData] of allRecords.entries()) {
2764
2887
  if (recordIdsToDiscard.has(recordId)) continue;
2765
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2888
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2766
2889
  if (uniqueKey === null) continue;
2767
2890
  if (!recordsByUniqueKey.has(uniqueKey)) {
2768
2891
  recordsByUniqueKey.set(uniqueKey, []);
@@ -2808,7 +2931,7 @@ var init_BaseModel = __esm({
2808
2931
  if (!recordData || !key) continue;
2809
2932
  const itemData = extractItemData(key, recordData);
2810
2933
  if (!itemData) continue;
2811
- if (change.action === "add" && recordData instanceof Y3.Map) {
2934
+ if (change.action === "add" && recordData instanceof Y5.Map) {
2812
2935
  Logger.verbose(
2813
2936
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2814
2937
  );
@@ -2906,9 +3029,12 @@ var init_BaseModel = __esm({
2906
3029
  const recordData = documentYMap.get(idToDiscard);
2907
3030
  if (!recordData) continue;
2908
3031
  for (const constraint of schema.resolvedUniqueConstraints) {
2909
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3032
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2910
3033
  if (uniqueKey === null) continue;
2911
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3034
+ const constraintMapName = uniqueIndexMapName(
3035
+ modelName,
3036
+ constraint.name
3037
+ );
2912
3038
  const constraintMap = yDoc.getMap(constraintMapName);
2913
3039
  const currentIndexValue = constraintMap.get(uniqueKey);
2914
3040
  if (currentIndexValue === idToDiscard) {
@@ -2926,11 +3052,14 @@ var init_BaseModel = __esm({
2926
3052
  }, `conflict-resolution-${modelName}-${docId}`);
2927
3053
  yDoc.transact(() => {
2928
3054
  for (const constraint of schema.resolvedUniqueConstraints) {
2929
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3055
+ const constraintMapName = uniqueIndexMapName(
3056
+ modelName,
3057
+ constraint.name
3058
+ );
2930
3059
  const constraintMap = yDoc.getMap(constraintMapName);
2931
3060
  for (const [recordId, recordData] of documentYMap.entries()) {
2932
3061
  if (!recordData) continue;
2933
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3062
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2934
3063
  if (uniqueKey === null) continue;
2935
3064
  const currentIndexValue = constraintMap.get(uniqueKey);
2936
3065
  if (currentIndexValue !== recordId) {
@@ -2953,7 +3082,7 @@ var init_BaseModel = __esm({
2953
3082
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
2954
3083
  );
2955
3084
  for (const [recordId, recordData] of documentYMap.entries()) {
2956
- if (recordData instanceof Y3.Map) {
3085
+ if (recordData instanceof Y5.Map) {
2957
3086
  Logger.verbose(
2958
3087
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
2959
3088
  );
@@ -3179,26 +3308,7 @@ var init_BaseModel = __esm({
3179
3308
  * if the field hasn't been touched since the wire-format change in #561.
3180
3309
  */
3181
3310
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3182
- let nested = recordYMap.get(fieldName);
3183
- if (!(nested instanceof Y3.Map)) {
3184
- const migrated = new Y3.Map();
3185
- if (nested && typeof nested === "object") {
3186
- for (const [member, marker] of Object.entries(
3187
- nested
3188
- )) {
3189
- if (Boolean(marker)) migrated.set(member, true);
3190
- }
3191
- }
3192
- recordYMap.set(fieldName, migrated);
3193
- nested = migrated;
3194
- }
3195
- const target = nested;
3196
- for (const member of change.additions) {
3197
- target.set(member, true);
3198
- }
3199
- for (const member of change.removals) {
3200
- target.delete(member);
3201
- }
3311
+ applyStringSetChange(recordYMap, fieldName, change);
3202
3312
  }
3203
3313
  /**
3204
3314
  * Deep equality check for comparing field values
@@ -3400,6 +3510,34 @@ var init_BaseModel = __esm({
3400
3510
  `[${modelName}.save] getCurrentJSState() returned:`,
3401
3511
  dataToSave
3402
3512
  );
3513
+ const uniqueMissConflicts = /* @__PURE__ */ new Map();
3514
+ for (const constraint of schema.resolvedUniqueConstraints) {
3515
+ const newUniqueKey = this._buildUniqueKey(
3516
+ constraint.fields,
3517
+ dataToSave,
3518
+ modelName,
3519
+ constraint.name
3520
+ );
3521
+ if (newUniqueKey === null) {
3522
+ continue;
3523
+ }
3524
+ const constraintMap = yDoc.getMap(
3525
+ uniqueIndexMapName(modelName, constraint.name)
3526
+ );
3527
+ if (!constraintMap.get(newUniqueKey)) {
3528
+ const constraintKeyValues = constraint.fields.map(
3529
+ (field) => dataToSave[field]
3530
+ );
3531
+ const conflictingRecord = await modelConstructor._resolveByUniqueFallback(
3532
+ constraint,
3533
+ constraintKeyValues,
3534
+ { excludeId: this.id }
3535
+ );
3536
+ if (conflictingRecord) {
3537
+ uniqueMissConflicts.set(constraint.name, conflictingRecord.id);
3538
+ }
3539
+ }
3540
+ }
3403
3541
  await yDoc.transact(async () => {
3404
3542
  if (upsertOnConstraint && upsertOnKey) {
3405
3543
  const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
@@ -3441,7 +3579,7 @@ var init_BaseModel = __esm({
3441
3579
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
3442
3580
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
3443
3581
  if (!recordYMap) {
3444
- recordYMap = new Y3.Map();
3582
+ recordYMap = new Y5.Map();
3445
3583
  Logger.verbose(
3446
3584
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
3447
3585
  );
@@ -3493,7 +3631,10 @@ var init_BaseModel = __esm({
3493
3631
  );
3494
3632
  continue;
3495
3633
  }
3496
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3634
+ const constraintMapName = uniqueIndexMapName(
3635
+ modelName,
3636
+ constraint.name
3637
+ );
3497
3638
  const constraintMap = yDoc.getMap(constraintMapName);
3498
3639
  Logger.verbose(
3499
3640
  `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
@@ -3522,6 +3663,26 @@ var init_BaseModel = __esm({
3522
3663
  this.id,
3523
3664
  existingRecordIdWithNewKey
3524
3665
  );
3666
+ } else if (!existingRecordIdWithNewKey) {
3667
+ const conflictingRecordId = uniqueMissConflicts.get(constraint.name);
3668
+ if (conflictingRecordId && conflictingRecordId !== this.id) {
3669
+ Logger.verbose(
3670
+ `[${modelName}] Save Check (Item '${this.id}'): index miss for '${constraint.name}', but engine fallback found conflicting index-less record ${conflictingRecordId}.`
3671
+ );
3672
+ throw new UniqueConstraintViolationError(
3673
+ `Unique constraint '${constraint.name}' violated for model '${modelName}' on fields [${constraint.fields.join(
3674
+ ", "
3675
+ )}]. Attempted ID: ${this.id}. Value(s) starting with '${newUniqueKey.substring(
3676
+ 0,
3677
+ 50
3678
+ )}...' already exist for index-less record ID ${conflictingRecordId}.`,
3679
+ modelName,
3680
+ constraint.name,
3681
+ constraint.fields,
3682
+ this.id,
3683
+ conflictingRecordId
3684
+ );
3685
+ }
3525
3686
  }
3526
3687
  }
3527
3688
  if (isUpdate && oldData) {
@@ -3865,34 +4026,46 @@ var init_BaseModel = __esm({
3865
4026
  const modelName = schema.options.name;
3866
4027
  Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
3867
4028
  const hasIncludes = options?.include && options.include.length > 0;
3868
- const translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4029
+ const requestedLimit = options?.limit;
4030
+ const overFetch = typeof requestedLimit === "number" && requestedLimit > 0;
4031
+ let translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4032
+ if (overFetch) {
4033
+ translatorOptions = {
4034
+ ...translatorOptions || {},
4035
+ limit: requestedLimit + 1
4036
+ };
4037
+ }
3869
4038
  const translator = new DocumentQueryTranslator(modelName, schema.fields);
3870
4039
  const translatedQuery = translator.translateFind(filter, translatorOptions);
3871
4040
  Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
3872
4041
  Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
3873
- const results = await _BaseModel.dbInstance.query(
4042
+ let results = await _BaseModel.dbInstance.query(
3874
4043
  translatedQuery.sql,
3875
4044
  translatedQuery.params
3876
4045
  );
3877
4046
  Logger.verbose(`[${modelName}] Query returned ${results.length} results`);
4047
+ let hasMore = false;
4048
+ if (overFetch && results.length > requestedLimit) {
4049
+ hasMore = true;
4050
+ results = results.slice(0, requestedLimit);
4051
+ }
3878
4052
  if (options?.include && options.include.length > 0) {
3879
4053
  const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
3880
4054
  const resolver = new IncludeResolver2(_BaseModel.dbInstance);
3881
4055
  await resolver.resolve(results, options.include, 0, modelName);
3882
4056
  }
3883
- const requestedLimit = options?.limit;
3884
- const hasMore = CursorManager.hasMoreResults(
3885
- requestedLimit,
3886
- results.length
3887
- );
3888
4057
  const isFirstPage = !options?.uniqueStartKey;
4058
+ const direction = options?.direction || 1;
3889
4059
  const cursors = CursorManager.generateResultCursors(
3890
4060
  results,
3891
4061
  translatedQuery.sortFields,
3892
- options?.direction || 1,
4062
+ direction,
3893
4063
  hasMore,
3894
4064
  isFirstPage
3895
4065
  );
4066
+ if (direction === -1) {
4067
+ results.reverse();
4068
+ }
3896
4069
  let transformedData;
3897
4070
  if (options?.projection && Object.keys(options.projection).length > 0) {
3898
4071
  transformedData = results.map((row) => {
@@ -4468,7 +4641,7 @@ var init_BaseModel = __esm({
4468
4641
  );
4469
4642
  }
4470
4643
  for (const [recordId, recordYMap] of documentYMap.entries()) {
4471
- if (recordYMap instanceof Y3.Map) {
4644
+ if (recordYMap instanceof Y5.Map) {
4472
4645
  const instance = new this({ id: recordId });
4473
4646
  instance._metaDocId = docId;
4474
4647
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -4493,6 +4666,50 @@ var init_BaseModel = __esm({
4493
4666
  }
4494
4667
  return allInstances;
4495
4668
  }
4669
+ /**
4670
+ * Resolve a record by its unique-constraint field values via an engine query.
4671
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
4672
+ * entry for a key.
4673
+ *
4674
+ * The index map is a cache over the SQLite projection, not the source of
4675
+ * truth: every record is loaded into the projection on `initializeForDocument`
4676
+ * regardless of index presence, but a record written by a client predating
4677
+ * index maintenance (legacy data) is missing from the index. Such a record
4678
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
4679
+ * check. A query over the constraint fields finds it reliably.
4680
+ *
4681
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
4682
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
4683
+ * whether to heal the missing index entry (write paths) and do their own
4684
+ * logging — this method never mutates the index, so read paths stay
4685
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
4686
+ * constraint field means the record does not participate in the constraint,
4687
+ * so no query runs.
4688
+ */
4689
+ static async _resolveByUniqueFallback(constraint, keyValues, options) {
4690
+ if (!_BaseModel.dbInstance) {
4691
+ return null;
4692
+ }
4693
+ const filter = {};
4694
+ for (let i = 0; i < constraint.fields.length; i++) {
4695
+ const value = keyValues[i];
4696
+ if (value === null || value === void 0) {
4697
+ return null;
4698
+ }
4699
+ filter[constraint.fields[i]] = value;
4700
+ }
4701
+ if (options?.excludeId) {
4702
+ if ("id" in filter) {
4703
+ if (filter.id === options.excludeId) {
4704
+ return null;
4705
+ }
4706
+ } else {
4707
+ filter.id = { $ne: options.excludeId };
4708
+ }
4709
+ }
4710
+ const found = await this.queryOne(filter);
4711
+ return found ?? null;
4712
+ }
4496
4713
  static async findByUnique(constraintName, value) {
4497
4714
  const modelConstructor = this;
4498
4715
  const schema = modelConstructor.getSchema();
@@ -4531,7 +4748,10 @@ var init_BaseModel = __esm({
4531
4748
  const documentInfo = modelConstructor.connectedDocuments.get(docId);
4532
4749
  if (!documentInfo) continue;
4533
4750
  const yDoc = documentInfo.yDoc;
4534
- const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4751
+ const constraintMapName2 = uniqueIndexMapName(
4752
+ currentModelName,
4753
+ constraint.name
4754
+ );
4535
4755
  const constraintMap2 = yDoc.getMap(constraintMapName2);
4536
4756
  const recordId2 = constraintMap2.get(uniqueKeyString);
4537
4757
  if (recordId2) {
@@ -4539,7 +4759,7 @@ var init_BaseModel = __esm({
4539
4759
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
4540
4760
  if (documentYMap) {
4541
4761
  const recordYMap2 = documentYMap.get(recordId2);
4542
- if (recordYMap2 && recordYMap2 instanceof Y3.Map) {
4762
+ if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
4543
4763
  const instance = new modelConstructor({
4544
4764
  id: recordId2
4545
4765
  });
@@ -4553,6 +4773,16 @@ var init_BaseModel = __esm({
4553
4773
  }
4554
4774
  }
4555
4775
  }
4776
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
4777
+ constraint,
4778
+ keyValues
4779
+ );
4780
+ if (fallbackRecord) {
4781
+ Logger.verbose(
4782
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
4783
+ );
4784
+ return fallbackRecord;
4785
+ }
4556
4786
  Logger.verbose(
4557
4787
  `[${currentModelName}.findByUnique] Record not found in any connected document`
4558
4788
  );
@@ -4566,17 +4796,30 @@ var init_BaseModel = __esm({
4566
4796
  `[${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.`
4567
4797
  );
4568
4798
  }
4569
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4799
+ const constraintMapName = uniqueIndexMapName(
4800
+ currentModelName,
4801
+ constraint.name
4802
+ );
4570
4803
  const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
4571
4804
  const recordId = constraintMap.get(uniqueKeyString);
4572
4805
  if (!recordId) {
4806
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
4807
+ constraint,
4808
+ keyValues
4809
+ );
4810
+ if (fallbackRecord) {
4811
+ Logger.verbose(
4812
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
4813
+ );
4814
+ return fallbackRecord;
4815
+ }
4573
4816
  return null;
4574
4817
  }
4575
4818
  const recordYMap = legacyDocumentMap.get(recordId);
4576
4819
  if (!recordYMap) {
4577
4820
  return null;
4578
4821
  }
4579
- if (recordYMap instanceof Y3.Map) {
4822
+ if (recordYMap instanceof Y5.Map) {
4580
4823
  const instance = new modelConstructor({
4581
4824
  id: recordId
4582
4825
  });
@@ -4634,7 +4877,10 @@ var init_BaseModel = __esm({
4634
4877
  `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
4635
4878
  );
4636
4879
  }
4637
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4880
+ const constraintMapName = uniqueIndexMapName(
4881
+ currentModelName,
4882
+ constraint.name
4883
+ );
4638
4884
  let existingRecord = null;
4639
4885
  if (connectedDocuments && connectedDocuments.size > 0) {
4640
4886
  for (const docId of connectedDocuments.keys()) {
@@ -4652,6 +4898,33 @@ var init_BaseModel = __esm({
4652
4898
  }
4653
4899
  } else {
4654
4900
  }
4901
+ if (!existingRecord) {
4902
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
4903
+ constraint,
4904
+ keyValuesForLookup
4905
+ );
4906
+ if (fallbackRecord) {
4907
+ existingRecord = fallbackRecord;
4908
+ const healDocId = fallbackRecord._metaDocId;
4909
+ const healDocInfo = healDocId ? connectedDocuments.get(healDocId) : void 0;
4910
+ const canHeal = !!healDocInfo && healDocInfo.permissionHint !== "read";
4911
+ if (healDocInfo && canHeal) {
4912
+ healDocInfo.yDoc.transact(() => {
4913
+ const healMap = healDocInfo.yDoc.getMap(constraintMapName);
4914
+ if (healMap.get(uniqueKeyString) !== fallbackRecord.id) {
4915
+ healMap.set(uniqueKeyString, fallbackRecord.id);
4916
+ }
4917
+ }, `heal-unique-index-${currentModelName}-${healDocId}`);
4918
+ Logger.verbose(
4919
+ `[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; healed missing index entry (legacy index-less record).`
4920
+ );
4921
+ } else {
4922
+ Logger.verbose(
4923
+ `[${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).`
4924
+ );
4925
+ }
4926
+ }
4927
+ }
4655
4928
  if (options?.objectMustExist) {
4656
4929
  if (!existingRecord) {
4657
4930
  throw new RecordNotFoundError(
@@ -4808,7 +5081,7 @@ var init_BaseModel = __esm({
4808
5081
  for (const [fieldKey, fieldOptions] of schema.fields) {
4809
5082
  if (fieldOptions?.type !== "stringset") continue;
4810
5083
  const value = recordYMap.get(fieldKey);
4811
- if (value instanceof Y3.Map) {
5084
+ if (value instanceof Y5.Map) {
4812
5085
  this.observeStringSetMapOnce(value);
4813
5086
  }
4814
5087
  }
@@ -4994,6 +5267,19 @@ var init_BaseModel = __esm({
4994
5267
  });
4995
5268
 
4996
5269
  // src/models/relationshipManager.ts
5270
+ function warnIfOrderFieldNullable(modelClass, orderByField, relationshipDescription) {
5271
+ if (ALWAYS_PRESENT_ORDER_FIELDS.has(orderByField)) return;
5272
+ const schema = modelClass.getSchema?.();
5273
+ const fields = schema?.fields;
5274
+ if (!fields || typeof fields.get !== "function") return;
5275
+ const fieldOptions = fields.get(orderByField);
5276
+ if (!fieldOptions) return;
5277
+ if (fieldOptions.required !== true) {
5278
+ Logger.warn(
5279
+ `[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.)`
5280
+ );
5281
+ }
5282
+ }
4997
5283
  function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
4998
5284
  return async function() {
4999
5285
  const relatedId = this[config.relatedIdField];
@@ -5002,6 +5288,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
5002
5288
  };
5003
5289
  }
5004
5290
  function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbEngine) {
5291
+ const {
5292
+ relatedIdField,
5293
+ orderByField = "id",
5294
+ orderDirection = "ASC"
5295
+ } = config;
5296
+ warnIfOrderFieldNullable(
5297
+ targetModelClass,
5298
+ orderByField,
5299
+ `hasMany(${targetModelClass.name})`
5300
+ );
5005
5301
  return async function(paginationArgs) {
5006
5302
  const {
5007
5303
  limit,
@@ -5009,71 +5305,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
5009
5305
  beforeCursor,
5010
5306
  direction = "forward"
5011
5307
  } = paginationArgs || {};
5012
- const {
5013
- relatedIdField,
5014
- orderByField = "id",
5015
- orderDirection = "ASC"
5016
- } = config;
5017
5308
  const targetSchema = targetModelClass.getSchema();
5018
5309
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
5019
5310
  Logger.error(
5020
5311
  `[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
5021
5312
  );
5022
- return { data: [], nextCursor: null, prevCursor: null };
5313
+ return { data: [], hasMore: false };
5023
5314
  }
5024
- const filter = { [relatedIdField]: this.id };
5315
+ const forward = direction !== "backward";
5316
+ const cursor = forward ? afterCursor : beforeCursor;
5025
5317
  const queryOptions = {
5026
- sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
5318
+ sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
5319
+ direction: forward ? 1 : -1
5027
5320
  };
5028
- if (limit !== void 0) {
5029
- queryOptions.limit = limit + 1;
5030
- }
5031
- if (direction === "forward" && afterCursor) {
5032
- filter[orderByField] = {
5033
- [orderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
5034
- };
5035
- } else if (direction === "backward") {
5036
- if (beforeCursor) {
5037
- filter[orderByField] = {
5038
- [orderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
5039
- };
5040
- }
5041
- queryOptions.sort = { [orderByField]: orderDirection === "ASC" ? -1 : 1 };
5042
- }
5043
- const result = await targetModelClass.query(filter, queryOptions);
5044
- const results = result.data;
5045
- let nextCursor = null;
5046
- let prevCursor = null;
5047
- const isFirstPage = direction === "forward" && !afterCursor;
5048
- if (limit !== void 0) {
5049
- if (direction === "forward") {
5050
- if (results.length > limit) {
5051
- nextCursor = results[limit - 1][orderByField];
5052
- results.pop();
5053
- }
5054
- if (results.length > 0 && !isFirstPage) {
5055
- prevCursor = results[0][orderByField];
5056
- }
5057
- } else {
5058
- if (results.length > limit) {
5059
- prevCursor = results[limit - 1][orderByField];
5060
- results.pop();
5061
- }
5062
- results.reverse();
5063
- if (results.length > 0 && beforeCursor) {
5064
- nextCursor = results[results.length - 1][orderByField];
5065
- }
5066
- if (results.length > 0 && !beforeCursor) {
5067
- if (results.length === limit) {
5068
- prevCursor = results[0][orderByField];
5069
- }
5070
- }
5071
- }
5072
- }
5073
- return { data: results, nextCursor, prevCursor };
5321
+ if (limit !== void 0) queryOptions.limit = limit;
5322
+ if (cursor != null) queryOptions.uniqueStartKey = cursor;
5323
+ return targetModelClass.query(
5324
+ { [relatedIdField]: this.id },
5325
+ queryOptions
5326
+ );
5074
5327
  };
5075
5328
  }
5076
5329
  function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClass, joinModelClass, _dbEngine) {
5330
+ const {
5331
+ joinModelLocalField,
5332
+ joinModelRelatedField,
5333
+ joinModelOrderByField = "id",
5334
+ joinModelOrderDirection = "ASC"
5335
+ } = config;
5336
+ warnIfOrderFieldNullable(
5337
+ joinModelClass,
5338
+ joinModelOrderByField,
5339
+ `hasManyThrough(${targetModelClass.name} via ${joinModelClass.name})`
5340
+ );
5077
5341
  return async function(paginationArgs) {
5078
5342
  const {
5079
5343
  limit,
@@ -5087,87 +5351,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
5087
5351
  Logger.error(
5088
5352
  `[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
5089
5353
  );
5090
- return { data: [], nextCursor: null, prevCursor: null };
5354
+ return { data: [], hasMore: false };
5091
5355
  }
5092
5356
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
5093
5357
  Logger.error(
5094
5358
  `[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
5095
5359
  );
5096
- return { data: [], nextCursor: null, prevCursor: null };
5360
+ return { data: [], hasMore: false };
5097
5361
  }
5098
- const {
5099
- joinModelLocalField,
5100
- joinModelRelatedField,
5101
- joinModelOrderByField = "id",
5102
- joinModelOrderDirection = "ASC"
5103
- } = config;
5104
- const joinFilter = { [joinModelLocalField]: this.id };
5362
+ const forward = direction !== "backward";
5363
+ const cursor = forward ? afterCursor : beforeCursor;
5105
5364
  const joinQueryOptions = {
5106
5365
  sort: {
5107
5366
  [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
5108
5367
  },
5109
- projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
5368
+ projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
5369
+ direction: forward ? 1 : -1
5110
5370
  };
5111
- if (limit !== void 0) {
5112
- joinQueryOptions.limit = limit + 1;
5113
- }
5114
- if (direction === "forward" && afterCursor) {
5115
- joinFilter[joinModelOrderByField] = {
5116
- [joinModelOrderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
5117
- };
5118
- } else if (direction === "backward") {
5119
- if (beforeCursor) {
5120
- joinFilter[joinModelOrderByField] = {
5121
- [joinModelOrderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
5122
- };
5123
- }
5124
- joinQueryOptions.sort = {
5125
- [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? -1 : 1
5126
- };
5127
- }
5371
+ if (limit !== void 0) joinQueryOptions.limit = limit;
5372
+ if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
5128
5373
  const joinResult = await joinModelClass.query(
5129
- joinFilter,
5374
+ { [joinModelLocalField]: this.id },
5130
5375
  joinQueryOptions
5131
5376
  );
5132
- const joinResults = joinResult.data;
5133
- let nextCursorFromJoin = null;
5134
- let prevCursorFromJoin = null;
5135
- const isFirstPage = direction === "forward" && !afterCursor;
5136
- if (limit !== void 0) {
5137
- if (direction === "forward") {
5138
- if (joinResults.length > limit) {
5139
- nextCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
5140
- joinResults.pop();
5141
- }
5142
- if (joinResults.length > 0 && !isFirstPage) {
5143
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
5144
- }
5145
- } else {
5146
- if (joinResults.length > limit) {
5147
- prevCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
5148
- joinResults.pop();
5149
- }
5150
- joinResults.reverse();
5151
- if (joinResults.length > 0 && beforeCursor) {
5152
- nextCursorFromJoin = joinResults[joinResults.length - 1][joinModelOrderByField];
5153
- }
5154
- if (joinResults.length > 0 && !beforeCursor) {
5155
- if (joinResults.length === limit) {
5156
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
5157
- }
5158
- }
5159
- }
5377
+ const relatedIds = joinResult.data.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
5378
+ let targetData = [];
5379
+ if (relatedIds.length > 0) {
5380
+ const targetResult = await targetModelClass.query({
5381
+ id: { $in: relatedIds }
5382
+ });
5383
+ targetData = targetResult.data;
5160
5384
  }
5161
- const relatedIds = joinResults.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
5162
- if (relatedIds.length === 0)
5163
- return { data: [], nextCursor: null, prevCursor: null };
5164
- const targetResult = await targetModelClass.query({
5165
- id: { $in: relatedIds }
5166
- });
5167
5385
  return {
5168
- data: targetResult.data,
5169
- nextCursor: nextCursorFromJoin,
5170
- prevCursor: prevCursorFromJoin
5386
+ data: targetData,
5387
+ nextCursor: joinResult.nextCursor,
5388
+ prevCursor: joinResult.prevCursor,
5389
+ hasMore: joinResult.hasMore
5171
5390
  };
5172
5391
  };
5173
5392
  }
@@ -5291,10 +5510,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
5291
5510
  }, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
5292
5511
  };
5293
5512
  }
5513
+ var ALWAYS_PRESENT_ORDER_FIELDS;
5294
5514
  var init_relationshipManager = __esm({
5295
5515
  "src/models/relationshipManager.ts"() {
5296
5516
  "use strict";
5297
5517
  init_BaseModel();
5518
+ ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
5298
5519
  }
5299
5520
  });
5300
5521
 
@@ -5764,13 +5985,100 @@ var init_environment = __esm({
5764
5985
  // src/engines/SqljsEngine.ts
5765
5986
  import initSqlJs from "sql.js";
5766
5987
  import { Mutex } from "async-mutex";
5767
- var SQL_WASM_URL, TransactionalSQLJSOperations, SqljsEngine;
5988
+ async function resolveNodeWasmPath() {
5989
+ if (!isNode()) {
5990
+ return null;
5991
+ }
5992
+ try {
5993
+ const cjsRequire = typeof __require !== "undefined" ? __require : void 0;
5994
+ if (cjsRequire) {
5995
+ return cjsRequire.resolve("sql.js/dist/sql-wasm.wasm");
5996
+ }
5997
+ } catch {
5998
+ }
5999
+ try {
6000
+ const metaUrl = import.meta.url;
6001
+ if (metaUrl) {
6002
+ const { createRequire } = await import(
6003
+ /* @vite-ignore */
6004
+ "module"
6005
+ );
6006
+ const req = createRequire(metaUrl);
6007
+ return req.resolve("sql.js/dist/sql-wasm.wasm");
6008
+ }
6009
+ } catch {
6010
+ }
6011
+ return null;
6012
+ }
6013
+ async function preflightNodeWasm(wasmPath) {
6014
+ if (!isNode()) {
6015
+ return;
6016
+ }
6017
+ if (/^[a-z]+:\/\//i.test(wasmPath)) {
6018
+ return;
6019
+ }
6020
+ const { existsSync } = await import(
6021
+ /* @vite-ignore */
6022
+ "fs"
6023
+ );
6024
+ if (!existsSync(wasmPath)) {
6025
+ throw new Error(
6026
+ `[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()).`
6027
+ );
6028
+ }
6029
+ }
6030
+ async function loadSqlJsModule(locateFile) {
6031
+ let abortReject;
6032
+ const toError = (reason) => reason instanceof Error ? reason : new Error(
6033
+ `[SqljsEngine] sql.js aborted during initialization: ${String(
6034
+ reason
6035
+ )}`
6036
+ );
6037
+ const nodeProcess = isNode() && typeof process !== "undefined" ? process : void 0;
6038
+ const onUnhandled = (reason) => {
6039
+ const err = toError(reason);
6040
+ if (/Aborted/.test(err.message) || /Aborted/.test(String(reason))) {
6041
+ abortReject?.(err);
6042
+ return;
6043
+ }
6044
+ if (nodeProcess && nodeProcess.listenerCount("unhandledRejection") === 1) {
6045
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6046
+ void Promise.reject(reason);
6047
+ }
6048
+ };
6049
+ if (nodeProcess) {
6050
+ nodeProcess.on("unhandledRejection", onUnhandled);
6051
+ }
6052
+ const abortGuard = new Promise((_resolve, reject) => {
6053
+ abortReject = reject;
6054
+ });
6055
+ abortGuard.catch(() => {
6056
+ });
6057
+ try {
6058
+ const initPromise = initSqlJs({
6059
+ locateFile,
6060
+ // Layer 1: emscripten onAbort.
6061
+ onAbort: (reason) => abortReject?.(toError(reason))
6062
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6063
+ });
6064
+ return await Promise.race([initPromise, abortGuard]);
6065
+ } finally {
6066
+ if (nodeProcess) {
6067
+ setTimeout(() => {
6068
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6069
+ }, 0);
6070
+ }
6071
+ }
6072
+ }
6073
+ var SQL_WASM_URL, SQL_WASM_FILENAME, TransactionalSQLJSOperations, SqljsEngine;
5768
6074
  var init_SqljsEngine = __esm({
5769
6075
  "src/engines/SqljsEngine.ts"() {
5770
6076
  "use strict";
5771
6077
  init_BaseModel();
5772
6078
  init_sql();
6079
+ init_environment();
5773
6080
  SQL_WASM_URL = "/sql-wasm.wasm";
6081
+ SQL_WASM_FILENAME = "sql-wasm.wasm";
5774
6082
  TransactionalSQLJSOperations = class {
5775
6083
  db;
5776
6084
  engine;
@@ -5836,22 +6144,37 @@ var init_SqljsEngine = __esm({
5836
6144
  }
5837
6145
  async initialize(engineOptions) {
5838
6146
  Logger.info("[SqljsEngine] Initializing SQL.js engine...");
6147
+ const loadingModuleThisCall = !_SqljsEngine.SQL;
5839
6148
  try {
5840
6149
  if (!_SqljsEngine.SQL) {
5841
- const locateFileConfig = engineOptions?.locateFile || ((_file) => engineOptions?.wasmURL || SQL_WASM_URL);
6150
+ let locateFileConfig;
6151
+ if (engineOptions?.locateFile) {
6152
+ locateFileConfig = engineOptions.locateFile;
6153
+ } else if (engineOptions?.wasmURL) {
6154
+ const url = engineOptions.wasmURL;
6155
+ locateFileConfig = (_file) => url;
6156
+ } else {
6157
+ const nodeWasmPath = await resolveNodeWasmPath();
6158
+ const resolved = nodeWasmPath ?? SQL_WASM_URL;
6159
+ locateFileConfig = (_file) => resolved;
6160
+ }
6161
+ const wasmPath = locateFileConfig(SQL_WASM_FILENAME);
5842
6162
  Logger.debug(
5843
6163
  "[SqljsEngine] Attempting to load SQL.js WASM from:",
5844
- locateFileConfig("sql-wasm.wasm")
6164
+ wasmPath
5845
6165
  );
5846
- _SqljsEngine.SQL = await initSqlJs({
5847
- locateFile: locateFileConfig
5848
- });
6166
+ await preflightNodeWasm(wasmPath);
6167
+ const SQL = await loadSqlJsModule(locateFileConfig);
6168
+ _SqljsEngine.SQL = SQL;
5849
6169
  Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
5850
6170
  }
5851
6171
  this.db = new _SqljsEngine.SQL.Database();
5852
6172
  Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
5853
6173
  this.updateTableNames();
5854
6174
  } catch (error) {
6175
+ if (loadingModuleThisCall) {
6176
+ _SqljsEngine.SQL = null;
6177
+ }
5855
6178
  Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
5856
6179
  throw error;
5857
6180
  }
@@ -7036,6 +7359,7 @@ init_StringSet();
7036
7359
  // src/models/schema.ts
7037
7360
  init_BaseModel();
7038
7361
  init_ModelRegistry();
7362
+ init_uniqueIndex();
7039
7363
  function defineModelSchema(input) {
7040
7364
  const { name, fields } = input;
7041
7365
  const options = {
@@ -7108,7 +7432,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
7108
7432
  for (const [fieldName, options] of fields.entries()) {
7109
7433
  if (options.unique) {
7110
7434
  resolved.push({
7111
- name: `${modelName}_${fieldName}_unique`,
7435
+ name: fieldUniqueConstraintName(modelName, fieldName),
7112
7436
  fields: [fieldName]
7113
7437
  });
7114
7438
  }
@@ -7436,7 +7760,7 @@ async function loadSchemaFromToml(filePath, options = {}) {
7436
7760
 
7437
7761
  // src/utils/yDocSchema.ts
7438
7762
  init_yjsValues();
7439
- import * as Y4 from "yjs";
7763
+ import * as Y6 from "yjs";
7440
7764
  function discoverSchema(yDoc) {
7441
7765
  const models = {};
7442
7766
  const metaNames = /* @__PURE__ */ new Set();
@@ -7470,7 +7794,7 @@ function discoverModelNames(yDoc) {
7470
7794
  function materializeMap(yDoc, key) {
7471
7795
  try {
7472
7796
  const map = yDoc.getMap(key);
7473
- return map instanceof Y4.Map ? map : null;
7797
+ return map instanceof Y6.Map ? map : null;
7474
7798
  } catch {
7475
7799
  return null;
7476
7800
  }
@@ -7480,11 +7804,11 @@ function readModelMeta(metaMap) {
7480
7804
  let constraints;
7481
7805
  let relationships;
7482
7806
  for (const [key, value] of metaMap.entries()) {
7483
- if (key === "_constraints" && value instanceof Y4.Map) {
7807
+ if (key === "_constraints" && value instanceof Y6.Map) {
7484
7808
  constraints = readConstraints(value);
7485
- } else if (key === "_relationships" && value instanceof Y4.Map) {
7809
+ } else if (key === "_relationships" && value instanceof Y6.Map) {
7486
7810
  relationships = readRelationships(value);
7487
- } else if (value instanceof Y4.Map) {
7811
+ } else if (value instanceof Y6.Map) {
7488
7812
  fields[key] = readFieldMeta(value);
7489
7813
  }
7490
7814
  }
@@ -7519,7 +7843,7 @@ function inferModelFromData(dataMap) {
7519
7843
  const fields = {};
7520
7844
  let sampled = 0;
7521
7845
  for (const [_recordId, recordValue] of dataMap.entries()) {
7522
- if (!(recordValue instanceof Y4.Map)) continue;
7846
+ if (!(recordValue instanceof Y6.Map)) continue;
7523
7847
  if (++sampled > 5) break;
7524
7848
  for (const [fieldName, value] of recordValue.entries()) {
7525
7849
  if (fieldName.startsWith("_")) continue;
@@ -7547,7 +7871,7 @@ function inferTypeFromValue(value) {
7547
7871
  function readConstraints(constraintsMap) {
7548
7872
  const out = {};
7549
7873
  for (const [name, value] of constraintsMap.entries()) {
7550
- if (!(value instanceof Y4.Map)) continue;
7874
+ if (!(value instanceof Y6.Map)) continue;
7551
7875
  let fields = [];
7552
7876
  const rawFields = value.get("fields");
7553
7877
  if (typeof rawFields === "string") {
@@ -7566,7 +7890,7 @@ function readConstraints(constraintsMap) {
7566
7890
  function readRelationships(relsMap) {
7567
7891
  const out = {};
7568
7892
  for (const [name, value] of relsMap.entries()) {
7569
- if (!(value instanceof Y4.Map)) continue;
7893
+ if (!(value instanceof Y6.Map)) continue;
7570
7894
  const rel = {};
7571
7895
  for (const [k, v] of value.entries()) {
7572
7896
  rel[k] = v;
@@ -7640,19 +7964,20 @@ function schemaToToml(schema) {
7640
7964
  }
7641
7965
 
7642
7966
  // src/index.ts
7967
+ init_uniqueIndex();
7643
7968
  init_BaseModel();
7644
7969
 
7645
7970
  // src/utils/yDocDump.ts
7646
- import * as Y5 from "yjs";
7971
+ import * as Y7 from "yjs";
7647
7972
  function toPlain(value) {
7648
- if (value instanceof Y5.Map) {
7973
+ if (value instanceof Y7.Map) {
7649
7974
  const obj = {};
7650
7975
  value.forEach((v, k) => {
7651
7976
  obj[k] = toPlain(v);
7652
7977
  });
7653
7978
  return obj;
7654
7979
  }
7655
- if (value instanceof Y5.Array) {
7980
+ if (value instanceof Y7.Array) {
7656
7981
  return value.toArray().map((item) => toPlain(item));
7657
7982
  }
7658
7983
  return value;
@@ -7661,7 +7986,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7661
7986
  const result = {};
7662
7987
  const includeIndexes = !!options.includeIndexes;
7663
7988
  yDoc.share.forEach((type, name) => {
7664
- if (!(type instanceof Y5.Map)) return;
7989
+ if (!(type instanceof Y7.Map)) return;
7665
7990
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7666
7991
  const records = {};
7667
7992
  type.forEach((recordValue, recordId) => {
@@ -7709,12 +8034,14 @@ export {
7709
8034
  attachAndRegisterModel,
7710
8035
  attachSchemaToClass,
7711
8036
  autoRegisterModel,
8037
+ buildUniqueKey,
7712
8038
  clearMetaSyncCache,
7713
8039
  createModelClass,
7714
8040
  defineModelSchema,
7715
8041
  discoverModelNames,
7716
8042
  discoverSchema,
7717
8043
  dumpYDocToPlain,
8044
+ fieldUniqueConstraintName,
7718
8045
  generateULID,
7719
8046
  inferFieldType,
7720
8047
  initJsBao,
@@ -7725,5 +8052,6 @@ export {
7725
8052
  schemaToToml,
7726
8053
  summarizePlainYDoc,
7727
8054
  syncInferredMeta,
7728
- syncModelMeta
8055
+ syncModelMeta,
8056
+ uniqueIndexMapName
7729
8057
  };