js-bao 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1183,9 +1183,46 @@ var init_DocumentQueryTranslator = __esm({
1183
1183
  }
1184
1184
  });
1185
1185
 
1186
+ // src/utils/yjsValues.ts
1187
+ function isYjsComposite(value) {
1188
+ return value instanceof Y.Map || value instanceof Y.Array || value instanceof Y.Text;
1189
+ }
1190
+ function isStringSetYMap(value) {
1191
+ if (!(value instanceof Y.Map)) return false;
1192
+ for (const v of value.values()) {
1193
+ if (v !== true) return false;
1194
+ }
1195
+ return true;
1196
+ }
1197
+ function normalizeYjsValue(value) {
1198
+ if (value instanceof Y.Text) {
1199
+ return value.toString();
1200
+ }
1201
+ if (value instanceof Y.Map || value instanceof Y.Array) {
1202
+ return value.toJSON();
1203
+ }
1204
+ return value;
1205
+ }
1206
+ function normalizeYjsValueForStorage(value) {
1207
+ if (value instanceof Y.Text) {
1208
+ return value.toString();
1209
+ }
1210
+ if (value instanceof Y.Map || value instanceof Y.Array) {
1211
+ return JSON.stringify(value.toJSON());
1212
+ }
1213
+ return value;
1214
+ }
1215
+ var Y;
1216
+ var init_yjsValues = __esm({
1217
+ "src/utils/yjsValues.ts"() {
1218
+ "use strict";
1219
+ Y = __toESM(require("yjs"), 1);
1220
+ }
1221
+ });
1222
+
1186
1223
  // src/models/metaSync.ts
