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/node.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,244 @@ 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 assertNever(x) {
1250
+ throw new Error(`Unhandled field type: ${String(x)}`);
1251
+ }
1252
+ function fieldWriteKind(type) {
1253
+ switch (type) {
1254
+ case "string":
1255
+ case "number":
1256
+ case "boolean":
1257
+ case "date":
1258
+ case "id":
1259
+ return "scalar";
1260
+ case "stringset":
1261
+ return "stringset";
1262
+ default:
1263
+ return assertNever(type);
1264
+ }
1265
+ }
1266
+ function isKnownFieldType(type) {
1267
+ return KNOWN_FIELD_TYPES.has(type);
1268
+ }
1269
+ function ensureStringSetYMap(rm, fieldName) {
1270
+ const nested = rm.get(fieldName);
1271
+ if (nested instanceof Y2.Map) return nested;
1272
+ const fresh = new Y2.Map();
1273
+ if (nested && typeof nested === "object") {
1274
+ for (const [member, marker] of Object.entries(
1275
+ nested
1276
+ )) {
1277
+ if (Boolean(marker)) fresh.set(member, true);
1278
+ }
1279
+ }
1280
+ rm.set(fieldName, fresh);
1281
+ return fresh;
1282
+ }
1283
+ function applyStringSetDelta(target, delta) {
1284
+ if (delta.clear) {
1285
+ for (const member of Array.from(target.keys())) target.delete(member);
1286
+ }
1287
+ if (delta.remove) {
1288
+ for (const member of delta.remove) target.delete(String(member));
1289
+ }
1290
+ if (delta.add) {
1291
+ for (const member of delta.add) {
1292
+ const key = String(member);
1293
+ if (target.get(key) !== true) target.set(key, true);
1294
+ }
1295
+ }
1296
+ }
1297
+ function replaceStringSet(rm, fieldName, members) {
1298
+ const target = ensureStringSetYMap(rm, fieldName);
1299
+ const desired = /* @__PURE__ */ new Set();
1300
+ for (const member of members) desired.add(String(member));
1301
+ for (const existing of Array.from(target.keys())) {
1302
+ if (!desired.has(existing)) target.delete(existing);
1303
+ }
1304
+ for (const member of desired) {
1305
+ if (target.get(member) !== true) target.set(member, true);
1306
+ }
1307
+ }
1308
+ function applyStringSetChange(recordYMap, fieldName, change) {
1309
+ const target = ensureStringSetYMap(recordYMap, fieldName);
1310
+ applyStringSetDelta(target, {
1311
+ add: change.additions,
1312
+ remove: change.removals
1313
+ });
1314
+ }
1315
+ function parseStringSetOp(value) {
1316
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value instanceof Y2.AbstractType) {
1317
+ return null;
1318
+ }
1319
+ const keys = Object.keys(value);
1320
+ const opKeys = keys.filter((k) => k.startsWith("$"));
1321
+ if (opKeys.length === 0) return null;
1322
+ const known = /* @__PURE__ */ new Set(["$add", "$remove", "$clear"]);
1323
+ const unknown = keys.filter((k) => !known.has(k));
1324
+ if (unknown.length > 0) {
1325
+ throw new Error(
1326
+ `Invalid stringset op: unexpected key(s) [${unknown.join(", ")}]. Only $add, $remove and $clear are supported.`
1327
+ );
1328
+ }
1329
+ const obj = value;
1330
+ const toMembers = (raw, opName) => {
1331
+ if (raw === void 0) return [];
1332
+ if (!Array.isArray(raw)) {
1333
+ throw new Error(`Invalid stringset op: ${opName} must be an array.`);
1334
+ }
1335
+ return raw.map((m) => String(m));
1336
+ };
1337
+ return {
1338
+ add: toMembers(obj.$add, "$add"),
1339
+ remove: toMembers(obj.$remove, "$remove"),
1340
+ clear: obj.$clear === true
1341
+ };
1342
+ }
1343
+ function writeField(rm, declaredType, key, value, modelName) {
1344
+ const kind = declaredType && isKnownFieldType(declaredType) ? fieldWriteKind(declaredType) : "scalar";
1345
+ if (Array.isArray(value)) {
1346
+ if (kind !== "stringset") {
1347
+ throw new Error(
1348
+ `Cannot write array value to field "${key}" on model "${modelName}": its declared type could not be resolved to a collection (stringset). Collection fields require schema metadata in the document (_meta_${modelName}); the model must be saved by a client at least once before a workflow writes one of its collection fields.`
1349
+ );
1350
+ }
1351
+ replaceStringSet(rm, key, value);
1352
+ return;
1353
+ }
1354
+ const op = parseStringSetOp(value);
1355
+ if (op) {
1356
+ if (kind !== "stringset") {
1357
+ throw new Error(
1358
+ `Cannot apply a stringset op ({ $add/$remove/$clear }) to field "${key}" on model "${modelName}": its declared type is ${declaredType ? `"${declaredType}"` : "unresolved"}, not a collection (stringset). Op-objects are only valid for stringset / refersToMany fields with schema metadata (_meta_${modelName}).`
1359
+ );
1360
+ }
1361
+ applyStringSetDelta(ensureStringSetYMap(rm, key), op);
1362
+ return;
1363
+ }
1364
+ if (kind === "stringset") {
1365
+ throw new Error(
1366
+ `Field "${key}" on model "${modelName}" is a stringset and requires an array (replace) or { $add/$remove/$clear } (delta) value; received ${value === null ? "null" : typeof value}.`
1367
+ );
1368
+ }
1369
+ rm.set(key, value);
1370
+ }
1371
+ function registerFunctionDefaultGenerator(name, fn) {
1372
+ FUNCTION_DEFAULT_GENERATORS.set(name, fn);
1373
+ }
1374
+ function decodeFunctionDefault(name) {
1375
+ return FUNCTION_DEFAULT_GENERATORS.get(name);
1376
+ }
1377
+ function resolveLiteralOrFunctionDefault(rawDefault) {
1378
+ return typeof rawDefault === "function" ? rawDefault() : rawDefault;
1379
+ }
1380
+ function resolveDefaultValue(rawDefault, opts = {}) {
1381
+ if (typeof rawDefault === "function") return rawDefault();
1382
+ if (typeof rawDefault === "string" && rawDefault.startsWith("$")) {
1383
+ if (rawDefault.startsWith("$$")) return rawDefault.slice(1);
1384
+ const name = rawDefault.slice(1);
1385
+ const gen = opts.decode?.(name) ?? decodeFunctionDefault(name);
1386
+ if (!gen) {
1387
+ throw new Error(
1388
+ `Unknown function default "${rawDefault}" for field "${opts.fieldName ?? "?"}"${opts.modelName ? ` on model "${opts.modelName}"` : ""}: no generator is registered for "${name}". Register it via registerFunctionDefaultGenerator("${name}", fn) so the server can populate the field the same way the client does.`
1389
+ );
1390
+ }
1391
+ return gen();
1392
+ }
1393
+ return rawDefault;
1394
+ }
1395
+ function applyDefaultsFromSchema(fields, data, opts = {}) {
1396
+ for (const [fieldName, field] of Object.entries(fields)) {
1397
+ if (fieldName === "id") continue;
1398
+ if (!field || field.default === void 0) continue;
1399
+ if (field.default === UNKNOWN_FUNCTION_DEFAULT) {
1400
+ continue;
1401
+ }
1402
+ if (Object.prototype.hasOwnProperty.call(data, fieldName) && data[fieldName] !== void 0) {
1403
+ continue;
1404
+ }
1405
+ data[fieldName] = resolveDefaultValue(field.default, {
1406
+ fieldName,
1407
+ modelName: opts.modelName,
1408
+ decode: opts.decodeFunctionDefault
1409
+ });
1410
+ }
1411
+ return data;
1412
+ }
1413
+ function applyAutoStampsFromSchema(fields, data, opts) {
1414
+ const { isNew, now } = opts;
1415
+ for (const [fieldName, field] of Object.entries(fields)) {
1416
+ const stamp = field?.autoStamp;
1417
+ if (!stamp) continue;
1418
+ const explicit = Object.prototype.hasOwnProperty.call(data, fieldName) && data[fieldName] !== null && data[fieldName] !== void 0;
1419
+ if (explicit) continue;
1420
+ if (stamp === "create" && !isNew) continue;
1421
+ data[fieldName] = now;
1422
+ }
1423
+ return data;
1424
+ }
1425
+ var Y2, import_ulid, KNOWN_FIELD_TYPE_MAP, KNOWN_FIELD_TYPES, FUNCTION_DEFAULT_GENERATORS, UNKNOWN_FUNCTION_DEFAULT;
1426
+ var init_writeLifecycle = __esm({
1427
+ "src/models/writeLifecycle.ts"() {
1428
+ "use strict";
1429
+ Y2 = __toESM(require("yjs"), 1);
1430
+ import_ulid = require("ulid");
1431
+ KNOWN_FIELD_TYPE_MAP = {
1432
+ string: true,
1433
+ number: true,
1434
+ boolean: true,
1435
+ date: true,
1436
+ id: true,
1437
+ stringset: true
1438
+ };
1439
+ KNOWN_FIELD_TYPES = new Set(Object.keys(KNOWN_FIELD_TYPE_MAP));
1440
+ FUNCTION_DEFAULT_GENERATORS = /* @__PURE__ */ new Map();
1441
+ registerFunctionDefaultGenerator("generate_ulid", () => (0, import_ulid.ulid)());
1442
+ UNKNOWN_FUNCTION_DEFAULT = "$unknown_function";
1443
+ }
1444
+ });
1445
+
1186
1446
  // src/models/metaSync.ts
