js-bao 0.5.0 → 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(
@@ -1183,9 +1208,109 @@ var init_DocumentQueryTranslator = __esm({
1183
1208
  }
1184
1209
  });
1185
1210
 
1211
+ // src/utils/yjsValues.ts
1212
+ function isYjsComposite(value) {
1213
+ return value instanceof Y.Map || value instanceof Y.Array || value instanceof Y.Text;
1214
+ }
1215
+ function isStringSetYMap(value) {
1216
+ if (!(value instanceof Y.Map)) return false;
1217
+ for (const v of value.values()) {
1218
+ if (v !== true) return false;
1219
+ }
1220
+ return true;
1221
+ }
1222
+ function normalizeYjsValue(value) {
1223
+ if (value instanceof Y.Text) {
1224
+ return value.toString();
1225
+ }
1226
+ if (value instanceof Y.Map || value instanceof Y.Array) {
1227
+ return value.toJSON();
1228
+ }
1229
+ return value;
1230
+ }
1231
+ function normalizeYjsValueForStorage(value) {
1232
+ if (value instanceof Y.Text) {
1233
+ return value.toString();
1234
+ }
1235
+ if (value instanceof Y.Map || value instanceof Y.Array) {
1236
+ return JSON.stringify(value.toJSON());
1237
+ }
1238
+ return value;
1239
+ }
1240
+ var Y;
1241
+ var init_yjsValues = __esm({
1242
+ "src/utils/yjsValues.ts"() {
1243
+ "use strict";
1244
+ Y = __toESM(require("yjs"), 1);
1245
+ }
1246
+ });
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
+
1186
1311
  // src/models/metaSync.ts