1187
1224
  function inferFieldType(value) {
1188
- if (value instanceof Y.Map) return "stringset";
1225
+ if (isStringSetYMap(value)) return "stringset";
1189
1226
  switch (typeof value) {
1190
1227
  case "string":
1191
1228
  return "string";
@@ -1230,7 +1267,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1230
1267
  if (compoundConstraints.length > 0) {
1231
1268
  let constraints = meta.get("_constraints");
1232
1269
  if (!constraints) {
1233
- constraints = new Y.Map();
1270
+ constraints = new Y2.Map();
1234
1271
  meta.set("_constraints", constraints);
1235
1272
  }
1236
1273
  for (const constraint of compoundConstraints) {
@@ -1241,7 +1278,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1241
1278
  if (relationships && Object.keys(relationships).length > 0) {
1242
1279
  let rels = meta.get("_relationships");
1243
1280
  if (!rels) {
1244
- rels = new Y.Map();
1281
+ rels = new Y2.Map();
1245
1282
  meta.set("_relationships", rels);
1246
1283
  }
1247
1284
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -1253,7 +1290,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1253
1290
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1254
1291
  let fieldMeta = metaMap.get(fieldName);
1255
1292
  if (!fieldMeta) {
1256
- fieldMeta = new Y.Map();
1293
+ fieldMeta = new Y2.Map();
1257
1294
  metaMap.set(fieldName, fieldMeta);
1258
1295
  }
1259
1296
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -1261,6 +1298,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1261
1298
  if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
1262
1299
  if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
1263
1300
  if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
1301
+ if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
1264
1302
  if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
1265
1303
  if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
1266
1304
  const encoded = encodeDefault(fieldOpts.default);
@@ -1269,7 +1307,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1269
1307
  function syncConstraintMeta(constraintsMap, constraint) {
1270
1308
  let cMeta = constraintsMap.get(constraint.name);
1271
1309
  if (!cMeta) {
1272
- cMeta = new Y.Map();
1310
+ cMeta = new Y2.Map();
1273
1311
  constraintsMap.set(constraint.name, cMeta);
1274
1312
  }
1275
1313
  setIfChanged(cMeta, "type", "unique");
@@ -1279,7 +1317,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
1279
1317
  function syncRelationshipMeta(relsMap, relName, relConfig) {
1280
1318
  let relMeta = relsMap.get(relName);
1281
1319
  if (!relMeta) {
1282
- relMeta = new Y.Map();
1320
+ relMeta = new Y2.Map();
1283
1321
  relsMap.set(relName, relMeta);
1284
1322
  }
1285
1323
  for (const [key, value] of Object.entries(relConfig)) {
@@ -1294,7 +1332,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
1294
1332
  if (fieldName.startsWith("_")) continue;
1295
1333
  let fieldMeta = meta.get(fieldName);
1296
1334
  if (!fieldMeta) {
1297
- fieldMeta = new Y.Map();
1335
+ fieldMeta = new Y2.Map();
1298
1336
  meta.set(fieldName, fieldMeta);
1299
1337
  }
1300
1338
  if (!fieldMeta.has("type")) {
@@ -1310,11 +1348,12 @@ function setIfChanged(map, key, value) {
1310
1348
  map.set(key, value);
1311
1349
  }
1312
1350
  }
1313
- var Y, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1351
+ var Y2, KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1314
1352
  var init_metaSync = __esm({
1315
1353
  "src/models/metaSync.ts"() {
1316
1354
  "use strict";
1317
- Y = __toESM(require("yjs"), 1);
1355
+ Y2 = __toESM(require("yjs"), 1);
1356
+ init_yjsValues();
1318
1357
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
1319
1358
  _syncedCache = /* @__PURE__ */ new WeakMap();
1320
1359
  }
@@ -1719,11 +1758,11 @@ __export(BaseModel_exports, {
1719
1758
  function generateULID() {
1720
1759
  return (0, import_ulid.ulid)();
1721
1760
  }
1722
- var Y2, import_ulid, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel;
1761
+ var Y3, import_ulid, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel;
1723
1762
  var init_BaseModel = __esm({
1724
1763
  "src/models/BaseModel.ts"() {
1725
1764
  "use strict";
1726
- Y2 = __toESM(require("yjs"), 1);
1765
+ Y3 = __toESM(require("yjs"), 1);
1727
1766
  import_ulid = require("ulid");
1728
1767
  init_StringSet();
1729
1768
  init_documentTypes();
@@ -1731,6 +1770,7 @@ var init_BaseModel = __esm({
1731
1770
  init_CursorManager();
1732
1771
  init_sql();
1733
1772
  init_metaSync();
1773
+ init_yjsValues();
1734
1774
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
1735
1775
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
1736
1776
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2239,7 +2279,30 @@ var init_BaseModel = __esm({
2239
2279
  if (verboseEnabled) {
2240
2280
  Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
2241
2281
  }
2242
- return fieldValue;
2282
+ return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
2283
+ }
2284
+ /**
2285
+ * Defensive normalization for the read/sync boundary (issue #625).
2286
+ *
2287
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
2288
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
2289
+ * through untouched. For any other field, a composite Yjs primitive
2290
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
2291
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
2292
+ * warn, rather than throwing. Scalars pass through unchanged.
2293
+ */
2294
+ normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
2295
+ const fieldOptions = schema?.fields?.get(fieldKey);
2296
+ if (fieldOptions?.type === "stringset") {
2297
+ return value;
2298
+ }
2299
+ if (isYjsComposite(value)) {
2300
+ Logger.warn(
2301
+ `[${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.`
2302
+ );
2303
+ return normalizeYjsValue(value);
2304
+ }
2305
+ return value;
2243
2306
  }
2244
2307
  getValue(fieldKey) {
2245
2308
  const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
@@ -2384,7 +2447,7 @@ var init_BaseModel = __esm({
2384
2447
  }
2385
2448
  getStringSetFromYjs(fieldName) {
2386
2449
  const yjsData = this.getFromYjs(fieldName);
2387
- if (yjsData instanceof Y2.Map) {
2450
+ if (yjsData instanceof Y3.Map) {
2388
2451
  return Array.from(yjsData.keys());
2389
2452
  }
2390
2453
  if (yjsData && typeof yjsData === "object") {
@@ -2492,7 +2555,7 @@ var init_BaseModel = __esm({
2492
2555
  }
2493
2556
  const extractStringSetValues = (value) => {
2494
2557
  if (!value) return [];
2495
- if (value instanceof Y2.Map) {
2558
+ if (value instanceof Y3.Map) {
2496
2559
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
2497
2560
  }
2498
2561
  if (Array.isArray(value)) {
@@ -2509,7 +2572,7 @@ var init_BaseModel = __esm({
2509
2572
  if (!recordId) continue;
2510
2573
  let itemData;
2511
2574
  const stringSetValuesByField = {};
2512
- if (recordData instanceof Y2.Map) {
2575
+ if (recordData instanceof Y3.Map) {
2513
2576
  itemData = {};
2514
2577
  const unknownFields = [];
2515
2578
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2628,7 +2691,7 @@ var init_BaseModel = __esm({
2628
2691
  const buildUniqueKey = (recordData, fields) => {
2629
2692
  const keyParts = [];
2630
2693
  for (const field of fields) {
2631
- const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
2694
+ const value = recordData instanceof Y3.Map ? recordData.get(field) : recordData[field];
2632
2695
  if (value === null || value === void 0) {
2633
2696
  return null;
2634
2697
  }
@@ -2638,7 +2701,7 @@ var init_BaseModel = __esm({
2638
2701
  };
2639
2702
  const extractItemData = (key, recordData) => {
2640
2703
  let itemData;
2641
- if (recordData instanceof Y2.Map) {
2704
+ if (recordData instanceof Y3.Map) {
2642
2705
  itemData = {};
2643
2706
  const unknownFields = [];
2644
2707
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2651,7 +2714,14 @@ var init_BaseModel = __esm({
2651
2714
  continue;
2652
2715
  }
2653
2716
  if (value !== void 0) {
2654
- itemData[fieldKey] = value;
2717
+ if (isYjsComposite(value)) {
2718
+ Logger.warn(
2719
+ `[${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.`
2720
+ );
2721
+ itemData[fieldKey] = normalizeYjsValueForStorage(value);
2722
+ } else {
2723
+ itemData[fieldKey] = value;
2724
+ }
2655
2725
  }
2656
2726
  }
2657
2727
  if (unknownFields.length > 0) {
@@ -2674,7 +2744,14 @@ var init_BaseModel = __esm({
2674
2744
  continue;
2675
2745
  }
2676
2746
  if (value !== void 0) {
2677
- filteredData[fieldKey] = value;
2747
+ if (isYjsComposite(value)) {
2748
+ Logger.warn(
2749
+ `[${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.`
2750
+ );
2751
+ filteredData[fieldKey] = normalizeYjsValueForStorage(value);
2752
+ } else {
2753
+ filteredData[fieldKey] = value;
2754
+ }
2678
2755
  }
2679
2756
  }
2680
2757
  if (unknownFields.length > 0) {
@@ -2754,7 +2831,7 @@ var init_BaseModel = __esm({
2754
2831
  if (!recordData || !key) continue;
2755
2832
  const itemData = extractItemData(key, recordData);
2756
2833
  if (!itemData) continue;
2757
- if (change.action === "add" && recordData instanceof Y2.Map) {
2834
+ if (change.action === "add" && recordData instanceof Y3.Map) {
2758
2835
  Logger.verbose(
2759
2836
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2760
2837
  );
@@ -2899,7 +2976,7 @@ var init_BaseModel = __esm({
2899
2976
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
2900
2977
  );
2901
2978
  for (const [recordId, recordData] of documentYMap.entries()) {
2902
- if (recordData instanceof Y2.Map) {
2979
+ if (recordData instanceof Y3.Map) {
2903
2980
  Logger.verbose(
2904
2981
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
2905
2982
  );
@@ -3084,6 +3161,11 @@ var init_BaseModel = __esm({
3084
3161
  Logger.debug(
3085
3162
  `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
3086
3163
  );
3164
+ if (isYjsComposite(yjsValue)) {
3165
+ Logger.warn(
3166
+ `[${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).`
3167
+ );
3168
+ }
3087
3169
  if (yjsValue === void 0) {
3088
3170
  Logger.debug(
3089
3171
  `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
@@ -3121,8 +3203,8 @@ var init_BaseModel = __esm({
3121
3203
  */
3122
3204
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3123
3205
  let nested = recordYMap.get(fieldName);
3124
- if (!(nested instanceof Y2.Map)) {
3125
- const migrated = new Y2.Map();
3206
+ if (!(nested instanceof Y3.Map)) {
3207
+ const migrated = new Y3.Map();
3126
3208
  if (nested && typeof nested === "object") {
3127
3209
  for (const [member, marker] of Object.entries(
3128
3210
  nested
@@ -3309,6 +3391,29 @@ var init_BaseModel = __esm({
3309
3391
  );
3310
3392
  throw new Error("Cannot save item without an id. Ensure id is set.");
3311
3393
  }
3394
+ const preTransactCreateStamps = /* @__PURE__ */ new Set();
3395
+ {
3396
+ const existingRecord = targetYMap.get(this.id);
3397
+ const isNew = !existingRecord;
3398
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
3399
+ const stamp = fieldOptions.autoStamp;
3400
+ if (!stamp) continue;
3401
+ const callerExplicit = this.hasLocalChange(fieldKey);
3402
+ if (callerExplicit) {
3403
+ const localValue = this._localChanges[fieldKey];
3404
+ if (localValue !== null && localValue !== void 0) continue;
3405
+ }
3406
+ if (stamp === "create") {
3407
+ if (!isNew) continue;
3408
+ const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
3409
+ if (persisted !== null && persisted !== void 0) continue;
3410
+ }
3411
+ this[fieldKey] = Date.now();
3412
+ if (stamp === "create") {
3413
+ preTransactCreateStamps.add(fieldKey);
3414
+ }
3415
+ }
3416
+ }
3312
3417
  this.validateBeforeSave();
3313
3418
  Logger.debug(`[${modelName}.save] About to get current JS state`);
3314
3419
  Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
@@ -3339,6 +3444,14 @@ var init_BaseModel = __esm({
3339
3444
  }
3340
3445
  }
3341
3446
  }
3447
+ for (const fieldKey of preTransactCreateStamps) {
3448
+ if (this._localChanges && fieldKey in this._localChanges) {
3449
+ delete this._localChanges[fieldKey];
3450
+ }
3451
+ if (fieldKey in dataToSave) {
3452
+ delete dataToSave[fieldKey];
3453
+ }
3454
+ }
3342
3455
  }
3343
3456
  }
3344
3457
  if (!targetYMap) {
@@ -3351,7 +3464,7 @@ var init_BaseModel = __esm({
3351
3464
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
3352
3465
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
3353
3466
  if (!recordYMap) {
3354
- recordYMap = new Y2.Map();
3467
+ recordYMap = new Y3.Map();
3355
3468
  Logger.verbose(
3356
3469
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
3357
3470
  );
@@ -4378,7 +4491,7 @@ var init_BaseModel = __esm({
4378
4491
  );
4379
4492
  }
4380
4493
  for (const [recordId, recordYMap] of documentYMap.entries()) {
4381
- if (recordYMap instanceof Y2.Map) {
4494
+ if (recordYMap instanceof Y3.Map) {
4382
4495
  const instance = new this({ id: recordId });
4383
4496
  instance._metaDocId = docId;
4384
4497
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -4449,7 +4562,7 @@ var init_BaseModel = __esm({
4449
4562
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
4450
4563
  if (documentYMap) {
4451
4564
  const recordYMap2 = documentYMap.get(recordId2);
4452
- if (recordYMap2 && recordYMap2 instanceof Y2.Map) {
4565
+ if (recordYMap2 && recordYMap2 instanceof Y3.Map) {
4453
4566
  const instance = new modelConstructor({
4454
4567
  id: recordId2
4455
4568
  });
@@ -4486,7 +4599,7 @@ var init_BaseModel = __esm({
4486
4599
  if (!recordYMap) {
4487
4600
  return null;
4488
4601
  }
4489
- if (recordYMap instanceof Y2.Map) {
4602
+ if (recordYMap instanceof Y3.Map) {
4490
4603
  const instance = new modelConstructor({
4491
4604
  id: recordId
4492
4605
  });
@@ -4718,7 +4831,7 @@ var init_BaseModel = __esm({
4718
4831
  for (const [fieldKey, fieldOptions] of schema.fields) {
4719
4832
  if (fieldOptions?.type !== "stringset") continue;
4720
4833
  const value = recordYMap.get(fieldKey);
4721
- if (value instanceof Y2.Map) {
4834
+ if (value instanceof Y3.Map) {
4722
4835
  this.observeStringSetMapOnce(value);
4723
4836
  }
4724
4837
  }
@@ -4765,7 +4878,14 @@ var init_BaseModel = __esm({
4765
4878
  continue;
4766
4879
  }
4767
4880
  if (value !== void 0) {
4768
- updatedData[key] = value;
4881
+ if (isYjsComposite(value)) {
4882
+ Logger.warn(
4883
+ `[${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.`
4884
+ );
4885
+ updatedData[key] = normalizeYjsValueForStorage(value);
4886
+ } else {
4887
+ updatedData[key] = value;
4888
+ }
4769
4889
  }
4770
4890
  }
4771
4891
  if (unknownFields.length > 0) {
@@ -4845,7 +4965,14 @@ var init_BaseModel = __esm({
4845
4965
  continue;
4846
4966
  }
4847
4967
  if (value !== void 0) {
4848
- updatedData[key] = value;
4968
+ if (isYjsComposite(value)) {
4969
+ Logger.warn(
4970
+ `[${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.`
4971
+ );
4972
+ updatedData[key] = normalizeYjsValueForStorage(value);
4973
+ } else {
4974
+ updatedData[key] = value;
4975
+ }
4849
4976
  }
4850
4977
  }
4851
4978
  if (unknownFields.length > 0) {
@@ -7161,10 +7288,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
7161
7288
  "unique",
7162
7289
  "required",
7163
7290
  "auto_assign",
7291
+ "auto_stamp",
7164
7292
  "max_length",
7165
7293
  "max_count",
7166
- "default"
7294
+ "default",
7295
+ "enum"
7167
7296
  ]);
7297
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
7168
7298
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
7169
7299
  "fields",
7170
7300
  "relationships",
@@ -7206,9 +7336,37 @@ function parseFieldOptions(raw, context, strict) {
7206
7336
  if (raw.unique === true) opts.unique = true;
7207
7337
  if (raw.required === true) opts.required = true;
7208
7338
  if (raw.auto_assign === true) opts.autoAssign = true;
7339
+ if (raw.auto_stamp !== void 0) {
7340
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
7341
+ throw new Error(
7342
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
7343
+ ...VALID_AUTO_STAMP_VALUES
7344
+ ].join(", ")}`
7345
+ );
7346
+ }
7347
+ opts.autoStamp = raw.auto_stamp;
7348
+ }
7209
7349
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
7210
7350
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
7211
7351
  if (raw.default !== void 0) opts.default = raw.default;
7352
+ if (raw.enum !== void 0) {
7353
+ if (raw.type !== "string") {
7354
+ throw new Error(
7355
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
7356
+ );
7357
+ }
7358
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
7359
+ throw new Error(
7360
+ `${context}: \`enum\` must be a non-empty array of strings.`
7361
+ );
7362
+ }
7363
+ if (!raw.enum.every((v) => typeof v === "string")) {
7364
+ throw new Error(
7365
+ `${context}: \`enum\` values must all be strings.`
7366
+ );
7367
+ }
7368
+ opts.enum = [...raw.enum];
7369
+ }
7212
7370
  return opts;
7213
7371
  }
7214
7372
  function requireField(raw, field, context) {
@@ -7336,7 +7494,8 @@ async function loadSchemaFromToml(filePath, options = {}) {
7336
7494
  }
7337
7495
 
7338
7496
  // src/utils/yDocSchema.ts
7339
- var Y3 = __toESM(require("yjs"), 1);
7497
+ var Y4 = __toESM(require("yjs"), 1);
7498
+ init_yjsValues();
7340
7499
  function discoverSchema(yDoc) {
7341
7500
  const models = {};
7342
7501
  const metaNames = /* @__PURE__ */ new Set();
@@ -7370,7 +7529,7 @@ function discoverModelNames(yDoc) {
7370
7529
  function materializeMap(yDoc, key) {
7371
7530
  try {
7372
7531
  const map = yDoc.getMap(key);
7373
- return map instanceof Y3.Map ? map : null;
7532
+ return map instanceof Y4.Map ? map : null;
7374
7533
  } catch {
7375
7534
  return null;
7376
7535
  }
@@ -7380,11 +7539,11 @@ function readModelMeta(metaMap) {
7380
7539
  let constraints;
7381
7540
  let relationships;
7382
7541
  for (const [key, value] of metaMap.entries()) {
7383
- if (key === "_constraints" && value instanceof Y3.Map) {
7542
+ if (key === "_constraints" && value instanceof Y4.Map) {
7384
7543
  constraints = readConstraints(value);
7385
- } else if (key === "_relationships" && value instanceof Y3.Map) {
7544
+ } else if (key === "_relationships" && value instanceof Y4.Map) {
7386
7545
  relationships = readRelationships(value);
7387
- } else if (value instanceof Y3.Map) {
7546
+ } else if (value instanceof Y4.Map) {
7388
7547
  fields[key] = readFieldMeta(value);
7389
7548
  }
7390
7549
  }
@@ -7403,6 +7562,10 @@ function readFieldMeta(fieldMap) {
7403
7562
  if (fieldMap.get("unique") === true) field.unique = true;
7404
7563
  if (fieldMap.get("required") === true) field.required = true;
7405
7564
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7565
+ const autoStamp = fieldMap.get("autoStamp");
7566
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
7567
+ field.autoStamp = autoStamp;
7568
+ }
7406
7569
  const def = fieldMap.get("default");
7407
7570
  if (def !== void 0) field.default = def;
7408
7571
  const maxLength = fieldMap.get("maxLength");
@@ -7415,7 +7578,7 @@ function inferModelFromData(dataMap) {
7415
7578
  const fields = {};
7416
7579
  let sampled = 0;
7417
7580
  for (const [_recordId, recordValue] of dataMap.entries()) {
7418
- if (!(recordValue instanceof Y3.Map)) continue;
7581
+ if (!(recordValue instanceof Y4.Map)) continue;
7419
7582
  if (++sampled > 5) break;
7420
7583
  for (const [fieldName, value] of recordValue.entries()) {
7421
7584
  if (fieldName.startsWith("_")) continue;
@@ -7428,7 +7591,7 @@ function inferModelFromData(dataMap) {
7428
7591
  return { fields };
7429
7592
  }
7430
7593
  function inferTypeFromValue(value) {
7431
- if (value instanceof Y3.Map) return "stringset";
7594
+ if (isStringSetYMap(value)) return "stringset";
7432
7595
  switch (typeof value) {
7433
7596
  case "string":
7434
7597
  return "string";
@@ -7443,7 +7606,7 @@ function inferTypeFromValue(value) {
7443
7606
  function readConstraints(constraintsMap) {
7444
7607
  const out = {};
7445
7608
  for (const [name, value] of constraintsMap.entries()) {
7446
- if (!(value instanceof Y3.Map)) continue;
7609
+ if (!(value instanceof Y4.Map)) continue;
7447
7610
  let fields = [];
7448
7611
  const rawFields = value.get("fields");
7449
7612
  if (typeof rawFields === "string") {
@@ -7462,7 +7625,7 @@ function readConstraints(constraintsMap) {
7462
7625
  function readRelationships(relsMap) {
7463
7626
  const out = {};
7464
7627
  for (const [name, value] of relsMap.entries()) {
7465
- if (!(value instanceof Y3.Map)) continue;
7628
+ if (!(value instanceof Y4.Map)) continue;
7466
7629
  const rel = {};
7467
7630
  for (const [k, v] of value.entries()) {
7468
7631
  rel[k] = v;
@@ -7473,6 +7636,7 @@ function readRelationships(relsMap) {
7473
7636
  }
7474
7637
  var CAMEL_TO_SNAKE = {
7475
7638
  autoAssign: "auto_assign",
7639
+ autoStamp: "auto_stamp",
7476
7640
  maxLength: "max_length",
7477
7641
  maxCount: "max_count",
7478
7642
  relatedIdField: "related_id_field",
@@ -7503,6 +7667,7 @@ function schemaToToml(schema) {
7503
7667
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
7504
7668
  lines.push(`type = ${tomlValue(field.type)}`);
7505
7669
  if (field.autoAssign) lines.push("auto_assign = true");
7670
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
7506
7671
  if (field.indexed) lines.push("indexed = true");
7507
7672
  if (field.unique) lines.push("unique = true");
7508
7673
  if (field.required) lines.push("required = true");
@@ -7537,16 +7702,16 @@ function schemaToToml(schema) {
7537
7702
  init_BaseModel();
7538
7703
 
7539
7704
  // src/utils/yDocDump.ts
7540
- var Y4 = __toESM(require("yjs"), 1);
7705
+ var Y5 = __toESM(require("yjs"), 1);
7541
7706
  function toPlain(value) {
7542
- if (value instanceof Y4.Map) {
7707
+ if (value instanceof Y5.Map) {
7543
7708
  const obj = {};
7544
7709
  value.forEach((v, k) => {
7545
7710
  obj[k] = toPlain(v);
7546
7711
  });
7547
7712
  return obj;
7548
7713
  }
7549
- if (value instanceof Y4.Array) {
7714
+ if (value instanceof Y5.Array) {
7550
7715
  return value.toArray().map((item) => toPlain(item));
7551
7716
  }
7552
7717
  return value;
@@ -7555,7 +7720,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7555
7720
  const result = {};
7556
7721
  const includeIndexes = !!options.includeIndexes;
7557
7722
  yDoc.share.forEach((type, name) => {
7558
- if (!(type instanceof Y4.Map)) return;
7723
+ if (!(type instanceof Y5.Map)) return;
7559
7724
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7560
7725
  const records = {};
7561
7726
  type.forEach((recordValue, recordId) => {
package/dist/index.d.cts CHANGED
@@ -31,8 +31,32 @@ interface FieldOptions {
31
31
  unique?: boolean;
32
32
  default?: any | (() => any);
33
33
  autoAssign?: boolean;
34
+ /**
35
+ * Auto-timestamp this field with `Date.now()` (milliseconds) on save.
36
+ *
37
+ * - `'create'` — stamp only on the first save (when `isNew === true`).
38
+ * - `'update'` — stamp on every save (create AND update).
39
+ * - `'both'` — stamp on every save (create AND update).
40
+ *
41
+ * If the caller passes an explicit value for the field in `data`, the
42
+ * explicit value wins and the stamp is skipped. The stamp is applied
43
+ * in `beforeSave` BEFORE any user-defined hooks, so user hooks may
44
+ * still overwrite the value if they wish (last writer wins).
45
+ */
46
+ autoStamp?: "create" | "update" | "both";
34
47
  maxLength?: number;
35
48
  maxCount?: number;
49
+ /**
50
+ * Allowed-value set for a `string` field. When present, the codegen
51
+ * generators (database-type codegen + doc-model v2 codegen) emit a
52
+ * TypeScript string-literal union (`"a" | "b" | "c"`) instead of a bare
53
+ * `string` for this field.
54
+ *
55
+ * Advisory / codegen-only: this is a TS-emission hint. The runtime and the
56
+ * server do NOT enforce enum membership on write (see #843). Only valid on
57
+ * `string` fields, and must be a non-empty array of strings.
58
+ */
59
+ enum?: string[];
36
60
  }
37
61
  interface UniqueConstraintConfig {
38
62
  name: string;
@@ -529,6 +553,17 @@ declare class BaseModel implements StringSetChangeTracker {
529
553
  private ensureLocalChanges;
530
554
  private hasLocalChange;
531
555
  private getFromYjs;
556
+ /**
557
+ * Defensive normalization for the read/sync boundary (issue #625).
558
+ *
559
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
560
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
561
+ * through untouched. For any other field, a composite Yjs primitive
562
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
563
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
564
+ * warn, rather than throwing. Scalars pass through unchanged.
565
+ */
566
+ private normalizeNonStringSetValue;
532
567
  private getValue;
533
568
  private setValue;
534
569
  get isDirty(): boolean;
@@ -835,6 +870,11 @@ declare function attachAndRegisterModel(modelClass: typeof BaseModel, schema: De
835
870
 
836
871
  /**
837
872
  * Infer a _meta_ type string from a JS runtime value.
873
+ *
874
+ * Only an all-`true` Y.Map (the stringset wire shape) is tagged as
875
+ * `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
876
+ * Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
877
+ * than being mis-tagged.
838
878
  */
839
879
  declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
840
880
  /**
@@ -919,6 +959,7 @@ interface DiscoveredField {
919
959
  required?: boolean;
920
960
  default?: string | number | boolean;
921
961
  autoAssign?: boolean;
962
+ autoStamp?: "create" | "update" | "both";
922
963
  maxLength?: number;
923
964
  maxCount?: number;
924
965
  }