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.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(
@@ -1161,10 +1192,109 @@ var init_DocumentQueryTranslator = __esm({
1161
1192
  }
1162
1193
  });
1163
1194
 
1164
- // src/models/metaSync.ts
1195
+ // src/utils/yjsValues.ts
1165
1196
  import * as Y from "yjs";
1197
+ function isYjsComposite(value) {
1198
+ return value instanceof Y.Map || value instanceof Y.Array || value instanceof Y.Text;
1199
+ }
1200
+ function isStringSetYMap(value) {
1201
+ if (!(value instanceof Y.Map)) return false;
1202
+ for (const v of value.values()) {
1203
+ if (v !== true) return false;
1204
+ }
1205
+ return true;
1206
+ }
1207
+ function normalizeYjsValue(value) {
1208
+ if (value instanceof Y.Text) {
1209
+ return value.toString();
1210
+ }
1211
+ if (value instanceof Y.Map || value instanceof Y.Array) {
1212
+ return value.toJSON();
1213
+ }
1214
+ return value;
1215
+ }
1216
+ function normalizeYjsValueForStorage(value) {
1217
+ if (value instanceof Y.Text) {
1218
+ return value.toString();
1219
+ }
1220
+ if (value instanceof Y.Map || value instanceof Y.Array) {
1221
+ return JSON.stringify(value.toJSON());
1222
+ }
1223
+ return value;
1224
+ }
1225
+ var init_yjsValues = __esm({
1226
+ "src/utils/yjsValues.ts"() {
1227
+ "use strict";
1228
+ }
1229
+ });
1230
+
1231
+ // src/models/writeLifecycle.ts
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";
1166
1296
  function inferFieldType(value) {
1167
- if (value instanceof Y.Map) return "stringset";
1297
+ if (isStringSetYMap(value)) return "stringset";
1168
1298
  switch (typeof value) {
1169
1299
  case "string":
1170
1300
  return "string";
@@ -1183,7 +1313,10 @@ function encodeDefault(value) {
1183
1313
  if (value === void 0 || value === null) return void 0;
1184
1314
  if (typeof value === "function") {
1185
1315
  const name = KNOWN_FUNCTION_DEFAULTS.get(value);
1186
- return name ? `$${name}` : "$unknown_function";
1316
+ return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
1317
+ }
1318
+ if (typeof value === "string" && value.startsWith("$")) {
1319
+ return `$${value}`;
1187
1320
  }
1188
1321
  return value;
1189
1322
  }
@@ -1209,7 +1342,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1209
1342
  if (compoundConstraints.length > 0) {
1210
1343
  let constraints = meta.get("_constraints");
1211
1344
  if (!constraints) {
1212
- constraints = new Y.Map();
1345
+ constraints = new Y3.Map();
1213
1346
  meta.set("_constraints", constraints);
1214
1347
  }
1215
1348
  for (const constraint of compoundConstraints) {
@@ -1220,7 +1353,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1220
1353
  if (relationships && Object.keys(relationships).length > 0) {
1221
1354
  let rels = meta.get("_relationships");
1222
1355
  if (!rels) {
1223
- rels = new Y.Map();
1356
+ rels = new Y3.Map();
1224
1357
  meta.set("_relationships", rels);
1225
1358
  }
1226
1359
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -1232,7 +1365,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1232
1365
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1233
1366
  let fieldMeta = metaMap.get(fieldName);
1234
1367
  if (!fieldMeta) {
1235
- fieldMeta = new Y.Map();
1368
+ fieldMeta = new Y3.Map();
1236
1369
  metaMap.set(fieldName, fieldMeta);
1237
1370
  }
1238
1371
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -1240,6 +1373,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1240
1373
  if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
1241
1374
  if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
1242
1375
  if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
1376
+ if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
1243
1377
  if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
1244
1378
  if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
1245
1379
  const encoded = encodeDefault(fieldOpts.default);
@@ -1248,7 +1382,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1248
1382
  function syncConstraintMeta(constraintsMap, constraint) {
1249
1383
  let cMeta = constraintsMap.get(constraint.name);
1250
1384
  if (!cMeta) {
1251
- cMeta = new Y.Map();
1385
+ cMeta = new Y3.Map();
1252
1386
  constraintsMap.set(constraint.name, cMeta);
1253
1387
  }
1254
1388
  setIfChanged(cMeta, "type", "unique");
@@ -1258,7 +1392,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
1258
1392
  function syncRelationshipMeta(relsMap, relName, relConfig) {
1259
1393
  let relMeta = relsMap.get(relName);
1260
1394
  if (!relMeta) {
1261
- relMeta = new Y.Map();
1395
+ relMeta = new Y3.Map();
1262
1396
  relsMap.set(relName, relMeta);
1263
1397
  }
1264
1398
  for (const [key, value] of Object.entries(relConfig)) {
@@ -1273,7 +1407,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
1273
1407
  if (fieldName.startsWith("_")) continue;
1274
1408
  let fieldMeta = meta.get(fieldName);
1275
1409
  if (!fieldMeta) {
1276
- fieldMeta = new Y.Map();
1410
+ fieldMeta = new Y3.Map();
1277
1411
  meta.set(fieldName, fieldMeta);
1278
1412
  }
1279
1413
  if (!fieldMeta.has("type")) {
@@ -1293,11 +1427,38 @@ var KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1293
1427
  var init_metaSync = __esm({
1294
1428
  "src/models/metaSync.ts"() {
1295
1429
  "use strict";
1430
+ init_yjsValues();
1431
+ init_writeLifecycle();
1296
1432
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
1297
1433
  _syncedCache = /* @__PURE__ */ new WeakMap();
1298
1434
  }
1299
1435
  });
1300
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
+
1301
1462
  // src/query/IncludeResolver.ts
1302
1463
  var IncludeResolver_exports = {};
1303
1464
  __export(IncludeResolver_exports, {
@@ -1656,7 +1817,13 @@ var init_IncludeResolver = __esm({
1656
1817
  }
1657
1818
  return null;
1658
1819
  }
1659
- /** 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
+ */
1660
1827
  _applyProjection(record, projection) {
1661
1828
  const fields = Object.entries(projection);
1662
1829
  if (fields.length === 0) return record;
@@ -1675,7 +1842,7 @@ var init_IncludeResolver = __esm({
1675
1842
  } else {
1676
1843
  const result = { ...record };
1677
1844
  for (const [f, v] of fields) {
1678
- if (v === 0) delete result[f];
1845
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
1679
1846
  }
1680
1847
  return result;
1681
1848
  }
@@ -1694,10 +1861,10 @@ __export(BaseModel_exports, {
1694
1861
  UniqueConstraintViolationError: () => UniqueConstraintViolationError,
1695
1862
  generateULID: () => generateULID
1696
1863
  });
1697
- import * as Y2 from "yjs";
1698
- import { ulid } from "ulid";
1864
+ import * as Y5 from "yjs";
1865
+ import { ulid as ulid2 } from "ulid";
1699
1866
  function generateULID() {
1700
- return ulid();
1867
+ return ulid2();
1701
1868
  }
1702
1869
  var LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel;
1703
1870
  var init_BaseModel = __esm({
@@ -1709,6 +1876,9 @@ var init_BaseModel = __esm({
1709
1876
  init_CursorManager();
1710
1877
  init_sql();
1711
1878
  init_metaSync();
1879
+ init_writeLifecycle();
1880
+ init_yjsValues();
1881
+ init_uniqueIndex();
1712
1882
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
1713
1883
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
1714
1884
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2093,7 +2263,9 @@ var init_BaseModel = __esm({
2093
2263
  this.setValue(fieldKey, data[fieldKey]);
2094
2264
  this._constructorProvidedFields.add(fieldKey);
2095
2265
  } else if (fieldOptions.default !== void 0) {
2096
- const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
2266
+ const defaultValue = resolveLiteralOrFunctionDefault(
2267
+ fieldOptions.default
2268
+ );
2097
2269
  this.setValue(fieldKey, defaultValue);
2098
2270
  }
2099
2271
  }
@@ -2217,7 +2389,30 @@ var init_BaseModel = __esm({
2217
2389
  if (verboseEnabled) {
2218
2390
  Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
2219
2391
  }
2220
- return fieldValue;
2392
+ return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
2393
+ }
2394
+ /**
2395
+ * Defensive normalization for the read/sync boundary (issue #625).
2396
+ *
2397
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
2398
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
2399
+ * through untouched. For any other field, a composite Yjs primitive
2400
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
2401
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
2402
+ * warn, rather than throwing. Scalars pass through unchanged.
2403
+ */
2404
+ normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
2405
+ const fieldOptions = schema?.fields?.get(fieldKey);
2406
+ if (fieldOptions?.type === "stringset") {
2407
+ return value;
2408
+ }
2409
+ if (isYjsComposite(value)) {
2410
+ Logger.warn(
2411
+ `[${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.`
2412
+ );
2413
+ return normalizeYjsValue(value);
2414
+ }
2415
+ return value;
2221
2416
  }
2222
2417
  getValue(fieldKey) {
2223
2418
  const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
@@ -2362,7 +2557,7 @@ var init_BaseModel = __esm({
2362
2557
  }
2363
2558
  getStringSetFromYjs(fieldName) {
2364
2559
  const yjsData = this.getFromYjs(fieldName);
2365
- if (yjsData instanceof Y2.Map) {
2560
+ if (yjsData instanceof Y5.Map) {
2366
2561
  return Array.from(yjsData.keys());
2367
2562
  }
2368
2563
  if (yjsData && typeof yjsData === "object") {
@@ -2470,7 +2665,7 @@ var init_BaseModel = __esm({
2470
2665
  }
2471
2666
  const extractStringSetValues = (value) => {
2472
2667
  if (!value) return [];
2473
- if (value instanceof Y2.Map) {
2668
+ if (value instanceof Y5.Map) {
2474
2669
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
2475
2670
  }
2476
2671
  if (Array.isArray(value)) {
@@ -2487,7 +2682,7 @@ var init_BaseModel = __esm({
2487
2682
  if (!recordId) continue;
2488
2683
  let itemData;
2489
2684
  const stringSetValuesByField = {};
2490
- if (recordData instanceof Y2.Map) {
2685
+ if (recordData instanceof Y5.Map) {
2491
2686
  itemData = {};
2492
2687
  const unknownFields = [];
2493
2688
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2603,20 +2798,10 @@ var init_BaseModel = __esm({
2603
2798
  Logger.verbose(
2604
2799
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
2605
2800
  );
2606
- const buildUniqueKey = (recordData, fields) => {
2607
- const keyParts = [];
2608
- for (const field of fields) {
2609
- const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
2610
- if (value === null || value === void 0) {
2611
- return null;
2612
- }
2613
- keyParts.push(String(value));
2614
- }
2615
- return keyParts.join("|");
2616
- };
2801
+ const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
2617
2802
  const extractItemData = (key, recordData) => {
2618
2803
  let itemData;
2619
- if (recordData instanceof Y2.Map) {
2804
+ if (recordData instanceof Y5.Map) {
2620
2805
  itemData = {};
2621
2806
  const unknownFields = [];
2622
2807
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2629,7 +2814,14 @@ var init_BaseModel = __esm({
2629
2814
  continue;
2630
2815
  }
2631
2816
  if (value !== void 0) {
2632
- itemData[fieldKey] = value;
2817
+ if (isYjsComposite(value)) {
2818
+ Logger.warn(
2819
+ `[${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.`
2820
+ );
2821
+ itemData[fieldKey] = normalizeYjsValueForStorage(value);
2822
+ } else {
2823
+ itemData[fieldKey] = value;
2824
+ }
2633
2825
  }
2634
2826
  }
2635
2827
  if (unknownFields.length > 0) {
@@ -2652,7 +2844,14 @@ var init_BaseModel = __esm({
2652
2844
  continue;
2653
2845
  }
2654
2846
  if (value !== void 0) {
2655
- filteredData[fieldKey] = value;
2847
+ if (isYjsComposite(value)) {
2848
+ Logger.warn(
2849
+ `[${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.`
2850
+ );
2851
+ filteredData[fieldKey] = normalizeYjsValueForStorage(value);
2852
+ } else {
2853
+ filteredData[fieldKey] = value;
2854
+ }
2656
2855
  }
2657
2856
  }
2658
2857
  if (unknownFields.length > 0) {
@@ -2686,7 +2885,7 @@ var init_BaseModel = __esm({
2686
2885
  const recordsByUniqueKey = /* @__PURE__ */ new Map();
2687
2886
  for (const [recordId, recordData] of allRecords.entries()) {
2688
2887
  if (recordIdsToDiscard.has(recordId)) continue;
2689
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
2888
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2690
2889
  if (uniqueKey === null) continue;
2691
2890
  if (!recordsByUniqueKey.has(uniqueKey)) {
2692
2891
  recordsByUniqueKey.set(uniqueKey, []);
@@ -2732,7 +2931,7 @@ var init_BaseModel = __esm({
2732
2931
  if (!recordData || !key) continue;
2733
2932
  const itemData = extractItemData(key, recordData);
2734
2933
  if (!itemData) continue;
2735
- if (change.action === "add" && recordData instanceof Y2.Map) {
2934
+ if (change.action === "add" && recordData instanceof Y5.Map) {
2736
2935
  Logger.verbose(
2737
2936
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2738
2937
  );
@@ -2830,9 +3029,12 @@ var init_BaseModel = __esm({
2830
3029
  const recordData = documentYMap.get(idToDiscard);
2831
3030
  if (!recordData) continue;
2832
3031
  for (const constraint of schema.resolvedUniqueConstraints) {
2833
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3032
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2834
3033
  if (uniqueKey === null) continue;
2835
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3034
+ const constraintMapName = uniqueIndexMapName(
3035
+ modelName,
3036
+ constraint.name
3037
+ );
2836
3038
  const constraintMap = yDoc.getMap(constraintMapName);
2837
3039
  const currentIndexValue = constraintMap.get(uniqueKey);
2838
3040
  if (currentIndexValue === idToDiscard) {
@@ -2850,11 +3052,14 @@ var init_BaseModel = __esm({
2850
3052
  }, `conflict-resolution-${modelName}-${docId}`);
2851
3053
  yDoc.transact(() => {
2852
3054
  for (const constraint of schema.resolvedUniqueConstraints) {
2853
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3055
+ const constraintMapName = uniqueIndexMapName(
3056
+ modelName,
3057
+ constraint.name
3058
+ );
2854
3059
  const constraintMap = yDoc.getMap(constraintMapName);
2855
3060
  for (const [recordId, recordData] of documentYMap.entries()) {
2856
3061
  if (!recordData) continue;
2857
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3062
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
2858
3063
  if (uniqueKey === null) continue;
2859
3064
  const currentIndexValue = constraintMap.get(uniqueKey);
2860
3065
  if (currentIndexValue !== recordId) {
@@ -2877,7 +3082,7 @@ var init_BaseModel = __esm({
2877
3082
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
2878
3083
  );
2879
3084
  for (const [recordId, recordData] of documentYMap.entries()) {
2880
- if (recordData instanceof Y2.Map) {
3085
+ if (recordData instanceof Y5.Map) {
2881
3086
  Logger.verbose(
2882
3087
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
2883
3088
  );
@@ -3062,6 +3267,11 @@ var init_BaseModel = __esm({
3062
3267
  Logger.debug(
3063
3268
  `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
3064
3269
  );
3270
+ if (isYjsComposite(yjsValue)) {
3271
+ Logger.warn(
3272
+ `[${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).`
3273
+ );
3274
+ }
3065
3275
  if (yjsValue === void 0) {
3066
3276
  Logger.debug(
3067
3277
  `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
@@ -3098,26 +3308,7 @@ var init_BaseModel = __esm({
3098
3308
  * if the field hasn't been touched since the wire-format change in #561.
3099
3309
  */
3100
3310
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3101
- let nested = recordYMap.get(fieldName);
3102
- if (!(nested instanceof Y2.Map)) {
3103
- const migrated = new Y2.Map();
3104
- if (nested && typeof nested === "object") {
3105
- for (const [member, marker] of Object.entries(
3106
- nested
3107
- )) {
3108
- if (Boolean(marker)) migrated.set(member, true);
3109
- }
3110
- }
3111
- recordYMap.set(fieldName, migrated);
3112
- nested = migrated;
3113
- }
3114
- const target = nested;
3115
- for (const member of change.additions) {
3116
- target.set(member, true);
3117
- }
3118
- for (const member of change.removals) {
3119
- target.delete(member);
3120
- }
3311
+ applyStringSetChange(recordYMap, fieldName, change);
3121
3312
  }
3122
3313
  /**
3123
3314
  * Deep equality check for comparing field values
@@ -3287,6 +3478,29 @@ var init_BaseModel = __esm({
3287
3478
  );
3288
3479
  throw new Error("Cannot save item without an id. Ensure id is set.");
3289
3480
  }
3481
+ const preTransactCreateStamps = /* @__PURE__ */ new Set();
3482
+ {
3483
+ const existingRecord = targetYMap.get(this.id);
3484
+ const isNew = !existingRecord;
3485
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
3486
+ const stamp = fieldOptions.autoStamp;
3487
+ if (!stamp) continue;
3488
+ const callerExplicit = this.hasLocalChange(fieldKey);
3489
+ if (callerExplicit) {
3490
+ const localValue = this._localChanges[fieldKey];
3491
+ if (localValue !== null && localValue !== void 0) continue;
3492
+ }
3493
+ if (stamp === "create") {
3494
+ if (!isNew) continue;
3495
+ const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
3496
+ if (persisted !== null && persisted !== void 0) continue;
3497
+ }
3498
+ this[fieldKey] = Date.now();
3499
+ if (stamp === "create") {
3500
+ preTransactCreateStamps.add(fieldKey);
3501
+ }
3502
+ }
3503
+ }
3290
3504
  this.validateBeforeSave();
3291
3505
  Logger.debug(`[${modelName}.save] About to get current JS state`);
3292
3506
  Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
@@ -3296,6 +3510,34 @@ var init_BaseModel = __esm({
3296
3510
  `[${modelName}.save] getCurrentJSState() returned:`,
3297
3511
  dataToSave
3298
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
+ }
3299
3541
  await yDoc.transact(async () => {
3300
3542
  if (upsertOnConstraint && upsertOnKey) {
3301
3543
  const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
@@ -3317,6 +3559,14 @@ var init_BaseModel = __esm({
3317
3559
  }
3318
3560
  }
3319
3561
  }
3562
+ for (const fieldKey of preTransactCreateStamps) {
3563
+ if (this._localChanges && fieldKey in this._localChanges) {
3564
+ delete this._localChanges[fieldKey];
3565
+ }
3566
+ if (fieldKey in dataToSave) {
3567
+ delete dataToSave[fieldKey];
3568
+ }
3569
+ }
3320
3570
  }
3321
3571
  }
3322
3572
  if (!targetYMap) {
@@ -3329,7 +3579,7 @@ var init_BaseModel = __esm({
3329
3579
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
3330
3580
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
3331
3581
  if (!recordYMap) {
3332
- recordYMap = new Y2.Map();
3582
+ recordYMap = new Y5.Map();
3333
3583
  Logger.verbose(
3334
3584
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
3335
3585
  );
@@ -3381,7 +3631,10 @@ var init_BaseModel = __esm({
3381
3631
  );
3382
3632
  continue;
3383
3633
  }
3384
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3634
+ const constraintMapName = uniqueIndexMapName(
3635
+ modelName,
3636
+ constraint.name
3637
+ );
3385
3638
  const constraintMap = yDoc.getMap(constraintMapName);
3386
3639
  Logger.verbose(
3387
3640
  `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
@@ -3410,6 +3663,26 @@ var init_BaseModel = __esm({
3410
3663
  this.id,
3411
3664
  existingRecordIdWithNewKey
3412
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
+ }
3413
3686
  }
3414
3687
  }
3415
3688
  if (isUpdate && oldData) {
@@ -3753,34 +4026,46 @@ var init_BaseModel = __esm({
3753
4026
  const modelName = schema.options.name;
3754
4027
  Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
3755
4028
  const hasIncludes = options?.include && options.include.length > 0;
3756
- 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
+ }
3757
4038
  const translator = new DocumentQueryTranslator(modelName, schema.fields);
3758
4039
  const translatedQuery = translator.translateFind(filter, translatorOptions);
3759
4040
  Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
3760
4041
  Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
3761
- const results = await _BaseModel.dbInstance.query(
4042
+ let results = await _BaseModel.dbInstance.query(
3762
4043
  translatedQuery.sql,
3763
4044
  translatedQuery.params
3764
4045
  );
3765
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
+ }
3766
4052
  if (options?.include && options.include.length > 0) {
3767
4053
  const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
3768
4054
  const resolver = new IncludeResolver2(_BaseModel.dbInstance);
3769
4055
  await resolver.resolve(results, options.include, 0, modelName);
3770
4056
  }
3771
- const requestedLimit = options?.limit;
3772
- const hasMore = CursorManager.hasMoreResults(
3773
- requestedLimit,
3774
- results.length
3775
- );
3776
4057
  const isFirstPage = !options?.uniqueStartKey;
4058
+ const direction = options?.direction || 1;
3777
4059
  const cursors = CursorManager.generateResultCursors(
3778
4060
  results,
3779
4061
  translatedQuery.sortFields,
3780
- options?.direction || 1,
4062
+ direction,
3781
4063
  hasMore,
3782
4064
  isFirstPage
3783
4065
  );
4066
+ if (direction === -1) {
4067
+ results.reverse();
4068
+ }
3784
4069
  let transformedData;
3785
4070
  if (options?.projection && Object.keys(options.projection).length > 0) {
3786
4071
  transformedData = results.map((row) => {
@@ -4356,7 +4641,7 @@ var init_BaseModel = __esm({
4356
4641
  );
4357
4642
  }
4358
4643
  for (const [recordId, recordYMap] of documentYMap.entries()) {
4359
- if (recordYMap instanceof Y2.Map) {
4644
+ if (recordYMap instanceof Y5.Map) {
4360
4645
  const instance = new this({ id: recordId });
4361
4646
  instance._metaDocId = docId;
4362
4647
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -4381,6 +4666,50 @@ var init_BaseModel = __esm({
4381
4666
  }
4382
4667
  return allInstances;
4383
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
+ }
4384
4713
  static async findByUnique(constraintName, value) {
4385
4714
  const modelConstructor = this;
4386
4715
  const schema = modelConstructor.getSchema();
@@ -4419,7 +4748,10 @@ var init_BaseModel = __esm({
4419
4748
  const documentInfo = modelConstructor.connectedDocuments.get(docId);
4420
4749
  if (!documentInfo) continue;
4421
4750
  const yDoc = documentInfo.yDoc;
4422
- const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4751
+ const constraintMapName2 = uniqueIndexMapName(
4752
+ currentModelName,
4753
+ constraint.name
4754
+ );
4423
4755
  const constraintMap2 = yDoc.getMap(constraintMapName2);
4424
4756
  const recordId2 = constraintMap2.get(uniqueKeyString);
4425
4757
  if (recordId2) {
@@ -4427,7 +4759,7 @@ var init_BaseModel = __esm({
4427
4759
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
4428
4760
  if (documentYMap) {
4429
4761
  const recordYMap2 = documentYMap.get(recordId2);
4430
- if (recordYMap2 && recordYMap2 instanceof Y2.Map) {
4762
+ if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
4431
4763
  const instance = new modelConstructor({
4432
4764
  id: recordId2
4433
4765
  });
@@ -4441,6 +4773,16 @@ var init_BaseModel = __esm({
4441
4773
  }
4442
4774
  }
4443
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
+ }
4444
4786
  Logger.verbose(
4445
4787
  `[${currentModelName}.findByUnique] Record not found in any connected document`
4446
4788
  );
@@ -4454,17 +4796,30 @@ var init_BaseModel = __esm({
4454
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.`
4455
4797
  );
4456
4798
  }
4457
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4799
+ const constraintMapName = uniqueIndexMapName(
4800
+ currentModelName,
4801
+ constraint.name
4802
+ );
4458
4803
  const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
4459
4804
  const recordId = constraintMap.get(uniqueKeyString);
4460
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
+ }
4461
4816
  return null;
4462
4817
  }
4463
4818
  const recordYMap = legacyDocumentMap.get(recordId);
4464
4819
  if (!recordYMap) {
4465
4820
  return null;
4466
4821
  }
4467
- if (recordYMap instanceof Y2.Map) {
4822
+ if (recordYMap instanceof Y5.Map) {
4468
4823
  const instance = new modelConstructor({
4469
4824
  id: recordId
4470
4825
  });
@@ -4522,7 +4877,10 @@ var init_BaseModel = __esm({
4522
4877
  `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
4523
4878
  );
4524
4879
  }
4525
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
4880
+ const constraintMapName = uniqueIndexMapName(
4881
+ currentModelName,
4882
+ constraint.name
4883
+ );
4526
4884
  let existingRecord = null;
4527
4885
  if (connectedDocuments && connectedDocuments.size > 0) {
4528
4886
  for (const docId of connectedDocuments.keys()) {
@@ -4540,6 +4898,33 @@ var init_BaseModel = __esm({
4540
4898
  }
4541
4899
  } else {
4542
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
+ }
4543
4928
  if (options?.objectMustExist) {
4544
4929
  if (!existingRecord) {
4545
4930
  throw new RecordNotFoundError(
@@ -4696,7 +5081,7 @@ var init_BaseModel = __esm({
4696
5081
  for (const [fieldKey, fieldOptions] of schema.fields) {
4697
5082
  if (fieldOptions?.type !== "stringset") continue;
4698
5083
  const value = recordYMap.get(fieldKey);
4699
- if (value instanceof Y2.Map) {
5084
+ if (value instanceof Y5.Map) {
4700
5085
  this.observeStringSetMapOnce(value);
4701
5086
  }
4702
5087
  }
@@ -4743,7 +5128,14 @@ var init_BaseModel = __esm({
4743
5128
  continue;
4744
5129
  }
4745
5130
  if (value !== void 0) {
4746
- updatedData[key] = value;
5131
+ if (isYjsComposite(value)) {
5132
+ Logger.warn(
5133
+ `[${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.`
5134
+ );
5135
+ updatedData[key] = normalizeYjsValueForStorage(value);
5136
+ } else {
5137
+ updatedData[key] = value;
5138
+ }
4747
5139
  }
4748
5140
  }
4749
5141
  if (unknownFields.length > 0) {
@@ -4823,7 +5215,14 @@ var init_BaseModel = __esm({
4823
5215
  continue;
4824
5216
  }
4825
5217
  if (value !== void 0) {
4826
- updatedData[key] = value;
5218
+ if (isYjsComposite(value)) {
5219
+ Logger.warn(
5220
+ `[${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.`
5221
+ );
5222
+ updatedData[key] = normalizeYjsValueForStorage(value);
5223
+ } else {
5224
+ updatedData[key] = value;
5225
+ }
4827
5226
  }
4828
5227
  }
4829
5228
  if (unknownFields.length > 0) {
@@ -4868,6 +5267,19 @@ var init_BaseModel = __esm({
4868
5267
  });
4869
5268
 
4870
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
+ }
4871
5283
  function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
4872
5284
  return async function() {
4873
5285
  const relatedId = this[config.relatedIdField];
@@ -4876,6 +5288,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
4876
5288
  };
4877
5289
  }
4878
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
+ );
4879
5301
  return async function(paginationArgs) {
4880
5302
  const {
4881
5303
  limit,
@@ -4883,71 +5305,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
4883
5305
  beforeCursor,
4884
5306
  direction = "forward"
4885
5307
  } = paginationArgs || {};
4886
- const {
4887
- relatedIdField,
4888
- orderByField = "id",
4889
- orderDirection = "ASC"
4890
- } = config;
4891
5308
  const targetSchema = targetModelClass.getSchema();
4892
5309
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
4893
5310
  Logger.error(
4894
5311
  `[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
4895
5312
  );
4896
- return { data: [], nextCursor: null, prevCursor: null };
5313
+ return { data: [], hasMore: false };
4897
5314
  }
4898
- const filter = { [relatedIdField]: this.id };
5315
+ const forward = direction !== "backward";
5316
+ const cursor = forward ? afterCursor : beforeCursor;
4899
5317
  const queryOptions = {
4900
- sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
5318
+ sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
5319
+ direction: forward ? 1 : -1
4901
5320
  };
4902
- if (limit !== void 0) {
4903
- queryOptions.limit = limit + 1;
4904
- }
4905
- if (direction === "forward" && afterCursor) {
4906
- filter[orderByField] = {
4907
- [orderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
4908
- };
4909
- } else if (direction === "backward") {
4910
- if (beforeCursor) {
4911
- filter[orderByField] = {
4912
- [orderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
4913
- };
4914
- }
4915
- queryOptions.sort = { [orderByField]: orderDirection === "ASC" ? -1 : 1 };
4916
- }
4917
- const result = await targetModelClass.query(filter, queryOptions);
4918
- const results = result.data;
4919
- let nextCursor = null;
4920
- let prevCursor = null;
4921
- const isFirstPage = direction === "forward" && !afterCursor;
4922
- if (limit !== void 0) {
4923
- if (direction === "forward") {
4924
- if (results.length > limit) {
4925
- nextCursor = results[limit - 1][orderByField];
4926
- results.pop();
4927
- }
4928
- if (results.length > 0 && !isFirstPage) {
4929
- prevCursor = results[0][orderByField];
4930
- }
4931
- } else {
4932
- if (results.length > limit) {
4933
- prevCursor = results[limit - 1][orderByField];
4934
- results.pop();
4935
- }
4936
- results.reverse();
4937
- if (results.length > 0 && beforeCursor) {
4938
- nextCursor = results[results.length - 1][orderByField];
4939
- }
4940
- if (results.length > 0 && !beforeCursor) {
4941
- if (results.length === limit) {
4942
- prevCursor = results[0][orderByField];
4943
- }
4944
- }
4945
- }
4946
- }
4947
- 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
+ );
4948
5327
  };
4949
5328
  }
4950
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
+ );
4951
5341
  return async function(paginationArgs) {
4952
5342
  const {
4953
5343
  limit,
@@ -4961,87 +5351,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
4961
5351
  Logger.error(
4962
5352
  `[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
4963
5353
  );
4964
- return { data: [], nextCursor: null, prevCursor: null };
5354
+ return { data: [], hasMore: false };
4965
5355
  }
4966
5356
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
4967
5357
  Logger.error(
4968
5358
  `[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
4969
5359
  );
4970
- return { data: [], nextCursor: null, prevCursor: null };
5360
+ return { data: [], hasMore: false };
4971
5361
  }
4972
- const {
4973
- joinModelLocalField,
4974
- joinModelRelatedField,
4975
- joinModelOrderByField = "id",
4976
- joinModelOrderDirection = "ASC"
4977
- } = config;
4978
- const joinFilter = { [joinModelLocalField]: this.id };
5362
+ const forward = direction !== "backward";
5363
+ const cursor = forward ? afterCursor : beforeCursor;
4979
5364
  const joinQueryOptions = {
4980
5365
  sort: {
4981
5366
  [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
4982
5367
  },
4983
- projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
5368
+ projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
5369
+ direction: forward ? 1 : -1
4984
5370
  };
4985
- if (limit !== void 0) {
4986
- joinQueryOptions.limit = limit + 1;
4987
- }
4988
- if (direction === "forward" && afterCursor) {
4989
- joinFilter[joinModelOrderByField] = {
4990
- [joinModelOrderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
4991
- };
4992
- } else if (direction === "backward") {
4993
- if (beforeCursor) {
4994
- joinFilter[joinModelOrderByField] = {
4995
- [joinModelOrderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
4996
- };
4997
- }
4998
- joinQueryOptions.sort = {
4999
- [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? -1 : 1
5000
- };
5001
- }
5371
+ if (limit !== void 0) joinQueryOptions.limit = limit;
5372
+ if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
5002
5373
  const joinResult = await joinModelClass.query(
5003
- joinFilter,
5374
+ { [joinModelLocalField]: this.id },
5004
5375
  joinQueryOptions
5005
5376
  );
5006
- const joinResults = joinResult.data;
5007
- let nextCursorFromJoin = null;
5008
- let prevCursorFromJoin = null;
5009
- const isFirstPage = direction === "forward" && !afterCursor;
5010
- if (limit !== void 0) {
5011
- if (direction === "forward") {
5012
- if (joinResults.length > limit) {
5013
- nextCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
5014
- joinResults.pop();
5015
- }
5016
- if (joinResults.length > 0 && !isFirstPage) {
5017
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
5018
- }
5019
- } else {
5020
- if (joinResults.length > limit) {
5021
- prevCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
5022
- joinResults.pop();
5023
- }
5024
- joinResults.reverse();
5025
- if (joinResults.length > 0 && beforeCursor) {
5026
- nextCursorFromJoin = joinResults[joinResults.length - 1][joinModelOrderByField];
5027
- }
5028
- if (joinResults.length > 0 && !beforeCursor) {
5029
- if (joinResults.length === limit) {
5030
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
5031
- }
5032
- }
5033
- }
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;
5034
5384
  }
5035
- const relatedIds = joinResults.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
5036
- if (relatedIds.length === 0)
5037
- return { data: [], nextCursor: null, prevCursor: null };
5038
- const targetResult = await targetModelClass.query({
5039
- id: { $in: relatedIds }
5040
- });
5041
5385
  return {
5042
- data: targetResult.data,
5043
- nextCursor: nextCursorFromJoin,
5044
- prevCursor: prevCursorFromJoin
5386
+ data: targetData,
5387
+ nextCursor: joinResult.nextCursor,
5388
+ prevCursor: joinResult.prevCursor,
5389
+ hasMore: joinResult.hasMore
5045
5390
  };
5046
5391
  };
5047
5392
  }
@@ -5165,10 +5510,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
5165
5510
  }, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
5166
5511
  };
5167
5512
  }
5513
+ var ALWAYS_PRESENT_ORDER_FIELDS;
5168
5514
  var init_relationshipManager = __esm({
5169
5515
  "src/models/relationshipManager.ts"() {
5170
5516
  "use strict";
5171
5517
  init_BaseModel();
5518
+ ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
5172
5519
  }
5173
5520
  });
5174
5521
 
@@ -5638,13 +5985,100 @@ var init_environment = __esm({
5638
5985
  // src/engines/SqljsEngine.ts
5639
5986
  import initSqlJs from "sql.js";
5640
5987
  import { Mutex } from "async-mutex";
5641
- 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;
5642
6074
  var init_SqljsEngine = __esm({
5643
6075
  "src/engines/SqljsEngine.ts"() {
5644
6076
  "use strict";
5645
6077
  init_BaseModel();
5646
6078
  init_sql();
6079
+ init_environment();
5647
6080
  SQL_WASM_URL = "/sql-wasm.wasm";
6081
+ SQL_WASM_FILENAME = "sql-wasm.wasm";
5648
6082
  TransactionalSQLJSOperations = class {
5649
6083
  db;
5650
6084
  engine;
@@ -5710,22 +6144,37 @@ var init_SqljsEngine = __esm({
5710
6144
  }
5711
6145
  async initialize(engineOptions) {
5712
6146
  Logger.info("[SqljsEngine] Initializing SQL.js engine...");
6147
+ const loadingModuleThisCall = !_SqljsEngine.SQL;
5713
6148
  try {
5714
6149
  if (!_SqljsEngine.SQL) {
5715
- 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);
5716
6162
  Logger.debug(
5717
6163
  "[SqljsEngine] Attempting to load SQL.js WASM from:",
5718
- locateFileConfig("sql-wasm.wasm")
6164
+ wasmPath
5719
6165
  );
5720
- _SqljsEngine.SQL = await initSqlJs({
5721
- locateFile: locateFileConfig
5722
- });
6166
+ await preflightNodeWasm(wasmPath);
6167
+ const SQL = await loadSqlJsModule(locateFileConfig);
6168
+ _SqljsEngine.SQL = SQL;
5723
6169
  Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
5724
6170
  }
5725
6171
  this.db = new _SqljsEngine.SQL.Database();
5726
6172
  Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
5727
6173
  this.updateTableNames();
5728
6174
  } catch (error) {
6175
+ if (loadingModuleThisCall) {
6176
+ _SqljsEngine.SQL = null;
6177
+ }
5729
6178
  Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
5730
6179
  throw error;
5731
6180
  }
@@ -6910,6 +7359,7 @@ init_StringSet();
6910
7359
  // src/models/schema.ts
6911
7360
  init_BaseModel();
6912
7361
  init_ModelRegistry();
7362
+ init_uniqueIndex();
6913
7363
  function defineModelSchema(input) {
6914
7364
  const { name, fields } = input;
6915
7365
  const options = {
@@ -6982,7 +7432,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
6982
7432
  for (const [fieldName, options] of fields.entries()) {
6983
7433
  if (options.unique) {
6984
7434
  resolved.push({
6985
- name: `${modelName}_${fieldName}_unique`,
7435
+ name: fieldUniqueConstraintName(modelName, fieldName),
6986
7436
  fields: [fieldName]
6987
7437
  });
6988
7438
  }
@@ -7103,10 +7553,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
7103
7553
  "unique",
7104
7554
  "required",
7105
7555
  "auto_assign",
7556
+ "auto_stamp",
7106
7557
  "max_length",
7107
7558
  "max_count",
7108
- "default"
7559
+ "default",
7560
+ "enum"
7109
7561
  ]);
7562
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
7110
7563
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
7111
7564
  "fields",
7112
7565
  "relationships",
@@ -7148,9 +7601,37 @@ function parseFieldOptions(raw, context, strict) {
7148
7601
  if (raw.unique === true) opts.unique = true;
7149
7602
  if (raw.required === true) opts.required = true;
7150
7603
  if (raw.auto_assign === true) opts.autoAssign = true;
7604
+ if (raw.auto_stamp !== void 0) {
7605
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
7606
+ throw new Error(
7607
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
7608
+ ...VALID_AUTO_STAMP_VALUES
7609
+ ].join(", ")}`
7610
+ );
7611
+ }
7612
+ opts.autoStamp = raw.auto_stamp;
7613
+ }
7151
7614
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
7152
7615
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
7153
7616
  if (raw.default !== void 0) opts.default = raw.default;
7617
+ if (raw.enum !== void 0) {
7618
+ if (raw.type !== "string") {
7619
+ throw new Error(
7620
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
7621
+ );
7622
+ }
7623
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
7624
+ throw new Error(
7625
+ `${context}: \`enum\` must be a non-empty array of strings.`
7626
+ );
7627
+ }
7628
+ if (!raw.enum.every((v) => typeof v === "string")) {
7629
+ throw new Error(
7630
+ `${context}: \`enum\` values must all be strings.`
7631
+ );
7632
+ }
7633
+ opts.enum = [...raw.enum];
7634
+ }
7154
7635
  return opts;
7155
7636
  }
7156
7637
  function requireField(raw, field, context) {
@@ -7278,7 +7759,8 @@ async function loadSchemaFromToml(filePath, options = {}) {
7278
7759
  }
7279
7760
 
7280
7761
  // src/utils/yDocSchema.ts
7281
- import * as Y3 from "yjs";
7762
+ init_yjsValues();
7763
+ import * as Y6 from "yjs";
7282
7764
  function discoverSchema(yDoc) {
7283
7765
  const models = {};
7284
7766
  const metaNames = /* @__PURE__ */ new Set();
@@ -7312,7 +7794,7 @@ function discoverModelNames(yDoc) {
7312
7794
  function materializeMap(yDoc, key) {
7313
7795
  try {
7314
7796
  const map = yDoc.getMap(key);
7315
- return map instanceof Y3.Map ? map : null;
7797
+ return map instanceof Y6.Map ? map : null;
7316
7798
  } catch {
7317
7799
  return null;
7318
7800
  }
@@ -7322,11 +7804,11 @@ function readModelMeta(metaMap) {
7322
7804
  let constraints;
7323
7805
  let relationships;
7324
7806
  for (const [key, value] of metaMap.entries()) {
7325
- if (key === "_constraints" && value instanceof Y3.Map) {
7807
+ if (key === "_constraints" && value instanceof Y6.Map) {
7326
7808
  constraints = readConstraints(value);
7327
- } else if (key === "_relationships" && value instanceof Y3.Map) {
7809
+ } else if (key === "_relationships" && value instanceof Y6.Map) {
7328
7810
  relationships = readRelationships(value);
7329
- } else if (value instanceof Y3.Map) {
7811
+ } else if (value instanceof Y6.Map) {
7330
7812
  fields[key] = readFieldMeta(value);
7331
7813
  }
7332
7814
  }
@@ -7345,6 +7827,10 @@ function readFieldMeta(fieldMap) {
7345
7827
  if (fieldMap.get("unique") === true) field.unique = true;
7346
7828
  if (fieldMap.get("required") === true) field.required = true;
7347
7829
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7830
+ const autoStamp = fieldMap.get("autoStamp");
7831
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
7832
+ field.autoStamp = autoStamp;
7833
+ }
7348
7834
  const def = fieldMap.get("default");
7349
7835
  if (def !== void 0) field.default = def;
7350
7836
  const maxLength = fieldMap.get("maxLength");
@@ -7357,7 +7843,7 @@ function inferModelFromData(dataMap) {
7357
7843
  const fields = {};
7358
7844
  let sampled = 0;
7359
7845
  for (const [_recordId, recordValue] of dataMap.entries()) {
7360
- if (!(recordValue instanceof Y3.Map)) continue;
7846
+ if (!(recordValue instanceof Y6.Map)) continue;
7361
7847
  if (++sampled > 5) break;
7362
7848
  for (const [fieldName, value] of recordValue.entries()) {
7363
7849
  if (fieldName.startsWith("_")) continue;
@@ -7370,7 +7856,7 @@ function inferModelFromData(dataMap) {
7370
7856
  return { fields };
7371
7857
  }
7372
7858
  function inferTypeFromValue(value) {
7373
- if (value instanceof Y3.Map) return "stringset";
7859
+ if (isStringSetYMap(value)) return "stringset";
7374
7860
  switch (typeof value) {
7375
7861
  case "string":
7376
7862
  return "string";
@@ -7385,7 +7871,7 @@ function inferTypeFromValue(value) {
7385
7871
  function readConstraints(constraintsMap) {
7386
7872
  const out = {};
7387
7873
  for (const [name, value] of constraintsMap.entries()) {
7388
- if (!(value instanceof Y3.Map)) continue;
7874
+ if (!(value instanceof Y6.Map)) continue;
7389
7875
  let fields = [];
7390
7876
  const rawFields = value.get("fields");
7391
7877
  if (typeof rawFields === "string") {
@@ -7404,7 +7890,7 @@ function readConstraints(constraintsMap) {
7404
7890
  function readRelationships(relsMap) {
7405
7891
  const out = {};
7406
7892
  for (const [name, value] of relsMap.entries()) {
7407
- if (!(value instanceof Y3.Map)) continue;
7893
+ if (!(value instanceof Y6.Map)) continue;
7408
7894
  const rel = {};
7409
7895
  for (const [k, v] of value.entries()) {
7410
7896
  rel[k] = v;
@@ -7415,6 +7901,7 @@ function readRelationships(relsMap) {
7415
7901
  }
7416
7902
  var CAMEL_TO_SNAKE = {
7417
7903
  autoAssign: "auto_assign",
7904
+ autoStamp: "auto_stamp",
7418
7905
  maxLength: "max_length",
7419
7906
  maxCount: "max_count",
7420
7907
  relatedIdField: "related_id_field",
@@ -7445,6 +7932,7 @@ function schemaToToml(schema) {
7445
7932
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
7446
7933
  lines.push(`type = ${tomlValue(field.type)}`);
7447
7934
  if (field.autoAssign) lines.push("auto_assign = true");
7935
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
7448
7936
  if (field.indexed) lines.push("indexed = true");
7449
7937
  if (field.unique) lines.push("unique = true");
7450
7938
  if (field.required) lines.push("required = true");
@@ -7476,19 +7964,20 @@ function schemaToToml(schema) {
7476
7964
  }
7477
7965
 
7478
7966
  // src/index.ts
7967
+ init_uniqueIndex();
7479
7968
  init_BaseModel();
7480
7969
 
7481
7970
  // src/utils/yDocDump.ts
7482
- import * as Y4 from "yjs";
7971
+ import * as Y7 from "yjs";
7483
7972
  function toPlain(value) {
7484
- if (value instanceof Y4.Map) {
7973
+ if (value instanceof Y7.Map) {
7485
7974
  const obj = {};
7486
7975
  value.forEach((v, k) => {
7487
7976
  obj[k] = toPlain(v);
7488
7977
  });
7489
7978
  return obj;
7490
7979
  }
7491
- if (value instanceof Y4.Array) {
7980
+ if (value instanceof Y7.Array) {
7492
7981
  return value.toArray().map((item) => toPlain(item));
7493
7982
  }
7494
7983
  return value;
@@ -7497,7 +7986,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7497
7986
  const result = {};
7498
7987
  const includeIndexes = !!options.includeIndexes;
7499
7988
  yDoc.share.forEach((type, name) => {
7500
- if (!(type instanceof Y4.Map)) return;
7989
+ if (!(type instanceof Y7.Map)) return;
7501
7990
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7502
7991
  const records = {};
7503
7992
  type.forEach((recordValue, recordId) => {
@@ -7545,12 +8034,14 @@ export {
7545
8034
  attachAndRegisterModel,
7546
8035
  attachSchemaToClass,
7547
8036
  autoRegisterModel,
8037
+ buildUniqueKey,
7548
8038
  clearMetaSyncCache,
7549
8039
  createModelClass,
7550
8040
  defineModelSchema,
7551
8041
  discoverModelNames,
7552
8042
  discoverSchema,
7553
8043
  dumpYDocToPlain,
8044
+ fieldUniqueConstraintName,
7554
8045
  generateULID,
7555
8046
  inferFieldType,
7556
8047
  initJsBao,
@@ -7561,5 +8052,6 @@ export {
7561
8052
  schemaToToml,
7562
8053
  summarizePlainYDoc,
7563
8054
  syncInferredMeta,
7564
- syncModelMeta
8055
+ syncModelMeta,
8056
+ uniqueIndexMapName
7565
8057
  };