1187
1312
  function inferFieldType(value) {
1188
- if (value instanceof Y.Map) return "stringset";
1313
+ if (isStringSetYMap(value)) return "stringset";
1189
1314
  switch (typeof value) {
1190
1315
  case "string":
1191
1316
  return "string";
@@ -1204,7 +1329,10 @@ function encodeDefault(value) {
1204
1329
  if (value === void 0 || value === null) return void 0;
1205
1330
  if (typeof value === "function") {
1206
1331
  const name = KNOWN_FUNCTION_DEFAULTS.get(value);
1207
- return name ? `$${name}` : "$unknown_function";
1332
+ return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
1333
+ }
1334
+ if (typeof value === "string" && value.startsWith("$")) {
1335
+ return `$${value}`;
1208
1336
  }
1209
1337
  return value;
1210
1338
  }
@@ -1230,7 +1358,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1230
1358
  if (compoundConstraints.length > 0) {
1231
1359
  let constraints = meta.get("_constraints");
1232
1360
  if (!constraints) {
1233
- constraints = new Y.Map();
1361
+ constraints = new Y3.Map();
1234
1362
  meta.set("_constraints", constraints);
1235
1363
  }
1236
1364
  for (const constraint of compoundConstraints) {
@@ -1241,7 +1369,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1241
1369
  if (relationships && Object.keys(relationships).length > 0) {
1242
1370
  let rels = meta.get("_relationships");
1243
1371
  if (!rels) {
1244
- rels = new Y.Map();
1372
+ rels = new Y3.Map();
1245
1373
  meta.set("_relationships", rels);
1246
1374
  }
1247
1375
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -1253,7 +1381,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1253
1381
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1254
1382
  let fieldMeta = metaMap.get(fieldName);
1255
1383
  if (!fieldMeta) {
1256
- fieldMeta = new Y.Map();
1384
+ fieldMeta = new Y3.Map();
1257
1385
  metaMap.set(fieldName, fieldMeta);
1258
1386
  }
1259
1387
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -1261,6 +1389,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1261
1389
  if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
1262
1390
  if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
1263
1391
  if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
1392
+ if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
1264
1393
  if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
1265
1394
  if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
1266
1395
  const encoded = encodeDefault(fieldOpts.default);
@@ -1269,7 +1398,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1269
1398
  function syncConstraintMeta(constraintsMap, constraint) {
1270
1399
  let cMeta = constraintsMap.get(constraint.name);
1271
1400
  if (!cMeta) {
1272
- cMeta = new Y.Map();
1401
+ cMeta = new Y3.Map();
1273
1402
  constraintsMap.set(constraint.name, cMeta);
1274
1403
  }
1275
1404
  setIfChanged(cMeta, "type", "unique");
@@ -1279,7 +1408,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
1279
1408
  function syncRelationshipMeta(relsMap, relName, relConfig) {
1280
1409
  let relMeta = relsMap.get(relName);
1281
1410
  if (!relMeta) {
1282
- relMeta = new Y.Map();
1411
+ relMeta = new Y3.Map();
1283
1412
  relsMap.set(relName, relMeta);
1284
1413
  }
1285
1414
  for (const [key, value] of Object.entries(relConfig)) {
@@ -1294,7 +1423,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
1294
1423
  if (fieldName.startsWith("_")) continue;
1295
1424
  let fieldMeta = meta.get(fieldName);
1296
1425
  if (!fieldMeta) {
1297
- fieldMeta = new Y.Map();
1426
+ fieldMeta = new Y3.Map();
1298
1427
  meta.set(fieldName, fieldMeta);
1299
1428
  }
1300
1429
  if (!fieldMeta.has("type")) {
@@ -1310,16 +1439,44 @@ function setIfChanged(map, key, value) {
1310
1439
  map.set(key, value);
1311
1440
  }
1312
1441
  }
1313
- var Y, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1442
+ var Y3, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1314
1443
  var init_metaSync = __esm({
1315
1444
  "src/models/metaSync.ts"() {
1316
1445
  "use strict";
1317
- Y = __toESM(require("yjs"), 1);
1446
+ Y3 = __toESM(require("yjs"), 1);
1447
+ init_yjsValues();
1448
+ init_writeLifecycle();
1318
1449
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
1319
1450
  _syncedCache = /* @__PURE__ */ new WeakMap();
1320
1451
  }
1321
1452
  });
1322
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
+
1323
1480
  // src/query/IncludeResolver.ts
1324
1481
  var IncludeResolver_exports = {};
1325
1482
  __export(IncludeResolver_exports, {
@@ -1678,7 +1835,13 @@ var init_IncludeResolver = __esm({
1678
1835
  }
1679
1836
  return null;
1680
1837
  }
1681
- /** 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
+ */
1682
1845
  _applyProjection(record, projection) {
1683
1846
  const fields = Object.entries(projection);
1684
1847
  if (fields.length === 0) return record;
@@ -1697,7 +1860,7 @@ var init_IncludeResolver = __esm({
1697
1860
  } else {
1698
1861
  const result = { ...record };
1699
1862
  for (const [f, v] of fields) {
1700
- if (v === 0) delete result[f];
1863
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
1701
1864
  }
1702
1865
  return result;
1703
1866
  }
@@ -1717,20 +1880,23 @@ __export(BaseModel_exports, {
1717
1880
  generateULID: () => generateULID
1718
1881
  });
1719
1882
  function generateULID() {
1720
- return (0, import_ulid.ulid)();
1883
+ return (0, import_ulid2.ulid)();
1721
1884
  }
1722
- var Y2, import_ulid, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel;
1885
+ var Y5, import_ulid2, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel;
1723
1886
  var init_BaseModel = __esm({
1724
1887
  "src/models/BaseModel.ts"() {
1725
1888
  "use strict";
1726
- Y2 = __toESM(require("yjs"), 1);
1727
- import_ulid = require("ulid");
1889
+ Y5 = __toESM(require("yjs"), 1);
1890
+ import_ulid2 = require("ulid");
1728
1891
  init_StringSet();
1729
1892
  init_documentTypes();
1730
1893
  init_DocumentQueryTranslator();
1731
1894
  init_CursorManager();
1732
1895
  init_sql();
1733
1896
  init_metaSync();
1897
+ init_writeLifecycle();
1898
+ init_yjsValues();
1899
+ init_uniqueIndex();
1734
1900
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
1735
1901
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
1736
1902
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2115,7 +2281,9 @@ var init_BaseModel = __esm({
2115
2281
  this.setValue(fieldKey, data[fieldKey]);
2116
2282
  this._constructorProvidedFields.add(fieldKey);
2117
2283
  } else if (fieldOptions.default !== void 0) {
2118
- const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
2284
+ const defaultValue = resolveLiteralOrFunctionDefault(
2285
+ fieldOptions.default
2286
+ );
2119
2287
  this.setValue(fieldKey, defaultValue);
2120
2288
  }
2121
2289
  }
@@ -2239,7 +2407,30 @@ var init_BaseModel = __esm({
2239
2407
  if (verboseEnabled) {
2240
2408
  Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
2241
2409
  }
2242
- return fieldValue;
2410
+ return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
2411
+ }
2412
+ /**
2413
+ * Defensive normalization for the read/sync boundary (issue #625).
2414
+ *
2415
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
2416
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
2417
+ * through untouched. For any other field, a composite Yjs primitive
2418
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
2419
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
2420
+ * warn, rather than throwing. Scalars pass through unchanged.
2421
+ */
2422
+ normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
2423
+ const fieldOptions = schema?.fields?.get(fieldKey);
2424
+ if (fieldOptions?.type === "stringset") {
2425
+ return value;
2426
+ }
2427
+ if (isYjsComposite(value)) {
2428
+ Logger.warn(
2429
+ `[${modelName ?? this.constructor.name}] Field '${fieldKey}' holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions?.type ?? "unknown"}'. Normalizing to plain JS \u2014 the writer disagreed with the schema.`
2430
+ );
2431
+ return normalizeYjsValue(value);
2432
+ }
2433
+ return value;
2243
2434
  }
2244
2435
  getValue(fieldKey) {
2245
2436
  const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
@@ -2384,7 +2575,7 @@ var init_BaseModel = __esm({
2384
2575
  }
2385
2576
  getStringSetFromYjs(fieldName) {
2386
2577
  const yjsData = this.getFromYjs(fieldName);
2387
- if (yjsData instanceof Y2.Map) {
2578
+ if (yjsData instanceof Y5.Map) {
2388
2579
  return Array.from(yjsData.keys());
2389
2580
  }
2390
2581
  if (yjsData && typeof yjsData === "object") {
@@ -2492,7 +2683,7 @@ var init_BaseModel = __esm({
2492
2683
  }
2493
2684
  const extractStringSetValues = (value) => {
2494
2685
  if (!value) return [];
2495
- if (value instanceof Y2.Map) {
2686
+ if (value instanceof Y5.Map) {
2496
2687
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
2497
2688
  }
2498
2689
  if (Array.isArray(value)) {
@@ -2509,7 +2700,7 @@ var init_BaseModel = __esm({
2509
2700
  if (!recordId) continue;
2510
2701
  let itemData;
2511
2702
  const stringSetValuesByField = {};
2512
- if (recordData instanceof Y2.Map) {
2703
+ if (recordData instanceof Y5.Map) {
2513
2704
  itemData = {};
2514
2705
  const unknownFields = [];
2515
2706
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2625,20 +2816,10 @@ var init_BaseModel = __esm({
2625
2816
  Logger.verbose(
2626
2817
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
2627
2818
  );
2628
- const buildUniqueKey = (recordData, fields) => {
2629
- const keyParts = [];
2630
- for (const field of fields) {
2631
- const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
2632
- if (value === null || value === void 0) {
2633
- return null;
2634
- }
2635
- keyParts.push(String(value));
2636
- }
2637
- return keyParts.join("|");
2638
- };
2819
+ const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
2639
2820
  const extractItemData = (key, recordData) => {
2640
2821
  let itemData;
2641
- if (recordData instanceof Y2.Map) {
2822
+ if (recordData instanceof Y5.Map) {
2642
2823
  itemData = {};
2643
2824
  const unknownFields = [];
2644
2825
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2651,7 +2832,14 @@ var init_BaseModel = __esm({
2651
2832
  continue;
2652
2833
  }
2653
2834
  if (value !== void 0) {
2654
- itemData[fieldKey] = value;
2835
+ if (isYjsComposite(value)) {
2836
+ Logger.warn(
2837
+ `[${this.name}] Field '${fieldKey}' on record ${key} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
2838
+ );
2839
+ itemData[fieldKey] = normalizeYjsValueForStorage(value);
2840
+ } else {
2841
+ itemData[fieldKey] = value;
2842
+ }
2655
2843
  }
2656
2844
  }
2657
2845
  if (unknownFields.length > 0) {
@@ -2674,7 +2862,14 @@ var init_BaseModel = __esm({
2674
2862
  continue;
2675
2863
  }
2676
2864
  if (value !== void 0) {
2677
- filteredData[fieldKey] = value;
2865
+ if (isYjsComposite(value)) {
2866
+ Logger.warn(
2867
+ `[${this.name}] Field '${fieldKey}' on legacy record ${key} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
2868
+ );
2869
+ filteredData[fieldKey] = normalizeYjsValueForStorage(value);
2870
+ } else {
2871
+ filteredData[fieldKey] = value;
2872
+ }
2678
2873
  }
2679
2874
  }
2680
2875
  if (unknownFields.length > 0) {
@@ -2708,7 +2903,7 @@ var init_BaseModel = __esm({
2708
2903
  const recordsByUniqueKey = /* @__PURE__ */ new Map();
2709
2904
  for (const [recordId, recordData] of allRecords.entries()) {
2710
2905
  if (recordIdsToDiscard.has(recordId)) continue;
2711
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2906
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2712
2907
  if (uniqueKey === null) continue;
2713
2908
  if (!recordsByUniqueKey.has(uniqueKey)) {
2714
2909
  recordsByUniqueKey.set(uniqueKey, []);
@@ -2754,7 +2949,7 @@ var init_BaseModel = __esm({
2754
2949
  if (!recordData || !key) continue;
2755
2950
  const itemData = extractItemData(key, recordData);
2756
2951
  if (!itemData) continue;
2757
- if (change.action === "add" && recordData instanceof Y2.Map) {
2952
+ if (change.action === "add" && recordData instanceof Y5.Map) {
2758
2953
  Logger.verbose(
2759
2954
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2760
2955
  );
@@ -2852,9 +3047,12 @@ var init_BaseModel = __esm({
2852
3047
  const recordData = documentYMap.get(idToDiscard);
2853
3048
  if (!recordData) continue;
2854
3049
  for (const constraint of schema.resolvedUniqueConstraints) {
2855
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3050
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2856
3051
  if (uniqueKey === null) continue;
2857
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3052
+ const constraintMapName = uniqueIndexMapName(
3053
+ modelName,
3054
+ constraint.name
3055
+ );
2858
3056
  const constraintMap = yDoc.getMap(constraintMapName);
2859
3057
  const currentIndexValue = constraintMap.get(uniqueKey);
2860
3058
  if (currentIndexValue === idToDiscard) {
@@ -2872,11 +3070,14 @@ var init_BaseModel = __esm({
2872
3070
  }, `conflict-resolution-${modelName}-${docId}`);
2873
3071
  yDoc.transact(() => {
2874
3072
  for (const constraint of schema.resolvedUniqueConstraints) {
2875
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3073
+ const constraintMapName = uniqueIndexMapName(
3074
+ modelName,
3075
+ constraint.name
3076
+ );
2876
3077
  const constraintMap = yDoc.getMap(constraintMapName);
2877
3078
  for (const [recordId, recordData] of documentYMap.entries()) {
2878
3079
  if (!recordData) continue;
2879
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3080
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2880
3081
  if (uniqueKey === null) continue;
2881
3082
  const currentIndexValue = constraintMap.get(uniqueKey);
2882
3083
  if (currentIndexValue !== recordId) {
@@ -2899,7 +3100,7 @@ var init_BaseModel = __esm({
2899
3100
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
2900
3101
  );
2901
3102
  for (const [recordId, recordData] of documentYMap.entries()) {
2902
- if (recordData instanceof Y2.Map) {
3103
+ if (recordData instanceof Y5.Map) {
2903
3104
  Logger.verbose(
2904
3105
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
2905
3106
  );
@@ -3084,6 +3285,11 @@ var init_BaseModel = __esm({
3084
3285
  Logger.debug(
3085
3286
  `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
3086
3287
  );
3288
+ if (isYjsComposite(yjsValue)) {
3289
+ Logger.warn(
3290
+ `[${modelConstructor.name}] Field '${key}' holds a composite Yjs value (${yjsValue?.constructor?.name}) in a non-stringset slot; the local scalar change will overwrite it (last-writer-wins).`
3291
+ );
3292
+ }
3087
3293
  if (yjsValue === void 0) {
3088
3294
  Logger.debug(
3089
3295
  `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
@@ -3120,26 +3326,7 @@ var init_BaseModel = __esm({
3120
3326
  * if the field hasn't been touched since the wire-format change in #561.
3121
3327
  */
3122
3328
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3123
- let nested = recordYMap.get(fieldName);
3124
- if (!(nested instanceof Y2.Map)) {
3125
- const migrated = new Y2.Map();
3126
- if (nested && typeof nested === "object") {
3127
- for (const [member, marker] of Object.entries(
3128
- nested
3129
- )) {
3130
- if (Boolean(marker)) migrated.set(member, true);
3131
- }
3132
- }
3133
- recordYMap.set(fieldName, migrated);
3134
- nested = migrated;
3135
- }
3136
- const target = nested;
3137
- for (const member of change.additions) {
3138
- target.set(member, true);
3139
- }
3140
- for (const member of change.removals) {
3141
- target.delete(member);
3142
- }
3329
+ applyStringSetChange(recordYMap, fieldName, change);
3143
3330
  }
3144
3331
  /**
3145
3332
  * Deep equality check for comparing field values
@@ -3309,6 +3496,29 @@ var init_BaseModel = __esm({
3309
3496
  );
3310
3497
  throw new Error("Cannot save item without an id. Ensure id is set.");
3311
3498
  }
3499
+ const preTransactCreateStamps = /* @__PURE__ */ new Set();
3500
+ {
3501
+ const existingRecord = targetYMap.get(this.id);
3502
+ const isNew = !existingRecord;
3503
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
3504
+ const stamp = fieldOptions.autoStamp;
3505
+ if (!stamp) continue;
3506
+ const callerExplicit = this.hasLocalChange(fieldKey);
3507
+ if (callerExplicit) {
3508
+ const localValue = this._localChanges[fieldKey];
3509
+ if (localValue !== null && localValue !== void 0) continue;
3510
+ }
3511
+ if (stamp === "create") {
3512
+ if (!isNew) continue;
3513
+ const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
3514
+ if (persisted !== null && persisted !== void 0) continue;
3515
+ }
3516
+ this[fieldKey] = Date.now();
3517
+ if (stamp === "create") {
3518
+ preTransactCreateStamps.add(fieldKey);
3519
+ }
3520
+ }
3521
+ }
3312
3522
  this.validateBeforeSave();
3313
3523
  Logger.debug(`[${modelName}.save] About to get current JS state`);
3314
3524
  Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
@@ -3318,6 +3528,34 @@ var init_BaseModel = __esm({
3318
3528
  `[${modelName}.save] getCurrentJSState() returned:`,
3319
3529
  dataToSave
3320
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
+ }
3321
3559
  await yDoc.transact(async () => {
3322
3560
  if (upsertOnConstraint && upsertOnKey) {
3323
3561
  const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
@@ -3339,6 +3577,14 @@ var init_BaseModel = __esm({
3339
3577
  }
3340
3578
  }
3341
3579
  }
3580
+ for (const fieldKey of preTransactCreateStamps) {
3581
+ if (this._localChanges && fieldKey in this._localChanges) {
3582
+ delete this._localChanges[fieldKey];
3583
+ }
3584
+ if (fieldKey in dataToSave) {
3585
+ delete dataToSave[fieldKey];
3586
+ }
3587
+ }
3342
3588
  }
3343
3589
  }
3344
3590
  if (!targetYMap) {
@@ -3351,7 +3597,7 @@ var init_BaseModel = __esm({
3351
3597
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
3352
3598
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
3353
3599
  if (!recordYMap) {
3354
- recordYMap = new Y2.Map();
3600
+ recordYMap = new Y5.Map();
3355
3601
  Logger.verbose(
3356
3602
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
3357
3603
  );
@@ -3403,7 +3649,10 @@ var init_BaseModel = __esm({
3403
3649
  );
3404
3650
  continue;
3405
3651
  }
3406
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3652
+ const constraintMapName = uniqueIndexMapName(
3653
+ modelName,
3654
+ constraint.name
3655
+ );
3407
3656
  const constraintMap = yDoc.getMap(constraintMapName);
3408
3657
  Logger.verbose(
3409
3658
  `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
@@ -3432,6 +3681,26 @@ var init_BaseModel = __esm({
3432
3681
  this.id,
3433
3682
  existingRecordIdWithNewKey
3434
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
+ }
3435
3704
  }
3436
3705
  }
3437
3706
  if (isUpdate && oldData) {
@@ -3775,34 +4044,46 @@ var init_BaseModel = __esm({
3775
4044
  const modelName = schema.options.name;
3776
4045
  Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
3777
4046
  const hasIncludes = options?.include && options.include.length > 0;
3778
- 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
+ }
3779
4056
  const translator = new DocumentQueryTranslator(modelName, schema.fields);
3780
4057
  const translatedQuery = translator.translateFind(filter, translatorOptions);
3781
4058
  Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
3782
4059
  Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
3783
- const results = await _BaseModel.dbInstance.query(
4060
+ let results = await _BaseModel.dbInstance.query(
3784
4061
  translatedQuery.sql,
3785
4062
  translatedQuery.params
3786
4063
  );
3787
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
+ }
3788
4070
  if (options?.include && options.include.length > 0) {
3789
4071
  const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
3790
4072
  const resolver = new IncludeResolver2(_BaseModel.dbInstance);
3791
4073
  await resolver.resolve(results, options.include, 0, modelName);
3792
4074
  }
3793
- const requestedLimit = options?.limit;
3794
- const hasMore = CursorManager.hasMoreResults(
3795
- requestedLimit,
3796
- results.length
3797
- );
3798
4075
  const isFirstPage = !options?.uniqueStartKey;
4076
+ const direction = options?.direction || 1;
3799
4077
  const cursors = CursorManager.generateResultCursors(
3800
4078
  results,
3801
4079
  translatedQuery.sortFields,
3802
- options?.direction || 1,
4080
+ direction,
3803
4081
  hasMore,
3804
4082
  isFirstPage
3805
4083
  );
4084
+ if (direction === -1) {
4085
+ results.reverse();
4086
+ }
3806
4087
  let transformedData;
3807
4088
  if (options?.projection && Object.keys(options.projection).length > 0) {
3808
4089
  transformedData = results.map((row) => {
@@ -4378,7 +4659,7 @@ var init_BaseModel = __esm({
4378
4659
  );
4379
4660
  }
4380
4661
  for (const [recordId, recordYMap] of documentYMap.entries()) {
4381
- if (recordYMap instanceof Y2.Map) {
4662
+ if (recordYMap instanceof Y5.Map) {
4382
4663
  const instance = new this({ id: recordId });
4383
4664
  instance._metaDocId = docId;
4384
4665
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -4403,6 +4684,50 @@ var init_BaseModel = __esm({
4403
4684
  }
4404
4685
  return allInstances;
4405
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
+ }
4406
4731
  static async findByUnique(constraintName, value) {
4407
4732
  const modelConstructor = this;
4408
4733
  const schema = modelConstructor.getSchema();
@@ -4441,7 +4766,10 @@ var init_BaseModel = __esm({
4441
4766
  const documentInfo = modelConstructor.connectedDocuments.get(docId);
4442
4767
  if (!documentInfo) continue;
4443
4768
  const yDoc = documentInfo.yDoc;
4444
- const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4769
+ const constraintMapName2 = uniqueIndexMapName(
4770
+ currentModelName,
4771
+ constraint.name
4772
+ );
4445
4773
  const constraintMap2 = yDoc.getMap(constraintMapName2);
4446
4774
  const recordId2 = constraintMap2.get(uniqueKeyString);
4447
4775
  if (recordId2) {
@@ -4449,7 +4777,7 @@ var init_BaseModel = __esm({
4449
4777
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
4450
4778
  if (documentYMap) {
4451
4779
  const recordYMap2 = documentYMap.get(recordId2);
4452
- if (recordYMap2 && recordYMap2 instanceof Y2.Map) {
4780
+ if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
4453
4781
  const instance = new modelConstructor({
4454
4782
  id: recordId2
4455
4783
  });
@@ -4463,6 +4791,16 @@ var init_BaseModel = __esm({
4463
4791
  }
4464
4792
  }
4465
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
+ }
4466
4804
  Logger.verbose(
4467
4805
  `[${currentModelName}.findByUnique] Record not found in any connected document`
4468
4806
  );
@@ -4476,17 +4814,30 @@ var init_BaseModel = __esm({
4476
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.`
4477
4815
  );
4478
4816
  }
4479
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4817
+ const constraintMapName = uniqueIndexMapName(
4818
+ currentModelName,
4819
+ constraint.name
4820
+ );
4480
4821
  const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
4481
4822
  const recordId = constraintMap.get(uniqueKeyString);
4482
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
+ }
4483
4834
  return null;
4484
4835
  }
4485
4836
  const recordYMap = legacyDocumentMap.get(recordId);
4486
4837
  if (!recordYMap) {
4487
4838
  return null;
4488
4839
  }
4489
- if (recordYMap instanceof Y2.Map) {
4840
+ if (recordYMap instanceof Y5.Map) {
4490
4841
  const instance = new modelConstructor({
4491
4842
  id: recordId
4492
4843
  });
@@ -4544,7 +4895,10 @@ var init_BaseModel = __esm({
4544
4895
  `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
4545
4896
  );
4546
4897
  }
4547
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4898
+ const constraintMapName = uniqueIndexMapName(
4899
+ currentModelName,
4900
+ constraint.name
4901
+ );
4548
4902
  let existingRecord = null;
4549
4903
  if (connectedDocuments && connectedDocuments.size > 0) {
4550
4904
  for (const docId of connectedDocuments.keys()) {
@@ -4562,6 +4916,33 @@ var init_BaseModel = __esm({
4562
4916
  }
4563
4917
  } else {
4564
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
+ }
4565
4946
  if (options?.objectMustExist) {
4566
4947
  if (!existingRecord) {
4567
4948
  throw new RecordNotFoundError(
@@ -4718,7 +5099,7 @@ var init_BaseModel = __esm({
4718
5099
  for (const [fieldKey, fieldOptions] of schema.fields) {
4719
5100
  if (fieldOptions?.type !== "stringset") continue;
4720
5101
  const value = recordYMap.get(fieldKey);
4721
- if (value instanceof Y2.Map) {
5102
+ if (value instanceof Y5.Map) {
4722
5103
  this.observeStringSetMapOnce(value);
4723
5104
  }
4724
5105
  }
@@ -4765,7 +5146,14 @@ var init_BaseModel = __esm({
4765
5146
  continue;
4766
5147
  }
4767
5148
  if (value !== void 0) {
4768
- updatedData[key] = value;
5149
+ if (isYjsComposite(value)) {
5150
+ Logger.warn(
5151
+ `[${modelName}] Field '${key}' on record ${recordId} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
5152
+ );
5153
+ updatedData[key] = normalizeYjsValueForStorage(value);
5154
+ } else {
5155
+ updatedData[key] = value;
5156
+ }
4769
5157
  }
4770
5158
  }
4771
5159
  if (unknownFields.length > 0) {
@@ -4845,7 +5233,14 @@ var init_BaseModel = __esm({
4845
5233
  continue;
4846
5234
  }
4847
5235
  if (value !== void 0) {
4848
- updatedData[key] = value;
5236
+ if (isYjsComposite(value)) {
5237
+ Logger.warn(
5238
+ `[${modelName}] Field '${key}' on record ${recordId} in document ${docId} holds a composite Yjs value (${value?.constructor?.name}) but its declared type is '${fieldOptions.type}'. Normalizing to plain JS before persisting to SQLite.`
5239
+ );
5240
+ updatedData[key] = normalizeYjsValueForStorage(value);
5241
+ } else {
5242
+ updatedData[key] = value;
5243
+ }
4849
5244
  }
4850
5245
  }
4851
5246
  if (unknownFields.length > 0) {
@@ -4890,6 +5285,19 @@ var init_BaseModel = __esm({
4890
5285
  });
4891
5286
 
4892
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
+ }
4893
5301
  function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
4894
5302
  return async function() {
4895
5303
  const relatedId = this[config.relatedIdField];
@@ -4898,6 +5306,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
4898
5306
  };
4899
5307
  }
4900
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
+ );
4901
5319
  return async function(paginationArgs) {
4902
5320
  const {
4903
5321
  limit,
@@ -4905,71 +5323,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
4905
5323
  beforeCursor,
4906
5324
  direction = "forward"
4907
5325
  } = paginationArgs || {};
4908
- const {
4909
- relatedIdField,
4910
- orderByField = "id",
4911
- orderDirection = "ASC"
4912
- } = config;
4913
5326
  const targetSchema = targetModelClass.getSchema();
4914
5327
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
4915
5328
  Logger.error(
4916
5329
  `[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
4917
5330
  );
4918
- return { data: [], nextCursor: null, prevCursor: null };
5331
+ return { data: [], hasMore: false };
4919
5332
  }
4920
- const filter = { [relatedIdField]: this.id };
5333
+ const forward = direction !== "backward";
5334
+ const cursor = forward ? afterCursor : beforeCursor;
4921
5335
  const queryOptions = {
4922
- sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
5336
+ sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
5337
+ direction: forward ? 1 : -1
4923
5338
  };
4924
- if (limit !== void 0) {
4925
- queryOptions.limit = limit + 1;
4926
- }
4927
- if (direction === "forward" && afterCursor) {
4928
- filter[orderByField] = {
4929
- [orderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
4930
- };
4931
- } else if (direction === "backward") {
4932
- if (beforeCursor) {
4933
- filter[orderByField] = {
4934
- [orderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
4935
- };
4936
- }
4937
- queryOptions.sort = { [orderByField]: orderDirection === "ASC" ? -1 : 1 };
4938
- }
4939
- const result = await targetModelClass.query(filter, queryOptions);
4940
- const results = result.data;
4941
- let nextCursor = null;
4942
- let prevCursor = null;
4943
- const isFirstPage = direction === "forward" && !afterCursor;
4944
- if (limit !== void 0) {
4945
- if (direction === "forward") {
4946
- if (results.length > limit) {
4947
- nextCursor = results[limit - 1][orderByField];
4948
- results.pop();
4949
- }
4950
- if (results.length > 0 && !isFirstPage) {
4951
- prevCursor = results[0][orderByField];
4952
- }
4953
- } else {
4954
- if (results.length > limit) {
4955
- prevCursor = results[limit - 1][orderByField];
4956
- results.pop();
4957
- }
4958
- results.reverse();
4959
- if (results.length > 0 && beforeCursor) {
4960
- nextCursor = results[results.length - 1][orderByField];
4961
- }
4962
- if (results.length > 0 && !beforeCursor) {
4963
- if (results.length === limit) {
4964
- prevCursor = results[0][orderByField];
4965
- }
4966
- }
4967
- }
4968
- }
4969
- 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
+ );
4970
5345
  };
4971
5346
  }
4972
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
+ );
4973
5359
  return async function(paginationArgs) {
4974
5360
  const {
4975
5361
  limit,
@@ -4983,87 +5369,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
4983
5369
  Logger.error(
4984
5370
  `[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
4985
5371
  );
4986
- return { data: [], nextCursor: null, prevCursor: null };
5372
+ return { data: [], hasMore: false };
4987
5373
  }
4988
5374
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
4989
5375
  Logger.error(
4990
5376
  `[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
4991
5377
  );
4992
- return { data: [], nextCursor: null, prevCursor: null };
5378
+ return { data: [], hasMore: false };
4993
5379
  }
4994
- const {
4995
- joinModelLocalField,
4996
- joinModelRelatedField,
4997
- joinModelOrderByField = "id",
4998
- joinModelOrderDirection = "ASC"
4999
- } = config;
5000
- const joinFilter = { [joinModelLocalField]: this.id };
5380
+ const forward = direction !== "backward";
5381
+ const cursor = forward ? afterCursor : beforeCursor;
5001
5382
  const joinQueryOptions = {
5002
5383
  sort: {
5003
5384
  [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
5004
5385
  },
5005
- projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
5386
+ projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
5387
+ direction: forward ? 1 : -1
5006
5388
  };
5007
- if (limit !== void 0) {
5008
- joinQueryOptions.limit = limit + 1;
5009
- }
5010
- if (direction === "forward" && afterCursor) {
5011
- joinFilter[joinModelOrderByField] = {
5012
- [joinModelOrderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
5013
- };
5014
- } else if (direction === "backward") {
5015
- if (beforeCursor) {
5016
- joinFilter[joinModelOrderByField] = {
5017
- [joinModelOrderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
5018
- };
5019
- }
5020
- joinQueryOptions.sort = {
5021
- [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? -1 : 1
5022
- };
5023
- }
5389
+ if (limit !== void 0) joinQueryOptions.limit = limit;
5390
+ if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
5024
5391
  const joinResult = await joinModelClass.query(
5025
- joinFilter,
5392
+ { [joinModelLocalField]: this.id },
5026
5393
  joinQueryOptions
5027
5394
  );
5028
- const joinResults = joinResult.data;
5029
- let nextCursorFromJoin = null;
5030
- let prevCursorFromJoin = null;
5031
- const isFirstPage = direction === "forward" && !afterCursor;
5032
- if (limit !== void 0) {
5033
- if (direction === "forward") {
5034
- if (joinResults.length > limit) {
5035
- nextCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
5036
- joinResults.pop();
5037
- }
5038
- if (joinResults.length > 0 && !isFirstPage) {
5039
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
5040
- }
5041
- } else {
5042
- if (joinResults.length > limit) {
5043
- prevCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
5044
- joinResults.pop();
5045
- }
5046
- joinResults.reverse();
5047
- if (joinResults.length > 0 && beforeCursor) {
5048
- nextCursorFromJoin = joinResults[joinResults.length - 1][joinModelOrderByField];
5049
- }
5050
- if (joinResults.length > 0 && !beforeCursor) {
5051
- if (joinResults.length === limit) {
5052
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
5053
- }
5054
- }
5055
- }
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;
5056
5402
  }
5057
- const relatedIds = joinResults.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
5058
- if (relatedIds.length === 0)
5059
- return { data: [], nextCursor: null, prevCursor: null };
5060
- const targetResult = await targetModelClass.query({
5061
- id: { $in: relatedIds }
5062
- });
5063
5403
  return {
5064
- data: targetResult.data,
5065
- nextCursor: nextCursorFromJoin,
5066
- prevCursor: prevCursorFromJoin
5404
+ data: targetData,
5405
+ nextCursor: joinResult.nextCursor,
5406
+ prevCursor: joinResult.prevCursor,
5407
+ hasMore: joinResult.hasMore
5067
5408
  };
5068
5409
  };
5069
5410
  }
@@ -5187,10 +5528,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
5187
5528
  }, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
5188
5529
  };
5189
5530
  }
5531
+ var ALWAYS_PRESENT_ORDER_FIELDS;
5190
5532
  var init_relationshipManager = __esm({
5191
5533
  "src/models/relationshipManager.ts"() {
5192
5534
  "use strict";
5193
5535
  init_BaseModel();
5536
+ ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
5194
5537
  }
5195
5538
  });
5196
5539
 
@@ -5658,7 +6001,92 @@ var init_environment = __esm({
5658
6001
  });
5659
6002
 
5660
6003
  // src/engines/SqljsEngine.ts
5661
- 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;
5662
6090
  var init_SqljsEngine = __esm({
5663
6091
  "src/engines/SqljsEngine.ts"() {
5664
6092
  "use strict";
@@ -5666,7 +6094,10 @@ var init_SqljsEngine = __esm({
5666
6094
  init_BaseModel();
5667
6095
  import_async_mutex = require("async-mutex");
5668
6096
  init_sql();
6097
+ init_environment();
6098
+ import_meta = {};
5669
6099
  SQL_WASM_URL = "/sql-wasm.wasm";
6100
+ SQL_WASM_FILENAME = "sql-wasm.wasm";
5670
6101
  TransactionalSQLJSOperations = class {
5671
6102
  db;
5672
6103
  engine;
@@ -5732,22 +6163,37 @@ var init_SqljsEngine = __esm({
5732
6163
  }
5733
6164
  async initialize(engineOptions) {
5734
6165
  Logger.info("[SqljsEngine] Initializing SQL.js engine...");
6166
+ const loadingModuleThisCall = !_SqljsEngine.SQL;
5735
6167
  try {
5736
6168
  if (!_SqljsEngine.SQL) {
5737
- 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);
5738
6181
  Logger.debug(
5739
6182
  "[SqljsEngine] Attempting to load SQL.js WASM from:",
5740
- locateFileConfig("sql-wasm.wasm")
6183
+ wasmPath
5741
6184
  );
5742
- _SqljsEngine.SQL = await (0, import_sql4.default)({
5743
- locateFile: locateFileConfig
5744
- });
6185
+ await preflightNodeWasm(wasmPath);
6186
+ const SQL = await loadSqlJsModule(locateFileConfig);
6187
+ _SqljsEngine.SQL = SQL;
5745
6188
  Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
5746
6189
  }
5747
6190
  this.db = new _SqljsEngine.SQL.Database();
5748
6191
  Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
5749
6192
  this.updateTableNames();
5750
6193
  } catch (error) {
6194
+ if (loadingModuleThisCall) {
6195
+ _SqljsEngine.SQL = null;
6196
+ }
5751
6197
  Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
5752
6198
  throw error;
5753
6199
  }
@@ -6729,12 +7175,14 @@ __export(index_exports, {
6729
7175
  attachAndRegisterModel: () => attachAndRegisterModel,
6730
7176
  attachSchemaToClass: () => attachSchemaToClass,
6731
7177
  autoRegisterModel: () => autoRegisterModel,
7178
+ buildUniqueKey: () => buildUniqueKey,
6732
7179
  clearMetaSyncCache: () => clearMetaSyncCache,
6733
7180
  createModelClass: () => createModelClass,
6734
7181
  defineModelSchema: () => defineModelSchema,
6735
7182
  discoverModelNames: () => discoverModelNames,
6736
7183
  discoverSchema: () => discoverSchema,
6737
7184
  dumpYDocToPlain: () => dumpYDocToPlain,
7185
+ fieldUniqueConstraintName: () => fieldUniqueConstraintName,
6738
7186
  generateULID: () => generateULID,
6739
7187
  inferFieldType: () => inferFieldType,
6740
7188
  initJsBao: () => initJsBao,
@@ -6745,7 +7193,8 @@ __export(index_exports, {
6745
7193
  schemaToToml: () => schemaToToml,
6746
7194
  summarizePlainYDoc: () => summarizePlainYDoc,
6747
7195
  syncInferredMeta: () => syncInferredMeta,
6748
- syncModelMeta: () => syncModelMeta
7196
+ syncModelMeta: () => syncModelMeta,
7197
+ uniqueIndexMapName: () => uniqueIndexMapName
6749
7198
  });
6750
7199
  module.exports = __toCommonJS(index_exports);
6751
7200
 
@@ -6968,6 +7417,7 @@ init_StringSet();
6968
7417
  // src/models/schema.ts
6969
7418
  init_BaseModel();
6970
7419
  init_ModelRegistry();
7420
+ init_uniqueIndex();
6971
7421
  function defineModelSchema(input) {
6972
7422
  const { name, fields } = input;
6973
7423
  const options = {
@@ -7040,7 +7490,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
7040
7490
  for (const [fieldName, options] of fields.entries()) {
7041
7491
  if (options.unique) {
7042
7492
  resolved.push({
7043
- name: `${modelName}_${fieldName}_unique`,
7493
+ name: fieldUniqueConstraintName(modelName, fieldName),
7044
7494
  fields: [fieldName]
7045
7495
  });
7046
7496
  }
@@ -7161,10 +7611,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
7161
7611
  "unique",
7162
7612
  "required",
7163
7613
  "auto_assign",
7614
+ "auto_stamp",
7164
7615
  "max_length",
7165
7616
  "max_count",
7166
- "default"
7617
+ "default",
7618
+ "enum"
7167
7619
  ]);
7620
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
7168
7621
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
7169
7622
  "fields",
7170
7623
  "relationships",
@@ -7206,9 +7659,37 @@ function parseFieldOptions(raw, context, strict) {
7206
7659
  if (raw.unique === true) opts.unique = true;
7207
7660
  if (raw.required === true) opts.required = true;
7208
7661
  if (raw.auto_assign === true) opts.autoAssign = true;
7662
+ if (raw.auto_stamp !== void 0) {
7663
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
7664
+ throw new Error(
7665
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
7666
+ ...VALID_AUTO_STAMP_VALUES
7667
+ ].join(", ")}`
7668
+ );
7669
+ }
7670
+ opts.autoStamp = raw.auto_stamp;
7671
+ }
7209
7672
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
7210
7673
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
7211
7674
  if (raw.default !== void 0) opts.default = raw.default;
7675
+ if (raw.enum !== void 0) {
7676
+ if (raw.type !== "string") {
7677
+ throw new Error(
7678
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
7679
+ );
7680
+ }
7681
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
7682
+ throw new Error(
7683
+ `${context}: \`enum\` must be a non-empty array of strings.`
7684
+ );
7685
+ }
7686
+ if (!raw.enum.every((v) => typeof v === "string")) {
7687
+ throw new Error(
7688
+ `${context}: \`enum\` values must all be strings.`
7689
+ );
7690
+ }
7691
+ opts.enum = [...raw.enum];
7692
+ }
7212
7693
  return opts;
7213
7694
  }
7214
7695
  function requireField(raw, field, context) {
@@ -7336,7 +7817,8 @@ async function loadSchemaFromToml(filePath, options = {}) {
7336
7817
  }
7337
7818
 
7338
7819
  // src/utils/yDocSchema.ts
7339
- var Y3 = __toESM(require("yjs"), 1);
7820
+ var Y6 = __toESM(require("yjs"), 1);
7821
+ init_yjsValues();
7340
7822
  function discoverSchema(yDoc) {
7341
7823
  const models = {};
7342
7824
  const metaNames = /* @__PURE__ */ new Set();
@@ -7370,7 +7852,7 @@ function discoverModelNames(yDoc) {
7370
7852
  function materializeMap(yDoc, key) {
7371
7853
  try {
7372
7854
  const map = yDoc.getMap(key);
7373
- return map instanceof Y3.Map ? map : null;
7855
+ return map instanceof Y6.Map ? map : null;
7374
7856
  } catch {
7375
7857
  return null;
7376
7858
  }
@@ -7380,11 +7862,11 @@ function readModelMeta(metaMap) {
7380
7862
  let constraints;
7381
7863
  let relationships;
7382
7864
  for (const [key, value] of metaMap.entries()) {
7383
- if (key === "_constraints" && value instanceof Y3.Map) {
7865
+ if (key === "_constraints" && value instanceof Y6.Map) {
7384
7866
  constraints = readConstraints(value);
7385
- } else if (key === "_relationships" && value instanceof Y3.Map) {
7867
+ } else if (key === "_relationships" && value instanceof Y6.Map) {
7386
7868
  relationships = readRelationships(value);
7387
- } else if (value instanceof Y3.Map) {
7869
+ } else if (value instanceof Y6.Map) {
7388
7870
  fields[key] = readFieldMeta(value);
7389
7871
  }
7390
7872
  }
@@ -7403,6 +7885,10 @@ function readFieldMeta(fieldMap) {
7403
7885
  if (fieldMap.get("unique") === true) field.unique = true;
7404
7886
  if (fieldMap.get("required") === true) field.required = true;
7405
7887
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7888
+ const autoStamp = fieldMap.get("autoStamp");
7889
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
7890
+ field.autoStamp = autoStamp;
7891
+ }
7406
7892
  const def = fieldMap.get("default");
7407
7893
  if (def !== void 0) field.default = def;
7408
7894
  const maxLength = fieldMap.get("maxLength");
@@ -7415,7 +7901,7 @@ function inferModelFromData(dataMap) {
7415
7901
  const fields = {};
7416
7902
  let sampled = 0;
7417
7903
  for (const [_recordId, recordValue] of dataMap.entries()) {
7418
- if (!(recordValue instanceof Y3.Map)) continue;
7904
+ if (!(recordValue instanceof Y6.Map)) continue;
7419
7905
  if (++sampled > 5) break;
7420
7906
  for (const [fieldName, value] of recordValue.entries()) {
7421
7907
  if (fieldName.startsWith("_")) continue;
@@ -7428,7 +7914,7 @@ function inferModelFromData(dataMap) {
7428
7914
  return { fields };
7429
7915
  }
7430
7916
  function inferTypeFromValue(value) {
7431
- if (value instanceof Y3.Map) return "stringset";
7917
+ if (isStringSetYMap(value)) return "stringset";
7432
7918
  switch (typeof value) {
7433
7919
  case "string":
7434
7920
  return "string";
@@ -7443,7 +7929,7 @@ function inferTypeFromValue(value) {
7443
7929
  function readConstraints(constraintsMap) {
7444
7930
  const out = {};
7445
7931
  for (const [name, value] of constraintsMap.entries()) {
7446
- if (!(value instanceof Y3.Map)) continue;
7932
+ if (!(value instanceof Y6.Map)) continue;
7447
7933
  let fields = [];
7448
7934
  const rawFields = value.get("fields");
7449
7935
  if (typeof rawFields === "string") {
@@ -7462,7 +7948,7 @@ function readConstraints(constraintsMap) {
7462
7948
  function readRelationships(relsMap) {
7463
7949
  const out = {};
7464
7950
  for (const [name, value] of relsMap.entries()) {
7465
- if (!(value instanceof Y3.Map)) continue;
7951
+ if (!(value instanceof Y6.Map)) continue;
7466
7952
  const rel = {};
7467
7953
  for (const [k, v] of value.entries()) {
7468
7954
  rel[k] = v;
@@ -7473,6 +7959,7 @@ function readRelationships(relsMap) {
7473
7959
  }
7474
7960
  var CAMEL_TO_SNAKE = {
7475
7961
  autoAssign: "auto_assign",
7962
+ autoStamp: "auto_stamp",
7476
7963
  maxLength: "max_length",
7477
7964
  maxCount: "max_count",
7478
7965
  relatedIdField: "related_id_field",
@@ -7503,6 +7990,7 @@ function schemaToToml(schema) {
7503
7990
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
7504
7991
  lines.push(`type = ${tomlValue(field.type)}`);
7505
7992
  if (field.autoAssign) lines.push("auto_assign = true");
7993
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
7506
7994
  if (field.indexed) lines.push("indexed = true");
7507
7995
  if (field.unique) lines.push("unique = true");
7508
7996
  if (field.required) lines.push("required = true");
@@ -7534,19 +8022,20 @@ function schemaToToml(schema) {
7534
8022
  }
7535
8023
 
7536
8024
  // src/index.ts
8025
+ init_uniqueIndex();
7537
8026
  init_BaseModel();
7538
8027
 
7539
8028
  // src/utils/yDocDump.ts
7540
- var Y4 = __toESM(require("yjs"), 1);
8029
+ var Y7 = __toESM(require("yjs"), 1);
7541
8030
  function toPlain(value) {
7542
- if (value instanceof Y4.Map) {
8031
+ if (value instanceof Y7.Map) {
7543
8032
  const obj = {};
7544
8033
  value.forEach((v, k) => {
7545
8034
  obj[k] = toPlain(v);
7546
8035
  });
7547
8036
  return obj;
7548
8037
  }
7549
- if (value instanceof Y4.Array) {
8038
+ if (value instanceof Y7.Array) {
7550
8039
  return value.toArray().map((item) => toPlain(item));
7551
8040
  }
7552
8041
  return value;
@@ -7555,7 +8044,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7555
8044
  const result = {};
7556
8045
  const includeIndexes = !!options.includeIndexes;
7557
8046
  yDoc.share.forEach((type, name) => {
7558
- if (!(type instanceof Y4.Map)) return;
8047
+ if (!(type instanceof Y7.Map)) return;
7559
8048
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7560
8049
  const records = {};
7561
8050
  type.forEach((recordValue, recordId) => {
@@ -7604,12 +8093,14 @@ function summarizePlainYDoc(dump) {
7604
8093
  attachAndRegisterModel,
7605
8094
  attachSchemaToClass,
7606
8095
  autoRegisterModel,
8096
+ buildUniqueKey,
7607
8097
  clearMetaSyncCache,
7608
8098
  createModelClass,
7609
8099
  defineModelSchema,
7610
8100
  discoverModelNames,
7611
8101
  discoverSchema,
7612
8102
  dumpYDocToPlain,
8103
+ fieldUniqueConstraintName,
7613
8104
  generateULID,
7614
8105
  inferFieldType,
7615
8106
  initJsBao,
@@ -7620,5 +8111,6 @@ function summarizePlainYDoc(dump) {
7620
8111
  schemaToToml,
7621
8112
  summarizePlainYDoc,
7622
8113
  syncInferredMeta,
7623
- syncModelMeta
8114
+ syncModelMeta,
8115
+ uniqueIndexMapName
7624
8116
  });