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/node.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) {
@@ -7168,7 +7295,8 @@ function attachAndRegisterModel(modelClass, schema) {
7168
7295
  }
7169
7296
 
7170
7297
  // src/utils/yDocSchema.ts
7171
- var Y3 = __toESM(require("yjs"), 1);
7298
+ var Y4 = __toESM(require("yjs"), 1);
7299
+ init_yjsValues();
7172
7300
  function discoverSchema(yDoc) {
7173
7301
  const models = {};
7174
7302
  const metaNames = /* @__PURE__ */ new Set();
@@ -7202,7 +7330,7 @@ function discoverModelNames(yDoc) {
7202
7330
  function materializeMap(yDoc, key) {
7203
7331
  try {
7204
7332
  const map = yDoc.getMap(key);
7205
- return map instanceof Y3.Map ? map : null;
7333
+ return map instanceof Y4.Map ? map : null;
7206
7334
  } catch {
7207
7335
  return null;
7208
7336
  }
@@ -7212,11 +7340,11 @@ function readModelMeta(metaMap) {
7212
7340
  let constraints;
7213
7341
  let relationships;
7214
7342
  for (const [key, value] of metaMap.entries()) {
7215
- if (key === "_constraints" && value instanceof Y3.Map) {
7343
+ if (key === "_constraints" && value instanceof Y4.Map) {
7216
7344
  constraints = readConstraints(value);
7217
- } else if (key === "_relationships" && value instanceof Y3.Map) {
7345
+ } else if (key === "_relationships" && value instanceof Y4.Map) {
7218
7346
  relationships = readRelationships(value);
7219
- } else if (value instanceof Y3.Map) {
7347
+ } else if (value instanceof Y4.Map) {
7220
7348
  fields[key] = readFieldMeta(value);
7221
7349
  }
7222
7350
  }
@@ -7235,6 +7363,10 @@ function readFieldMeta(fieldMap) {
7235
7363
  if (fieldMap.get("unique") === true) field.unique = true;
7236
7364
  if (fieldMap.get("required") === true) field.required = true;
7237
7365
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7366
+ const autoStamp = fieldMap.get("autoStamp");
7367
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
7368
+ field.autoStamp = autoStamp;
7369
+ }
7238
7370
  const def = fieldMap.get("default");
7239
7371
  if (def !== void 0) field.default = def;
7240
7372
  const maxLength = fieldMap.get("maxLength");
@@ -7247,7 +7379,7 @@ function inferModelFromData(dataMap) {
7247
7379
  const fields = {};
7248
7380
  let sampled = 0;
7249
7381
  for (const [_recordId, recordValue] of dataMap.entries()) {
7250
- if (!(recordValue instanceof Y3.Map)) continue;
7382
+ if (!(recordValue instanceof Y4.Map)) continue;
7251
7383
  if (++sampled > 5) break;
7252
7384
  for (const [fieldName, value] of recordValue.entries()) {
7253
7385
  if (fieldName.startsWith("_")) continue;
@@ -7260,7 +7392,7 @@ function inferModelFromData(dataMap) {
7260
7392
  return { fields };
7261
7393
  }
7262
7394
  function inferTypeFromValue(value) {
7263
- if (value instanceof Y3.Map) return "stringset";
7395
+ if (isStringSetYMap(value)) return "stringset";
7264
7396
  switch (typeof value) {
7265
7397
  case "string":
7266
7398
  return "string";
@@ -7275,7 +7407,7 @@ function inferTypeFromValue(value) {
7275
7407
  function readConstraints(constraintsMap) {
7276
7408
  const out = {};
7277
7409
  for (const [name, value] of constraintsMap.entries()) {
7278
- if (!(value instanceof Y3.Map)) continue;
7410
+ if (!(value instanceof Y4.Map)) continue;
7279
7411
  let fields = [];
7280
7412
  const rawFields = value.get("fields");
7281
7413
  if (typeof rawFields === "string") {
@@ -7294,7 +7426,7 @@ function readConstraints(constraintsMap) {
7294
7426
  function readRelationships(relsMap) {
7295
7427
  const out = {};
7296
7428
  for (const [name, value] of relsMap.entries()) {
7297
- if (!(value instanceof Y3.Map)) continue;
7429
+ if (!(value instanceof Y4.Map)) continue;
7298
7430
  const rel = {};
7299
7431
  for (const [k, v] of value.entries()) {
7300
7432
  rel[k] = v;
@@ -7305,6 +7437,7 @@ function readRelationships(relsMap) {
7305
7437
  }
7306
7438
  var CAMEL_TO_SNAKE = {
7307
7439
  autoAssign: "auto_assign",
7440
+ autoStamp: "auto_stamp",
7308
7441
  maxLength: "max_length",
7309
7442
  maxCount: "max_count",
7310
7443
  relatedIdField: "related_id_field",
@@ -7335,6 +7468,7 @@ function schemaToToml(schema) {
7335
7468
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
7336
7469
  lines.push(`type = ${tomlValue(field.type)}`);
7337
7470
  if (field.autoAssign) lines.push("auto_assign = true");
7471
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
7338
7472
  if (field.indexed) lines.push("indexed = true");
7339
7473
  if (field.unique) lines.push("unique = true");
7340
7474
  if (field.required) lines.push("required = true");
@@ -7381,10 +7515,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
7381
7515
  "unique",
7382
7516
  "required",
7383
7517
  "auto_assign",
7518
+ "auto_stamp",
7384
7519
  "max_length",
7385
7520
  "max_count",
7386
- "default"
7521
+ "default",
7522
+ "enum"
7387
7523
  ]);
7524
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
7388
7525
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
7389
7526
  "fields",
7390
7527
  "relationships",
@@ -7426,9 +7563,37 @@ function parseFieldOptions(raw, context, strict) {
7426
7563
  if (raw.unique === true) opts.unique = true;
7427
7564
  if (raw.required === true) opts.required = true;
7428
7565
  if (raw.auto_assign === true) opts.autoAssign = true;
7566
+ if (raw.auto_stamp !== void 0) {
7567
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
7568
+ throw new Error(
7569
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
7570
+ ...VALID_AUTO_STAMP_VALUES
7571
+ ].join(", ")}`
7572
+ );
7573
+ }
7574
+ opts.autoStamp = raw.auto_stamp;
7575
+ }
7429
7576
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
7430
7577
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
7431
7578
  if (raw.default !== void 0) opts.default = raw.default;
7579
+ if (raw.enum !== void 0) {
7580
+ if (raw.type !== "string") {
7581
+ throw new Error(
7582
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
7583
+ );
7584
+ }
7585
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
7586
+ throw new Error(
7587
+ `${context}: \`enum\` must be a non-empty array of strings.`
7588
+ );
7589
+ }
7590
+ if (!raw.enum.every((v) => typeof v === "string")) {
7591
+ throw new Error(
7592
+ `${context}: \`enum\` values must all be strings.`
7593
+ );
7594
+ }
7595
+ opts.enum = [...raw.enum];
7596
+ }
7432
7597
  return opts;
7433
7598
  }
7434
7599
  function requireField(raw, field, context) {
@@ -7559,16 +7724,16 @@ async function loadSchemaFromToml(filePath, options = {}) {
7559
7724
  init_metaSync();
7560
7725
 
7561
7726
  // src/utils/yDocDump.ts
7562
- var Y4 = __toESM(require("yjs"), 1);
7727
+ var Y5 = __toESM(require("yjs"), 1);
7563
7728
  function toPlain(value) {
7564
- if (value instanceof Y4.Map) {
7729
+ if (value instanceof Y5.Map) {
7565
7730
  const obj = {};
7566
7731
  value.forEach((v, k) => {
7567
7732
  obj[k] = toPlain(v);
7568
7733
  });
7569
7734
  return obj;
7570
7735
  }
7571
- if (value instanceof Y4.Array) {
7736
+ if (value instanceof Y5.Array) {
7572
7737
  return value.toArray().map((item) => toPlain(item));
7573
7738
  }
7574
7739
  return value;
@@ -7577,7 +7742,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7577
7742
  const result = {};
7578
7743
  const includeIndexes = !!options.includeIndexes;
7579
7744
  yDoc.share.forEach((type, name) => {
7580
- if (!(type instanceof Y4.Map)) return;
7745
+ if (!(type instanceof Y5.Map)) return;
7581
7746
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7582
7747
  const records = {};
7583
7748
  type.forEach((recordValue, recordId) => {
package/dist/node.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;
@@ -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;
@@ -951,6 +986,7 @@ interface DiscoveredField {
951
986
  required?: boolean;
952
987
  default?: string | number | boolean;
953
988
  autoAssign?: boolean;
989
+ autoStamp?: "create" | "update" | "both";
954
990
  maxLength?: number;
955
991
  maxCount?: number;
956
992
  }
@@ -1044,6 +1080,11 @@ declare function loadSchemaFromToml(filePath: string, options?: LoadSchemaOption
1044
1080
 
1045
1081
  /**
1046
1082
  * Infer a _meta_ type string from a JS runtime value.
1083
+ *
1084
+ * Only an all-`true` Y.Map (the stringset wire shape) is tagged as
1085
+ * `stringset` (issue #625) — a Y.Map carrying a composite payload, or a
1086
+ * Y.Array / Y.Text, is not a stringset and stays untyped (`null`) rather
1087
+ * than being mis-tagged.
1047
1088
  */
1048
1089
  declare function inferFieldType(value: unknown): "string" | "number" | "boolean" | "stringset" | null;
1049
1090
  /**