1187
1447
  function inferFieldType(value) {
1188
- if (value instanceof Y.Map) return "stringset";
1448
+ if (isStringSetYMap(value)) return "stringset";
1189
1449
  switch (typeof value) {
1190
1450
  case "string":
1191
1451
  return "string";
@@ -1204,7 +1464,10 @@ function encodeDefault(value) {
1204
1464
  if (value === void 0 || value === null) return void 0;
1205
1465
  if (typeof value === "function") {
1206
1466
  const name = KNOWN_FUNCTION_DEFAULTS.get(value);
1207
- return name ? `$${name}` : "$unknown_function";
1467
+ return name ? `$${name}` : UNKNOWN_FUNCTION_DEFAULT;
1468
+ }
1469
+ if (typeof value === "string" && value.startsWith("$")) {
1470
+ return `$${value}`;
1208
1471
  }
1209
1472
  return value;
1210
1473
  }
@@ -1230,7 +1493,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1230
1493
  if (compoundConstraints.length > 0) {
1231
1494
  let constraints = meta.get("_constraints");
1232
1495
  if (!constraints) {
1233
- constraints = new Y.Map();
1496
+ constraints = new Y3.Map();
1234
1497
  meta.set("_constraints", constraints);
1235
1498
  }
1236
1499
  for (const constraint of compoundConstraints) {
@@ -1241,7 +1504,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1241
1504
  if (relationships && Object.keys(relationships).length > 0) {
1242
1505
  let rels = meta.get("_relationships");
1243
1506
  if (!rels) {
1244
- rels = new Y.Map();
1507
+ rels = new Y3.Map();
1245
1508
  meta.set("_relationships", rels);
1246
1509
  }
1247
1510
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -1253,7 +1516,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1253
1516
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1254
1517
  let fieldMeta = metaMap.get(fieldName);
1255
1518
  if (!fieldMeta) {
1256
- fieldMeta = new Y.Map();
1519
+ fieldMeta = new Y3.Map();
1257
1520
  metaMap.set(fieldName, fieldMeta);
1258
1521
  }
1259
1522
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -1261,6 +1524,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1261
1524
  if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
1262
1525
  if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
1263
1526
  if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
1527
+ if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
1264
1528
  if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
1265
1529
  if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
1266
1530
  const encoded = encodeDefault(fieldOpts.default);
@@ -1269,7 +1533,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1269
1533
  function syncConstraintMeta(constraintsMap, constraint) {
1270
1534
  let cMeta = constraintsMap.get(constraint.name);
1271
1535
  if (!cMeta) {
1272
- cMeta = new Y.Map();
1536
+ cMeta = new Y3.Map();
1273
1537
  constraintsMap.set(constraint.name, cMeta);
1274
1538
  }
1275
1539
  setIfChanged(cMeta, "type", "unique");
@@ -1279,7 +1543,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
1279
1543
  function syncRelationshipMeta(relsMap, relName, relConfig) {
1280
1544
  let relMeta = relsMap.get(relName);
1281
1545
  if (!relMeta) {
1282
- relMeta = new Y.Map();
1546
+ relMeta = new Y3.Map();
1283
1547
  relsMap.set(relName, relMeta);
1284
1548
  }
1285
1549
  for (const [key, value] of Object.entries(relConfig)) {
@@ -1294,7 +1558,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
1294
1558
  if (fieldName.startsWith("_")) continue;
1295
1559
  let fieldMeta = meta.get(fieldName);
1296
1560
  if (!fieldMeta) {
1297
- fieldMeta = new Y.Map();
1561
+ fieldMeta = new Y3.Map();
1298
1562
  meta.set(fieldName, fieldMeta);
1299
1563
  }
1300
1564
  if (!fieldMeta.has("type")) {
@@ -1310,17 +1574,58 @@ function setIfChanged(map, key, value) {
1310
1574
  map.set(key, value);
1311
1575
  }
1312
1576
  }
1313
- var Y, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1577
+ var Y3, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1314
1578
  var init_metaSync = __esm({
1315
1579
  "src/models/metaSync.ts"() {
1316
1580
  "use strict";
1317
- Y = __toESM(require("yjs"), 1);
1581
+ Y3 = __toESM(require("yjs"), 1);
1582
+ init_yjsValues();
1583
+ init_writeLifecycle();
1318
1584
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
1319
1585
  _syncedCache = /* @__PURE__ */ new WeakMap();
1320
1586
  }
1321
1587
  });
1322
1588
 
1589
+ // src/utils/uniqueIndex.ts
1590
+ function buildUniqueKey(recordData, fields) {
1591
+ const keyParts = [];
1592
+ for (const field of fields) {
1593
+ const value = recordData instanceof Y4.Map ? recordData.get(field) : recordData[field];
1594
+ if (value === null || value === void 0) {
1595
+ return null;
1596
+ }
1597
+ keyParts.push(String(value));
1598
+ }
1599
+ return keyParts.join("|");
1600
+ }
1601
+ function uniqueIndexMapName(modelName, constraintName) {
1602
+ return `_uniqueIdx_${modelName}_${constraintName}`;
1603
+ }
1604
+ function fieldUniqueConstraintName(modelName, fieldName) {
1605
+ return `${modelName}_${fieldName}_unique`;
1606
+ }
1607
+ var Y4;
1608
+ var init_uniqueIndex = __esm({
1609
+ "src/utils/uniqueIndex.ts"() {
1610
+ "use strict";
1611
+ Y4 = __toESM(require("yjs"), 1);
1612
+ }
1613
+ });
1614
+
1323
1615
  // src/models/relationshipManager.ts
1616
+ function warnIfOrderFieldNullable(modelClass, orderByField, relationshipDescription) {
1617
+ if (ALWAYS_PRESENT_ORDER_FIELDS.has(orderByField)) return;
1618
+ const schema = modelClass.getSchema?.();
1619
+ const fields = schema?.fields;
1620
+ if (!fields || typeof fields.get !== "function") return;
1621
+ const fieldOptions = fields.get(orderByField);
1622
+ if (!fieldOptions) return;
1623
+ if (fieldOptions.required !== true) {
1624
+ Logger.warn(
1625
+ `[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.)`
1626
+ );
1627
+ }
1628
+ }
1324
1629
  function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1325
1630
  return async function() {
1326
1631
  const relatedId = this[config.relatedIdField];
@@ -1329,6 +1634,16 @@ function generateRefersToMethod(_sourceModelClass, config, targetModelClass) {
1329
1634
  };
1330
1635
  }
1331
1636
  function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbEngine) {
1637
+ const {
1638
+ relatedIdField,
1639
+ orderByField = "id",
1640
+ orderDirection = "ASC"
1641
+ } = config;
1642
+ warnIfOrderFieldNullable(
1643
+ targetModelClass,
1644
+ orderByField,
1645
+ `hasMany(${targetModelClass.name})`
1646
+ );
1332
1647
  return async function(paginationArgs) {
1333
1648
  const {
1334
1649
  limit,
@@ -1336,71 +1651,39 @@ function generateHasManyMethod(_sourceModelClass, config, targetModelClass, _dbE
1336
1651
  beforeCursor,
1337
1652
  direction = "forward"
1338
1653
  } = paginationArgs || {};
1339
- const {
1340
- relatedIdField,
1341
- orderByField = "id",
1342
- orderDirection = "ASC"
1343
- } = config;
1344
1654
  const targetSchema = targetModelClass.getSchema();
1345
1655
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1346
1656
  Logger.error(
1347
1657
  `[RelationshipManager] Target model ${targetModelClass.name} for hasMany relationship has no valid schema name.`
1348
1658
  );
1349
- return { data: [], nextCursor: null, prevCursor: null };
1659
+ return { data: [], hasMore: false };
1350
1660
  }
1351
- const filter = { [relatedIdField]: this.id };
1661
+ const forward = direction !== "backward";
1662
+ const cursor = forward ? afterCursor : beforeCursor;
1352
1663
  const queryOptions = {
1353
- sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 }
1664
+ sort: { [orderByField]: orderDirection === "ASC" ? 1 : -1 },
1665
+ direction: forward ? 1 : -1
1354
1666
  };
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 };
1667
+ if (limit !== void 0) queryOptions.limit = limit;
1668
+ if (cursor != null) queryOptions.uniqueStartKey = cursor;
1669
+ return targetModelClass.query(
1670
+ { [relatedIdField]: this.id },
1671
+ queryOptions
1672
+ );
1401
1673
  };
1402
1674
  }
1403
1675
  function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClass, joinModelClass, _dbEngine) {
1676
+ const {
1677
+ joinModelLocalField,
1678
+ joinModelRelatedField,
1679
+ joinModelOrderByField = "id",
1680
+ joinModelOrderDirection = "ASC"
1681
+ } = config;
1682
+ warnIfOrderFieldNullable(
1683
+ joinModelClass,
1684
+ joinModelOrderByField,
1685
+ `hasManyThrough(${targetModelClass.name} via ${joinModelClass.name})`
1686
+ );
1404
1687
  return async function(paginationArgs) {
1405
1688
  const {
1406
1689
  limit,
@@ -1414,87 +1697,42 @@ function generateHasManyThroughMethod(_sourceModelClass, config, targetModelClas
1414
1697
  Logger.error(
1415
1698
  `[RelationshipManager] Join model ${joinModelClass.name} for hasManyThrough relationship has no valid schema name.`
1416
1699
  );
1417
- return { data: [], nextCursor: null, prevCursor: null };
1700
+ return { data: [], hasMore: false };
1418
1701
  }
1419
1702
  if (!targetSchema || !targetSchema.options || !targetSchema.options.name) {
1420
1703
  Logger.error(
1421
1704
  `[RelationshipManager] Target model ${targetModelClass.name} for hasManyThrough relationship has no valid schema name.`
1422
1705
  );
1423
- return { data: [], nextCursor: null, prevCursor: null };
1706
+ return { data: [], hasMore: false };
1424
1707
  }
1425
- const {
1426
- joinModelLocalField,
1427
- joinModelRelatedField,
1428
- joinModelOrderByField = "id",
1429
- joinModelOrderDirection = "ASC"
1430
- } = config;
1431
- const joinFilter = { [joinModelLocalField]: this.id };
1708
+ const forward = direction !== "backward";
1709
+ const cursor = forward ? afterCursor : beforeCursor;
1432
1710
  const joinQueryOptions = {
1433
1711
  sort: {
1434
1712
  [joinModelOrderByField]: joinModelOrderDirection === "ASC" ? 1 : -1
1435
1713
  },
1436
- projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 }
1714
+ projection: { [joinModelRelatedField]: 1, [joinModelOrderByField]: 1 },
1715
+ direction: forward ? 1 : -1
1437
1716
  };
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
- }
1717
+ if (limit !== void 0) joinQueryOptions.limit = limit;
1718
+ if (cursor != null) joinQueryOptions.uniqueStartKey = cursor;
1455
1719
  const joinResult = await joinModelClass.query(
1456
- joinFilter,
1720
+ { [joinModelLocalField]: this.id },
1457
1721
  joinQueryOptions
1458
1722
  );
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
- }
1723
+ const relatedIds = joinResult.data.map((r) => r[joinModelRelatedField]).filter((id) => id != null && typeof id === "string");
1724
+ let targetData = [];
1725
+ if (relatedIds.length > 0) {
1726
+ const targetResult = await targetModelClass.query({
1727
+ id: { $in: relatedIds }
1728
+ });
1729
+ targetData = targetResult.data;
1487
1730
  }
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
1731
  return {
1495
- data: targetResult.data,
1496
- nextCursor: nextCursorFromJoin,
1497
- prevCursor: prevCursorFromJoin
1732
+ data: targetData,
1733
+ nextCursor: joinResult.nextCursor,
1734
+ prevCursor: joinResult.prevCursor,
1735
+ hasMore: joinResult.hasMore
1498
1736
  };
1499
1737
  };
1500
1738
  }
@@ -1618,10 +1856,12 @@ function generateHasManyThroughRemoveMethod(_sourceModelClass, config, _targetMo
1618
1856
  }, `remove-join-${config.joinModel}-${this.id}-${targetId}`);
1619
1857
  };
