js-bao 0.4.2 → 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/browser.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
  }
@@ -2433,11 +2472,11 @@ __export(BaseModel_exports, {
2433
2472
  function generateULID() {
2434
2473
  return (0, import_ulid.ulid)();
2435
2474
  }
2436
- var Y2, import_ulid, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2475
+ var Y3, import_ulid, LogLevel, Logger, UniqueConstraintViolationError, RecordNotFoundError, SCHEMA_ACCESSORS_KEY, BaseModel2;
2437
2476
  var init_BaseModel = __esm({
2438
2477
  "src/models/BaseModel.ts"() {
2439
2478
  "use strict";
2440
- Y2 = __toESM(require("yjs"), 1);
2479
+ Y3 = __toESM(require("yjs"), 1);
2441
2480
  import_ulid = require("ulid");
2442
2481
  init_StringSet();
2443
2482
  init_documentTypes();
@@ -2445,6 +2484,7 @@ var init_BaseModel = __esm({
2445
2484
  init_CursorManager();
2446
2485
  init_sql();
2447
2486
  init_metaSync();
2487
+ init_yjsValues();
2448
2488
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2449
2489
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
2450
2490
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2953,7 +2993,30 @@ var init_BaseModel = __esm({
2953
2993
  if (verboseEnabled) {
2954
2994
  Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
2955
2995
  }
2956
- return fieldValue;
2996
+ return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
2997
+ }
2998
+ /**
2999
+ * Defensive normalization for the read/sync boundary (issue #625).
3000
+ *
3001
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
3002
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
3003
+ * through untouched. For any other field, a composite Yjs primitive
3004
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
3005
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
3006
+ * warn, rather than throwing. Scalars pass through unchanged.
3007
+ */
3008
+ normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
3009
+ const fieldOptions = schema?.fields?.get(fieldKey);
3010
+ if (fieldOptions?.type === "stringset") {
3011
+ return value;
3012
+ }
3013
+ if (isYjsComposite(value)) {
3014
+ Logger.warn(
3015
+ `[${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.`
3016
+ );
3017
+ return normalizeYjsValue(value);
3018
+ }
3019
+ return value;
2957
3020
  }
2958
3021
  getValue(fieldKey) {
2959
3022
  const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
@@ -3034,7 +3097,7 @@ var init_BaseModel = __esm({
3034
3097
  validateBeforeSave() {
3035
3098
  const schema = this.constructor.getSchema();
3036
3099
  for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
3037
- const currentValue = this.getValue(fieldKey);
3100
+ const currentValue = fieldKey === "id" ? this.id : this.getValue(fieldKey);
3038
3101
  if (fieldOptions.required && (currentValue === null || currentValue === void 0)) {
3039
3102
  throw new Error(`Field ${fieldKey} is required before save`);
3040
3103
  }
@@ -3098,7 +3161,7 @@ var init_BaseModel = __esm({
3098
3161
  }
3099
3162
  getStringSetFromYjs(fieldName) {
3100
3163
  const yjsData = this.getFromYjs(fieldName);
3101
- if (yjsData instanceof Y2.Map) {
3164
+ if (yjsData instanceof Y3.Map) {
3102
3165
  return Array.from(yjsData.keys());
3103
3166
  }
3104
3167
  if (yjsData && typeof yjsData === "object") {
@@ -3206,7 +3269,7 @@ var init_BaseModel = __esm({
3206
3269
  }
3207
3270
  const extractStringSetValues = (value) => {
3208
3271
  if (!value) return [];
3209
- if (value instanceof Y2.Map) {
3272
+ if (value instanceof Y3.Map) {
3210
3273
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
3211
3274
  }
3212
3275
  if (Array.isArray(value)) {
@@ -3223,7 +3286,7 @@ var init_BaseModel = __esm({
3223
3286
  if (!recordId) continue;
3224
3287
  let itemData;
3225
3288
  const stringSetValuesByField = {};
3226
- if (recordData instanceof Y2.Map) {
3289
+ if (recordData instanceof Y3.Map) {
3227
3290
  itemData = {};
3228
3291
  const unknownFields = [];
3229
3292
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3342,7 +3405,7 @@ var init_BaseModel = __esm({
3342
3405
  const buildUniqueKey = (recordData, fields) => {
3343
3406
  const keyParts = [];
3344
3407
  for (const field of fields) {
3345
- const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
3408
+ const value = recordData instanceof Y3.Map ? recordData.get(field) : recordData[field];
3346
3409
  if (value === null || value === void 0) {
3347
3410
  return null;
3348
3411
  }
@@ -3352,7 +3415,7 @@ var init_BaseModel = __esm({
3352
3415
  };
3353
3416
  const extractItemData = (key, recordData) => {
3354
3417
  let itemData;
3355
- if (recordData instanceof Y2.Map) {
3418
+ if (recordData instanceof Y3.Map) {
3356
3419
  itemData = {};
3357
3420
  const unknownFields = [];
3358
3421
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3365,7 +3428,14 @@ var init_BaseModel = __esm({
3365
3428
  continue;
3366
3429
  }
3367
3430
  if (value !== void 0) {
3368
- itemData[fieldKey] = value;
3431
+ if (isYjsComposite(value)) {
3432
+ Logger.warn(
3433
+ `[${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.`
3434
+ );
3435
+ itemData[fieldKey] = normalizeYjsValueForStorage(value);
3436
+ } else {
3437
+ itemData[fieldKey] = value;
3438
+ }
3369
3439
  }
3370
3440
  }
3371
3441
  if (unknownFields.length > 0) {
@@ -3388,7 +3458,14 @@ var init_BaseModel = __esm({
3388
3458
  continue;
3389
3459
  }
3390
3460
  if (value !== void 0) {
3391
- filteredData[fieldKey] = value;
3461
+ if (isYjsComposite(value)) {
3462
+ Logger.warn(
3463
+ `[${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.`
3464
+ );
3465
+ filteredData[fieldKey] = normalizeYjsValueForStorage(value);
3466
+ } else {
3467
+ filteredData[fieldKey] = value;
3468
+ }
3392
3469
  }
3393
3470
  }
3394
3471
  if (unknownFields.length > 0) {
@@ -3468,7 +3545,7 @@ var init_BaseModel = __esm({
3468
3545
  if (!recordData || !key) continue;
3469
3546
  const itemData = extractItemData(key, recordData);
3470
3547
  if (!itemData) continue;
3471
- if (change.action === "add" && recordData instanceof Y2.Map) {
3548
+ if (change.action === "add" && recordData instanceof Y3.Map) {
3472
3549
  Logger.verbose(
3473
3550
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
3474
3551
  );
@@ -3613,7 +3690,7 @@ var init_BaseModel = __esm({
3613
3690
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
3614
3691
  );
3615
3692
  for (const [recordId, recordData] of documentYMap.entries()) {
3616
- if (recordData instanceof Y2.Map) {
3693
+ if (recordData instanceof Y3.Map) {
3617
3694
  Logger.verbose(
3618
3695
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
3619
3696
  );
@@ -3798,6 +3875,11 @@ var init_BaseModel = __esm({
3798
3875
  Logger.debug(
3799
3876
  `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
3800
3877
  );
3878
+ if (isYjsComposite(yjsValue)) {
3879
+ Logger.warn(
3880
+ `[${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).`
3881
+ );
3882
+ }
3801
3883
  if (yjsValue === void 0) {
3802
3884
  Logger.debug(
3803
3885
  `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
@@ -3835,8 +3917,8 @@ var init_BaseModel = __esm({
3835
3917
  */
3836
3918
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3837
3919
  let nested = recordYMap.get(fieldName);
3838
- if (!(nested instanceof Y2.Map)) {
3839
- const migrated = new Y2.Map();
3920
+ if (!(nested instanceof Y3.Map)) {
3921
+ const migrated = new Y3.Map();
3840
3922
  if (nested && typeof nested === "object") {
3841
3923
  for (const [member, marker] of Object.entries(
3842
3924
  nested
@@ -4023,6 +4105,29 @@ var init_BaseModel = __esm({
4023
4105
  );
4024
4106
  throw new Error("Cannot save item without an id. Ensure id is set.");
4025
4107
  }
4108
+ const preTransactCreateStamps = /* @__PURE__ */ new Set();
4109
+ {
4110
+ const existingRecord = targetYMap.get(this.id);
4111
+ const isNew = !existingRecord;
4112
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
4113
+ const stamp = fieldOptions.autoStamp;
4114
+ if (!stamp) continue;
4115
+ const callerExplicit = this.hasLocalChange(fieldKey);
4116
+ if (callerExplicit) {
4117
+ const localValue = this._localChanges[fieldKey];
4118
+ if (localValue !== null && localValue !== void 0) continue;
4119
+ }
4120
+ if (stamp === "create") {
4121
+ if (!isNew) continue;
4122
+ const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
4123
+ if (persisted !== null && persisted !== void 0) continue;
4124
+ }
4125
+ this[fieldKey] = Date.now();
4126
+ if (stamp === "create") {
4127
+ preTransactCreateStamps.add(fieldKey);
4128
+ }
4129
+ }
4130
+ }
4026
4131
  this.validateBeforeSave();
4027
4132
  Logger.debug(`[${modelName}.save] About to get current JS state`);
4028
4133
  Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
@@ -4053,6 +4158,14 @@ var init_BaseModel = __esm({
4053
4158
  }
4054
4159
  }
4055
4160
  }
4161
+ for (const fieldKey of preTransactCreateStamps) {
4162
+ if (this._localChanges && fieldKey in this._localChanges) {
4163
+ delete this._localChanges[fieldKey];
4164
+ }
4165
+ if (fieldKey in dataToSave) {
4166
+ delete dataToSave[fieldKey];
4167
+ }
4168
+ }
4056
4169
  }
4057
4170
  }
4058
4171
  if (!targetYMap) {
@@ -4065,7 +4178,7 @@ var init_BaseModel = __esm({
4065
4178
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
4066
4179
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
4067
4180
  if (!recordYMap) {
4068
- recordYMap = new Y2.Map();
4181
+ recordYMap = new Y3.Map();
4069
4182
  Logger.verbose(
4070
4183
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
4071
4184
  );
@@ -5092,7 +5205,7 @@ var init_BaseModel = __esm({
5092
5205
  );
5093
5206
  }
5094
5207
  for (const [recordId, recordYMap] of documentYMap.entries()) {
5095
- if (recordYMap instanceof Y2.Map) {
5208
+ if (recordYMap instanceof Y3.Map) {
5096
5209
  const instance = new this({ id: recordId });
5097
5210
  instance._metaDocId = docId;
5098
5211
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -5163,7 +5276,7 @@ var init_BaseModel = __esm({
5163
5276
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
5164
5277
  if (documentYMap) {
5165
5278
  const recordYMap2 = documentYMap.get(recordId2);
5166
- if (recordYMap2 && recordYMap2 instanceof Y2.Map) {
5279
+ if (recordYMap2 && recordYMap2 instanceof Y3.Map) {
5167
5280
  const instance = new modelConstructor({
5168
5281
  id: recordId2
5169
5282
  });
@@ -5200,7 +5313,7 @@ var init_BaseModel = __esm({
5200
5313
  if (!recordYMap) {
5201
5314
  return null;
5202
5315
  }
5203
- if (recordYMap instanceof Y2.Map) {
5316
+ if (recordYMap instanceof Y3.Map) {
5204
5317
  const instance = new modelConstructor({
5205
5318
  id: recordId
5206
5319
  });
@@ -5432,7 +5545,7 @@ var init_BaseModel = __esm({
5432
5545
  for (const [fieldKey, fieldOptions] of schema.fields) {
5433
5546
  if (fieldOptions?.type !== "stringset") continue;
5434
5547
  const value = recordYMap.get(fieldKey);
5435
- if (value instanceof Y2.Map) {
5548
+ if (value instanceof Y3.Map) {
5436
5549
  this.observeStringSetMapOnce(value);
5437
5550
  }
5438
5551
  }
@@ -5479,7 +5592,14 @@ var init_BaseModel = __esm({
5479
5592
  continue;
5480
5593
  }
5481
5594
  if (value !== void 0) {
5482
- updatedData[key] = value;
5595
+ if (isYjsComposite(value)) {
5596
+ Logger.warn(
5597
+ `[${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.`
5598
+ );
5599
+ updatedData[key] = normalizeYjsValueForStorage(value);
5600
+ } else {
5601
+ updatedData[key] = value;
5602
+ }
5483
5603
  }
5484
5604
  }
5485
5605
  if (unknownFields.length > 0) {
@@ -5559,7 +5679,14 @@ var init_BaseModel = __esm({
5559
5679
  continue;
5560
5680
  }
5561
5681
  if (value !== void 0) {
5562
- updatedData[key] = value;
5682
+ if (isYjsComposite(value)) {
5683
+ Logger.warn(
5684
+ `[${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.`
5685
+ );
5686
+ updatedData[key] = normalizeYjsValueForStorage(value);
5687
+ } else {
5688
+ updatedData[key] = value;
5689
+ }
5563
5690
  }
5564
5691
  }
5565
5692
  if (unknownFields.length > 0) {
@@ -6546,7 +6673,8 @@ function attachAndRegisterModel(modelClass, schema) {
6546
6673
  }
6547
6674
 
6548
6675
  // src/utils/yDocSchema.ts
6549
- var Y3 = __toESM(require("yjs"), 1);
6676
+ var Y4 = __toESM(require("yjs"), 1);
6677
+ init_yjsValues();
6550
6678
  function discoverSchema(yDoc) {
6551
6679
  const models = {};
6552
6680
  const metaNames = /* @__PURE__ */ new Set();
@@ -6580,7 +6708,7 @@ function discoverModelNames(yDoc) {
6580
6708
  function materializeMap(yDoc, key) {
6581
6709
  try {
6582
6710
  const map = yDoc.getMap(key);
6583
- return map instanceof Y3.Map ? map : null;
6711
+ return map instanceof Y4.Map ? map : null;
6584
6712
  } catch {
6585
6713
  return null;
6586
6714
  }
@@ -6590,11 +6718,11 @@ function readModelMeta(metaMap) {
6590
6718
  let constraints;
6591
6719
  let relationships;
6592
6720
  for (const [key, value] of metaMap.entries()) {
6593
- if (key === "_constraints" && value instanceof Y3.Map) {
6721
+ if (key === "_constraints" && value instanceof Y4.Map) {
6594
6722
  constraints = readConstraints(value);
6595
- } else if (key === "_relationships" && value instanceof Y3.Map) {
6723
+ } else if (key === "_relationships" && value instanceof Y4.Map) {
6596
6724
  relationships = readRelationships(value);
6597
- } else if (value instanceof Y3.Map) {
6725
+ } else if (value instanceof Y4.Map) {
6598
6726
  fields[key] = readFieldMeta(value);
6599
6727
  }
6600
6728
  }
@@ -6613,6 +6741,10 @@ function readFieldMeta(fieldMap) {
6613
6741
  if (fieldMap.get("unique") === true) field.unique = true;
6614
6742
  if (fieldMap.get("required") === true) field.required = true;
6615
6743
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
6744
+ const autoStamp = fieldMap.get("autoStamp");
6745
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
6746
+ field.autoStamp = autoStamp;
6747
+ }
6616
6748
  const def = fieldMap.get("default");
6617
6749
  if (def !== void 0) field.default = def;
6618
6750
  const maxLength = fieldMap.get("maxLength");
@@ -6625,7 +6757,7 @@ function inferModelFromData(dataMap) {
6625
6757
  const fields = {};
6626
6758
  let sampled = 0;
6627
6759
  for (const [_recordId, recordValue] of dataMap.entries()) {
6628
- if (!(recordValue instanceof Y3.Map)) continue;
6760
+ if (!(recordValue instanceof Y4.Map)) continue;
6629
6761
  if (++sampled > 5) break;
6630
6762
  for (const [fieldName, value] of recordValue.entries()) {
6631
6763
  if (fieldName.startsWith("_")) continue;
@@ -6638,7 +6770,7 @@ function inferModelFromData(dataMap) {
6638
6770
  return { fields };
6639
6771
  }
6640
6772
  function inferTypeFromValue(value) {
6641
- if (value instanceof Y3.Map) return "stringset";
6773
+ if (isStringSetYMap(value)) return "stringset";
6642
6774
  switch (typeof value) {
6643
6775
  case "string":
6644
6776
  return "string";
@@ -6653,7 +6785,7 @@ function inferTypeFromValue(value) {
6653
6785
  function readConstraints(constraintsMap) {
6654
6786
  const out = {};
6655
6787
  for (const [name, value] of constraintsMap.entries()) {
6656
- if (!(value instanceof Y3.Map)) continue;
6788
+ if (!(value instanceof Y4.Map)) continue;
6657
6789
  let fields = [];
6658
6790
  const rawFields = value.get("fields");
6659
6791
  if (typeof rawFields === "string") {
@@ -6672,7 +6804,7 @@ function readConstraints(constraintsMap) {
6672
6804
  function readRelationships(relsMap) {
6673
6805
  const out = {};
6674
6806
  for (const [name, value] of relsMap.entries()) {
6675
- if (!(value instanceof Y3.Map)) continue;
6807
+ if (!(value instanceof Y4.Map)) continue;
6676
6808
  const rel = {};
6677
6809
  for (const [k, v] of value.entries()) {
6678
6810
  rel[k] = v;
@@ -6683,6 +6815,7 @@ function readRelationships(relsMap) {
6683
6815
  }
6684
6816
  var CAMEL_TO_SNAKE = {
6685
6817
  autoAssign: "auto_assign",
6818
+ autoStamp: "auto_stamp",
6686
6819
  maxLength: "max_length",
6687
6820
  maxCount: "max_count",
6688
6821
  relatedIdField: "related_id_field",
@@ -6713,6 +6846,7 @@ function schemaToToml(schema) {
6713
6846
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
6714
6847
  lines.push(`type = ${tomlValue(field.type)}`);
6715
6848
  if (field.autoAssign) lines.push("auto_assign = true");
6849
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
6716
6850
  if (field.indexed) lines.push("indexed = true");
6717
6851
  if (field.unique) lines.push("unique = true");
6718
6852
  if (field.required) lines.push("required = true");
@@ -6759,10 +6893,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
6759
6893
  "unique",
6760
6894
  "required",
6761
6895
  "auto_assign",
6896
+ "auto_stamp",
6762
6897
  "max_length",
6763
6898
  "max_count",
6764
- "default"
6899
+ "default",
6900
+ "enum"
6765
6901
  ]);
6902
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
6766
6903
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
6767
6904
  "fields",
6768
6905
  "relationships",
@@ -6804,9 +6941,37 @@ function parseFieldOptions(raw, context, strict) {
6804
6941
  if (raw.unique === true) opts.unique = true;
6805
6942
  if (raw.required === true) opts.required = true;
6806
6943
  if (raw.auto_assign === true) opts.autoAssign = true;
6944
+ if (raw.auto_stamp !== void 0) {
6945
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
6946
+ throw new Error(
6947
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
6948
+ ...VALID_AUTO_STAMP_VALUES
6949
+ ].join(", ")}`
6950
+ );
6951
+ }
6952
+ opts.autoStamp = raw.auto_stamp;
6953
+ }
6807
6954
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
6808
6955
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
6809
6956
  if (raw.default !== void 0) opts.default = raw.default;
6957
+ if (raw.enum !== void 0) {
6958
+ if (raw.type !== "string") {
6959
+ throw new Error(
6960
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
6961
+ );
6962
+ }
6963
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
6964
+ throw new Error(
6965
+ `${context}: \`enum\` must be a non-empty array of strings.`
6966
+ );
6967
+ }
6968
+ if (!raw.enum.every((v) => typeof v === "string")) {
6969
+ throw new Error(
6970
+ `${context}: \`enum\` values must all be strings.`
6971
+ );
6972
+ }
6973
+ opts.enum = [...raw.enum];
6974
+ }
6810
6975
  return opts;
6811
6976
  }
6812
6977
  function requireField(raw, field, context) {
@@ -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;
@@ -495,6 +519,17 @@ declare class BaseModel implements StringSetChangeTracker {
495
519
  private ensureLocalChanges;
496
520
  private hasLocalChange;
497
521
  private getFromYjs;
522
+ /**
523
+ * Defensive normalization for the read/sync boundary (issue #625).
524
+ *
525
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
526
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
527
+ * through untouched. For any other field, a composite Yjs primitive
528
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
529
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
530
+ * warn, rather than throwing. Scalars pass through unchanged.
531
+ */
532
+ private normalizeNonStringSetValue;
498
533
  private getValue;
499
534
  private setValue;
500
535
  get isDirty(): boolean;
@@ -922,6 +957,7 @@ interface DiscoveredField {
922
957
  required?: boolean;
923
958
  default?: string | number | boolean;
924
959
  autoAssign?: boolean;
960
+ autoStamp?: "create" | "update" | "both";
925
961
  maxLength?: number;
926
962
  maxCount?: number;
927
963
  }
@@ -1010,6 +1046,11 @@ declare function loadSchemaFromTomlString(tomlString: string, options?: LoadSche
1010
1046
 
1011
1047
  /**
1012
1048
  * Infer a _meta_ type string from a JS runtime value.
1049
+ *
1050
+ * Only an all-`true` Y.Map (the stringset wire shape) is tagged as
1051
+ * `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
1052
+ * Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
1053
+ * than being mis-tagged.
1013
1054
  */
1014
1055
  declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
1015
1056
  /**