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.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,12 +1427,52 @@ 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/models/relationshipManager.ts
1463
+ function warnIfOrderFieldNullable(modelClass, orderByField, relationshipDescription) {
1464
+ if (ALWAYS_PRESENT_ORDER_FIELDS.has(orderByField)) return;
1465
+ const schema = modelClass.getSchema?.();
1466
+ const fields = schema?.fields;
1467
+ if (!fields || typeof fields.get !== "function") return;
1468
+ const fieldOptions = fields.get(orderByField);
1469
+ if (!fieldOptions) return;
1470
+ if (fieldOptions.required !== true) {
1471
+ Logger.warn(
1472
+ `[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.)`
1473
+ );
1474
+ }
1475
+ }
1302
1476
  function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1303
1477
  return async function() {
1304
1478
  const relatedId = this[config.relatedIdField];
@@ -1307,6 +1481,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1307
1481
  };
1308
1482
  }
1309
1483
  function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbEngine) {
1484
+ const {
1485
+ relatedIdField,
1486
+ orderByField = "id",
1487
+ orderDirection = "ASC"
1488
+ } = config;
1489
+ warnIfOrderFieldNullable(
1490
+ targetModelClass,
1491
+ orderByField,
1492
+ `hasMany(${targetModelClass.name})`
1493
+ );
1310
1494
  return async function(paginationArgs) {
1311
1495
  const {
1312
1496
  limit,
@@ -1314,71 +1498,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
1314
1498
  beforeCursor,
1315
1499
  direction = "forward"
1316
1500
  } = paginationArgs || {};
1317
- const {
1318
- relatedIdField,
1319
- orderByField = "id",
1320
- orderDirection = "ASC"
1321
- } = config;
1322
1501
  const targetSchema = targetModelClass.getSchema();
1323
1502
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1324
1503
  Logger.error(
1325
1504
  `[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
1326
1505
  );
1327
- return { data: [], nextCursor: null, prevCursor: null };
1506
+ return { data: [], hasMore: false };
1328
1507
  }
1329
- const filter = { [relatedIdField]: this.id };
1508
+ const forward = direction !== "backward";
1509
+ const cursor = forward ? afterCursor : beforeCursor;
1330
1510
  const queryOptions = {
1331
- sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
1511
+ sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
1512
+ direction: forward ? 1 : -1
1332
1513
  };
1333
- if (limit !== void 0) {
1334
- queryOptions.limit = limit + 1;
1335
- }
1336
- if (direction === "forward" && afterCursor) {
1337
- filter[orderByField] = {
1338
- [orderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
1339
- };
1340
- } else if (direction === "backward") {
1341
- if (beforeCursor) {
1342
- filter[orderByField] = {
1343
- [orderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
1344
- };
1345
- }
1346
- queryOptions.sort = { [orderByField]: orderDirection === "ASC" ? -1 : 1 };
1347
- }
1348
- const result = await targetModelClass.query(filter, queryOptions);
1349
- const results = result.data;
1350
- let nextCursor = null;
1351
- let prevCursor = null;
1352
- const isFirstPage = direction === "forward" && !afterCursor;
1353
- if (limit !== void 0) {
1354
- if (direction === "forward") {
1355
- if (results.length > limit) {
1356
- nextCursor = results[limit - 1][orderByField];
1357
- results.pop();
1358
- }
1359
- if (results.length > 0 && !isFirstPage) {
1360
- prevCursor = results[0][orderByField];
1361
- }
1362
- } else {
1363
- if (results.length > limit) {
1364
- prevCursor = results[limit - 1][orderByField];
1365
- results.pop();
1366
- }
1367
- results.reverse();
1368
- if (results.length > 0 && beforeCursor) {
1369
- nextCursor = results[results.length - 1][orderByField];
1370
- }
1371
- if (results.length > 0 && !beforeCursor) {
1372
- if (results.length === limit) {
1373
- prevCursor = results[0][orderByField];
1374
- }
1375
- }
1376
- }
1377
- }
1378
- return { data: results, nextCursor, prevCursor };
1514
+ if (limit !== void 0) queryOptions.limit = limit;
1515
+ if (cursor != null) queryOptions.uniqueStartKey = cursor;
1516
+ return targetModelClass.query(
1517
+ { [relatedIdField]: this.id },
1518
+ queryOptions
1519
+ );
1379
1520
  };
1380
1521
  }
1381
1522
  function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClass, joinModelClass, _dbEngine) {
1523
+ const {
1524
+ joinModelLocalField,
1525
+ joinModelRelatedField,
1526
+ joinModelOrderByField = "id",
1527
+ joinModelOrderDirection = "ASC"
1528
+ } = config;
1529
+ warnIfOrderFieldNullable(
1530
+ joinModelClass,
1531
+ joinModelOrderByField,
1532
+ `hasManyThrough(${targetModelClass.name} via ${joinModelClass.name})`
1533
+ );
1382
1534
  return async function(paginationArgs) {
1383
1535
  const {
1384
1536
  limit,
@@ -1392,87 +1544,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
1392
1544
  Logger.error(
1393
1545
  `[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
1394
1546
  );
1395
- return { data: [], nextCursor: null, prevCursor: null };
1547
+ return { data: [], hasMore: false };
1396
1548
  }
1397
1549
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1398
1550
  Logger.error(
1399
1551
  `[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
1400
1552
  );
1401
- return { data: [], nextCursor: null, prevCursor: null };
1553
+ return { data: [], hasMore: false };
1402
1554
  }
1403
- const {
1404
- joinModelLocalField,
1405
- joinModelRelatedField,
1406
- joinModelOrderByField = "id",
1407
- joinModelOrderDirection = "ASC"
1408
- } = config;
1409
- const joinFilter = { [joinModelLocalField]: this.id };
1555
+ const forward = direction !== "backward";
1556
+ const cursor = forward ? afterCursor : beforeCursor;
1410
1557
  const joinQueryOptions = {
1411
1558
  sort: {
1412
1559
  [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
1413
1560
  },
1414
- projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
1561
+ projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
1562
+ direction: forward ? 1 : -1
1415
1563
  };
1416
- if (limit !== void 0) {
1417
- joinQueryOptions.limit = limit + 1;
1418
- }
1419
- if (direction === "forward" && afterCursor) {
1420
- joinFilter[joinModelOrderByField] = {
1421
- [joinModelOrderDirection === "ASC" ? "$gt" : "$lt"]: afterCursor
1422
- };
1423
- } else if (direction === "backward") {
1424
- if (beforeCursor) {
1425
- joinFilter[joinModelOrderByField] = {
1426
- [joinModelOrderDirection === "ASC" ? "$lt" : "$gt"]: beforeCursor
1427
- };
1428
- }
1429
- joinQueryOptions.sort = {
1430
- [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? -1 : 1
1431
- };
1432
- }
1564
+ if (limit !== void 0) joinQueryOptions.limit = limit;
1565
+ if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
1433
1566
  const joinResult = await joinModelClass.query(
1434
- joinFilter,
1567
+ { [joinModelLocalField]: this.id },
1435
1568
  joinQueryOptions
1436
1569
  );
1437
- const joinResults = joinResult.data;
1438
- let nextCursorFromJoin = null;
1439
- let prevCursorFromJoin = null;
1440
- const isFirstPage = direction === "forward" && !afterCursor;
1441
- if (limit !== void 0) {
1442
- if (direction === "forward") {
1443
- if (joinResults.length > limit) {
1444
- nextCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
1445
- joinResults.pop();
1446
- }
1447
- if (joinResults.length > 0 && !isFirstPage) {
1448
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
1449
- }
1450
- } else {
1451
- if (joinResults.length > limit) {
1452
- prevCursorFromJoin = joinResults[limit - 1][joinModelOrderByField];
1453
- joinResults.pop();
1454
- }
1455
- joinResults.reverse();
1456
- if (joinResults.length > 0 && beforeCursor) {
1457
- nextCursorFromJoin = joinResults[joinResults.length - 1][joinModelOrderByField];
1458
- }
1459
- if (joinResults.length > 0 && !beforeCursor) {
1460
- if (joinResults.length === limit) {
1461
- prevCursorFromJoin = joinResults[0][joinModelOrderByField];
1462
- }
1463
- }
1464
- }
1570
+ const relatedIds = joinResult.data.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1571
+ let targetData = [];
1572
+ if (relatedIds.length > 0) {
1573
+ const targetResult = await targetModelClass.query({
1574
+ id: { $in: relatedIds }
1575
+ });
1576
+ targetData = targetResult.data;
1465
1577
  }
1466
- const relatedIds = joinResults.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1467
- if (relatedIds.length === 0)
1468
- return { data: [], nextCursor: null, prevCursor: null };
1469
- const targetResult = await targetModelClass.query({
1470
- id: { $in: relatedIds }
1471
- });
1472
1578
  return {
1473
- data: targetResult.data,
1474
- nextCursor: nextCursorFromJoin,
1475
- prevCursor: prevCursorFromJoin
1579
+ data: targetData,
1580
+ nextCursor: joinResult.nextCursor,
1581
+ prevCursor: joinResult.prevCursor,
1582
+ hasMore: joinResult.hasMore
1476
1583
  };
1477
1584
  };
1478
1585
  }
@@ -1596,10 +1703,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
1596
1703
  }, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
1597
1704
  };
1598
1705
  }
1706
+ var ALWAYS_PRESENT_ORDER_FIELDS;
1599
1707
  var init_relationshipManager = __esm({
1600
1708
  "src/models/relationshipManager.ts"() {
1601
1709
  "use strict";
1602
1710
  init_BaseModel();
1711
+ ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
1603
1712
  }
1604
1713
  });
1605
1714
 
@@ -2370,7 +2479,13 @@ var init_IncludeResolver = __esm({
2370
2479
  }
2371
2480
  return null;
2372
2481
  }
2373
- /** Apply projection to a single record. */
2482
+ /**
2483
+ * Apply a projection to a single record. Assumes the projection was already
2484
+ * validated upstream (mixed inclusion+exclusion rejected by the translator):
2485
+ * `isInclusion` treats "any value === 1" as inclusion mode. `id` and `type`
2486
+ * are system-identity fields and are always kept in BOTH modes (mirrors the
2487
+ * DatabaseDO `_applyProjection`); `_meta` stays excludable.
2488
+ */
2374
2489
  _applyProjection(record, projection) {
2375
2490
  const fields = Object.entries(projection);
2376
2491
  if (fields.length === 0) return record;
@@ -2389,7 +2504,7 @@ var init_IncludeResolver = __esm({
2389
2504
  } else {
2390
2505
  const result = { ...record };
2391
2506
  for (const [f, v] of fields) {
2392
- if (v === 0) delete result[f];
2507
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
2393
2508
  }
2394
2509
  return result;
2395
2510
  }
@@ -2408,10 +2523,10 @@ __export(BaseModel_exports, {
2408
2523
  UniqueConstraintViolationError: () => UniqueConstraintViolationError,
2409
2524
  generateULID: () => generateULID
2410
2525
  });
2411
- import * as Y2 from "yjs";
2412
- import { ulid } from "ulid";
2526
+ import * as Y5 from "yjs";
2527
+ import { ulid as ulid2 } from "ulid";
2413
2528
  function generateULID() {
2414
- return ulid();
2529
+ return ulid2();
2415
2530
  }
2416
2531
  var LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2417
2532
  var init_BaseModel = __esm({
@@ -2423,6 +2538,9 @@ var init_BaseModel = __esm({
2423
2538
  init_CursorManager();
2424
2539
  init_sql();
2425
2540
  init_metaSync();
2541
+ init_writeLifecycle();
2542
+ init_yjsValues();
2543
+ init_uniqueIndex();
2426
2544
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2427
2545
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
2428
2546
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2807,7 +2925,9 @@ var init_BaseModel = __esm({
2807
2925
  this.setValue(fieldKey, data[fieldKey]);
2808
2926
  this._constructorProvidedFields.add(fieldKey);
2809
2927
  } else if (fieldOptions.default !== void 0) {
2810
- const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
2928
+ const defaultValue = resolveLiteralOrFunctionDefault(
2929
+ fieldOptions.default
2930
+ );
2811
2931
  this.setValue(fieldKey, defaultValue);
2812
2932
  }
2813
2933
  }
@@ -2931,7 +3051,30 @@ var init_BaseModel = __esm({
2931
3051
  if (verboseEnabled) {
2932
3052
  Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
2933
3053
  }
2934
- return fieldValue;
3054
+ return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
3055
+ }
3056
+ /**
3057
+ * Defensive normalization for the read/sync boundary (issue #625).
3058
+ *
3059
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
3060
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
3061
+ * through untouched. For any other field, a composite Yjs primitive
3062
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
3063
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
3064
+ * warn, rather than throwing. Scalars pass through unchanged.
3065
+ */
3066
+ normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
3067
+ const fieldOptions = schema?.fields?.get(fieldKey);
3068
+ if (fieldOptions?.type === "stringset") {
3069
+ return value;
3070
+ }
3071
+ if (isYjsComposite(value)) {
3072
+ Logger.warn(
3073
+ `[${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.`
3074
+ );
3075
+ return normalizeYjsValue(value);
3076
+ }
3077
+ return value;
2935
3078
  }
2936
3079
  getValue(fieldKey) {
2937
3080
  const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
@@ -3076,7 +3219,7 @@ var init_BaseModel = __esm({
3076
3219
  }
3077
3220
  getStringSetFromYjs(fieldName) {
3078
3221
  const yjsData = this.getFromYjs(fieldName);
3079
- if (yjsData instanceof Y2.Map) {
3222
+ if (yjsData instanceof Y5.Map) {
3080
3223
  return Array.from(yjsData.keys());
3081
3224
  }
3082
3225
  if (yjsData && typeof yjsData === "object") {
@@ -3184,7 +3327,7 @@ var init_BaseModel = __esm({
3184
3327
  }
3185
3328
  const extractStringSetValues = (value) => {
3186
3329
  if (!value) return [];
3187
- if (value instanceof Y2.Map) {
3330
+ if (value instanceof Y5.Map) {
3188
3331
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
3189
3332
  }
3190
3333
  if (Array.isArray(value)) {
@@ -3201,7 +3344,7 @@ var init_BaseModel = __esm({
3201
3344
  if (!recordId) continue;
3202
3345
  let itemData;
3203
3346
  const stringSetValuesByField = {};
3204
- if (recordData instanceof Y2.Map) {
3347
+ if (recordData instanceof Y5.Map) {
3205
3348
  itemData = {};
3206
3349
  const unknownFields = [];
3207
3350
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3317,20 +3460,10 @@ var init_BaseModel = __esm({
3317
3460
  Logger.verbose(
3318
3461
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
3319
3462
  );
3320
- const buildUniqueKey = (recordData, fields) => {
3321
- const keyParts = [];
3322
- for (const field of fields) {
3323
- const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
3324
- if (value === null || value === void 0) {
3325
- return null;
3326
- }
3327
- keyParts.push(String(value));
3328
- }
3329
- return keyParts.join("|");
3330
- };
3463
+ const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
3331
3464
  const extractItemData = (key, recordData) => {
3332
3465
  let itemData;
3333
- if (recordData instanceof Y2.Map) {
3466
+ if (recordData instanceof Y5.Map) {
3334
3467
  itemData = {};
3335
3468
  const unknownFields = [];
3336
3469
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3343,7 +3476,14 @@ var init_BaseModel = __esm({
3343
3476
  continue;
3344
3477
  }
3345
3478
  if (value !== void 0) {
3346
- itemData[fieldKey] = value;
3479
+ if (isYjsComposite(value)) {
3480
+ Logger.warn(
3481
+ `[${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.`
3482
+ );
3483
+ itemData[fieldKey] = normalizeYjsValueForStorage(value);
3484
+ } else {
3485
+ itemData[fieldKey] = value;
3486
+ }
3347
3487
  }
3348
3488
  }
3349
3489
  if (unknownFields.length > 0) {
@@ -3366,7 +3506,14 @@ var init_BaseModel = __esm({
3366
3506
  continue;
3367
3507
  }
3368
3508
  if (value !== void 0) {
3369
- filteredData[fieldKey] = value;
3509
+ if (isYjsComposite(value)) {
3510
+ Logger.warn(
3511
+ `[${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.`
3512
+ );
3513
+ filteredData[fieldKey] = normalizeYjsValueForStorage(value);
3514
+ } else {
3515
+ filteredData[fieldKey] = value;
3516
+ }
3370
3517
  }
3371
3518
  }
3372
3519
  if (unknownFields.length > 0) {
@@ -3400,7 +3547,7 @@ var init_BaseModel = __esm({
3400
3547
  const recordsByUniqueKey = /* @__PURE__ */ new Map();
3401
3548
  for (const [recordId, recordData] of allRecords.entries()) {
3402
3549
  if (recordIdsToDiscard.has(recordId)) continue;
3403
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3550
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3404
3551
  if (uniqueKey === null) continue;
3405
3552
  if (!recordsByUniqueKey.has(uniqueKey)) {
3406
3553
  recordsByUniqueKey.set(uniqueKey, []);
@@ -3446,7 +3593,7 @@ var init_BaseModel = __esm({
3446
3593
  if (!recordData || !key) continue;
3447
3594
  const itemData = extractItemData(key, recordData);
3448
3595
  if (!itemData) continue;
3449
- if (change.action === "add" && recordData instanceof Y2.Map) {
3596
+ if (change.action === "add" && recordData instanceof Y5.Map) {
3450
3597
  Logger.verbose(
3451
3598
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
3452
3599
  );
@@ -3544,9 +3691,12 @@ var init_BaseModel = __esm({
3544
3691
  const recordData = documentYMap.get(idToDiscard);
3545
3692
  if (!recordData) continue;
3546
3693
  for (const constraint of schema.resolvedUniqueConstraints) {
3547
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3694
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3548
3695
  if (uniqueKey === null) continue;
3549
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3696
+ const constraintMapName = uniqueIndexMapName(
3697
+ modelName,
3698
+ constraint.name
3699
+ );
3550
3700
  const constraintMap = yDoc.getMap(constraintMapName);
3551
3701
  const currentIndexValue = constraintMap.get(uniqueKey);
3552
3702
  if (currentIndexValue === idToDiscard) {
@@ -3564,11 +3714,14 @@ var init_BaseModel = __esm({
3564
3714
  }, `conflict-resolution-${modelName}-${docId}`);
3565
3715
  yDoc.transact(() => {
3566
3716
  for (const constraint of schema.resolvedUniqueConstraints) {
3567
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3717
+ const constraintMapName = uniqueIndexMapName(
3718
+ modelName,
3719
+ constraint.name
3720
+ );
3568
3721
  const constraintMap = yDoc.getMap(constraintMapName);
3569
3722
  for (const [recordId, recordData] of documentYMap.entries()) {
3570
3723
  if (!recordData) continue;
3571
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3724
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3572
3725
  if (uniqueKey === null) continue;
3573
3726
  const currentIndexValue = constraintMap.get(uniqueKey);
3574
3727
  if (currentIndexValue !== recordId) {
@@ -3591,7 +3744,7 @@ var init_BaseModel = __esm({
3591
3744
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
3592
3745
  );
3593
3746
  for (const [recordId, recordData] of documentYMap.entries()) {
3594
- if (recordData instanceof Y2.Map) {
3747
+ if (recordData instanceof Y5.Map) {
3595
3748
  Logger.verbose(
3596
3749
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
3597
3750
  );
@@ -3776,6 +3929,11 @@ var init_BaseModel = __esm({
3776
3929
  Logger.debug(
3777
3930
  `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
3778
3931
  );
3932
+ if (isYjsComposite(yjsValue)) {
3933
+ Logger.warn(
3934
+ `[${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).`
3935
+ );
3936
+ }
3779
3937
  if (yjsValue === void 0) {
3780
3938
  Logger.debug(
3781
3939
  `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
@@ -3812,26 +3970,7 @@ var init_BaseModel = __esm({
3812
3970
  * if the field hasn't been touched since the wire-format change in #561.
3813
3971
  */
3814
3972
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3815
- let nested = recordYMap.get(fieldName);
3816
- if (!(nested instanceof Y2.Map)) {
3817
- const migrated = new Y2.Map();
3818
- if (nested && typeof nested === "object") {
3819
- for (const [member, marker] of Object.entries(
3820
- nested
3821
- )) {
3822
- if (Boolean(marker)) migrated.set(member, true);
3823
- }
3824
- }
3825
- recordYMap.set(fieldName, migrated);
3826
- nested = migrated;
3827
- }
3828
- const target = nested;
3829
- for (const member of change.additions) {
3830
- target.set(member, true);
3831
- }
3832
- for (const member of change.removals) {
3833
- target.delete(member);
3834
- }
3973
+ applyStringSetChange(recordYMap, fieldName, change);
3835
3974
  }
3836
3975
  /**
3837
3976
  * Deep equality check for comparing field values
@@ -4001,6 +4140,29 @@ var init_BaseModel = __esm({
4001
4140
  );
4002
4141
  throw new Error("Cannot save item without an id. Ensure id is set.");
4003
4142
  }
4143
+ const preTransactCreateStamps = /* @__PURE__ */ new Set();
4144
+ {
4145
+ const existingRecord = targetYMap.get(this.id);
4146
+ const isNew = !existingRecord;
4147
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
4148
+ const stamp = fieldOptions.autoStamp;
4149
+ if (!stamp) continue;
4150
+ const callerExplicit = this.hasLocalChange(fieldKey);
4151
+ if (callerExplicit) {
4152
+ const localValue = this._localChanges[fieldKey];
4153
+ if (localValue !== null && localValue !== void 0) continue;
4154
+ }
4155
+ if (stamp === "create") {
4156
+ if (!isNew) continue;
4157
+ const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
4158
+ if (persisted !== null && persisted !== void 0) continue;
4159
+ }
4160
+ this[fieldKey] = Date.now();
4161
+ if (stamp === "create") {
4162
+ preTransactCreateStamps.add(fieldKey);
4163
+ }
4164
+ }
4165
+ }
4004
4166
  this.validateBeforeSave();
4005
4167
  Logger.debug(`[${modelName}.save] About to get current JS state`);
4006
4168
  Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
@@ -4010,6 +4172,34 @@ var init_BaseModel = __esm({
4010
4172
  `[${modelName}.save] getCurrentJSState() returned:`,
4011
4173
  dataToSave
4012
4174
  );
4175
+ const uniqueMissConflicts = /* @__PURE__ */ new Map();
4176
+ for (const constraint of schema.resolvedUniqueConstraints) {
4177
+ const newUniqueKey = this._buildUniqueKey(
4178
+ constraint.fields,
4179
+ dataToSave,
4180
+ modelName,
4181
+ constraint.name
4182
+ );
4183
+ if (newUniqueKey === null) {
4184
+ continue;
4185
+ }
4186
+ const constraintMap = yDoc.getMap(
4187
+ uniqueIndexMapName(modelName, constraint.name)
4188
+ );
4189
+ if (!constraintMap.get(newUniqueKey)) {
4190
+ const constraintKeyValues = constraint.fields.map(
4191
+ (field) => dataToSave[field]
4192
+ );
4193
+ const conflictingRecord = await modelConstructor._resolveByUniqueFallback(
4194
+ constraint,
4195
+ constraintKeyValues,
4196
+ { excludeId: this.id }
4197
+ );
4198
+ if (conflictingRecord) {
4199
+ uniqueMissConflicts.set(constraint.name, conflictingRecord.id);
4200
+ }
4201
+ }
4202
+ }
4013
4203
  await yDoc.transact(async () => {
4014
4204
  if (upsertOnConstraint && upsertOnKey) {
4015
4205
  const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
@@ -4031,6 +4221,14 @@ var init_BaseModel = __esm({
4031
4221
  }
4032
4222
  }
4033
4223
  }
4224
+ for (const fieldKey of preTransactCreateStamps) {
4225
+ if (this._localChanges && fieldKey in this._localChanges) {
4226
+ delete this._localChanges[fieldKey];
4227
+ }
4228
+ if (fieldKey in dataToSave) {
4229
+ delete dataToSave[fieldKey];
4230
+ }
4231
+ }
4034
4232
  }
4035
4233
  }
4036
4234
  if (!targetYMap) {
@@ -4043,7 +4241,7 @@ var init_BaseModel = __esm({
4043
4241
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
4044
4242
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
4045
4243
  if (!recordYMap) {
4046
- recordYMap = new Y2.Map();
4244
+ recordYMap = new Y5.Map();
4047
4245
  Logger.verbose(
4048
4246
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
4049
4247
  );
@@ -4095,7 +4293,10 @@ var init_BaseModel = __esm({
4095
4293
  );
4096
4294
  continue;
4097
4295
  }
4098
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
4296
+ const constraintMapName = uniqueIndexMapName(
4297
+ modelName,
4298
+ constraint.name
4299
+ );
4099
4300
  const constraintMap = yDoc.getMap(constraintMapName);
4100
4301
  Logger.verbose(
4101
4302
  `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
@@ -4124,6 +4325,26 @@ var init_BaseModel = __esm({
4124
4325
  this.id,
4125
4326
  existingRecordIdWithNewKey
4126
4327
  );
4328
+ } else if (!existingRecordIdWithNewKey) {
4329
+ const conflictingRecordId = uniqueMissConflicts.get(constraint.name);
4330
+ if (conflictingRecordId && conflictingRecordId !== this.id) {
4331
+ Logger.verbose(
4332
+ `[${modelName}] Save Check (Item '${this.id}'): index miss for '${constraint.name}', but engine fallback found conflicting index-less record ${conflictingRecordId}.`
4333
+ );
4334
+ throw new UniqueConstraintViolationError(
4335
+ `Unique constraint '${constraint.name}' violated for model '${modelName}' on fields [${constraint.fields.join(
4336
+ ", "
4337
+ )}]. Attempted ID: ${this.id}. Value(s) starting with '${newUniqueKey.substring(
4338
+ 0,
4339
+ 50
4340
+ )}...' already exist for index-less record ID ${conflictingRecordId}.`,
4341
+ modelName,
4342
+ constraint.name,
4343
+ constraint.fields,
4344
+ this.id,
4345
+ conflictingRecordId
4346
+ );
4347
+ }
4127
4348
  }
4128
4349
  }
4129
4350
  if (isUpdate && oldData) {
@@ -4467,34 +4688,46 @@ var init_BaseModel = __esm({
4467
4688
  const modelName = schema.options.name;
4468
4689
  Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
4469
4690
  const hasIncludes = options?.include && options.include.length > 0;
4470
- const translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4691
+ const requestedLimit = options?.limit;
4692
+ const overFetch = typeof requestedLimit === "number" && requestedLimit > 0;
4693
+ let translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4694
+ if (overFetch) {
4695
+ translatorOptions = {
4696
+ ...translatorOptions || {},
4697
+ limit: requestedLimit + 1
4698
+ };
4699
+ }
4471
4700
  const translator = new DocumentQueryTranslator(modelName, schema.fields);
4472
4701
  const translatedQuery = translator.translateFind(filter, translatorOptions);
4473
4702
  Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
4474
4703
  Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
4475
- const results = await _BaseModel.dbInstance.query(
4704
+ let results = await _BaseModel.dbInstance.query(
4476
4705
  translatedQuery.sql,
4477
4706
  translatedQuery.params
4478
4707
  );
4479
4708
  Logger.verbose(`[${modelName}] Query returned ${results.length} results`);
4709
+ let hasMore = false;
4710
+ if (overFetch && results.length > requestedLimit) {
4711
+ hasMore = true;
4712
+ results = results.slice(0, requestedLimit);
4713
+ }
4480
4714
  if (options?.include && options.include.length > 0) {
4481
4715
  const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
4482
4716
  const resolver = new IncludeResolver2(_BaseModel.dbInstance);
4483
4717
  await resolver.resolve(results, options.include, 0, modelName);
4484
4718
  }
4485
- const requestedLimit = options?.limit;
4486
- const hasMore = CursorManager.hasMoreResults(
4487
- requestedLimit,
4488
- results.length
4489
- );
4490
4719
  const isFirstPage = !options?.uniqueStartKey;
4720
+ const direction = options?.direction || 1;
4491
4721
  const cursors = CursorManager.generateResultCursors(
4492
4722
  results,
4493
4723
  translatedQuery.sortFields,
4494
- options?.direction || 1,
4724
+ direction,
4495
4725
  hasMore,
4496
4726
  isFirstPage
4497
4727
  );
4728
+ if (direction === -1) {
4729
+ results.reverse();
4730
+ }
4498
4731
  let transformedData;
4499
4732
  if (options?.projection && Object.keys(options.projection).length > 0) {
4500
4733
  transformedData = results.map((row) => {
@@ -5070,7 +5303,7 @@ var init_BaseModel = __esm({
5070
5303
  );
5071
5304
  }
5072
5305
  for (const [recordId, recordYMap] of documentYMap.entries()) {
5073
- if (recordYMap instanceof Y2.Map) {
5306
+ if (recordYMap instanceof Y5.Map) {
5074
5307
  const instance = new this({ id: recordId });
5075
5308
  instance._metaDocId = docId;
5076
5309
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -5095,6 +5328,50 @@ var init_BaseModel = __esm({
5095
5328
  }
5096
5329
  return allInstances;
5097
5330
  }
5331
+ /**
5332
+ * Resolve a record by its unique-constraint field values via an engine query.
5333
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
5334
+ * entry for a key.
5335
+ *
5336
+ * The index map is a cache over the SQLite projection, not the source of
5337
+ * truth: every record is loaded into the projection on `initializeForDocument`
5338
+ * regardless of index presence, but a record written by a client predating
5339
+ * index maintenance (legacy data) is missing from the index. Such a record
5340
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
5341
+ * check. A query over the constraint fields finds it reliably.
5342
+ *
5343
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
5344
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
5345
+ * whether to heal the missing index entry (write paths) and do their own
5346
+ * logging — this method never mutates the index, so read paths stay
5347
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
5348
+ * constraint field means the record does not participate in the constraint,
5349
+ * so no query runs.
5350
+ */
5351
+ static async _resolveByUniqueFallback(constraint, keyValues, options) {
5352
+ if (!_BaseModel.dbInstance) {
5353
+ return null;
5354
+ }
5355
+ const filter = {};
5356
+ for (let i = 0; i < constraint.fields.length; i++) {
5357
+ const value = keyValues[i];
5358
+ if (value === null || value === void 0) {
5359
+ return null;
5360
+ }
5361
+ filter[constraint.fields[i]] = value;
5362
+ }
5363
+ if (options?.excludeId) {
5364
+ if ("id" in filter) {
5365
+ if (filter.id === options.excludeId) {
5366
+ return null;
5367
+ }
5368
+ } else {
5369
+ filter.id = { $ne: options.excludeId };
5370
+ }
5371
+ }
5372
+ const found = await this.queryOne(filter);
5373
+ return found ?? null;
5374
+ }
5098
5375
  static async findByUnique(constraintName, value) {
5099
5376
  const modelConstructor = this;
5100
5377
  const schema = modelConstructor.getSchema();
@@ -5133,7 +5410,10 @@ var init_BaseModel = __esm({
5133
5410
  const documentInfo = modelConstructor.connectedDocuments.get(docId);
5134
5411
  if (!documentInfo) continue;
5135
5412
  const yDoc = documentInfo.yDoc;
5136
- const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5413
+ const constraintMapName2 = uniqueIndexMapName(
5414
+ currentModelName,
5415
+ constraint.name
5416
+ );
5137
5417
  const constraintMap2 = yDoc.getMap(constraintMapName2);
5138
5418
  const recordId2 = constraintMap2.get(uniqueKeyString);
5139
5419
  if (recordId2) {
@@ -5141,7 +5421,7 @@ var init_BaseModel = __esm({
5141
5421
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
5142
5422
  if (documentYMap) {
5143
5423
  const recordYMap2 = documentYMap.get(recordId2);
5144
- if (recordYMap2 && recordYMap2 instanceof Y2.Map) {
5424
+ if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
5145
5425
  const instance = new modelConstructor({
5146
5426
  id: recordId2
5147
5427
  });
@@ -5155,6 +5435,16 @@ var init_BaseModel = __esm({
5155
5435
  }
5156
5436
  }
5157
5437
  }
5438
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5439
+ constraint,
5440
+ keyValues
5441
+ );
5442
+ if (fallbackRecord) {
5443
+ Logger.verbose(
5444
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5445
+ );
5446
+ return fallbackRecord;
5447
+ }
5158
5448
  Logger.verbose(
5159
5449
  `[${currentModelName}.findByUnique] Record not found in any connected document`
5160
5450
  );
@@ -5168,17 +5458,30 @@ var init_BaseModel = __esm({
5168
5458
  `[${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.`
5169
5459
  );
5170
5460
  }
5171
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5461
+ const constraintMapName = uniqueIndexMapName(
5462
+ currentModelName,
5463
+ constraint.name
5464
+ );
5172
5465
  const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
5173
5466
  const recordId = constraintMap.get(uniqueKeyString);
5174
5467
  if (!recordId) {
5468
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5469
+ constraint,
5470
+ keyValues
5471
+ );
5472
+ if (fallbackRecord) {
5473
+ Logger.verbose(
5474
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5475
+ );
5476
+ return fallbackRecord;
5477
+ }
5175
5478
  return null;
5176
5479
  }
5177
5480
  const recordYMap = legacyDocumentMap.get(recordId);
5178
5481
  if (!recordYMap) {
5179
5482
  return null;
5180
5483
  }
5181
- if (recordYMap instanceof Y2.Map) {
5484
+ if (recordYMap instanceof Y5.Map) {
5182
5485
  const instance = new modelConstructor({
5183
5486
  id: recordId
5184
5487
  });
@@ -5236,7 +5539,10 @@ var init_BaseModel = __esm({
5236
5539
  `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
5237
5540
  );
5238
5541
  }
5239
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5542
+ const constraintMapName = uniqueIndexMapName(
5543
+ currentModelName,
5544
+ constraint.name
5545
+ );
5240
5546
  let existingRecord = null;
5241
5547
  if (connectedDocuments && connectedDocuments.size > 0) {
5242
5548
  for (const docId of connectedDocuments.keys()) {
@@ -5254,6 +5560,33 @@ var init_BaseModel = __esm({
5254
5560
  }
5255
5561
  } else {
5256
5562
  }
5563
+ if (!existingRecord) {
5564
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5565
+ constraint,
5566
+ keyValuesForLookup
5567
+ );
5568
+ if (fallbackRecord) {
5569
+ existingRecord = fallbackRecord;
5570
+ const healDocId = fallbackRecord._metaDocId;
5571
+ const healDocInfo = healDocId ? connectedDocuments.get(healDocId) : void 0;
5572
+ const canHeal = !!healDocInfo && healDocInfo.permissionHint !== "read";
5573
+ if (healDocInfo && canHeal) {
5574
+ healDocInfo.yDoc.transact(() => {
5575
+ const healMap = healDocInfo.yDoc.getMap(constraintMapName);
5576
+ if (healMap.get(uniqueKeyString) !== fallbackRecord.id) {
5577
+ healMap.set(uniqueKeyString, fallbackRecord.id);
5578
+ }
5579
+ }, `heal-unique-index-${currentModelName}-${healDocId}`);
5580
+ Logger.verbose(
5581
+ `[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; healed missing index entry (legacy index-less record).`
5582
+ );
5583
+ } else {
5584
+ Logger.verbose(
5585
+ `[${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).`
5586
+ );
5587
+ }
5588
+ }
5589
+ }
5257
5590
  if (options?.objectMustExist) {
5258
5591
  if (!existingRecord) {
5259
5592
  throw new RecordNotFoundError(
@@ -5410,7 +5743,7 @@ var init_BaseModel = __esm({
5410
5743
  for (const [fieldKey, fieldOptions] of schema.fields) {
5411
5744
  if (fieldOptions?.type !== "stringset") continue;
5412
5745
  const value = recordYMap.get(fieldKey);
5413
- if (value instanceof Y2.Map) {
5746
+ if (value instanceof Y5.Map) {
5414
5747
  this.observeStringSetMapOnce(value);
5415
5748
  }
5416
5749
  }
@@ -5457,7 +5790,14 @@ var init_BaseModel = __esm({
5457
5790
  continue;
5458
5791
  }
5459
5792
  if (value !== void 0) {
5460
- updatedData[key] = value;
5793
+ if (isYjsComposite(value)) {
5794
+ Logger.warn(
5795
+ `[${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.`
5796
+ );
5797
+ updatedData[key] = normalizeYjsValueForStorage(value);
5798
+ } else {
5799
+ updatedData[key] = value;
5800
+ }
5461
5801
  }
5462
5802
  }
5463
5803
  if (unknownFields.length > 0) {
@@ -5537,7 +5877,14 @@ var init_BaseModel = __esm({
5537
5877
  continue;
5538
5878
  }
5539
5879
  if (value !== void 0) {
5540
- updatedData[key] = value;
5880
+ if (isYjsComposite(value)) {
5881
+ Logger.warn(
5882
+ `[${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.`
5883
+ );
5884
+ updatedData[key] = normalizeYjsValueForStorage(value);
5885
+ } else {
5886
+ updatedData[key] = value;
5887
+ }
5541
5888
  }
5542
5889
  }
5543
5890
  if (unknownFields.length > 0) {
@@ -5628,7 +5975,143 @@ init_BaseModel();
5628
5975
  init_sql();
5629
5976
  import initSqlJs from "sql.js";
5630
5977
  import { Mutex } from "async-mutex";
5978
+
5979
+ // src/utils/environment.ts
5980
+ function detectEnvironment() {
5981
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
5982
+ return "node";
5983
+ }
5984
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
5985
+ return "browser";
5986
+ }
5987
+ if (typeof self !== "undefined" && typeof self.importScripts === "function") {
5988
+ return "browser";
5989
+ }
5990
+ return "unknown";
5991
+ }
5992
+ function isNode() {
5993
+ return detectEnvironment() === "node";
5994
+ }
5995
+ function isBrowser() {
5996
+ return detectEnvironment() === "browser";
5997
+ }
5998
+ async function isNodeModuleAvailable(moduleName) {
5999
+ if (!isNode()) {
6000
+ return false;
6001
+ }
6002
+ try {
6003
+ await import(
6004
+ /* @vite-ignore */
6005
+ moduleName
6006
+ );
6007
+ return true;
6008
+ } catch {
6009
+ return false;
6010
+ }
6011
+ }
6012
+ var features = {
6013
+ get hasWebWorkers() {
6014
+ return isBrowser() && typeof Worker !== "undefined";
6015
+ },
6016
+ get hasFileSystem() {
6017
+ return isNode();
6018
+ },
6019
+ get hasWebAssembly() {
6020
+ return typeof WebAssembly !== "undefined";
6021
+ },
6022
+ async hasBetterSqlite3() {
6023
+ return await isNodeModuleAvailable("better-sqlite3");
6024
+ }
6025
+ };
6026
+
6027
+ // src/engines/SqljsEngine.ts
5631
6028
  var SQL_WASM_URL = "/sql-wasm.wasm";
6029
+ var SQL_WASM_FILENAME = "sql-wasm.wasm";
6030
+ async function resolveNodeWasmPath() {
6031
+ if (!isNode()) {
6032
+ return null;
6033
+ }
6034
+ try {
6035
+ const cjsRequire = typeof __require !== "undefined" ? __require : void 0;
6036
+ if (cjsRequire) {
6037
+ return cjsRequire.resolve("sql.js/dist/sql-wasm.wasm");
6038
+ }
6039
+ } catch {
6040
+ }
6041
+ try {
6042
+ const metaUrl = import.meta.url;
6043
+ if (metaUrl) {
6044
+ const { createRequire } = await import(
6045
+ /* @vite-ignore */
6046
+ "module"
6047
+ );
6048
+ const req = createRequire(metaUrl);
6049
+ return req.resolve("sql.js/dist/sql-wasm.wasm");
6050
+ }
6051
+ } catch {
6052
+ }
6053
+ return null;
6054
+ }
6055
+ async function preflightNodeWasm(wasmPath) {
6056
+ if (!isNode()) {
6057
+ return;
6058
+ }
6059
+ if (/^[a-z]+:\/\//i.test(wasmPath)) {
6060
+ return;
6061
+ }
6062
+ const { existsSync } = await import(
6063
+ /* @vite-ignore */
6064
+ "fs"
6065
+ );
6066
+ if (!existsSync(wasmPath)) {
6067
+ throw new Error(
6068
+ `[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()).`
6069
+ );
6070
+ }
6071
+ }
6072
+ async function loadSqlJsModule(locateFile) {
6073
+ let abortReject;
6074
+ const toError = (reason) => reason instanceof Error ? reason : new Error(
6075
+ `[SqljsEngine] sql.js aborted during initialization: ${String(
6076
+ reason
6077
+ )}`
6078
+ );
6079
+ const nodeProcess = isNode() && typeof process !== "undefined" ? process : void 0;
6080
+ const onUnhandled = (reason) => {
6081
+ const err = toError(reason);
6082
+ if (/Aborted/.test(err.message) || /Aborted/.test(String(reason))) {
6083
+ abortReject?.(err);
6084
+ return;
6085
+ }
6086
+ if (nodeProcess && nodeProcess.listenerCount("unhandledRejection") === 1) {
6087
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6088
+ void Promise.reject(reason);
6089
+ }
6090
+ };
6091
+ if (nodeProcess) {
6092
+ nodeProcess.on("unhandledRejection", onUnhandled);
6093
+ }
6094
+ const abortGuard = new Promise((_resolve, reject) => {
6095
+ abortReject = reject;
6096
+ });
6097
+ abortGuard.catch(() => {
6098
+ });
6099
+ try {
6100
+ const initPromise = initSqlJs({
6101
+ locateFile,
6102
+ // Layer 1: emscripten onAbort.
6103
+ onAbort: (reason) => abortReject?.(toError(reason))
6104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6105
+ });
6106
+ return await Promise.race([initPromise, abortGuard]);
6107
+ } finally {
6108
+ if (nodeProcess) {
6109
+ setTimeout(() => {
6110
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6111
+ }, 0);
6112
+ }
6113
+ }
6114
+ }
5632
6115
  var TransactionalSQLJSOperations = class {
5633
6116
  db;
5634
6117
  engine;
@@ -5694,22 +6177,37 @@ var SqljsEngine = class _SqljsEngine {
5694
6177
  }
5695
6178
  async initialize(engineOptions) {
5696
6179
  Logger.info("[SqljsEngine] Initializing SQL.js engine...");
6180
+ const loadingModuleThisCall = !_SqljsEngine.SQL;
5697
6181
  try {
5698
6182
  if (!_SqljsEngine.SQL) {
5699
- const locateFileConfig = engineOptions?.locateFile || ((_file) => engineOptions?.wasmURL || SQL_WASM_URL);
6183
+ let locateFileConfig;
6184
+ if (engineOptions?.locateFile) {
6185
+ locateFileConfig = engineOptions.locateFile;
6186
+ } else if (engineOptions?.wasmURL) {
6187
+ const url = engineOptions.wasmURL;
6188
+ locateFileConfig = (_file) => url;
6189
+ } else {
6190
+ const nodeWasmPath = await resolveNodeWasmPath();
6191
+ const resolved = nodeWasmPath ?? SQL_WASM_URL;
6192
+ locateFileConfig = (_file) => resolved;
6193
+ }
6194
+ const wasmPath = locateFileConfig(SQL_WASM_FILENAME);
5700
6195
  Logger.debug(
5701
6196
  "[SqljsEngine] Attempting to load SQL.js WASM from:",
5702
- locateFileConfig("sql-wasm.wasm")
6197
+ wasmPath
5703
6198
  );
5704
- _SqljsEngine.SQL = await initSqlJs({
5705
- locateFile: locateFileConfig
5706
- });
6199
+ await preflightNodeWasm(wasmPath);
6200
+ const SQL = await loadSqlJsModule(locateFileConfig);
6201
+ _SqljsEngine.SQL = SQL;
5707
6202
  Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
5708
6203
  }
5709
6204
  this.db = new _SqljsEngine.SQL.Database();
5710
6205
  Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
5711
6206
  this.updateTableNames();
5712
6207
  } catch (error) {
6208
+ if (loadingModuleThisCall) {
6209
+ _SqljsEngine.SQL = null;
6210
+ }
5713
6211
  Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
5714
6212
  throw error;
5715
6213
  }
@@ -5995,54 +6493,6 @@ var SqljsEngine = class _SqljsEngine {
5995
6493
  }
5996
6494
  };
5997
6495
 
5998
- // src/utils/environment.ts
5999
- function detectEnvironment() {
6000
- if (typeof process !== "undefined" && process.versions && process.versions.node) {
6001
- return "node";
6002
- }
6003
- if (typeof window !== "undefined" && typeof document !== "undefined") {
6004
- return "browser";
6005
- }
6006
- if (typeof self !== "undefined" && typeof self.importScripts === "function") {
6007
- return "browser";
6008
- }
6009
- return "unknown";
6010
- }
6011
- function isNode() {
6012
- return detectEnvironment() === "node";
6013
- }
6014
- function isBrowser() {
6015
- return detectEnvironment() === "browser";
6016
- }
6017
- async function isNodeModuleAvailable(moduleName) {
6018
- if (!isNode()) {
6019
- return false;
6020
- }
6021
- try {
6022
- await import(
6023
- /* @vite-ignore */
6024
- moduleName
6025
- );
6026
- return true;
6027
- } catch {
6028
- return false;
6029
- }
6030
- }
6031
- var features = {
6032
- get hasWebWorkers() {
6033
- return isBrowser() && typeof Worker !== "undefined";
6034
- },
6035
- get hasFileSystem() {
6036
- return isNode();
6037
- },
6038
- get hasWebAssembly() {
6039
- return typeof WebAssembly !== "undefined";
6040
- },
6041
- async hasBetterSqlite3() {
6042
- return await isNodeModuleAvailable("better-sqlite3");
6043
- }
6044
- };
6045
-
6046
6496
  // src/engines/BrowserDatabaseFactory.ts
6047
6497
  init_BaseModel();
6048
6498
  var BrowserDatabaseFactory = class {
@@ -6353,6 +6803,7 @@ init_StringSet();
6353
6803
  // src/models/schema.ts
6354
6804
  init_BaseModel();
6355
6805
  init_ModelRegistry();
6806
+ init_uniqueIndex();
6356
6807
  function defineModelSchema(input) {
6357
6808
  const { name, fields } = input;
6358
6809
  const options = {
@@ -6425,7 +6876,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
6425
6876
  for (const [fieldName, options] of fields.entries()) {
6426
6877
  if (options.unique) {
6427
6878
  resolved.push({
6428
- name: `${modelName}_${fieldName}_unique`,
6879
+ name: fieldUniqueConstraintName(modelName, fieldName),
6429
6880
  fields: [fieldName]
6430
6881
  });
6431
6882
  }
@@ -6489,7 +6940,8 @@ function attachAndRegisterModel(modelClass, schema) {
6489
6940
  }
6490
6941
 
6491
6942
  // src/utils/yDocSchema.ts
6492
- import * as Y3 from "yjs";
6943
+ init_yjsValues();
6944
+ import * as Y6 from "yjs";
6493
6945
  function discoverSchema(yDoc) {
6494
6946
  const models = {};
6495
6947
  const metaNames = /* @__PURE__ */ new Set();
@@ -6523,7 +6975,7 @@ function discoverModelNames(yDoc) {
6523
6975
  function materializeMap(yDoc, key) {
6524
6976
  try {
6525
6977
  const map = yDoc.getMap(key);
6526
- return map instanceof Y3.Map ? map : null;
6978
+ return map instanceof Y6.Map ? map : null;
6527
6979
  } catch {
6528
6980
  return null;
6529
6981
  }
@@ -6533,11 +6985,11 @@ function readModelMeta(metaMap) {
6533
6985
  let constraints;
6534
6986
  let relationships;
6535
6987
  for (const [key, value] of metaMap.entries()) {
6536
- if (key === "_constraints" && value instanceof Y3.Map) {
6988
+ if (key === "_constraints" && value instanceof Y6.Map) {
6537
6989
  constraints = readConstraints(value);
6538
- } else if (key === "_relationships" && value instanceof Y3.Map) {
6990
+ } else if (key === "_relationships" && value instanceof Y6.Map) {
6539
6991
  relationships = readRelationships(value);
6540
- } else if (value instanceof Y3.Map) {
6992
+ } else if (value instanceof Y6.Map) {
6541
6993
  fields[key] = readFieldMeta(value);
6542
6994
  }
6543
6995
  }
@@ -6556,6 +7008,10 @@ function readFieldMeta(fieldMap) {
6556
7008
  if (fieldMap.get("unique") === true) field.unique = true;
6557
7009
  if (fieldMap.get("required") === true) field.required = true;
6558
7010
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7011
+ const autoStamp = fieldMap.get("autoStamp");
7012
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
7013
+ field.autoStamp = autoStamp;
7014
+ }
6559
7015
  const def = fieldMap.get("default");
6560
7016
  if (def !== void 0) field.default = def;
6561
7017
  const maxLength = fieldMap.get("maxLength");
@@ -6568,7 +7024,7 @@ function inferModelFromData(dataMap) {
6568
7024
  const fields = {};
6569
7025
  let sampled = 0;
6570
7026
  for (const [_recordId, recordValue] of dataMap.entries()) {
6571
- if (!(recordValue instanceof Y3.Map)) continue;
7027
+ if (!(recordValue instanceof Y6.Map)) continue;
6572
7028
  if (++sampled > 5) break;
6573
7029
  for (const [fieldName, value] of recordValue.entries()) {
6574
7030
  if (fieldName.startsWith("_")) continue;
@@ -6581,7 +7037,7 @@ function inferModelFromData(dataMap) {
6581
7037
  return { fields };
6582
7038
  }
6583
7039
  function inferTypeFromValue(value) {
6584
- if (value instanceof Y3.Map) return "stringset";
7040
+ if (isStringSetYMap(value)) return "stringset";
6585
7041
  switch (typeof value) {
6586
7042
  case "string":
6587
7043
  return "string";
@@ -6596,7 +7052,7 @@ function inferTypeFromValue(value) {
6596
7052
  function readConstraints(constraintsMap) {
6597
7053
  const out = {};
6598
7054
  for (const [name, value] of constraintsMap.entries()) {
6599
- if (!(value instanceof Y3.Map)) continue;
7055
+ if (!(value instanceof Y6.Map)) continue;
6600
7056
  let fields = [];
6601
7057
  const rawFields = value.get("fields");
6602
7058
  if (typeof rawFields === "string") {
@@ -6615,7 +7071,7 @@ function readConstraints(constraintsMap) {
6615
7071
  function readRelationships(relsMap) {
6616
7072
  const out = {};
6617
7073
  for (const [name, value] of relsMap.entries()) {
6618
- if (!(value instanceof Y3.Map)) continue;
7074
+ if (!(value instanceof Y6.Map)) continue;
6619
7075
  const rel = {};
6620
7076
  for (const [k, v] of value.entries()) {
6621
7077
  rel[k] = v;
@@ -6626,6 +7082,7 @@ function readRelationships(relsMap) {
6626
7082
  }
6627
7083
  var CAMEL_TO_SNAKE = {
6628
7084
  autoAssign: "auto_assign",
7085
+ autoStamp: "auto_stamp",
6629
7086
  maxLength: "max_length",
6630
7087
  maxCount: "max_count",
6631
7088
  relatedIdField: "related_id_field",
@@ -6656,6 +7113,7 @@ function schemaToToml(schema) {
6656
7113
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
6657
7114
  lines.push(`type = ${tomlValue(field.type)}`);
6658
7115
  if (field.autoAssign) lines.push("auto_assign = true");
7116
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
6659
7117
  if (field.indexed) lines.push("indexed = true");
6660
7118
  if (field.unique) lines.push("unique = true");
6661
7119
  if (field.required) lines.push("required = true");
@@ -6702,10 +7160,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
6702
7160
  "unique",
6703
7161
  "required",
6704
7162
  "auto_assign",
7163
+ "auto_stamp",
6705
7164
  "max_length",
6706
7165
  "max_count",
6707
- "default"
7166
+ "default",
7167
+ "enum"
6708
7168
  ]);
7169
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
6709
7170
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
6710
7171
  "fields",
6711
7172
  "relationships",
@@ -6747,9 +7208,37 @@ function parseFieldOptions(raw, context, strict) {
6747
7208
  if (raw.unique === true) opts.unique = true;
6748
7209
  if (raw.required === true) opts.required = true;
6749
7210
  if (raw.auto_assign === true) opts.autoAssign = true;
7211
+ if (raw.auto_stamp !== void 0) {
7212
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
7213
+ throw new Error(
7214
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
7215
+ ...VALID_AUTO_STAMP_VALUES
7216
+ ].join(", ")}`
7217
+ );
7218
+ }
7219
+ opts.autoStamp = raw.auto_stamp;
7220
+ }
6750
7221
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
6751
7222
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
6752
7223
  if (raw.default !== void 0) opts.default = raw.default;
7224
+ if (raw.enum !== void 0) {
7225
+ if (raw.type !== "string") {
7226
+ throw new Error(
7227
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
7228
+ );
7229
+ }
7230
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
7231
+ throw new Error(
7232
+ `${context}: \`enum\` must be a non-empty array of strings.`
7233
+ );
7234
+ }
7235
+ if (!raw.enum.every((v) => typeof v === "string")) {
7236
+ throw new Error(
7237
+ `${context}: \`enum\` values must all be strings.`
7238
+ );
7239
+ }
7240
+ opts.enum = [...raw.enum];
7241
+ }
6753
7242
  return opts;
6754
7243
  }
6755
7244
  function requireField(raw, field, context) {