1620
1858
  }
1859
+ var ALWAYS_PRESENT_ORDER_FIELDS;
1621
1860
  var init_relationshipManager = __esm({
1622
1861
  "src/models/relationshipManager.ts"() {
1623
1862
  "use strict";
1624
1863
  init_BaseModel();
1864
+ ALWAYS_PRESENT_ORDER_FIELDS = /* @__PURE__ */ new Set(["id", "type"]);
1625
1865
  }
1626
1866
  });
1627
1867
 
@@ -2392,7 +2632,13 @@ var init_IncludeResolver = __esm({
2392
2632
  }
2393
2633
  return null;
2394
2634
  }
2395
- /** Apply projection to a single record. */
2635
+ /**
2636
+ * Apply a projection to a single record. Assumes the projection was already
2637
+ * validated upstream (mixed inclusion+exclusion rejected by the translator):
2638
+ * `isInclusion` treats "any value === 1" as inclusion mode. `id` and `type`
2639
+ * are system-identity fields and are always kept in BOTH modes (mirrors the
2640
+ * DatabaseDO `_applyProjection`); `_meta` stays excludable.
2641
+ */
2396
2642
  _applyProjection(record, projection) {
2397
2643
  const fields = Object.entries(projection);
2398
2644
  if (fields.length === 0) return record;
@@ -2411,7 +2657,7 @@ var init_IncludeResolver = __esm({
2411
2657
  } else {
2412
2658
  const result = { ...record };
2413
2659
  for (const [f, v] of fields) {
2414
- if (v === 0) delete result[f];
2660
+ if (v === 0 && f !== "id" && f !== "type") delete result[f];
2415
2661
  }
2416
2662
  return result;
2417
2663
  }
@@ -2431,20 +2677,23 @@ __export(BaseModel_exports, {
2431
2677
  generateULID: () => generateULID
2432
2678
  });
2433
2679
  function generateULID() {
2434
- return (0, import_ulid.ulid)();
2680
+ return (0, import_ulid2.ulid)();
2435
2681
  }
2436
- var Y2, import_ulid, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2682
+ var Y5, import_ulid2, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2437
2683
  var init_BaseModel = __esm({
2438
2684
  "src/models/BaseModel.ts"() {
2439
2685
  "use strict";
2440
- Y2 = __toESM(require("yjs"), 1);
2441
- import_ulid = require("ulid");
2686
+ Y5 = __toESM(require("yjs"), 1);
2687
+ import_ulid2 = require("ulid");
2442
2688
  init_StringSet();
2443
2689
  init_documentTypes();
2444
2690
  init_DocumentQueryTranslator();
2445
2691
  init_CursorManager();
2446
2692
  init_sql();
2447
2693
  init_metaSync();
2694
+ init_writeLifecycle();
2695
+ init_yjsValues();
2696
+ init_uniqueIndex();
2448
2697
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2449
2698
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
2450
2699
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2829,7 +3078,9 @@ var init_BaseModel = __esm({
2829
3078
  this.setValue(fieldKey, data[fieldKey]);
2830
3079
  this._constructorProvidedFields.add(fieldKey);
2831
3080
  } else if (fieldOptions.default !== void 0) {
2832
- const defaultValue = typeof fieldOptions.default === "function" ? fieldOptions.default() : fieldOptions.default;
3081
+ const defaultValue = resolveLiteralOrFunctionDefault(
3082
+ fieldOptions.default
3083
+ );
2833
3084
  this.setValue(fieldKey, defaultValue);
2834
3085
  }
2835
3086
  }
@@ -2953,7 +3204,30 @@ var init_BaseModel = __esm({
2953
3204
  if (verboseEnabled) {
2954
3205
  Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
2955
3206
  }
2956
- return fieldValue;
3207
+ return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
3208
+ }
3209
+ /**
3210
+ * Defensive normalization for the read/sync boundary (issue #625).
3211
+ *
3212
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
3213
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
3214
+ * through untouched. For any other field, a composite Yjs primitive
3215
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
3216
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
3217
+ * warn, rather than throwing. Scalars pass through unchanged.
3218
+ */
3219
+ normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
3220
+ const fieldOptions = schema?.fields?.get(fieldKey);
3221
+ if (fieldOptions?.type === "stringset") {
3222
+ return value;
3223
+ }
3224
+ if (isYjsComposite(value)) {
3225
+ Logger.warn(
3226
+ `[${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.`
3227
+ );
3228
+ return normalizeYjsValue(value);
3229
+ }
3230
+ return value;
2957
3231
  }
2958
3232
  getValue(fieldKey) {
2959
3233
  const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
@@ -3098,7 +3372,7 @@ var init_BaseModel = __esm({
3098
3372
  }
3099
3373
  getStringSetFromYjs(fieldName) {
3100
3374
  const yjsData = this.getFromYjs(fieldName);
3101
- if (yjsData instanceof Y2.Map) {
3375
+ if (yjsData instanceof Y5.Map) {
3102
3376
  return Array.from(yjsData.keys());
3103
3377
  }
3104
3378
  if (yjsData && typeof yjsData === "object") {
@@ -3206,7 +3480,7 @@ var init_BaseModel = __esm({
3206
3480
  }
3207
3481
  const extractStringSetValues = (value) => {
3208
3482
  if (!value) return [];
3209
- if (value instanceof Y2.Map) {
3483
+ if (value instanceof Y5.Map) {
3210
3484
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
3211
3485
  }
3212
3486
  if (Array.isArray(value)) {
@@ -3223,7 +3497,7 @@ var init_BaseModel = __esm({
3223
3497
  if (!recordId) continue;
3224
3498
  let itemData;
3225
3499
  const stringSetValuesByField = {};
3226
- if (recordData instanceof Y2.Map) {
3500
+ if (recordData instanceof Y5.Map) {
3227
3501
  itemData = {};
3228
3502
  const unknownFields = [];
3229
3503
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3339,20 +3613,10 @@ var init_BaseModel = __esm({
3339
3613
  Logger.verbose(
3340
3614
  `[${this.name}] Setting up YMap observer for document ${docId}/${modelName}...`
3341
3615
  );
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
- };
3616
+ const buildUniqueKey2 = (recordData, fields) => buildUniqueKey(recordData, fields);
3353
3617
  const extractItemData = (key, recordData) => {
3354
3618
  let itemData;
3355
- if (recordData instanceof Y2.Map) {
3619
+ if (recordData instanceof Y5.Map) {
3356
3620
  itemData = {};
3357
3621
  const unknownFields = [];
3358
3622
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3365,7 +3629,14 @@ var init_BaseModel = __esm({
3365
3629
  continue;
3366
3630
  }
3367
3631
  if (value !== void 0) {
3368
- itemData[fieldKey] = value;
3632
+ if (isYjsComposite(value)) {
3633
+ Logger.warn(
3634
+ `[${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.`
3635
+ );
3636
+ itemData[fieldKey] = normalizeYjsValueForStorage(value);
3637
+ } else {
3638
+ itemData[fieldKey] = value;
3639
+ }
3369
3640
  }
3370
3641
  }
3371
3642
  if (unknownFields.length > 0) {
@@ -3388,7 +3659,14 @@ var init_BaseModel = __esm({
3388
3659
  continue;
3389
3660
  }
3390
3661
  if (value !== void 0) {
3391
- filteredData[fieldKey] = value;
3662
+ if (isYjsComposite(value)) {
3663
+ Logger.warn(
3664
+ `[${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.`
3665
+ );
3666
+ filteredData[fieldKey] = normalizeYjsValueForStorage(value);
3667
+ } else {
3668
+ filteredData[fieldKey] = value;
3669
+ }
3392
3670
  }
3393
3671
  }
3394
3672
  if (unknownFields.length > 0) {
@@ -3422,7 +3700,7 @@ var init_BaseModel = __esm({
3422
3700
  const recordsByUniqueKey = /* @__PURE__ */ new Map();
3423
3701
  for (const [recordId, recordData] of allRecords.entries()) {
3424
3702
  if (recordIdsToDiscard.has(recordId)) continue;
3425
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3703
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3426
3704
  if (uniqueKey === null) continue;
3427
3705
  if (!recordsByUniqueKey.has(uniqueKey)) {
3428
3706
  recordsByUniqueKey.set(uniqueKey, []);
@@ -3468,7 +3746,7 @@ var init_BaseModel = __esm({
3468
3746
  if (!recordData || !key) continue;
3469
3747
  const itemData = extractItemData(key, recordData);
3470
3748
  if (!itemData) continue;
3471
- if (change.action === "add" && recordData instanceof Y2.Map) {
3749
+ if (change.action === "add" && recordData instanceof Y5.Map) {
3472
3750
  Logger.verbose(
3473
3751
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
3474
3752
  );
@@ -3566,9 +3844,12 @@ var init_BaseModel = __esm({
3566
3844
  const recordData = documentYMap.get(idToDiscard);
3567
3845
  if (!recordData) continue;
3568
3846
  for (const constraint of schema.resolvedUniqueConstraints) {
3569
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3847
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3570
3848
  if (uniqueKey === null) continue;
3571
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3849
+ const constraintMapName = uniqueIndexMapName(
3850
+ modelName,
3851
+ constraint.name
3852
+ );
3572
3853
  const constraintMap = yDoc.getMap(constraintMapName);
3573
3854
  const currentIndexValue = constraintMap.get(uniqueKey);
3574
3855
  if (currentIndexValue === idToDiscard) {
@@ -3586,11 +3867,14 @@ var init_BaseModel = __esm({
3586
3867
  }, `conflict-resolution-${modelName}-${docId}`);
3587
3868
  yDoc.transact(() => {
3588
3869
  for (const constraint of schema.resolvedUniqueConstraints) {
3589
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
3870
+ const constraintMapName = uniqueIndexMapName(
3871
+ modelName,
3872
+ constraint.name
3873
+ );
3590
3874
  const constraintMap = yDoc.getMap(constraintMapName);
3591
3875
  for (const [recordId, recordData] of documentYMap.entries()) {
3592
3876
  if (!recordData) continue;
3593
- const uniqueKey = buildUniqueKey(recordData, constraint.fields);
3877
+ const uniqueKey = buildUniqueKey2(recordData, constraint.fields);
3594
3878
  if (uniqueKey === null) continue;
3595
3879
  const currentIndexValue = constraintMap.get(uniqueKey);
3596
3880
  if (currentIndexValue !== recordId) {
@@ -3613,7 +3897,7 @@ var init_BaseModel = __esm({
3613
3897
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
3614
3898
  );
3615
3899
  for (const [recordId, recordData] of documentYMap.entries()) {
3616
- if (recordData instanceof Y2.Map) {
3900
+ if (recordData instanceof Y5.Map) {
3617
3901
  Logger.verbose(
3618
3902
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
3619
3903
  );
@@ -3798,6 +4082,11 @@ var init_BaseModel = __esm({
3798
4082
  Logger.debug(
3799
4083
  `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
3800
4084
  );
4085
+ if (isYjsComposite(yjsValue)) {
4086
+ Logger.warn(
4087
+ `[${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).`
4088
+ );
4089
+ }
3801
4090
  if (yjsValue === void 0) {
3802
4091
  Logger.debug(
3803
4092
  `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
@@ -3834,26 +4123,7 @@ var init_BaseModel = __esm({
3834
4123
  * if the field hasn't been touched since the wire-format change in #561.
3835
4124
  */
3836
4125
  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
- }
4126
+ applyStringSetChange(recordYMap, fieldName, change);
3857
4127
  }
3858
4128
  /**
3859
4129
  * Deep equality check for comparing field values
@@ -4023,6 +4293,29 @@ var init_BaseModel = __esm({
4023
4293
  );
4024
4294
  throw new Error("Cannot save item without an id. Ensure id is set.");
4025
4295
  }
4296
+ const preTransactCreateStamps = /* @__PURE__ */ new Set();
4297
+ {
4298
+ const existingRecord = targetYMap.get(this.id);
4299
+ const isNew = !existingRecord;
4300
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
4301
+ const stamp = fieldOptions.autoStamp;
4302
+ if (!stamp) continue;
4303
+ const callerExplicit = this.hasLocalChange(fieldKey);
4304
+ if (callerExplicit) {
4305
+ const localValue = this._localChanges[fieldKey];
4306
+ if (localValue !== null && localValue !== void 0) continue;
4307
+ }
4308
+ if (stamp === "create") {
4309
+ if (!isNew) continue;
4310
+ const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
4311
+ if (persisted !== null && persisted !== void 0) continue;
4312
+ }
4313
+ this[fieldKey] = Date.now();
4314
+ if (stamp === "create") {
4315
+ preTransactCreateStamps.add(fieldKey);
4316
+ }
4317
+ }
4318
+ }
4026
4319
  this.validateBeforeSave();
4027
4320
  Logger.debug(`[${modelName}.save] About to get current JS state`);
4028
4321
  Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
@@ -4032,6 +4325,34 @@ var init_BaseModel = __esm({
4032
4325
  `[${modelName}.save] getCurrentJSState() returned:`,
4033
4326
  dataToSave
4034
4327
  );
4328
+ const uniqueMissConflicts = /* @__PURE__ */ new Map();
4329
+ for (const constraint of schema.resolvedUniqueConstraints) {
4330
+ const newUniqueKey = this._buildUniqueKey(
4331
+ constraint.fields,
4332
+ dataToSave,
4333
+ modelName,
4334
+ constraint.name
4335
+ );
4336
+ if (newUniqueKey === null) {
4337
+ continue;
4338
+ }
4339
+ const constraintMap = yDoc.getMap(
4340
+ uniqueIndexMapName(modelName, constraint.name)
4341
+ );
4342
+ if (!constraintMap.get(newUniqueKey)) {
4343
+ const constraintKeyValues = constraint.fields.map(
4344
+ (field) => dataToSave[field]
4345
+ );
4346
+ const conflictingRecord = await modelConstructor._resolveByUniqueFallback(
4347
+ constraint,
4348
+ constraintKeyValues,
4349
+ { excludeId: this.id }
4350
+ );
4351
+ if (conflictingRecord) {
4352
+ uniqueMissConflicts.set(constraint.name, conflictingRecord.id);
4353
+ }
4354
+ }
4355
+ }
4035
4356
  await yDoc.transact(async () => {
4036
4357
  if (upsertOnConstraint && upsertOnKey) {
4037
4358
  const constraintMapName = `_uniqueIdx_${modelName}_${upsertOnConstraint.name}`;
@@ -4053,6 +4374,14 @@ var init_BaseModel = __esm({
4053
4374
  }
4054
4375
  }
4055
4376
  }
4377
+ for (const fieldKey of preTransactCreateStamps) {
4378
+ if (this._localChanges && fieldKey in this._localChanges) {
4379
+ delete this._localChanges[fieldKey];
4380
+ }
4381
+ if (fieldKey in dataToSave) {
4382
+ delete dataToSave[fieldKey];
4383
+ }
4384
+ }
4056
4385
  }
4057
4386
  }
4058
4387
  if (!targetYMap) {
@@ -4065,7 +4394,7 @@ var init_BaseModel = __esm({
4065
4394
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
4066
4395
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
4067
4396
  if (!recordYMap) {
4068
- recordYMap = new Y2.Map();
4397
+ recordYMap = new Y5.Map();
4069
4398
  Logger.verbose(
4070
4399
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
4071
4400
  );
@@ -4117,7 +4446,10 @@ var init_BaseModel = __esm({
4117
4446
  );
4118
4447
  continue;
4119
4448
  }
4120
- const constraintMapName = `_uniqueIdx_${modelName}_${constraint.name}`;
4449
+ const constraintMapName = uniqueIndexMapName(
4450
+ modelName,
4451
+ constraint.name
4452
+ );
4121
4453
  const constraintMap = yDoc.getMap(constraintMapName);
4122
4454
  Logger.verbose(
4123
4455
  `[${modelName}] Save Check (Item '${this.id}'): GET from '${constraintMapName}' with Key '${newUniqueKey.substring(
@@ -4146,6 +4478,26 @@ var init_BaseModel = __esm({
4146
4478
  this.id,
4147
4479
  existingRecordIdWithNewKey
4148
4480
  );
4481
+ } else if (!existingRecordIdWithNewKey) {
4482
+ const conflictingRecordId = uniqueMissConflicts.get(constraint.name);
4483
+ if (conflictingRecordId && conflictingRecordId !== this.id) {
4484
+ Logger.verbose(
4485
+ `[${modelName}] Save Check (Item '${this.id}'): index miss for '${constraint.name}', but engine fallback found conflicting index-less record ${conflictingRecordId}.`
4486
+ );
4487
+ throw new UniqueConstraintViolationError(
4488
+ `Unique constraint '${constraint.name}' violated for model '${modelName}' on fields [${constraint.fields.join(
4489
+ ", "
4490
+ )}]. Attempted ID: ${this.id}. Value(s) starting with '${newUniqueKey.substring(
4491
+ 0,
4492
+ 50
4493
+ )}...' already exist for index-less record ID ${conflictingRecordId}.`,
4494
+ modelName,
4495
+ constraint.name,
4496
+ constraint.fields,
4497
+ this.id,
4498
+ conflictingRecordId
4499
+ );
4500
+ }
4149
4501
  }
4150
4502
  }
4151
4503
  if (isUpdate && oldData) {
@@ -4489,34 +4841,46 @@ var init_BaseModel = __esm({
4489
4841
  const modelName = schema.options.name;
4490
4842
  Logger.verbose(`[${modelName}] Executing query:`, { filter, options });
4491
4843
  const hasIncludes = options?.include && options.include.length > 0;
4492
- const translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4844
+ const requestedLimit = options?.limit;
4845
+ const overFetch = typeof requestedLimit === "number" && requestedLimit > 0;
4846
+ let translatorOptions = hasIncludes && options?.projection ? { ...options, projection: void 0 } : options;
4847
+ if (overFetch) {
4848
+ translatorOptions = {
4849
+ ...translatorOptions || {},
4850
+ limit: requestedLimit + 1
4851
+ };
4852
+ }
4493
4853
  const translator = new DocumentQueryTranslator(modelName, schema.fields);
4494
4854
  const translatedQuery = translator.translateFind(filter, translatorOptions);
4495
4855
  Logger.verbose(`[${modelName}] Translated SQL:`, translatedQuery.sql);
4496
4856
  Logger.verbose(`[${modelName}] SQL params:`, translatedQuery.params);
4497
- const results = await _BaseModel.dbInstance.query(
4857
+ let results = await _BaseModel.dbInstance.query(
4498
4858
  translatedQuery.sql,
4499
4859
  translatedQuery.params
4500
4860
  );
4501
4861
  Logger.verbose(`[${modelName}] Query returned ${results.length} results`);
4862
+ let hasMore = false;
4863
+ if (overFetch && results.length > requestedLimit) {
4864
+ hasMore = true;
4865
+ results = results.slice(0, requestedLimit);
4866
+ }
4502
4867
  if (options?.include && options.include.length > 0) {
4503
4868
  const { IncludeResolver: IncludeResolver2 } = await Promise.resolve().then(() => (init_IncludeResolver(), IncludeResolver_exports));
4504
4869
  const resolver = new IncludeResolver2(_BaseModel.dbInstance);
4505
4870
  await resolver.resolve(results, options.include, 0, modelName);
4506
4871
  }
4507
- const requestedLimit = options?.limit;
4508
- const hasMore = CursorManager.hasMoreResults(
4509
- requestedLimit,
4510
- results.length
4511
- );
4512
4872
  const isFirstPage = !options?.uniqueStartKey;
4873
+ const direction = options?.direction || 1;
4513
4874
  const cursors = CursorManager.generateResultCursors(
4514
4875
  results,
4515
4876
  translatedQuery.sortFields,
4516
- options?.direction || 1,
4877
+ direction,
4517
4878
  hasMore,
4518
4879
  isFirstPage
4519
4880
  );
4881
+ if (direction === -1) {
4882
+ results.reverse();
4883
+ }
4520
4884
  let transformedData;
4521
4885
  if (options?.projection && Object.keys(options.projection).length > 0) {
4522
4886
  transformedData = results.map((row) => {
@@ -5092,7 +5456,7 @@ var init_BaseModel = __esm({
5092
5456
  );
5093
5457
  }
5094
5458
  for (const [recordId, recordYMap] of documentYMap.entries()) {
5095
- if (recordYMap instanceof Y2.Map) {
5459
+ if (recordYMap instanceof Y5.Map) {
5096
5460
  const instance = new this({ id: recordId });
5097
5461
  instance._metaDocId = docId;
5098
5462
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -5117,6 +5481,50 @@ var init_BaseModel = __esm({
5117
5481
  }
5118
5482
  return allInstances;
5119
5483
  }
5484
+ /**
5485
+ * Resolve a record by its unique-constraint field values via an engine query.
5486
+ * Used as a fallback when the `_uniqueIdx_<model>_<constraint>` map has no
5487
+ * entry for a key.
5488
+ *
5489
+ * The index map is a cache over the SQLite projection, not the source of
5490
+ * truth: every record is loaded into the projection on `initializeForDocument`
5491
+ * regardless of index presence, but a record written by a client predating
5492
+ * index maintenance (legacy data) is missing from the index. Such a record
5493
+ * silently escapes both `upsertByUnique` lookups and the `save()` uniqueness
5494
+ * check. A query over the constraint fields finds it reliably.
5495
+ *
5496
+ * Contract: RESOLVE ONLY. Returns the owning record (with `_metaDocId` set to
5497
+ * the document that holds it, via `queryOne`) or `null`. Callers decide
5498
+ * whether to heal the missing index entry (write paths) and do their own
5499
+ * logging — this method never mutates the index, so read paths stay
5500
+ * read-only. Mirrors `buildUniqueKey`: a null/undefined value in any
5501
+ * constraint field means the record does not participate in the constraint,
5502
+ * so no query runs.
5503
+ */
5504
+ static async _resolveByUniqueFallback(constraint, keyValues, options) {
5505
+ if (!_BaseModel.dbInstance) {
5506
+ return null;
5507
+ }
5508
+ const filter = {};
5509
+ for (let i = 0; i < constraint.fields.length; i++) {
5510
+ const value = keyValues[i];
5511
+ if (value === null || value === void 0) {
5512
+ return null;
5513
+ }
5514
+ filter[constraint.fields[i]] = value;
5515
+ }
5516
+ if (options?.excludeId) {
5517
+ if ("id" in filter) {
5518
+ if (filter.id === options.excludeId) {
5519
+ return null;
5520
+ }
5521
+ } else {
5522
+ filter.id = { $ne: options.excludeId };
5523
+ }
5524
+ }
5525
+ const found = await this.queryOne(filter);
5526
+ return found ?? null;
5527
+ }
5120
5528
  static async findByUnique(constraintName, value) {
5121
5529
  const modelConstructor = this;
5122
5530
  const schema = modelConstructor.getSchema();
@@ -5155,7 +5563,10 @@ var init_BaseModel = __esm({
5155
5563
  const documentInfo = modelConstructor.connectedDocuments.get(docId);
5156
5564
  if (!documentInfo) continue;
5157
5565
  const yDoc = documentInfo.yDoc;
5158
- const constraintMapName2 = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5566
+ const constraintMapName2 = uniqueIndexMapName(
5567
+ currentModelName,
5568
+ constraint.name
5569
+ );
5159
5570
  const constraintMap2 = yDoc.getMap(constraintMapName2);
5160
5571
  const recordId2 = constraintMap2.get(uniqueKeyString);
5161
5572
  if (recordId2) {
@@ -5163,7 +5574,7 @@ var init_BaseModel = __esm({
5163
5574
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
5164
5575
  if (documentYMap) {
5165
5576
  const recordYMap2 = documentYMap.get(recordId2);
5166
- if (recordYMap2 && recordYMap2 instanceof Y2.Map) {
5577
+ if (recordYMap2 && recordYMap2 instanceof Y5.Map) {
5167
5578
  const instance = new modelConstructor({
5168
5579
  id: recordId2
5169
5580
  });
@@ -5177,6 +5588,16 @@ var init_BaseModel = __esm({
5177
5588
  }
5178
5589
  }
5179
5590
  }
5591
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5592
+ constraint,
5593
+ keyValues
5594
+ );
5595
+ if (fallbackRecord) {
5596
+ Logger.verbose(
5597
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5598
+ );
5599
+ return fallbackRecord;
5600
+ }
5180
5601
  Logger.verbose(
5181
5602
  `[${currentModelName}.findByUnique] Record not found in any connected document`
5182
5603
  );
@@ -5190,17 +5611,30 @@ var init_BaseModel = __esm({
5190
5611
  `[${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
5612
  );
5192
5613
  }
5193
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5614
+ const constraintMapName = uniqueIndexMapName(
5615
+ currentModelName,
5616
+ constraint.name
5617
+ );
5194
5618
  const constraintMap = legacyDocInfo.yDoc.getMap(constraintMapName);
5195
5619
  const recordId = constraintMap.get(uniqueKeyString);
5196
5620
  if (!recordId) {
5621
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5622
+ constraint,
5623
+ keyValues
5624
+ );
5625
+ if (fallbackRecord) {
5626
+ Logger.verbose(
5627
+ `[${currentModelName}.findByUnique] Index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id} (legacy index-less record; index left unchanged on read path).`
5628
+ );
5629
+ return fallbackRecord;
5630
+ }
5197
5631
  return null;
5198
5632
  }
5199
5633
  const recordYMap = legacyDocumentMap.get(recordId);
5200
5634
  if (!recordYMap) {
5201
5635
  return null;
5202
5636
  }
5203
- if (recordYMap instanceof Y2.Map) {
5637
+ if (recordYMap instanceof Y5.Map) {
5204
5638
  const instance = new modelConstructor({
5205
5639
  id: recordId
5206
5640
  });
@@ -5258,7 +5692,10 @@ var init_BaseModel = __esm({
5258
5692
  `[${currentModelName}] upsertByUnique for '${constraintName}': unique key cannot be built due to null/undefined values. Upsert cannot proceed reliably.`
5259
5693
  );
5260
5694
  }
5261
- const constraintMapName = `_uniqueIdx_${currentModelName}_${constraint.name}`;
5695
+ const constraintMapName = uniqueIndexMapName(
5696
+ currentModelName,
5697
+ constraint.name
5698
+ );
5262
5699
  let existingRecord = null;
5263
5700
  if (connectedDocuments && connectedDocuments.size > 0) {
5264
5701
  for (const docId of connectedDocuments.keys()) {
@@ -5276,6 +5713,33 @@ var init_BaseModel = __esm({
5276
5713
  }
5277
5714
  } else {
5278
5715
  }
5716
+ if (!existingRecord) {
5717
+ const fallbackRecord = await modelConstructor._resolveByUniqueFallback(
5718
+ constraint,
5719
+ keyValuesForLookup
5720
+ );
5721
+ if (fallbackRecord) {
5722
+ existingRecord = fallbackRecord;
5723
+ const healDocId = fallbackRecord._metaDocId;
5724
+ const healDocInfo = healDocId ? connectedDocuments.get(healDocId) : void 0;
5725
+ const canHeal = !!healDocInfo && healDocInfo.permissionHint !== "read";
5726
+ if (healDocInfo && canHeal) {
5727
+ healDocInfo.yDoc.transact(() => {
5728
+ const healMap = healDocInfo.yDoc.getMap(constraintMapName);
5729
+ if (healMap.get(uniqueKeyString) !== fallbackRecord.id) {
5730
+ healMap.set(uniqueKeyString, fallbackRecord.id);
5731
+ }
5732
+ }, `heal-unique-index-${currentModelName}-${healDocId}`);
5733
+ Logger.verbose(
5734
+ `[${currentModelName}] upsertByUnique: index miss for '${constraintName}' resolved via engine fallback to record ${fallbackRecord.id}; healed missing index entry (legacy index-less record).`
5735
+ );
5736
+ } else {
5737
+ Logger.verbose(
5738
+ `[${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).`
5739
+ );
5740
+ }
5741
+ }
5742
+ }
5279
5743
  if (options?.objectMustExist) {
5280
5744
  if (!existingRecord) {
5281
5745
  throw new RecordNotFoundError(
@@ -5432,7 +5896,7 @@ var init_BaseModel = __esm({
5432
5896
  for (const [fieldKey, fieldOptions] of schema.fields) {
5433
5897
  if (fieldOptions?.type !== "stringset") continue;
5434
5898
  const value = recordYMap.get(fieldKey);
5435
- if (value instanceof Y2.Map) {
5899
+ if (value instanceof Y5.Map) {
5436
5900
  this.observeStringSetMapOnce(value);
5437
5901
  }
5438
5902
  }
@@ -5479,7 +5943,14 @@ var init_BaseModel = __esm({
5479
5943
  continue;
5480
5944
  }
5481
5945
  if (value !== void 0) {
5482
- updatedData[key] = value;
5946
+ if (isYjsComposite(value)) {
5947
+ Logger.warn(
5948
+ `[${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.`
5949
+ );
5950
+ updatedData[key] = normalizeYjsValueForStorage(value);
5951
+ } else {
5952
+ updatedData[key] = value;
5953
+ }
5483
5954
  }
5484
5955
  }
5485
5956
  if (unknownFields.length > 0) {
@@ -5559,7 +6030,14 @@ var init_BaseModel = __esm({
5559
6030
  continue;
5560
6031
  }
5561
6032
  if (value !== void 0) {
5562
- updatedData[key] = value;
6033
+ if (isYjsComposite(value)) {
6034
+ Logger.warn(
6035
+ `[${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.`
6036
+ );
6037
+ updatedData[key] = normalizeYjsValueForStorage(value);
6038
+ } else {
6039
+ updatedData[key] = value;
6040
+ }
5563
6041
  }
5564
6042
  }
5565
6043
  if (unknownFields.length > 0) {
@@ -5603,8 +6081,160 @@ var init_BaseModel = __esm({
5603
6081
  }
5604
6082
  });
5605
6083
 
6084
+ // src/utils/environment.ts
6085
+ var environment_exports = {};
6086
+ __export(environment_exports, {
6087
+ constants: () => constants,
6088
+ detectEnvironment: () => detectEnvironment,
6089
+ features: () => features,
6090
+ isBrowser: () => isBrowser,
6091
+ isNode: () => isNode,
6092
+ isNodeModuleAvailable: () => isNodeModuleAvailable
6093
+ });
6094
+ function detectEnvironment() {
6095
+ if (typeof process !== "undefined" && process.versions && process.versions.node) {
6096
+ return "node";
6097
+ }
6098
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
6099
+ return "browser";
6100
+ }
6101
+ if (typeof self !== "undefined" && typeof self.importScripts === "function") {
6102
+ return "browser";
6103
+ }
6104
+ return "unknown";
6105
+ }
6106
+ function isNode() {
6107
+ return detectEnvironment() === "node";
6108
+ }
6109
+ function isBrowser() {
6110
+ return detectEnvironment() === "browser";
6111
+ }
6112
+ async function isNodeModuleAvailable(moduleName) {
6113
+ if (!isNode()) {
6114
+ return false;
6115
+ }
6116
+ try {
6117
+ await import(
6118
+ /* @vite-ignore */
6119
+ moduleName
6120
+ );
6121
+ return true;
6122
+ } catch {
6123
+ return false;
6124
+ }
6125
+ }
6126
+ var features, constants;
6127
+ var init_environment = __esm({
6128
+ "src/utils/environment.ts"() {
6129
+ "use strict";
6130
+ features = {
6131
+ get hasWebWorkers() {
6132
+ return isBrowser() && typeof Worker !== "undefined";
6133
+ },
6134
+ get hasFileSystem() {
6135
+ return isNode();
6136
+ },
6137
+ get hasWebAssembly() {
6138
+ return typeof WebAssembly !== "undefined";
6139
+ },
6140
+ async hasBetterSqlite3() {
6141
+ return await isNodeModuleAvailable("better-sqlite3");
6142
+ }
6143
+ };
6144
+ constants = {
6145
+ DEFAULT_NODE_SQLITE_PATH: ":memory:",
6146
+ DEFAULT_BROWSER_WASM_PATH: "/sql-wasm.wasm"
6147
+ };
6148
+ }
6149
+ });
6150
+
5606
6151
  // src/engines/SqljsEngine.ts
5607
- var import_sql4, import_async_mutex, SQL_WASM_URL, TransactionalSQLJSOperations, SqljsEngine;
6152
+ async function resolveNodeWasmPath() {
6153
+ if (!isNode()) {
6154
+ return null;
6155
+ }
6156
+ try {
6157
+ const cjsRequire = typeof require !== "undefined" ? require : void 0;
6158
+ if (cjsRequire) {
6159
+ return cjsRequire.resolve("sql.js/dist/sql-wasm.wasm");
6160
+ }
6161
+ } catch {
6162
+ }
6163
+ try {
6164
+ const metaUrl = import_meta.url;
6165
+ if (metaUrl) {
6166
+ const { createRequire } = await import(
6167
+ /* @vite-ignore */
6168
+ "module"
6169
+ );
6170
+ const req = createRequire(metaUrl);
6171
+ return req.resolve("sql.js/dist/sql-wasm.wasm");
6172
+ }
6173
+ } catch {
6174
+ }
6175
+ return null;
6176
+ }
6177
+ async function preflightNodeWasm(wasmPath) {
6178
+ if (!isNode()) {
6179
+ return;
6180
+ }
6181
+ if (/^[a-z]+:\/\//i.test(wasmPath)) {
6182
+ return;
6183
+ }
6184
+ const { existsSync } = await import(
6185
+ /* @vite-ignore */
6186
+ "fs"
6187
+ );
6188
+ if (!existsSync(wasmPath)) {
6189
+ throw new Error(
6190
+ `[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()).`
6191
+ );
6192
+ }
6193
+ }
6194
+ async function loadSqlJsModule(locateFile) {
6195
+ let abortReject;
6196
+ const toError = (reason) => reason instanceof Error ? reason : new Error(
6197
+ `[SqljsEngine] sql.js aborted during initialization: ${String(
6198
+ reason
6199
+ )}`
6200
+ );
6201
+ const nodeProcess = isNode() && typeof process !== "undefined" ? process : void 0;
6202
+ const onUnhandled = (reason) => {
6203
+ const err = toError(reason);
6204
+ if (/Aborted/.test(err.message) || /Aborted/.test(String(reason))) {
6205
+ abortReject?.(err);
6206
+ return;
6207
+ }
6208
+ if (nodeProcess && nodeProcess.listenerCount("unhandledRejection") === 1) {
6209
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6210
+ void Promise.reject(reason);
6211
+ }
6212
+ };
6213
+ if (nodeProcess) {
6214
+ nodeProcess.on("unhandledRejection", onUnhandled);
6215
+ }
6216
+ const abortGuard = new Promise((_resolve, reject) => {
6217
+ abortReject = reject;
6218
+ });
6219
+ abortGuard.catch(() => {
6220
+ });
6221
+ try {
6222
+ const initPromise = (0, import_sql4.default)({
6223
+ locateFile,
6224
+ // Layer 1: emscripten onAbort.
6225
+ onAbort: (reason) => abortReject?.(toError(reason))
6226
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6227
+ });
6228
+ return await Promise.race([initPromise, abortGuard]);
6229
+ } finally {
6230
+ if (nodeProcess) {
6231
+ setTimeout(() => {
6232
+ nodeProcess.removeListener("unhandledRejection", onUnhandled);
6233
+ }, 0);
6234
+ }
6235
+ }
6236
+ }
6237
+ var import_sql4, import_async_mutex, import_meta, SQL_WASM_URL, SQL_WASM_FILENAME, TransactionalSQLJSOperations, SqljsEngine;
5608
6238
  var init_SqljsEngine = __esm({
5609
6239
  "src/engines/SqljsEngine.ts"() {
5610
6240
  "use strict";
@@ -5612,7 +6242,10 @@ var init_SqljsEngine = __esm({
5612
6242
  init_BaseModel();
5613
6243
  import_async_mutex = require("async-mutex");
5614
6244
  init_sql();
6245
+ init_environment();
6246
+ import_meta = {};
5615
6247
  SQL_WASM_URL = "/sql-wasm.wasm";
6248
+ SQL_WASM_FILENAME = "sql-wasm.wasm";
5616
6249
  TransactionalSQLJSOperations = class {
5617
6250
  db;
5618
6251
  engine;
@@ -5678,22 +6311,37 @@ var init_SqljsEngine = __esm({
5678
6311
  }
5679
6312
  async initialize(engineOptions) {
5680
6313
  Logger.info("[SqljsEngine] Initializing SQL.js engine...");
6314
+ const loadingModuleThisCall = !_SqljsEngine.SQL;
5681
6315
  try {
5682
6316
  if (!_SqljsEngine.SQL) {
5683
- const locateFileConfig = engineOptions?.locateFile || ((_file) => engineOptions?.wasmURL || SQL_WASM_URL);
6317
+ let locateFileConfig;
6318
+ if (engineOptions?.locateFile) {
6319
+ locateFileConfig = engineOptions.locateFile;
6320
+ } else if (engineOptions?.wasmURL) {
6321
+ const url = engineOptions.wasmURL;
6322
+ locateFileConfig = (_file) => url;
6323
+ } else {
6324
+ const nodeWasmPath = await resolveNodeWasmPath();
6325
+ const resolved = nodeWasmPath ?? SQL_WASM_URL;
6326
+ locateFileConfig = (_file) => resolved;
6327
+ }
6328
+ const wasmPath = locateFileConfig(SQL_WASM_FILENAME);
5684
6329
  Logger.debug(
5685
6330
  "[SqljsEngine] Attempting to load SQL.js WASM from:",
5686
- locateFileConfig("sql-wasm.wasm")
6331
+ wasmPath
5687
6332
  );
5688
- _SqljsEngine.SQL = await (0, import_sql4.default)({
5689
- locateFile: locateFileConfig
5690
- });
6333
+ await preflightNodeWasm(wasmPath);
6334
+ const SQL = await loadSqlJsModule(locateFileConfig);
6335
+ _SqljsEngine.SQL = SQL;
5691
6336
  Logger.info("[SqljsEngine] SQL.js WASM loaded successfully.");
5692
6337
  }
5693
6338
  this.db = new _SqljsEngine.SQL.Database();
5694
6339
  Logger.info("[SqljsEngine] SQL.js Database initialized in memory.");
5695
6340
  this.updateTableNames();
5696
6341
  } catch (error) {
6342
+ if (loadingModuleThisCall) {
6343
+ _SqljsEngine.SQL = null;
6344
+ }
5697
6345
  Logger.error("[SqljsEngine] Error during SQL.js initialization:", error);
5698
6346
  throw error;
5699
6347
  }
@@ -5981,73 +6629,6 @@ var init_SqljsEngine = __esm({
5981
6629
  }
5982
6630
  });
5983
6631
 
5984
- // src/utils/environment.ts
5985
- var environment_exports = {};
5986
- __export(environment_exports, {
5987
- constants: () => constants,
5988
- detectEnvironment: () => detectEnvironment,
5989
- features: () => features,
5990
- isBrowser: () => isBrowser,
5991
- isNode: () => isNode,
5992
- isNodeModuleAvailable: () => isNodeModuleAvailable
5993
- });
5994
- function detectEnvironment() {
5995
- if (typeof process !== "undefined" && process.versions && process.versions.node) {
5996
- return "node";
5997
- }
5998
- if (typeof window !== "undefined" && typeof document !== "undefined") {
5999
- return "browser";
6000
- }
6001
- if (typeof self !== "undefined" && typeof self.importScripts === "function") {
6002
- return "browser";
6003
- }
6004
- return "unknown";
6005
- }
6006
- function isNode() {
6007
- return detectEnvironment() === "node";
6008
- }
6009
- function isBrowser() {
6010
- return detectEnvironment() === "browser";
6011
- }
6012
- async function isNodeModuleAvailable(moduleName) {
6013
- if (!isNode()) {
6014
- return false;
6015
- }
6016
- try {
6017
- await import(
6018
- /* @vite-ignore */
6019
- moduleName
6020
- );
6021
- return true;
6022
- } catch {
6023
- return false;
6024
- }
6025
- }
6026
- var features, constants;
6027
- var init_environment = __esm({
6028
- "src/utils/environment.ts"() {
6029
- "use strict";
6030
- features = {
6031
- get hasWebWorkers() {
6032
- return isBrowser() && typeof Worker !== "undefined";
6033
- },
6034
- get hasFileSystem() {
6035
- return isNode();
6036
- },
6037
- get hasWebAssembly() {
6038
- return typeof WebAssembly !== "undefined";
6039
- },
6040
- async hasBetterSqlite3() {
6041
- return await isNodeModuleAvailable("better-sqlite3");
6042
- }
6043
- };
6044
- constants = {
6045
- DEFAULT_NODE_SQLITE_PATH: ":memory:",
6046
- DEFAULT_BROWSER_WASM_PATH: "/sql-wasm.wasm"
6047
- };
6048
- }
6049
- });
6050
-
6051
6632
  // src/engines/node/NodeSqliteEngine.ts
6052
6633
  var NodeSqliteEngine_exports = {};
6053
6634
  __export(NodeSqliteEngine_exports, {
@@ -6739,33 +7320,47 @@ __export(node_exports, {
6739
7320
  SqljsEngine: () => SqljsEngine,
6740
7321
  StringSet: () => StringSet,
6741
7322
  UniqueConstraintViolationError: () => UniqueConstraintViolationError,
7323
+ applyAutoStampsFromSchema: () => applyAutoStampsFromSchema,
7324
+ applyDefaultsFromSchema: () => applyDefaultsFromSchema,
7325
+ applyStringSetChange: () => applyStringSetChange,
7326
+ applyStringSetDelta: () => applyStringSetDelta,
7327
+ assertNever: () => assertNever,
6742
7328
  attachAndRegisterModel: () => attachAndRegisterModel,
6743
7329
  attachSchemaToClass: () => attachSchemaToClass,
6744
7330
  autoRegisterModel: () => autoRegisterModel,
6745
7331
  clearMetaSyncCache: () => clearMetaSyncCache,
6746
7332
  constants: () => constants,
6747
7333
  createModelClass: () => createModelClass,
7334
+ decodeFunctionDefault: () => decodeFunctionDefault,
6748
7335
  defineModelSchema: () => defineModelSchema,
6749
7336
  detectEnvironment: () => detectEnvironment,
6750
7337
  discoverModelNames: () => discoverModelNames,
6751
7338
  discoverSchema: () => discoverSchema,
6752
7339
  dumpYDocToPlain: () => dumpYDocToPlain,
7340
+ ensureStringSetYMap: () => ensureStringSetYMap,
6753
7341
  features: () => features,
7342
+ fieldWriteKind: () => fieldWriteKind,
6754
7343
  generateULID: () => generateULID,
6755
7344
  getRecommendedNodeEngine: () => getRecommendedNodeEngine,
6756
7345
  inferFieldType: () => inferFieldType,
6757
7346
  initJsBao: () => initJsBao,
6758
7347
  isBrowser: () => isBrowser,
7348
+ isKnownFieldType: () => isKnownFieldType,
6759
7349
  isNode: () => isNode,
6760
7350
  isNodeModuleAvailable: () => isNodeModuleAvailable,
6761
7351
  loadSchemaFromToml: () => loadSchemaFromToml,
6762
7352
  loadSchemaFromTomlString: () => loadSchemaFromTomlString,
7353
+ parseStringSetOp: () => parseStringSetOp,
6763
7354
  registerFunctionDefault: () => registerFunctionDefault,
7355
+ registerFunctionDefaultGenerator: () => registerFunctionDefaultGenerator,
7356
+ replaceStringSet: () => replaceStringSet,
6764
7357
  resetJsBao: () => resetJsBao,
7358
+ resolveDefaultValue: () => resolveDefaultValue,
6765
7359
  schemaToToml: () => schemaToToml,
6766
7360
  summarizePlainYDoc: () => summarizePlainYDoc,
6767
7361
  syncInferredMeta: () => syncInferredMeta,
6768
- syncModelMeta: () => syncModelMeta
7362
+ syncModelMeta: () => syncModelMeta,
7363
+ writeField: () => writeField
6769
7364
  });
6770
7365
  module.exports = __toCommonJS(node_exports);
6771
7366
  init_BaseModel();
@@ -7032,6 +7627,7 @@ init_StringSet();
7032
7627
  // src/models/schema.ts
7033
7628
  init_BaseModel();
7034
7629
  init_ModelRegistry();
7630
+ init_uniqueIndex();
7035
7631
  function defineModelSchema(input) {
7036
7632
  const { name, fields } = input;
7037
7633
  const options = {
@@ -7104,7 +7700,7 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
7104
7700
  for (const [fieldName, options] of fields.entries()) {
7105
7701
  if (options.unique) {
7106
7702
  resolved.push({
7107
- name: `${modelName}_${fieldName}_unique`,
7703
+ name: fieldUniqueConstraintName(modelName, fieldName),
7108
7704
  fields: [fieldName]
7109
7705
  });
7110
7706
  }
@@ -7168,7 +7764,8 @@ function attachAndRegisterModel(modelClass, schema) {
7168
7764
  }
7169
7765
 
7170
7766
  // src/utils/yDocSchema.ts
7171
- var Y3 = __toESM(require("yjs"), 1);
7767
+ var Y6 = __toESM(require("yjs"), 1);
7768
+ init_yjsValues();
7172
7769
  function discoverSchema(yDoc) {
7173
7770
  const models = {};
7174
7771
  const metaNames = /* @__PURE__ */ new Set();
@@ -7202,7 +7799,7 @@ function discoverModelNames(yDoc) {
7202
7799
  function materializeMap(yDoc, key) {
7203
7800
  try {
7204
7801
  const map = yDoc.getMap(key);
7205
- return map instanceof Y3.Map ? map : null;
7802
+ return map instanceof Y6.Map ? map : null;
7206
7803
  } catch {
7207
7804
  return null;
7208
7805
  }
@@ -7212,11 +7809,11 @@ function readModelMeta(metaMap) {
7212
7809
  let constraints;
7213
7810
  let relationships;
7214
7811
  for (const [key, value] of metaMap.entries()) {
7215
- if (key === "_constraints" && value instanceof Y3.Map) {
7812
+ if (key === "_constraints" && value instanceof Y6.Map) {
7216
7813
  constraints = readConstraints(value);
7217
- } else if (key === "_relationships" && value instanceof Y3.Map) {
7814
+ } else if (key === "_relationships" && value instanceof Y6.Map) {
7218
7815
  relationships = readRelationships(value);
7219
- } else if (value instanceof Y3.Map) {
7816
+ } else if (value instanceof Y6.Map) {
7220
7817
  fields[key] = readFieldMeta(value);
7221
7818
  }
7222
7819
  }
@@ -7235,6 +7832,10 @@ function readFieldMeta(fieldMap) {
7235
7832
  if (fieldMap.get("unique") === true) field.unique = true;
7236
7833
  if (fieldMap.get("required") === true) field.required = true;
7237
7834
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7835
+ const autoStamp = fieldMap.get("autoStamp");
7836
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
7837
+ field.autoStamp = autoStamp;
7838
+ }
7238
7839
  const def = fieldMap.get("default");
7239
7840
  if (def !== void 0) field.default = def;
7240
7841
  const maxLength = fieldMap.get("maxLength");
@@ -7247,7 +7848,7 @@ function inferModelFromData(dataMap) {
7247
7848
  const fields = {};
7248
7849
  let sampled = 0;
7249
7850
  for (const [_recordId, recordValue] of dataMap.entries()) {
7250
- if (!(recordValue instanceof Y3.Map)) continue;
7851
+ if (!(recordValue instanceof Y6.Map)) continue;
7251
7852
  if (++sampled > 5) break;
7252
7853
  for (const [fieldName, value] of recordValue.entries()) {
7253
7854
  if (fieldName.startsWith("_")) continue;
@@ -7260,7 +7861,7 @@ function inferModelFromData(dataMap) {
7260
7861
  return { fields };
7261
7862
  }
7262
7863
  function inferTypeFromValue(value) {
7263
- if (value instanceof Y3.Map) return "stringset";
7864
+ if (isStringSetYMap(value)) return "stringset";
7264
7865
  switch (typeof value) {
7265
7866
  case "string":
7266
7867
  return "string";
@@ -7275,7 +7876,7 @@ function inferTypeFromValue(value) {
7275
7876
  function readConstraints(constraintsMap) {
7276
7877
  const out = {};
7277
7878
  for (const [name, value] of constraintsMap.entries()) {
7278
- if (!(value instanceof Y3.Map)) continue;
7879
+ if (!(value instanceof Y6.Map)) continue;
7279
7880
  let fields = [];
7280
7881
  const rawFields = value.get("fields");
7281
7882
  if (typeof rawFields === "string") {
@@ -7294,7 +7895,7 @@ function readConstraints(constraintsMap) {
7294
7895
  function readRelationships(relsMap) {
7295
7896
  const out = {};
7296
7897
  for (const [name, value] of relsMap.entries()) {
7297
- if (!(value instanceof Y3.Map)) continue;
7898
+ if (!(value instanceof Y6.Map)) continue;
7298
7899
  const rel = {};
7299
7900
  for (const [k, v] of value.entries()) {
7300
7901
  rel[k] = v;
@@ -7305,6 +7906,7 @@ function readRelationships(relsMap) {
7305
7906
  }
7306
7907
  var CAMEL_TO_SNAKE = {
7307
7908
  autoAssign: "auto_assign",
7909
+ autoStamp: "auto_stamp",
7308
7910
  maxLength: "max_length",
7309
7911
  maxCount: "max_count",
7310
7912
  relatedIdField: "related_id_field",
@@ -7335,6 +7937,7 @@ function schemaToToml(schema) {
7335
7937
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
7336
7938
  lines.push(`type = ${tomlValue(field.type)}`);
7337
7939
  if (field.autoAssign) lines.push("auto_assign = true");
7940
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
7338
7941
  if (field.indexed) lines.push("indexed = true");
7339
7942
  if (field.unique) lines.push("unique = true");
7340
7943
  if (field.required) lines.push("required = true");
@@ -7381,10 +7984,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
7381
7984
  "unique",
7382
7985
  "required",
7383
7986
  "auto_assign",
7987
+ "auto_stamp",
7384
7988
  "max_length",
7385
7989
  "max_count",
7386
- "default"
7990
+ "default",
7991
+ "enum"
7387
7992
  ]);
7993
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
7388
7994
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
7389
7995
  "fields",
7390
7996
  "relationships",
@@ -7426,9 +8032,37 @@ function parseFieldOptions(raw, context, strict) {
7426
8032
  if (raw.unique === true) opts.unique = true;
7427
8033
  if (raw.required === true) opts.required = true;
7428
8034
  if (raw.auto_assign === true) opts.autoAssign = true;
8035
+ if (raw.auto_stamp !== void 0) {
8036
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
8037
+ throw new Error(
8038
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
8039
+ ...VALID_AUTO_STAMP_VALUES
8040
+ ].join(", ")}`
8041
+ );
8042
+ }
8043
+ opts.autoStamp = raw.auto_stamp;
8044
+ }
7429
8045
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
7430
8046
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
7431
8047
  if (raw.default !== void 0) opts.default = raw.default;
8048
+ if (raw.enum !== void 0) {
8049
+ if (raw.type !== "string") {
8050
+ throw new Error(
8051
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
8052
+ );
8053
+ }
8054
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
8055
+ throw new Error(
8056
+ `${context}: \`enum\` must be a non-empty array of strings.`
8057
+ );
8058
+ }
8059
+ if (!raw.enum.every((v) => typeof v === "string")) {
8060
+ throw new Error(
8061
+ `${context}: \`enum\` values must all be strings.`
8062
+ );
8063
+ }
8064
+ opts.enum = [...raw.enum];
8065
+ }
7432
8066
  return opts;
7433
8067
  }
7434
8068
  function requireField(raw, field, context) {
@@ -7557,18 +8191,19 @@ async function loadSchemaFromToml(filePath, options = {}) {
7557
8191
 
7558
8192
  // src/node.ts
7559
8193
  init_metaSync();
8194
+ init_writeLifecycle();
7560
8195
 
7561
8196
  // src/utils/yDocDump.ts
7562
- var Y4 = __toESM(require("yjs"), 1);
8197
+ var Y7 = __toESM(require("yjs"), 1);
7563
8198
  function toPlain(value) {
7564
- if (value instanceof Y4.Map) {
8199
+ if (value instanceof Y7.Map) {
7565
8200
  const obj = {};
7566
8201
  value.forEach((v, k) => {
7567
8202
  obj[k] = toPlain(v);
7568
8203
  });
7569
8204
  return obj;
7570
8205
  }
7571
- if (value instanceof Y4.Array) {
8206
+ if (value instanceof Y7.Array) {
7572
8207
  return value.toArray().map((item) => toPlain(item));
7573
8208
  }
7574
8209
  return value;
@@ -7577,7 +8212,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7577
8212
  const result = {};
7578
8213
  const includeIndexes = !!options.includeIndexes;
7579
8214
  yDoc.share.forEach((type, name) => {
7580
- if (!(type instanceof Y4.Map)) return;
8215
+ if (!(type instanceof Y7.Map)) return;
7581
8216
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7582
8217
  const records = {};
7583
8218
  type.forEach((recordValue, recordId) => {
@@ -7638,31 +8273,45 @@ async function getRecommendedNodeEngine() {
7638
8273
  SqljsEngine,
7639
8274
  StringSet,
7640
8275
  UniqueConstraintViolationError,
8276
+ applyAutoStampsFromSchema,
8277
+ applyDefaultsFromSchema,
8278
+ applyStringSetChange,
8279
+ applyStringSetDelta,
8280
+ assertNever,
7641
8281
  attachAndRegisterModel,
7642
8282
  attachSchemaToClass,
7643
8283
  autoRegisterModel,
7644
8284
  clearMetaSyncCache,
7645
8285
  constants,
7646
8286
  createModelClass,
8287
+ decodeFunctionDefault,
7647
8288
  defineModelSchema,
7648
8289
  detectEnvironment,
7649
8290
  discoverModelNames,
7650
8291
  discoverSchema,
7651
8292
  dumpYDocToPlain,
8293
+ ensureStringSetYMap,
7652
8294
  features,
8295
+ fieldWriteKind,
7653
8296
  generateULID,
7654
8297
  getRecommendedNodeEngine,
7655
8298
  inferFieldType,
7656
8299
  initJsBao,
7657
8300
  isBrowser,
8301
+ isKnownFieldType,
7658
8302
  isNode,
7659
8303
  isNodeModuleAvailable,
7660
8304
  loadSchemaFromToml,
7661
8305
  loadSchemaFromTomlString,
8306
+ parseStringSetOp,
7662
8307
  registerFunctionDefault,
8308
+ registerFunctionDefaultGenerator,
8309
+ replaceStringSet,
7663
8310
  resetJsBao,
8311
+ resolveDefaultValue,
7664
8312
  schemaToToml,
7665
8313
  summarizePlainYDoc,
7666
8314
  syncInferredMeta,
7667
- syncModelMeta
8315
+ syncModelMeta,
8316
+ writeField
7668
8317
  });