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/browser.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,17 +1439,58 @@ 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/models/relationshipManager.ts
1481
+ function warnIfOrderFieldNullable(modelClass, orderByField, relationshipDescription) {
1482
+ if (ALWAYS_PRESENT_ORDER_FIELDS.has(orderByField)) return;
1483
+ const schema = modelClass.getSchema?.();
1484
+ const fields = schema?.fields;
1485
+ if (!fields || typeof fields.get !== "function") return;
1486
+ const fieldOptions = fields.get(orderByField);
1487
+ if (!fieldOptions) return;
1488
+ if (fieldOptions.required !== true) {
1489
+ Logger.warn(
1490
+ `[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.)`
1491
+ );
1492
+ }
1493
+ }
1324
1494
  function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1325
1495
  return async function() {
1326
1496
  const relatedId = this[config.relatedIdField];
@@ -1329,6 +1499,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1329
1499
  };
1330
1500
  }
1331
1501
  function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbEngine) {
1502
+ const {
1503
+ relatedIdField,
1504
+ orderByField = "id",
1505
+ orderDirection = "ASC"
1506
+ } = config;
1507
+ warnIfOrderFieldNullable(
1508
+ targetModelClass,
1509
+ orderByField,
1510
+ `hasMany(${targetModelClass.name})`
1511
+ );
1332
1512
  return async function(paginationArgs) {
1333
1513
  const {
1334
1514
  limit,
@@ -1336,71 +1516,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
1336
1516
  beforeCursor,
1337
1517
  direction = "forward"
1338
1518
  } = paginationArgs || {};
1339
- const {
1340
- relatedIdField,
1341
- orderByField = "id",
1342
- orderDirection = "ASC"
1343
- } = config;
1344
1519
  const targetSchema = targetModelClass.getSchema();
1345
1520
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1346
1521
  Logger.error(
1347
1522
  `[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
1348
1523
  );
1349
- return { data: [], nextCursor: null, prevCursor: null };
1524
+ return { data: [], hasMore: false };
1350
1525
  }
1351
- const filter = { [relatedIdField]: this.id };
1526
+ const forward = direction !== "backward";
1527
+ const cursor = forward ? afterCursor : beforeCursor;
1352
1528
  const queryOptions = {
1353
- sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
1529
+ sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
1530
+ direction: forward ? 1 : -1
1354
1531
  };
1355
- if (limit !== void 0) {
1356
- queryOptions.limit = limit + 1;
1357
- }
1358
- if (direction === "forward" && afterCursor) {
1359
- filter[orderByField] = {
1360
- [orderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
1361
- };
1362
- } else if (direction === "backward") {
1363
- if (beforeCursor) {
1364
- filter[orderByField] = {
1365
- [orderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
1366
- };
1367
- }
1368
- queryOptions.sort = { [orderByField]: orderDirection === "ASC" ? -1 : 1 };
1369
- }
1370
- const result = await targetModelClass.query(filter, queryOptions);
1371
- const results = result.data;
1372
- let nextCursor = null;
1373
- let prevCursor = null;
1374
- const isFirstPage = direction === "forward" && !afterCursor;
1375
- if (limit !== void 0) {
1376
- if (direction === "forward") {
1377
- if (results.length > limit) {
1378
- nextCursor = results[limit - 1][orderByField];
1379
- results.pop();
1380
- }
1381
- if (results.length > 0 && !isFirstPage) {
1382
- prevCursor = results[0][orderByField];
1383
- }
1384
- } else {
1385
- if (results.length > limit) {
1386
- prevCursor = results[limit - 1][orderByField];
1387
- results.pop();
1388
- }
1389
- results.reverse();
1390
- if (results.length > 0 && beforeCursor) {
1391
- nextCursor = results[results.length - 1][orderByField];
1392
- }
1393
- if (results.length > 0 && !beforeCursor) {
1394
- if (results.length === limit) {
1395
- prevCursor = results[0][orderByField];
1396
- }
1397
- }
1398
- }
1399
- }
1400
- return { data: results, nextCursor, prevCursor };
1532
+ if (limit !== void 0) queryOptions.limit = limit;
1533
+ if (cursor != null) queryOptions.uniqueStartKey = cursor;
1534
+ return targetModelClass.query(
1535
+ { [relatedIdField]: this.id },
1536
+ queryOptions
1537
+ );
1401
1538
  };
1402
1539
  }
1403
1540
  function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClass, joinModelClass, _dbEngine) {
1541
+ const {
1542
+ joinModelLocalField,
1543
+ joinModelRelatedField,
1544
+ joinModelOrderByField = "id",
1545
+ joinModelOrderDirection = "ASC"
1546
+ } = config;
1547
+ warnIfOrderFieldNullable(
1548
+ joinModelClass,
1549
+ joinModelOrderByField,
1550
+ `hasManyThrough(${targetModelClass.name} via ${joinModelClass.name})`
1551
+ );
1404
1552
  return async function(paginationArgs) {
1405
1553
  const {
1406
1554
  limit,
@@ -1414,87 +1562,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
1414
1562
  Logger.error(
1415
1563
  `[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
1416
1564
  );
1417
- return { data: [], nextCursor: null, prevCursor: null };
1565
+ return { data: [], hasMore: false };
1418
1566
  }
1419
1567
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1420
1568
  Logger.error(
1421
1569
  `[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
1422
1570
  );
1423
- return { data: [], nextCursor: null, prevCursor: null };
1571
+ return { data: [], hasMore: false };
1424
1572
  }
1425
- const {
1426
- joinModelLocalField,
1427
- joinModelRelatedField,
1428
- joinModelOrderByField = "id",
1429
- joinModelOrderDirection = "ASC"
1430
- } = config;
1431
- const joinFilter = { [joinModelLocalField]: this.id };
1573
+ const forward = direction !== "backward";
1574
+ const cursor = forward ? afterCursor : beforeCursor;
1432
1575
  const joinQueryOptions = {
1433
1576
  sort: {
1434
1577
  [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
1435
1578
  },
1436
- projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
1579
+ projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
1580
+ direction: forward ? 1 : -1
1437
1581
  };
1438
- if (limit !== void 0) {
1439
- joinQueryOptions.limit = limit + 1;
1440
- }
1441
- if (direction === "forward" && afterCursor) {
1442
- joinFilter[joinModelOrderByField] = {
1443
- [joinModelOrderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
1444
- };
1445
- } else if (direction === "backward") {
1446
- if (beforeCursor) {
1447
- joinFilter[joinModelOrderByField] = {
1448
- [joinModelOrderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
1449
- };
1450
- }
1451
- joinQueryOptions.sort = {
1452
- [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? -1 : 1
1453
- };
1454
- }
1582
+ if (limit !== void 0) joinQueryOptions.limit = limit;
1583
+ if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
1455
1584
  const joinResult = await joinModelClass.query(
1456
- joinFilter,
1585
+ { [joinModelLocalField]: this.id },
1457
1586
  joinQueryOptions
1458
1587
  );
1459
- const joinResults = joinResult.data;
1460
- let nextCursorFromJoin = null;
1461
- let prevCursorFromJoin = null;
1462
- const isFirstPage = direction === "forward" && !afterCursor;
1463
- if (limit !== void 0) {
1464
- if (direction === "forward") {
1465
- if (joinResults.length > limit) {
1466
- nextCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
1467
- joinResults.pop();
1468
- }
1469
- if (joinResults.length > 0 && !isFirstPage) {
1470
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
1471
- }
1472
- } else {
1473
- if (joinResults.length > limit) {
1474
- prevCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
1475
- joinResults.pop();
1476
- }
1477
- joinResults.reverse();
1478
- if (joinResults.length > 0 && beforeCursor) {
1479
- nextCursorFromJoin = joinResults[joinResults.length - 1][joinModelOrderByField];
1480
- }
1481
- if (joinResults.length > 0 && !beforeCursor) {
1482
- if (joinResults.length === limit) {
1483
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
1484
- }
1485
- }
1486
- }
1588
+ const relatedIds = joinResult.data.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1589
+ let targetData = [];
1590
+ if (relatedIds.length > 0) {
1591
+ const targetResult = await targetModelClass.query({
1592
+ id: { $in: relatedIds }
1593
+ });
1594
+ targetData = targetResult.data;
1487
1595
  }
1488
- const relatedIds = joinResults.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1489
- if (relatedIds.length === 0)
1490
- return { data: [], nextCursor: null, prevCursor: null };
1491
- const targetResult = await targetModelClass.query({
1492
- id: { $in: relatedIds }
1493
- });
1494
1596
  return {
1495
- data: targetResult.data,
1496
- nextCursor: nextCursorFromJoin,
1497
- prevCursor: prevCursorFromJoin
1597
+ data: targetData,
1598
+ nextCursor: joinResult.nextCursor,
1599
+ prevCursor: joinResult.prevCursor,
1600
+ hasMore: joinResult.hasMore
1498
1601
  };
1499
1602
  };
1500
1603
  }
@@ -1618,10 +1721,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
1618
1721
  }, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
1619
1722
  };
1620
1723
  }
1724
+ var ALWAYS_PRESENT_ORDER_FIELDS;
1621
1725
  var init_relationshipManager = __esm({
1622
1726
  "src/models/relationshipManager.ts"() {
1623
1727
  "use strict";
1624
1728
  init_BaseModel();
1729
+ ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
1625
1730
  }
1626
1731
  });
1627
1732
 
@@ -2392,7 +2497,13 @@ var init_IncludeResolver = __esm({
2392
2497
  }
2393
2498
  return null;
2394
2499
  }
2395
- /** Apply projection to a single record. */
2500
+ /**
2501
+ * Apply a projection to a single record. Assumes the projection was already
2502
+ * validated upstream (mixed inclusion+exclusion rejected by the translator):
2503
+ * `isInclusion` treats "any value === 1" as inclusion mode. `id` and `type`
2504
+ * are system-identity fields and are always kept in BOTH modes (mirrors the
2505
+ * DatabaseDO `_applyProjection`); `_meta` stays excludable.
2506
+ */
2396
2507
  _applyProjection(record, projection) {
2397
2508
  const fields = Object.entries(projection);
2398
2509
  if (fields.length === 0) return record;
@@ -2411,7 +2522,7 @@ var init_IncludeResolver = __esm({
2411
2522
  } else {
2412
2523
  const result = { ...record };
2413
2524
  for (const [f, v] of fields) {
2414
- if (v === 0) delete result[f];
2525
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
2415
2526
  }
2416
2527
  return result;
2417
2528
  }
@@ -2431,20 +2542,23 @@ __export(BaseModel_exports, {
2431
2542
  generateULID: () => generateULID
2432
2543
  });
2433
2544
  function generateULID() {
2434
- return (0, import_ulid.ulid)();
2545
+ return (0, import_ulid2.ulid)();
2435
2546
  }
2436
- var Y2, import_ulid, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2547
+ var Y5, import_ulid2, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2437
2548
  var init_BaseModel = __esm({
2438
2549
  "src/models/BaseModel.ts"() {
2439
2550
  "use strict";
2440
- Y2 = __toESM(require("yjs"), 1);
2441
- import_ulid = require("ulid");
2551
+ Y5 = __toESM(require("yjs"), 1);
2552
+ import_ulid2 = require("ulid");
2442
2553
  init_StringSet();
2443
2554
  init_documentTypes();
2444
2555
  init_DocumentQueryTranslator();
2445
2556
  init_CursorManager();
2446
2557
  init_sql();
2447
2558
  init_metaSync();
2559
+ init_writeLifecycle();
2560
+ init_yjsValues();
2561
+ init_uniqueIndex();
2448
2562
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2449
2563
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
2450
2564
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2829,7 +2943,9 @@ var init_BaseModel = __esm({
2829
2943
  this.setValue(fieldKey, data[fieldKey]);
2830
2944
  this._constructorProvidedFields.add(fieldKey);
2831
2945
  } else if (fieldOptions.default !== void 0) {
2832
- const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
2946
+ const defaultValue = resolveLiteralOrFunctionDefault(
2947
+ fieldOptions.default
2948
+ );
2833
2949
  this.setValue(fieldKey, defaultValue);
2834
2950
  }
2835
2951
  }
@@ -2953,7 +3069,30 @@ var init_BaseModel = __esm({
2953
3069
  if (verboseEnabled) {
2954
3070
  Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
2955
3071
  }
2956
- return fieldValue;
3072
+ return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
3073
+ }
3074
+ /**
3075
+ * Defensive normalization for the read/sync boundary (issue #625).
3076
+ *
3077
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
3078
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
3079
+ * through untouched. For any other field, a composite Yjs primitive
3080
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
3081
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
3082
+ * warn, rather than throwing. Scalars pass through unchanged.
3083
+ */
3084
+ normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
3085
+ const fieldOptions = schema?.fields?.get(fieldKey);
3086
+ if (fieldOptions?.type === "stringset") {
3087
+ return value;
3088
+ }
3089
+ if (isYjsComposite(value)) {
3090
+ Logger.warn(
3091
+ `[${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.`
3092
+ );
3093
+ return normalizeYjsValue(value);
3094
+ }
3095
+ return value;
2957
3096
  }
2958
3097
  getValue(fieldKey) {
2959
3098
  const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
@@ -3098,7 +3237,7 @@ var init_BaseModel = __esm({
3098
3237
  }
3099
3238
  getStringSetFromYjs(fieldName) {
3100
3239
  const yjsData = this.getFromYjs(fieldName);
3101
- if (yjsData instanceof Y2.Map) {
3240
+ if (yjsData instanceof Y5.Map) {
3102
3241
  return Array.from(yjsData.keys());
3103
3242
  }
3104
3243
  if (yjsData && typeof yjsData === "object") {
@@ -3206,7 +3345,7 @@ var init_BaseModel = __esm({
3206
3345
  }
3207
3346
  const extractStringSetValues = (value) => {
3208
3347
  if (!value) return [];
3209
- if (value instanceof Y2.Map) {
3348
+ if (value instanceof Y5.Map) {
3210
3349
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
3211
3350
  }
3212
3351
  if (Array.isArray(value)) {
@@ -3223,7 +3362,7 @@ var init_BaseModel = __esm({
3223
3362
  if (!recordId) continue;
3224
3363
  let itemData;
3225
3364
  const stringSetValuesByField = {};
3226
- if (recordData instanceof Y2.Map) {
3365
+ if (recordData instanceof Y5.Map) {
3227
3366
  itemData = {};
3228
3367
  const unknownFields = [];
3229
3368
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3339,20 +3478,10 @@ var init_BaseModel = __esm({
3339
3478
  Logger.verbose(
3340
3479
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
3341
3480
  );
3342
- const buildUniqueKey = (recordData, fields) => {
3343
- const keyParts = [];
3344
- for (const field of fields) {
3345
- const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
3346
- if (value === null || value === void 0) {
3347
- return null;
3348
- }
3349
- keyParts.push(String(value));
3350
- }
3351
- return keyParts.join("|");
3352
- };
3481
+ const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
3353
3482
  const extractItemData = (key, recordData) => {
3354
3483
  let itemData;
3355
- if (recordData instanceof Y2.Map) {
3484
+ if (recordData instanceof Y5.Map) {
3356
3485
  itemData = {};
3357
3486
  const unknownFields = [];
3358
3487
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3365,7 +3494,14 @@ var init_BaseModel = __esm({
3365
3494
  continue;
3366
3495
  }
3367
3496
  if (value !== void 0) {
3368
- itemData[fieldKey] = value;
3497
+ if (isYjsComposite(value)) {
3498
+ Logger.warn(
3499
+ `[${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.`
3500
+ );
3501
+ itemData[fieldKey] = normalizeYjsValueForStorage(value);
3502
+ } else {
3503
+ itemData[fieldKey] = value;
3504
+ }
3369
3505
  }
3370
3506
  }
3371
3507
  if (unknownFields.length > 0) {
@@ -3388,7 +3524,14 @@ var init_BaseModel = __esm({
3388
3524
  continue;
3389
3525
  }
3390
3526
  if (value !== void 0) {
3391
- filteredData[fieldKey] = value;
3527
+ if (isYjsComposite(value)) {
3528
+ Logger.warn(
3529
+ `[${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.`
3530
+ );
3531
+ filteredData[fieldKey] = normalizeYjsValueForStorage(value);
3532
+ } else {
3533
+ filteredData[fieldKey] = value;
3534
+ }
3392
3535
  }
3393
3536
  }
3394
3537
  if (unknownFields.length > 0) {
@@ -3422,7 +3565,7 @@ var init_BaseModel = __esm({
3422
3565
  const recordsByUniqueKey = /* @__PURE__ */ new Map();
3423
3566
  for (const [recordId, recordData] of allRecords.entries()) {
3424
3567
  if (recordIdsToDiscard.has(recordId)) continue;
3425
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3568
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3426
3569
  if (uniqueKey === null) continue;
3427
3570
  if (!recordsByUniqueKey.has(uniqueKey)) {
3428
3571
  recordsByUniqueKey.set(uniqueKey, []);
@@ -3468,7 +3611,7 @@ var init_BaseModel = __esm({
3468
3611
  if (!recordData || !key) continue;
3469
3612
  const itemData = extractItemData(key, recordData);
3470
3613
  if (!itemData) continue;
3471
- if (change.action === "add" && recordData instanceof Y2.Map) {
3614
+ if (change.action === "add" && recordData instanceof Y5.Map) {
3472
3615
  Logger.verbose(
3473
3616
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
3474
3617
  );
@@ -3566,9 +3709,12 @@ var init_BaseModel = __esm({
3566
3709
  const recordData = documentYMap.get(idToDiscard);
3567
3710
  if (!recordData) continue;
3568
3711
  for (const constraint of schema.resolvedUniqueConstraints) {
3569
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3712
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3570
3713
  if (uniqueKey === null) continue;
3571
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3714
+ const constraintMapName = uniqueIndexMapName(
3715
+ modelName,
3716
+ constraint.name
3717
+ );
3572
3718
  const constraintMap = yDoc.getMap(constraintMapName);
3573
3719
  const currentIndexValue = constraintMap.get(uniqueKey);
3574
3720
  if (currentIndexValue === idToDiscard) {
@@ -3586,11 +3732,14 @@ var init_BaseModel = __esm({
3586
3732
  }, `conflict-resolution-${modelName}-${docId}`);
3587
3733
  yDoc.transact(() => {
3588
3734
  for (const constraint of schema.resolvedUniqueConstraints) {
3589
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3735
+ const constraintMapName = uniqueIndexMapName(
3736
+ modelName,
3737
+ constraint.name
3738
+ );
3590
3739
  const constraintMap = yDoc.getMap(constraintMapName);
3591
3740
  for (const [recordId, recordData] of documentYMap.entries()) {
3592
3741
  if (!recordData) continue;
3593
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3742
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3594
3743
  if (uniqueKey === null) continue;
3595
3744
  const currentIndexValue = constraintMap.get(uniqueKey);
3596
3745
  if (currentIndexValue !== recordId) {
@@ -3613,7 +3762,7 @@ var init_BaseModel = __esm({
3613
3762
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
3614
3763
  );
3615
3764
  for (const [recordId, recordData] of documentYMap.entries()) {
3616
- if (recordData instanceof Y2.Map) {
3765
+ if (recordData instanceof Y5.Map) {
3617
3766
  Logger.verbose(
3618
3767
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
3619
3768
  );
@@ -3798,6 +3947,11 @@ var init_BaseModel = __esm({
3798
3947
  Logger.debug(
3799
3948
  `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
3800
3949
  );
3950
+ if (isYjsComposite(yjsValue)) {
3951
+ Logger.warn(
3952
+ `[${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).`
3953
+ );
3954
+ }
3801
3955
  if (yjsValue === void 0) {
3802
3956
  Logger.debug(
3803
3957
  `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
@@ -3834,26 +3988,7 @@ var init_BaseModel = __esm({
3834
3988
  * if the field hasn't been touched since the wire-format change in #561.
3835
3989
  */
3836
3990
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3837
- let nested = recordYMap.get(fieldName);
3838
- if (!(nested instanceof Y2.Map)) {
3839
- const migrated = new Y2.Map();
3840
- if (nested && typeof nested === "object") {
3841
- for (const [member, marker] of Object.entries(
3842
- nested
3843
- )) {
3844
- if (Boolean(marker)) migrated.set(member, true);
3845
- }
3846
- }
3847
- recordYMap.set(fieldName, migrated);
3848
- nested = migrated;
3849
- }
3850
- const target = nested;
3851
- for (const member of change.additions) {
3852
- target.set(member, true);
3853
- }
3854
- for (const member of change.removals) {
3855
- target.delete(member);
3856
- }
3991
+ applyStringSetChange(recordYMap, fieldName, change);
3857
3992
  }
3858
3993
  /**
3859
3994
  * Deep equality check for comparing field values
@@ -4023,6 +4158,29 @@ var init_BaseModel = __esm({
4023
4158
  );
4024
4159
  throw new Error("Cannot save item without an id. Ensure id is set.");
4025
4160
  }
4161
+ const preTransactCreateStamps = /* @__PURE__ */ new Set();
4162
+ {
4163
+ const existingRecord = targetYMap.get(this.id);
4164
+ const isNew = !existingRecord;
4165
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
4166
+ const stamp = fieldOptions.autoStamp;
4167
+ if (!stamp) continue;
4168
+ const callerExplicit = this.hasLocalChange(fieldKey);
4169
+ if (callerExplicit) {
4170
+ const localValue = this._localChanges[fieldKey];
4171
+ if (localValue !== null && localValue !== void 0) continue;
4172
+ }
4173
+ if (stamp === "create") {
4174
+ if (!isNew) continue;
4175
+ const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
4176
+ if (persisted !== null && persisted !== void 0) continue;
4177
+ }
4178
+ this[fieldKey] = Date.now();
4179
+ if (stamp === "create") {
4180
+ preTransactCreateStamps.add(fieldKey);
4181
+ }
4182
+ }
4183
+ }
4026
4184
  this.validateBeforeSave();
4027
4185
  Logger.debug(`[${modelName}.save] About to get current JS state`);
4028
4186
  Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
@@ -4032,6 +4190,34 @@ var init_BaseModel = __esm({
4032
4190
  `[${modelName}.save] getCurrentJSState() returned:`,
4033
4191
  dataToSave
4034
4192
  );
4193
+ const uniqueMissConflicts = /* @__PURE__ */ new Map();
4194
+ for (const constraint of schema.resolvedUniqueConstraints) {
4195
+ const newUniqueKey = this._buildUniqueKey(
4196
+ constraint.fields,
4197
+ dataToSave,
4198
+ modelName,
4199
+ constraint.name
4200
+ );
4201
+ if (newUniqueKey === null) {
4202
+ continue;
4203
+ }
4204
+ const constraintMap = yDoc.getMap(
4205
+ uniqueIndexMapName(modelName, constraint.name)
4206
+ );
4207
+ if (!constraintMap.get(newUniqueKey)) {
4208
+ const constraintKeyValues = constraint.fields.map(
4209
+ (field) => dataToSave[field]
4210
+ );
4211
+ const conflictingRecord = await modelConstructor._resolveByUniqueFallback(
4212
+ constraint,
4213
+ constraintKeyValues,
4214
+ { excludeId: this.id }
4215
+ );
4216
+ if (conflictingRecord) {
4217
+ uniqueMissConflicts.set(constraint.name, conflictingRecord.id);
4218
+ }
4219
+ }
4220
+ }
4035
4221
  await yDoc.transact(async () => {
4036
4222
  if (upsertOnConstraint && upsertOnKey) {
4037
4223
  const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
@@ -4053,6 +4239,14 @@ var init_BaseModel = __esm({
4053
4239
  }
4054
4240
  }
4055
4241
  }
4242
+ for (const fieldKey of preTransactCreateStamps) {
4243
+ if (this._localChanges && fieldKey in this._localChanges) {
4244
+ delete this._localChanges[fieldKey];
4245
+ }
4246
+ if (fieldKey in dataToSave) {
4247
+ delete dataToSave[fieldKey];
4248
+ }
4249
+ }
4056
4250
  }
4057
4251
  }
4058
4252
  if (!targetYMap) {
@@ -4065,7 +4259,7 @@ var init_BaseModel = __esm({
4065
4259
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
4066
4260
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
4067
4261
  if (!recordYMap) {
4068
- recordYMap = new Y2.Map();
4262
+ recordYMap = new Y5.Map();
4069
4263
  Logger.verbose(
4070
4264
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
4071
4265
  );
@@ -4117,7 +4311,10 @@ var init_BaseModel = __esm({
4117
4311
  );
4118
4312
  continue;
4119
4313
  }
4120
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
4314
+ const constraintMapName = uniqueIndexMapName(
4315
+ modelName,
4316
+ constraint.name
4317
+ );
4121
4318
  const constraintMap = yDoc.getMap(constraintMapName);
4122
4319
  Logger.verbose(
4123
4320
  `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
@@ -4146,6 +4343,26 @@ var init_BaseModel = __esm({
4146
4343
  this.id,
4147
4344
  existingRecordIdWithNewKey
4148
4345
  );
4346
+ } else if (!existingRecordIdWithNewKey) {
4347
+ const conflictingRecordId = uniqueMissConflicts.get(constraint.name);
4348
+ if (conflictingRecordId && conflictingRecordId !== this.id) {
4349
+ Logger.verbose(
4350
+ `[${modelName}] Save Check (Item '${this.id}'): index miss for '${constraint.name}', but engine fallback found conflicting index-less record ${conflictingRecordId}.`
4351
+ );
4352
+ throw new UniqueConstraintViolationError(
4353
+ `Unique constraint '${constraint.name}' violated for model '${modelName}' on fields [${constraint.fields.join(
4354
+ ", "
4355
+ )}]. Attempted ID: ${this.id}. Value(s) starting with '${newUniqueKey.substring(
4356
+ 0,
4357
+ 50
4358
+ )}...' already exist for index-less record ID ${conflictingRecordId}.`,
4359
+ modelName,
4360
+ constraint.name,
4361
+ constraint.fields,
4362
+ this.id,
4363
+ conflictingRecordId
4364
+ );
4365
+ }
4149
4366
  }
4150
4367
  }
4151
4368
  if (isUpdate && oldData) {
@@ -4489,34 +4706,46 @@ var init_BaseModel = __esm({
4489
4706
  const modelName = schema.options.name;
4490
4707
  Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
4491
4708
  const hasIncludes = options?.include && options.include.length > 0;
4492
- const translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4709
+ const requestedLimit = options?.limit;
4710
+ const overFetch = typeof requestedLimit === "number" && requestedLimit > 0;
4711
+ let translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4712
+ if (overFetch) {
4713
+ translatorOptions = {
4714
+ ...translatorOptions || {},
4715
+ limit: requestedLimit + 1
4716
+ };
4717
+ }
4493
4718
  const translator = new DocumentQueryTranslator(modelName, schema.fields);
4494
4719
  const translatedQuery = translator.translateFind(filter, translatorOptions);
4495
4720
  Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
4496
4721
  Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
4497
- const results = await _BaseModel.dbInstance.query(
4722
+ let results = await _BaseModel.dbInstance.query(
4498
4723
  translatedQuery.sql,
4499
4724
  translatedQuery.params
4500
4725
  );
4501
4726
  Logger.verbose(`[${modelName}] Query returned ${results.length} results`);
4727
+ let hasMore = false;
4728
+ if (overFetch && results.length > requestedLimit) {
4729
+ hasMore = true;
4730
+ results = results.slice(0, requestedLimit);
4731
+ }
4502
4732
  if (options?.include && options.include.length > 0) {
4503
4733
  const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
4504
4734
  const resolver = new IncludeResolver2(_BaseModel.dbInstance);
4505
4735
  await resolver.resolve(results, options.include, 0, modelName);
4506
4736
  }
4507
- const requestedLimit = options?.limit;
4508
- const hasMore = CursorManager.hasMoreResults(
4509
- requestedLimit,
4510
- results.length
4511
- );
4512
4737
  const isFirstPage = !options?.uniqueStartKey;
4738
+ const direction = options?.direction || 1;
4513
4739
  const cursors = CursorManager.generateResultCursors(
4514
4740
  results,
4515
4741
  translatedQuery.sortFields,
4516
- options?.direction || 1,
4742
+ direction,
4517
4743
  hasMore,
4518
4744
  isFirstPage
4519
4745
  );
4746
+ if (direction === -1) {
4747
+ results.reverse();
4748
+ }
4520
4749
  let transformedData;
4521
4750
  if (options?.projection && Object.keys(options.projection).length > 0) {
4522
4751
  transformedData = results.map((row) => {
@@ -5092,7 +5321,7 @@ var init_BaseModel = __esm({
5092
5321
  );
5093
5322
  }
5094
5323
  for (const [recordId, recordYMap] of documentYMap.entries()) {
5095
- if (recordYMap instanceof Y2.Map) {
5324
+ if (recordYMap instanceof Y5.Map) {
5096
5325
  const instance = new this({ id: recordId });
5097
5326
  instance._metaDocId = docId;
5098
5327
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -5117,6 +5346,50 @@ var init_BaseModel = __esm({
5117
5346
  }
5118
5347
  return allInstances;
5119
5348
  }
5349
+ /**
5350
+ * Resolve a record by its unique-constraint field values via an engine query.
5351
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
5352
+ * entry for a key.
5353
+ *
5354
+ * The index map is a cache over the SQLite projection, not the source of
5355
+ * truth: every record is loaded into the projection on `initializeForDocument`
5356
+ * regardless of index presence, but a record written by a client predating
5357
+ * index maintenance (legacy data) is missing from the index. Such a record
5358
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
5359
+ * check. A query over the constraint fields finds it reliably.
5360
+ *
5361
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
5362
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
5363
+ * whether to heal the missing index entry (write paths) and do their own
5364
+ * logging — this method never mutates the index, so read paths stay
5365
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
5366
+ * constraint field means the record does not participate in the constraint,
5367
+ * so no query runs.
5368
+ */
5369
+ static async _resolveByUniqueFallback(constraint, keyValues, options) {
5370
+ if (!_BaseModel.dbInstance) {
5371
+ return null;
5372
+ }
5373
+ const filter = {};
5374
+ for (let i = 0; i < constraint.fields.length; i++) {
5375
+ const value = keyValues[i];
5376
+ if (value === null || value === void 0) {
5377
+ return null;
5378
+ }
5379
+ filter[constraint.fields[i]] = value;
5380
+ }
5381
+ if (options?.excludeId) {
5382
+ if ("id" in filter) {
5383
+ if (filter.id === options.excludeId) {
5384
+ return null;
5385
+ }
5386
+ } else {
5387
+ filter.id = { $ne: options.excludeId };
5388
+ }
5389
+ }
5390
+ const found = await this.queryOne(filter);
5391
+ return found ?? null;
5392
+ }
5120
5393
  static async findByUnique(constraintName, value) {
5121
5394
  const modelConstructor = this;
5122
5395
  const schema = modelConstructor.getSchema();
@@ -5155,7 +5428,10 @@ var init_BaseModel = __esm({
5155
5428
  const documentInfo = modelConstructor.connectedDocuments.get(docId);
5156
5429
  if (!documentInfo) continue;
5157
5430
  const yDoc = documentInfo.yDoc;
5158
- const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5431
+ const constraintMapName2 = uniqueIndexMapName(
5432
+ currentModelName,
5433
+ constraint.name
5434
+ );
5159
5435
  const constraintMap2 = yDoc.getMap(constraintMapName2);
5160
5436
  const recordId2 = constraintMap2.get(uniqueKeyString);
5161
5437
  if (recordId2) {
@@ -5163,7 +5439,7 @@ var init_BaseModel = __esm({
5163
5439
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
5164
5440
  if (documentYMap) {
5165
5441
  const recordYMap2 = documentYMap.get(recordId2);
5166
- if (recordYMap2 && recordYMap2 instanceof Y2.Map) {
5442
+ if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
5167
5443
  const instance = new modelConstructor({
5168
5444
  id: recordId2
5169
5445
  });
@@ -5177,6 +5453,16 @@ var init_BaseModel = __esm({
5177
5453
  }
5178
5454
  }
5179
5455
  }
5456
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5457
+ constraint,
5458
+ keyValues
5459
+ );
5460
+ if (fallbackRecord) {
5461
+ Logger.verbose(
5462
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5463
+ );
5464
+ return fallbackRecord;
5465
+ }
5180
5466
  Logger.verbose(
5181
5467
  `[${currentModelName}.findByUnique] Record not found in any connected document`
5182
5468
  );
@@ -5190,17 +5476,30 @@ var init_BaseModel = __esm({
5190
5476
  `[${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.`
5191
5477
  );
5192
5478
  }
5193
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5479
+ const constraintMapName = uniqueIndexMapName(
5480
+ currentModelName,
5481
+ constraint.name
5482
+ );
5194
5483
  const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
5195
5484
  const recordId = constraintMap.get(uniqueKeyString);
5196
5485
  if (!recordId) {
5486
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5487
+ constraint,
5488
+ keyValues
5489
+ );
5490
+ if (fallbackRecord) {
5491
+ Logger.verbose(
5492
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5493
+ );
5494
+ return fallbackRecord;
5495
+ }
5197
5496
  return null;
5198
5497
  }
5199
5498
  const recordYMap = legacyDocumentMap.get(recordId);
5200
5499
  if (!recordYMap) {
5201
5500
  return null;
5202
5501
  }
5203
- if (recordYMap instanceof Y2.Map) {
5502
+ if (recordYMap instanceof Y5.Map) {
5204
5503
  const instance = new modelConstructor({
5205
5504
  id: recordId
5206
5505
  });
@@ -5258,7 +5557,10 @@ var init_BaseModel = __esm({
5258
5557
  `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
5259
5558
  );
5260
5559
  }
5261
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5560
+ const constraintMapName = uniqueIndexMapName(
5561
+ currentModelName,
5562
+ constraint.name
5563
+ );
5262
5564
  let existingRecord = null;
5263
5565
  if (connectedDocuments && connectedDocuments.size > 0) {
5264
5566
  for (const docId of connectedDocuments.keys()) {
@@ -5276,6 +5578,33 @@ var init_BaseModel = __esm({
5276
5578
  }
5277
5579
  } else {
5278
5580
  }
5581
+ if (!existingRecord) {
5582
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5583
+ constraint,
5584
+ keyValuesForLookup
5585
+ );
5586
+ if (fallbackRecord) {
5587
+ existingRecord = fallbackRecord;
5588
+ const healDocId = fallbackRecord._metaDocId;
5589
+ const healDocInfo = healDocId ? connectedDocuments.get(healDocId) : void 0;
5590
+ const canHeal = !!healDocInfo && healDocInfo.permissionHint !== "read";
5591
+ if (healDocInfo && canHeal) {
5592
+ healDocInfo.yDoc.transact(() => {
5593
+ const healMap = healDocInfo.yDoc.getMap(constraintMapName);
5594
+ if (healMap.get(uniqueKeyString) !== fallbackRecord.id) {
5595
+ healMap.set(uniqueKeyString, fallbackRecord.id);
5596
+ }
5597
+ }, `heal-unique-index-${currentModelName}-${healDocId}`);
5598
+ Logger.verbose(
5599
+ `[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; healed missing index entry (legacy index-less record).`
5600
+ );
5601
+ } else {
5602
+ Logger.verbose(
5603
+ `[${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).`
5604
+ );
5605
+ }
5606
+ }
5607
+ }
5279
5608
  if (options?.objectMustExist) {
5280
5609
  if (!existingRecord) {
5281
5610
  throw new RecordNotFoundError(
@@ -5432,7 +5761,7 @@ var init_BaseModel = __esm({
5432
5761
  for (const [fieldKey, fieldOptions] of schema.fields) {
5433
5762
  if (fieldOptions?.type !== "stringset") continue;
5434
5763
  const value = recordYMap.get(fieldKey);
5435
- if (value instanceof Y2.Map) {
5764
+ if (value instanceof Y5.Map) {
5436
5765
  this.observeStringSetMapOnce(value);
5437
5766
  }
5438
5767
  }
@@ -5479,7 +5808,14 @@ var init_BaseModel = __esm({
5479
5808
  continue;
5480
5809
  }
5481
5810
  if (value !== void 0) {
5482
- updatedData[key] = value;
5811
+ if (isYjsComposite(value)) {
5812
+ Logger.warn(
5813
+ `[${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.`
5814
+ );
5815
+ updatedData[key] = normalizeYjsValueForStorage(value);
5816
+ } else {
5817
+ updatedData[key] = value;
5818
+ }
5483
5819
  }
5484
5820
  }
5485
5821
  if (unknownFields.length > 0) {
@@ -5559,7 +5895,14 @@ var init_BaseModel = __esm({
5559
5895
  continue;
5560
5896
  }
5561
5897
  if (value !== void 0) {
5562
- updatedData[key] = value;
5898
+ if (isYjsComposite(value)) {
5899
+ Logger.warn(
5900
+ `[${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.`
5901
+ );
5902
+ updatedData[key] = normalizeYjsValueForStorage(value);
5903
+ } else {
5904
+ updatedData[key] = value;
5905
+ }
5563
5906
  }
5564
5907
  }
5565
5908
  if (unknownFields.length > 0) {
@@ -5685,7 +6028,144 @@ var import_sql4 = __toESM(require("sql.js"), 1);
5685
6028
  init_BaseModel();
5686
6029
  var import_async_mutex = require("async-mutex");
5687
6030
  init_sql();
6031
+
6032
+ // src/utils/environment.ts
6033
+ function detectEnvironment() {
6034
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
6035
+ return "node";
6036
+ }
6037
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
6038
+ return "browser";
6039
+ }
6040
+ if (typeof self !== "undefined" && typeof self.importScripts === "function") {
6041
+ return "browser";
6042
+ }
6043
+ return "unknown";
6044
+ }
6045
+ function isNode() {
6046
+ return detectEnvironment() === "node";
6047
+ }
6048
+ function isBrowser() {
6049
+ return detectEnvironment() === "browser";
6050
+ }
6051
+ async function isNodeModuleAvailable(moduleName) {
6052
+ if (!isNode()) {
6053
+ return false;
6054
+ }
6055
+ try {
6056
+ await import(
6057
+ /* @vite-ignore */
6058
+ moduleName
6059
+ );
6060
+ return true;
6061
+ } catch {
6062
+ return false;
6063
+ }
6064
+ }
6065
+ var features = {
6066
+ get hasWebWorkers() {
6067
+ return isBrowser() && typeof Worker !== "undefined";
6068
+ },
6069
+ get hasFileSystem() {
6070
+ return isNode();
6071
+ },
6072
+ get hasWebAssembly() {
6073
+ return typeof WebAssembly !== "undefined";
6074
+ },
6075
+ async hasBetterSqlite3() {
6076
+ return await isNodeModuleAvailable("better-sqlite3");
6077
+ }
6078
+ };
6079
+
6080
+ // src/engines/SqljsEngine.ts
6081
+ var import_meta = {};
5688
6082
  var SQL_WASM_URL = "/sql-wasm.wasm";
6083
+ var SQL_WASM_FILENAME = "sql-wasm.wasm";
6084
+ async function resolveNodeWasmPath() {
6085
+ if (!isNode()) {
6086
+ return null;
6087
+ }
6088
+ try {
6089
+ const cjsRequire = typeof require !== "undefined" ? require : void 0;
6090
+ if (cjsRequire) {
6091
+ return cjsRequire.resolve("sql.js/dist/sql-wasm.wasm");
6092
+ }
6093
+ } catch {
6094
+ }
6095
+ try {
6096
+ const metaUrl = import_meta.url;
6097
+ if (metaUrl) {
6098
+ const { createRequire } = await import(
6099
+ /* @vite-ignore */
6100
+ "module"
6101
+ );
6102
+ const req = createRequire(metaUrl);
6103
+ return req.resolve("sql.js/dist/sql-wasm.wasm");
6104
+ }
6105
+ } catch {
6106
+ }
6107
+ return null;
6108
+ }
6109
+ async function preflightNodeWasm(wasmPath) {
6110
+ if (!isNode()) {
6111
+ return;
6112
+ }
6113
+ if (/^[a-z]+:\/\//i.test(wasmPath)) {
6114
+ return;
6115
+ }
6116
+ const { existsSync } = await import(
6117
+ /* @vite-ignore */
6118
+ "fs"
6119
+ );
6120
+ if (!existsSync(wasmPath)) {
6121
+ throw new Error(
6122
+ `[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()).`
6123
+ );
6124
+ }
6125
+ }
6126
+ async function loadSqlJsModule(locateFile) {
6127
+ let abortReject;
6128
+ const toError = (reason) => reason instanceof Error ? reason : new Error(
6129
+ `[SqljsEngine] sql.js aborted during initialization: ${String(
6130
+ reason
6131
+ )}`
6132
+ );
6133
+ const nodeProcess = isNode() && typeof process !== "undefined" ? process : void 0;
6134
+ const onUnhandled = (reason) => {
6135
+ const err = toError(reason);
6136
+ if (/Aborted/.test(err.message) || /Aborted/.test(String(reason))) {
6137
+ abortReject?.(err);
6138
+ return;
6139
+ }
6140
+ if (nodeProcess && nodeProcess.listenerCount("unhandledRejection") === 1) {
6141
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6142
+ void Promise.reject(reason);
6143
+ }
6144
+ };
6145
+ if (nodeProcess) {
6146
+ nodeProcess.on("unhandledRejection", onUnhandled);
6147
+ }
6148
+ const abortGuard = new Promise((_resolve, reject) => {
6149
+ abortReject = reject;
6150
+ });
6151
+ abortGuard.catch(() => {
6152
+ });
6153
+ try {
6154
+ const initPromise = (0, import_sql4.default)({
6155
+ locateFile,
6156
+ // Layer 1: emscripten onAbort.
6157
+ onAbort: (reason) => abortReject?.(toError(reason))
6158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6159
+ });
6160
+ return await Promise.race([initPromise, abortGuard]);
6161
+ } finally {
6162
+ if (nodeProcess) {
6163
+ setTimeout(() => {
6164
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6165
+ }, 0);
6166
+ }
6167
+ }
6168
+ }
5689
6169
  var TransactionalSQLJSOperations = class {
5690
6170
  db;
5691
6171
  engine;
@@ -5751,22 +6231,37 @@ var SqljsEngine = class _SqljsEngine {
5751
6231
  }
5752
6232
  async initialize(engineOptions) {
5753
6233
  Logger.info("[SqljsEngine] Initializing SQL.js engine...");
6234
+ const loadingModuleThisCall = !_SqljsEngine.SQL;
5754
6235
  try {
5755
6236
  if (!_SqljsEngine.SQL) {
5756
- const locateFileConfig = engineOptions?.locateFile || ((_file) => engineOptions?.wasmURL || SQL_WASM_URL);
6237
+ let locateFileConfig;
6238
+ if (engineOptions?.locateFile) {
6239
+ locateFileConfig = engineOptions.locateFile;
6240
+ } else if (engineOptions?.wasmURL) {
6241
+ const url = engineOptions.wasmURL;
6242
+ locateFileConfig = (_file) => url;
6243
+ } else {
6244
+ const nodeWasmPath = await resolveNodeWasmPath();
6245
+ const resolved = nodeWasmPath ?? SQL_WASM_URL;
6246
+ locateFileConfig = (_file) => resolved;
6247
+ }
6248
+ const wasmPath = locateFileConfig(SQL_WASM_FILENAME);
5757
6249
  Logger.debug(
5758
6250
  "[SqljsEngine] Attempting to load SQL.js WASM from:",
5759
- locateFileConfig("sql-wasm.wasm")
6251
+ wasmPath
5760
6252
  );
5761
- _SqljsEngine.SQL = await (0, import_sql4.default)({
5762
- locateFile: locateFileConfig
5763
- });
6253
+ await preflightNodeWasm(wasmPath);
6254
+ const SQL = await loadSqlJsModule(locateFileConfig);
6255
+ _SqljsEngine.SQL = SQL;
5764
6256
  Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
5765
6257
  }
5766
6258
  this.db = new _SqljsEngine.SQL.Database();
5767
6259
  Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
5768
6260
  this.updateTableNames();
5769
6261
  } catch (error) {
6262
+ if (loadingModuleThisCall) {
6263
+ _SqljsEngine.SQL = null;
6264
+ }
5770
6265
  Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
5771
6266
  throw error;
5772
6267
  }
@@ -6052,54 +6547,6 @@ var SqljsEngine = class _SqljsEngine {
6052
6547
  }
6053
6548
  };
6054
6549
 
6055
- // src/utils/environment.ts
6056
- function detectEnvironment() {
6057
- if (typeof process !== "undefined" && process.versions && process.versions.node) {
6058
- return "node";
6059
- }
6060
- if (typeof window !== "undefined" && typeof document !== "undefined") {
6061
- return "browser";
6062
- }
6063
- if (typeof self !== "undefined" && typeof self.importScripts === "function") {
6064
- return "browser";
6065
- }
6066
- return "unknown";
6067
- }
6068
- function isNode() {
6069
- return detectEnvironment() === "node";
6070
- }
6071
- function isBrowser() {
6072
- return detectEnvironment() === "browser";
6073
- }
6074
- async function isNodeModuleAvailable(moduleName) {
6075
- if (!isNode()) {
6076
- return false;
6077
- }
6078
- try {
6079
- await import(
6080
- /* @vite-ignore */
6081
- moduleName
6082
- );
6083
- return true;
6084
- } catch {
6085
- return false;
6086
- }
6087
- }
6088
- var features = {
6089
- get hasWebWorkers() {
6090
- return isBrowser() && typeof Worker !== "undefined";
6091
- },
6092
- get hasFileSystem() {
6093
- return isNode();
6094
- },
6095
- get hasWebAssembly() {
6096
- return typeof WebAssembly !== "undefined";
6097
- },
6098
- async hasBetterSqlite3() {
6099
- return await isNodeModuleAvailable("better-sqlite3");
6100
- }
6101
- };
6102
-
6103
6550
  // src/engines/BrowserDatabaseFactory.ts
6104
6551
  init_BaseModel();
6105
6552
  var BrowserDatabaseFactory = class {
@@ -6410,6 +6857,7 @@ init_StringSet();
6410
6857
  // src/models/schema.ts
6411
6858
  init_BaseModel();
6412
6859
  init_ModelRegistry();
6860
+ init_uniqueIndex();
6413
6861
  function defineModelSchema(input) {
6414
6862
  const { name, fields } = input;
6415
6863
  const options = {
@@ -6482,7 +6930,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
6482
6930
  for (const [fieldName, options] of fields.entries()) {
6483
6931
  if (options.unique) {
6484
6932
  resolved.push({
6485
- name: `${modelName}_${fieldName}_unique`,
6933
+ name: fieldUniqueConstraintName(modelName, fieldName),
6486
6934
  fields: [fieldName]
6487
6935
  });
6488
6936
  }
@@ -6546,7 +6994,8 @@ function attachAndRegisterModel(modelClass, schema) {
6546
6994
  }
6547
6995
 
6548
6996
  // src/utils/yDocSchema.ts
6549
- var Y3 = __toESM(require("yjs"), 1);
6997
+ var Y6 = __toESM(require("yjs"), 1);
6998
+ init_yjsValues();
6550
6999
  function discoverSchema(yDoc) {
6551
7000
  const models = {};
6552
7001
  const metaNames = /* @__PURE__ */ new Set();
@@ -6580,7 +7029,7 @@ function discoverModelNames(yDoc) {
6580
7029
  function materializeMap(yDoc, key) {
6581
7030
  try {
6582
7031
  const map = yDoc.getMap(key);
6583
- return map instanceof Y3.Map ? map : null;
7032
+ return map instanceof Y6.Map ? map : null;
6584
7033
  } catch {
6585
7034
  return null;
6586
7035
  }
@@ -6590,11 +7039,11 @@ function readModelMeta(metaMap) {
6590
7039
  let constraints;
6591
7040
  let relationships;
6592
7041
  for (const [key, value] of metaMap.entries()) {
6593
- if (key === "_constraints" && value instanceof Y3.Map) {
7042
+ if (key === "_constraints" && value instanceof Y6.Map) {
6594
7043
  constraints = readConstraints(value);
6595
- } else if (key === "_relationships" && value instanceof Y3.Map) {
7044
+ } else if (key === "_relationships" && value instanceof Y6.Map) {
6596
7045
  relationships = readRelationships(value);
6597
- } else if (value instanceof Y3.Map) {
7046
+ } else if (value instanceof Y6.Map) {
6598
7047
  fields[key] = readFieldMeta(value);
6599
7048
  }
6600
7049
  }
@@ -6613,6 +7062,10 @@ function readFieldMeta(fieldMap) {
6613
7062
  if (fieldMap.get("unique") === true) field.unique = true;
6614
7063
  if (fieldMap.get("required") === true) field.required = true;
6615
7064
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7065
+ const autoStamp = fieldMap.get("autoStamp");
7066
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
7067
+ field.autoStamp = autoStamp;
7068
+ }
6616
7069
  const def = fieldMap.get("default");
6617
7070
  if (def !== void 0) field.default = def;
6618
7071
  const maxLength = fieldMap.get("maxLength");
@@ -6625,7 +7078,7 @@ function inferModelFromData(dataMap) {
6625
7078
  const fields = {};
6626
7079
  let sampled = 0;
6627
7080
  for (const [_recordId, recordValue] of dataMap.entries()) {
6628
- if (!(recordValue instanceof Y3.Map)) continue;
7081
+ if (!(recordValue instanceof Y6.Map)) continue;
6629
7082
  if (++sampled > 5) break;
6630
7083
  for (const [fieldName, value] of recordValue.entries()) {
6631
7084
  if (fieldName.startsWith("_")) continue;
@@ -6638,7 +7091,7 @@ function inferModelFromData(dataMap) {
6638
7091
  return { fields };
6639
7092
  }
6640
7093
  function inferTypeFromValue(value) {
6641
- if (value instanceof Y3.Map) return "stringset";
7094
+ if (isStringSetYMap(value)) return "stringset";
6642
7095
  switch (typeof value) {
6643
7096
  case "string":
6644
7097
  return "string";
@@ -6653,7 +7106,7 @@ function inferTypeFromValue(value) {
6653
7106
  function readConstraints(constraintsMap) {
6654
7107
  const out = {};
6655
7108
  for (const [name, value] of constraintsMap.entries()) {
6656
- if (!(value instanceof Y3.Map)) continue;
7109
+ if (!(value instanceof Y6.Map)) continue;
6657
7110
  let fields = [];
6658
7111
  const rawFields = value.get("fields");
6659
7112
  if (typeof rawFields === "string") {
@@ -6672,7 +7125,7 @@ function readConstraints(constraintsMap) {
6672
7125
  function readRelationships(relsMap) {
6673
7126
  const out = {};
6674
7127
  for (const [name, value] of relsMap.entries()) {
6675
- if (!(value instanceof Y3.Map)) continue;
7128
+ if (!(value instanceof Y6.Map)) continue;
6676
7129
  const rel = {};
6677
7130
  for (const [k, v] of value.entries()) {
6678
7131
  rel[k] = v;
@@ -6683,6 +7136,7 @@ function readRelationships(relsMap) {
6683
7136
  }
6684
7137
  var CAMEL_TO_SNAKE = {
6685
7138
  autoAssign: "auto_assign",
7139
+ autoStamp: "auto_stamp",
6686
7140
  maxLength: "max_length",
6687
7141
  maxCount: "max_count",
6688
7142
  relatedIdField: "related_id_field",
@@ -6713,6 +7167,7 @@ function schemaToToml(schema) {
6713
7167
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
6714
7168
  lines.push(`type = ${tomlValue(field.type)}`);
6715
7169
  if (field.autoAssign) lines.push("auto_assign = true");
7170
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
6716
7171
  if (field.indexed) lines.push("indexed = true");
6717
7172
  if (field.unique) lines.push("unique = true");
6718
7173
  if (field.required) lines.push("required = true");
@@ -6759,10 +7214,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
6759
7214
  "unique",
6760
7215
  "required",
6761
7216
  "auto_assign",
7217
+ "auto_stamp",
6762
7218
  "max_length",
6763
7219
  "max_count",
6764
- "default"
7220
+ "default",
7221
+ "enum"
6765
7222
  ]);
7223
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
6766
7224
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
6767
7225
  "fields",
6768
7226
  "relationships",
@@ -6804,9 +7262,37 @@ function parseFieldOptions(raw, context, strict) {
6804
7262
  if (raw.unique === true) opts.unique = true;
6805
7263
  if (raw.required === true) opts.required = true;
6806
7264
  if (raw.auto_assign === true) opts.autoAssign = true;
7265
+ if (raw.auto_stamp !== void 0) {
7266
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
7267
+ throw new Error(
7268
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
7269
+ ...VALID_AUTO_STAMP_VALUES
7270
+ ].join(", ")}`
7271
+ );
7272
+ }
7273
+ opts.autoStamp = raw.auto_stamp;
7274
+ }
6807
7275
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
6808
7276
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
6809
7277
  if (raw.default !== void 0) opts.default = raw.default;
7278
+ if (raw.enum !== void 0) {
7279
+ if (raw.type !== "string") {
7280
+ throw new Error(
7281
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
7282
+ );
7283
+ }
7284
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
7285
+ throw new Error(
7286
+ `${context}: \`enum\` must be a non-empty array of strings.`
7287
+ );
7288
+ }
7289
+ if (!raw.enum.every((v) => typeof v === "string")) {
7290
+ throw new Error(
7291
+ `${context}: \`enum\` values must all be strings.`
7292
+ );
7293
+ }
7294
+ opts.enum = [...raw.enum];
7295
+ }
6810
7296
  return opts;
6811
7297
  }
6812
7298
  function requireField(raw, field, context) {