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.d.ts 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
  }
package/dist/index.js CHANGED
@@ -1161,10 +1161,46 @@ var init_DocumentQueryTranslator = __esm({
1161
1161
  }
1162
1162
  });
1163
1163
 
1164
- // src/models/metaSync.ts
1164
+ // src/utils/yjsValues.ts
1165
1165
  import * as Y from "yjs";
1166
+ function isYjsComposite(value) {
1167
+ return value instanceof Y.Map || value instanceof Y.Array || value instanceof Y.Text;
1168
+ }
1169
+ function isStringSetYMap(value) {
1170
+ if (!(value instanceof Y.Map)) return false;
1171
+ for (const v of value.values()) {
1172
+ if (v !== true) return false;
1173
+ }
1174
+ return true;
1175
+ }
1176
+ function normalizeYjsValue(value) {
1177
+ if (value instanceof Y.Text) {
1178
+ return value.toString();
1179
+ }
1180
+ if (value instanceof Y.Map || value instanceof Y.Array) {
1181
+ return value.toJSON();
1182
+ }
1183
+ return value;
1184
+ }
1185
+ function normalizeYjsValueForStorage(value) {
1186
+ if (value instanceof Y.Text) {
1187
+ return value.toString();
1188
+ }
1189
+ if (value instanceof Y.Map || value instanceof Y.Array) {
1190
+ return JSON.stringify(value.toJSON());
1191
+ }
1192
+ return value;
1193
+ }
1194
+ var init_yjsValues = __esm({
1195
+ "src/utils/yjsValues.ts"() {
1196
+ "use strict";
1197
+ }
1198
+ });
1199
+
1200
+ // src/models/metaSync.ts
1201
+ import * as Y2 from "yjs";
1166
1202
  function inferFieldType(value) {
1167
- if (value instanceof Y.Map) return "stringset";
1203
+ if (isStringSetYMap(value)) return "stringset";
1168
1204
  switch (typeof value) {
1169
1205
  case "string":
1170
1206
  return "string";
@@ -1209,7 +1245,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1209
1245
  if (compoundConstraints.length > 0) {
1210
1246
  let constraints = meta.get("_constraints");
1211
1247
  if (!constraints) {
1212
- constraints = new Y.Map();
1248
+ constraints = new Y2.Map();
1213
1249
  meta.set("_constraints", constraints);
1214
1250
  }
1215
1251
  for (const constraint of compoundConstraints) {
@@ -1220,7 +1256,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1220
1256
  if (relationships && Object.keys(relationships).length > 0) {
1221
1257
  let rels = meta.get("_relationships");
1222
1258
  if (!rels) {
1223
- rels = new Y.Map();
1259
+ rels = new Y2.Map();
1224
1260
  meta.set("_relationships", rels);
1225
1261
  }
1226
1262
  for (const [relName, relConfig] of Object.entries(relationships)) {
@@ -1232,7 +1268,7 @@ function syncModelMeta(yDoc, modelName, schema) {
1232
1268
  function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1233
1269
  let fieldMeta = metaMap.get(fieldName);
1234
1270
  if (!fieldMeta) {
1235
- fieldMeta = new Y.Map();
1271
+ fieldMeta = new Y2.Map();
1236
1272
  metaMap.set(fieldName, fieldMeta);
1237
1273
  }
1238
1274
  setIfChanged(fieldMeta, "type", fieldOpts.type);
@@ -1240,6 +1276,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1240
1276
  if (fieldOpts.unique) setIfChanged(fieldMeta, "unique", true);
1241
1277
  if (fieldOpts.required) setIfChanged(fieldMeta, "required", true);
1242
1278
  if (fieldOpts.autoAssign) setIfChanged(fieldMeta, "autoAssign", true);
1279
+ if (fieldOpts.autoStamp) setIfChanged(fieldMeta, "autoStamp", fieldOpts.autoStamp);
1243
1280
  if (fieldOpts.maxLength !== void 0) setIfChanged(fieldMeta, "maxLength", fieldOpts.maxLength);
1244
1281
  if (fieldOpts.maxCount !== void 0) setIfChanged(fieldMeta, "maxCount", fieldOpts.maxCount);
1245
1282
  const encoded = encodeDefault(fieldOpts.default);
@@ -1248,7 +1285,7 @@ function syncFieldMeta(metaMap, fieldName, fieldOpts) {
1248
1285
  function syncConstraintMeta(constraintsMap, constraint) {
1249
1286
  let cMeta = constraintsMap.get(constraint.name);
1250
1287
  if (!cMeta) {
1251
- cMeta = new Y.Map();
1288
+ cMeta = new Y2.Map();
1252
1289
  constraintsMap.set(constraint.name, cMeta);
1253
1290
  }
1254
1291
  setIfChanged(cMeta, "type", "unique");
@@ -1258,7 +1295,7 @@ function syncConstraintMeta(constraintsMap, constraint) {
1258
1295
  function syncRelationshipMeta(relsMap, relName, relConfig) {
1259
1296
  let relMeta = relsMap.get(relName);
1260
1297
  if (!relMeta) {
1261
- relMeta = new Y.Map();
1298
+ relMeta = new Y2.Map();
1262
1299
  relsMap.set(relName, relMeta);
1263
1300
  }
1264
1301
  for (const [key, value] of Object.entries(relConfig)) {
@@ -1273,7 +1310,7 @@ function syncInferredMeta(yDoc, modelName, recordData) {
1273
1310
  if (fieldName.startsWith("_")) continue;
1274
1311
  let fieldMeta = meta.get(fieldName);
1275
1312
  if (!fieldMeta) {
1276
- fieldMeta = new Y.Map();
1313
+ fieldMeta = new Y2.Map();
1277
1314
  meta.set(fieldName, fieldMeta);
1278
1315
  }
1279
1316
  if (!fieldMeta.has("type")) {
@@ -1293,6 +1330,7 @@ var KNOWN_FUNCTION_DEFAULTS, _syncedCache;
1293
1330
  var init_metaSync = __esm({
1294
1331
  "src/models/metaSync.ts"() {
1295
1332
  "use strict";
1333
+ init_yjsValues();
1296
1334
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
1297
1335
  _syncedCache = /* @__PURE__ */ new WeakMap();
1298
1336
  }
@@ -1694,7 +1732,7 @@ __export(BaseModel_exports, {
1694
1732
  UniqueConstraintViolationError: () => UniqueConstraintViolationError,
1695
1733
  generateULID: () => generateULID
1696
1734
  });
1697
- import * as Y2 from "yjs";
1735
+ import * as Y3 from "yjs";
1698
1736
  import { ulid } from "ulid";
1699
1737
  function generateULID() {
1700
1738
  return ulid();
@@ -1709,6 +1747,7 @@ var init_BaseModel = __esm({
1709
1747
  init_CursorManager();
1710
1748
  init_sql();
1711
1749
  init_metaSync();
1750
+ init_yjsValues();
1712
1751
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
1713
1752
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
1714
1753
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2217,7 +2256,30 @@ var init_BaseModel = __esm({
2217
2256
  if (verboseEnabled) {
2218
2257
  Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
2219
2258
  }
2220
- return fieldValue;
2259
+ return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
2260
+ }
2261
+ /**
2262
+ * Defensive normalization for the read/sync boundary (issue #625).
2263
+ *
2264
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
2265
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
2266
+ * through untouched. For any other field, a composite Yjs primitive
2267
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
2268
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
2269
+ * warn, rather than throwing. Scalars pass through unchanged.
2270
+ */
2271
+ normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
2272
+ const fieldOptions = schema?.fields?.get(fieldKey);
2273
+ if (fieldOptions?.type === "stringset") {
2274
+ return value;
2275
+ }
2276
+ if (isYjsComposite(value)) {
2277
+ Logger.warn(
2278
+ `[${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.`
2279
+ );
2280
+ return normalizeYjsValue(value);
2281
+ }
2282
+ return value;
2221
2283
  }
2222
2284
  getValue(fieldKey) {
2223
2285
  const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
@@ -2362,7 +2424,7 @@ var init_BaseModel = __esm({
2362
2424
  }
2363
2425
  getStringSetFromYjs(fieldName) {
2364
2426
  const yjsData = this.getFromYjs(fieldName);
2365
- if (yjsData instanceof Y2.Map) {
2427
+ if (yjsData instanceof Y3.Map) {
2366
2428
  return Array.from(yjsData.keys());
2367
2429
  }
2368
2430
  if (yjsData && typeof yjsData === "object") {
@@ -2470,7 +2532,7 @@ var init_BaseModel = __esm({
2470
2532
  }
2471
2533
  const extractStringSetValues = (value) => {
2472
2534
  if (!value) return [];
2473
- if (value instanceof Y2.Map) {
2535
+ if (value instanceof Y3.Map) {
2474
2536
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
2475
2537
  }
2476
2538
  if (Array.isArray(value)) {
@@ -2487,7 +2549,7 @@ var init_BaseModel = __esm({
2487
2549
  if (!recordId) continue;
2488
2550
  let itemData;
2489
2551
  const stringSetValuesByField = {};
2490
- if (recordData instanceof Y2.Map) {
2552
+ if (recordData instanceof Y3.Map) {
2491
2553
  itemData = {};
2492
2554
  const unknownFields = [];
2493
2555
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2606,7 +2668,7 @@ var init_BaseModel = __esm({
2606
2668
  const buildUniqueKey = (recordData, fields) => {
2607
2669
  const keyParts = [];
2608
2670
  for (const field of fields) {
2609
- const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
2671
+ const value = recordData instanceof Y3.Map ? recordData.get(field) : recordData[field];
2610
2672
  if (value === null || value === void 0) {
2611
2673
  return null;
2612
2674
  }
@@ -2616,7 +2678,7 @@ var init_BaseModel = __esm({
2616
2678
  };
2617
2679
  const extractItemData = (key, recordData) => {
2618
2680
  let itemData;
2619
- if (recordData instanceof Y2.Map) {
2681
+ if (recordData instanceof Y3.Map) {
2620
2682
  itemData = {};
2621
2683
  const unknownFields = [];
2622
2684
  for (const [fieldKey, value] of recordData.entries()) {
@@ -2629,7 +2691,14 @@ var init_BaseModel = __esm({
2629
2691
  continue;
2630
2692
  }
2631
2693
  if (value !== void 0) {
2632
- itemData[fieldKey] = value;
2694
+ if (isYjsComposite(value)) {
2695
+ Logger.warn(
2696
+ `[${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.`
2697
+ );
2698
+ itemData[fieldKey] = normalizeYjsValueForStorage(value);
2699
+ } else {
2700
+ itemData[fieldKey] = value;
2701
+ }
2633
2702
  }
2634
2703
  }
2635
2704
  if (unknownFields.length > 0) {
@@ -2652,7 +2721,14 @@ var init_BaseModel = __esm({
2652
2721
  continue;
2653
2722
  }
2654
2723
  if (value !== void 0) {
2655
- filteredData[fieldKey] = value;
2724
+ if (isYjsComposite(value)) {
2725
+ Logger.warn(
2726
+ `[${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.`
2727
+ );
2728
+ filteredData[fieldKey] = normalizeYjsValueForStorage(value);
2729
+ } else {
2730
+ filteredData[fieldKey] = value;
2731
+ }
2656
2732
  }
2657
2733
  }
2658
2734
  if (unknownFields.length > 0) {
@@ -2732,7 +2808,7 @@ var init_BaseModel = __esm({
2732
2808
  if (!recordData || !key) continue;
2733
2809
  const itemData = extractItemData(key, recordData);
2734
2810
  if (!itemData) continue;
2735
- if (change.action === "add" && recordData instanceof Y2.Map) {
2811
+ if (change.action === "add" && recordData instanceof Y3.Map) {
2736
2812
  Logger.verbose(
2737
2813
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
2738
2814
  );
@@ -2877,7 +2953,7 @@ var init_BaseModel = __esm({
2877
2953
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
2878
2954
  );
2879
2955
  for (const [recordId, recordData] of documentYMap.entries()) {
2880
- if (recordData instanceof Y2.Map) {
2956
+ if (recordData instanceof Y3.Map) {
2881
2957
  Logger.verbose(
2882
2958
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
2883
2959
  );
@@ -3062,6 +3138,11 @@ var init_BaseModel = __esm({
3062
3138
  Logger.debug(
3063
3139
  `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
3064
3140
  );
3141
+ if (isYjsComposite(yjsValue)) {
3142
+ Logger.warn(
3143
+ `[${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).`
3144
+ );
3145
+ }
3065
3146
  if (yjsValue === void 0) {
3066
3147
  Logger.debug(
3067
3148
  `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
@@ -3099,8 +3180,8 @@ var init_BaseModel = __esm({
3099
3180
  */
3100
3181
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3101
3182
  let nested = recordYMap.get(fieldName);
3102
- if (!(nested instanceof Y2.Map)) {
3103
- const migrated = new Y2.Map();
3183
+ if (!(nested instanceof Y3.Map)) {
3184
+ const migrated = new Y3.Map();
3104
3185
  if (nested && typeof nested === "object") {
3105
3186
  for (const [member, marker] of Object.entries(
3106
3187
  nested
@@ -3287,6 +3368,29 @@ var init_BaseModel = __esm({
3287
3368
  );
3288
3369
  throw new Error("Cannot save item without an id. Ensure id is set.");
3289
3370
  }
3371
+ const preTransactCreateStamps = /* @__PURE__ */ new Set();
3372
+ {
3373
+ const existingRecord = targetYMap.get(this.id);
3374
+ const isNew = !existingRecord;
3375
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
3376
+ const stamp = fieldOptions.autoStamp;
3377
+ if (!stamp) continue;
3378
+ const callerExplicit = this.hasLocalChange(fieldKey);
3379
+ if (callerExplicit) {
3380
+ const localValue = this._localChanges[fieldKey];
3381
+ if (localValue !== null && localValue !== void 0) continue;
3382
+ }
3383
+ if (stamp === "create") {
3384
+ if (!isNew) continue;
3385
+ const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
3386
+ if (persisted !== null && persisted !== void 0) continue;
3387
+ }
3388
+ this[fieldKey] = Date.now();
3389
+ if (stamp === "create") {
3390
+ preTransactCreateStamps.add(fieldKey);
3391
+ }
3392
+ }
3393
+ }
3290
3394
  this.validateBeforeSave();
3291
3395
  Logger.debug(`[${modelName}.save] About to get current JS state`);
3292
3396
  Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
@@ -3317,6 +3421,14 @@ var init_BaseModel = __esm({
3317
3421
  }
3318
3422
  }
3319
3423
  }
3424
+ for (const fieldKey of preTransactCreateStamps) {
3425
+ if (this._localChanges && fieldKey in this._localChanges) {
3426
+ delete this._localChanges[fieldKey];
3427
+ }
3428
+ if (fieldKey in dataToSave) {
3429
+ delete dataToSave[fieldKey];
3430
+ }
3431
+ }
3320
3432
  }
3321
3433
  }
3322
3434
  if (!targetYMap) {
@@ -3329,7 +3441,7 @@ var init_BaseModel = __esm({
3329
3441
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
3330
3442
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
3331
3443
  if (!recordYMap) {
3332
- recordYMap = new Y2.Map();
3444
+ recordYMap = new Y3.Map();
3333
3445
  Logger.verbose(
3334
3446
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
3335
3447
  );
@@ -4356,7 +4468,7 @@ var init_BaseModel = __esm({
4356
4468
  );
4357
4469
  }
4358
4470
  for (const [recordId, recordYMap] of documentYMap.entries()) {
4359
- if (recordYMap instanceof Y2.Map) {
4471
+ if (recordYMap instanceof Y3.Map) {
4360
4472
  const instance = new this({ id: recordId });
4361
4473
  instance._metaDocId = docId;
4362
4474
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -4427,7 +4539,7 @@ var init_BaseModel = __esm({
4427
4539
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
4428
4540
  if (documentYMap) {
4429
4541
  const recordYMap2 = documentYMap.get(recordId2);
4430
- if (recordYMap2 && recordYMap2 instanceof Y2.Map) {
4542
+ if (recordYMap2 && recordYMap2 instanceof Y3.Map) {
4431
4543
  const instance = new modelConstructor({
4432
4544
  id: recordId2
4433
4545
  });
@@ -4464,7 +4576,7 @@ var init_BaseModel = __esm({
4464
4576
  if (!recordYMap) {
4465
4577
  return null;
4466
4578
  }
4467
- if (recordYMap instanceof Y2.Map) {
4579
+ if (recordYMap instanceof Y3.Map) {
4468
4580
  const instance = new modelConstructor({
4469
4581
  id: recordId
4470
4582
  });
@@ -4696,7 +4808,7 @@ var init_BaseModel = __esm({
4696
4808
  for (const [fieldKey, fieldOptions] of schema.fields) {
4697
4809
  if (fieldOptions?.type !== "stringset") continue;
4698
4810
  const value = recordYMap.get(fieldKey);
4699
- if (value instanceof Y2.Map) {
4811
+ if (value instanceof Y3.Map) {
4700
4812
  this.observeStringSetMapOnce(value);
4701
4813
  }
4702
4814
  }
@@ -4743,7 +4855,14 @@ var init_BaseModel = __esm({
4743
4855
  continue;
4744
4856
  }
4745
4857
  if (value !== void 0) {
4746
- updatedData[key] = value;
4858
+ if (isYjsComposite(value)) {
4859
+ Logger.warn(
4860
+ `[${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.`
4861
+ );
4862
+ updatedData[key] = normalizeYjsValueForStorage(value);
4863
+ } else {
4864
+ updatedData[key] = value;
4865
+ }
4747
4866
  }
4748
4867
  }
4749
4868
  if (unknownFields.length > 0) {
@@ -4823,7 +4942,14 @@ var init_BaseModel = __esm({
4823
4942
  continue;
4824
4943
  }
4825
4944
  if (value !== void 0) {
4826
- updatedData[key] = value;
4945
+ if (isYjsComposite(value)) {
4946
+ Logger.warn(
4947
+ `[${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.`
4948
+ );
4949
+ updatedData[key] = normalizeYjsValueForStorage(value);
4950
+ } else {
4951
+ updatedData[key] = value;
4952
+ }
4827
4953
  }
4828
4954
  }
4829
4955
  if (unknownFields.length > 0) {
@@ -7103,10 +7229,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
7103
7229
  "unique",
7104
7230
  "required",
7105
7231
  "auto_assign",
7232
+ "auto_stamp",
7106
7233
  "max_length",
7107
7234
  "max_count",
7108
- "default"
7235
+ "default",
7236
+ "enum"
7109
7237
  ]);
7238
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
7110
7239
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
7111
7240
  "fields",
7112
7241
  "relationships",
@@ -7148,9 +7277,37 @@ function parseFieldOptions(raw, context, strict) {
7148
7277
  if (raw.unique === true) opts.unique = true;
7149
7278
  if (raw.required === true) opts.required = true;
7150
7279
  if (raw.auto_assign === true) opts.autoAssign = true;
7280
+ if (raw.auto_stamp !== void 0) {
7281
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
7282
+ throw new Error(
7283
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
7284
+ ...VALID_AUTO_STAMP_VALUES
7285
+ ].join(", ")}`
7286
+ );
7287
+ }
7288
+ opts.autoStamp = raw.auto_stamp;
7289
+ }
7151
7290
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
7152
7291
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
7153
7292
  if (raw.default !== void 0) opts.default = raw.default;
7293
+ if (raw.enum !== void 0) {
7294
+ if (raw.type !== "string") {
7295
+ throw new Error(
7296
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
7297
+ );
7298
+ }
7299
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
7300
+ throw new Error(
7301
+ `${context}: \`enum\` must be a non-empty array of strings.`
7302
+ );
7303
+ }
7304
+ if (!raw.enum.every((v) => typeof v === "string")) {
7305
+ throw new Error(
7306
+ `${context}: \`enum\` values must all be strings.`
7307
+ );
7308
+ }
7309
+ opts.enum = [...raw.enum];
7310
+ }
7154
7311
  return opts;
7155
7312
  }
7156
7313
  function requireField(raw, field, context) {
@@ -7278,7 +7435,8 @@ async function loadSchemaFromToml(filePath, options = {}) {
7278
7435
  }
7279
7436
 
7280
7437
  // src/utils/yDocSchema.ts
7281
- import * as Y3 from "yjs";
7438
+ init_yjsValues();
7439
+ import * as Y4 from "yjs";
7282
7440
  function discoverSchema(yDoc) {
7283
7441
  const models = {};
7284
7442
  const metaNames = /* @__PURE__ */ new Set();
@@ -7312,7 +7470,7 @@ function discoverModelNames(yDoc) {
7312
7470
  function materializeMap(yDoc, key) {
7313
7471
  try {
7314
7472
  const map = yDoc.getMap(key);
7315
- return map instanceof Y3.Map ? map : null;
7473
+ return map instanceof Y4.Map ? map : null;
7316
7474
  } catch {
7317
7475
  return null;
7318
7476
  }
@@ -7322,11 +7480,11 @@ function readModelMeta(metaMap) {
7322
7480
  let constraints;
7323
7481
  let relationships;
7324
7482
  for (const [key, value] of metaMap.entries()) {
7325
- if (key === "_constraints" && value instanceof Y3.Map) {
7483
+ if (key === "_constraints" && value instanceof Y4.Map) {
7326
7484
  constraints = readConstraints(value);
7327
- } else if (key === "_relationships" && value instanceof Y3.Map) {
7485
+ } else if (key === "_relationships" && value instanceof Y4.Map) {
7328
7486
  relationships = readRelationships(value);
7329
- } else if (value instanceof Y3.Map) {
7487
+ } else if (value instanceof Y4.Map) {
7330
7488
  fields[key] = readFieldMeta(value);
7331
7489
  }
7332
7490
  }
@@ -7345,6 +7503,10 @@ function readFieldMeta(fieldMap) {
7345
7503
  if (fieldMap.get("unique") === true) field.unique = true;
7346
7504
  if (fieldMap.get("required") === true) field.required = true;
7347
7505
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7506
+ const autoStamp = fieldMap.get("autoStamp");
7507
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
7508
+ field.autoStamp = autoStamp;
7509
+ }
7348
7510
  const def = fieldMap.get("default");
7349
7511
  if (def !== void 0) field.default = def;
7350
7512
  const maxLength = fieldMap.get("maxLength");
@@ -7357,7 +7519,7 @@ function inferModelFromData(dataMap) {
7357
7519
  const fields = {};
7358
7520
  let sampled = 0;
7359
7521
  for (const [_recordId, recordValue] of dataMap.entries()) {
7360
- if (!(recordValue instanceof Y3.Map)) continue;
7522
+ if (!(recordValue instanceof Y4.Map)) continue;
7361
7523
  if (++sampled > 5) break;
7362
7524
  for (const [fieldName, value] of recordValue.entries()) {
7363
7525
  if (fieldName.startsWith("_")) continue;
@@ -7370,7 +7532,7 @@ function inferModelFromData(dataMap) {
7370
7532
  return { fields };
7371
7533
  }
7372
7534
  function inferTypeFromValue(value) {
7373
- if (value instanceof Y3.Map) return "stringset";
7535
+ if (isStringSetYMap(value)) return "stringset";
7374
7536
  switch (typeof value) {
7375
7537
  case "string":
7376
7538
  return "string";
@@ -7385,7 +7547,7 @@ function inferTypeFromValue(value) {
7385
7547
  function readConstraints(constraintsMap) {
7386
7548
  const out = {};
7387
7549
  for (const [name, value] of constraintsMap.entries()) {
7388
- if (!(value instanceof Y3.Map)) continue;
7550
+ if (!(value instanceof Y4.Map)) continue;
7389
7551
  let fields = [];
7390
7552
  const rawFields = value.get("fields");
7391
7553
  if (typeof rawFields === "string") {
@@ -7404,7 +7566,7 @@ function readConstraints(constraintsMap) {
7404
7566
  function readRelationships(relsMap) {
7405
7567
  const out = {};
7406
7568
  for (const [name, value] of relsMap.entries()) {
7407
- if (!(value instanceof Y3.Map)) continue;
7569
+ if (!(value instanceof Y4.Map)) continue;
7408
7570
  const rel = {};
7409
7571
  for (const [k, v] of value.entries()) {
7410
7572
  rel[k] = v;
@@ -7415,6 +7577,7 @@ function readRelationships(relsMap) {
7415
7577
  }
7416
7578
  var CAMEL_TO_SNAKE = {
7417
7579
  autoAssign: "auto_assign",
7580
+ autoStamp: "auto_stamp",
7418
7581
  maxLength: "max_length",
7419
7582
  maxCount: "max_count",
7420
7583
  relatedIdField: "related_id_field",
@@ -7445,6 +7608,7 @@ function schemaToToml(schema) {
7445
7608
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
7446
7609
  lines.push(`type = ${tomlValue(field.type)}`);
7447
7610
  if (field.autoAssign) lines.push("auto_assign = true");
7611
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
7448
7612
  if (field.indexed) lines.push("indexed = true");
7449
7613
  if (field.unique) lines.push("unique = true");
7450
7614
  if (field.required) lines.push("required = true");
@@ -7479,16 +7643,16 @@ function schemaToToml(schema) {
7479
7643
  init_BaseModel();
7480
7644
 
7481
7645
  // src/utils/yDocDump.ts
7482
- import * as Y4 from "yjs";
7646
+ import * as Y5 from "yjs";
7483
7647
  function toPlain(value) {
7484
- if (value instanceof Y4.Map) {
7648
+ if (value instanceof Y5.Map) {
7485
7649
  const obj = {};
7486
7650
  value.forEach((v, k) => {
7487
7651
  obj[k] = toPlain(v);
7488
7652
  });
7489
7653
  return obj;
7490
7654
  }
7491
- if (value instanceof Y4.Array) {
7655
+ if (value instanceof Y5.Array) {
7492
7656
  return value.toArray().map((item) => toPlain(item));
7493
7657
  }
7494
7658
  return value;
@@ -7497,7 +7661,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7497
7661
  const result = {};
7498
7662
  const includeIndexes = !!options.includeIndexes;
7499
7663
  yDoc.share.forEach((type, name) => {
7500
- if (!(type instanceof Y4.Map)) return;
7664
+ if (!(type instanceof Y5.Map)) return;
7501
7665
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7502
7666
  const records = {};
7503
7667
  type.forEach((recordValue, recordId) => {