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.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;
@@ -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
  /**
package/dist/browser.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
  }
@@ -2408,7 +2446,7 @@ __export(BaseModel_exports, {
2408
2446
  UniqueConstraintViolationError: () => UniqueConstraintViolationError,
2409
2447
  generateULID: () => generateULID
2410
2448
  });
2411
- import * as Y2 from "yjs";
2449
+ import * as Y3 from "yjs";
2412
2450
  import { ulid } from "ulid";
2413
2451
  function generateULID() {
2414
2452
  return ulid();
@@ -2423,6 +2461,7 @@ var init_BaseModel = __esm({
2423
2461
  init_CursorManager();
2424
2462
  init_sql();
2425
2463
  init_metaSync();
2464
+ init_yjsValues();
2426
2465
  LogLevel = /* @__PURE__ */ ((LogLevel2) => {
2427
2466
  LogLevel2[LogLevel2["SILENT"] = 0] = "SILENT";
2428
2467
  LogLevel2[LogLevel2["ERROR"] = 1] = "ERROR";
@@ -2931,7 +2970,30 @@ var init_BaseModel = __esm({
2931
2970
  if (verboseEnabled) {
2932
2971
  Logger.verbose(`[getFromYjs] Returning field value: ${fieldValue}`);
2933
2972
  }
2934
- return fieldValue;
2973
+ return this.normalizeNonStringSetValue(fieldKey, fieldValue, schema, modelName);
2974
+ }
2975
+ /**
2976
+ * Defensive normalization for the read/sync boundary (issue #625).
2977
+ *
2978
+ * Stringset fields are owned by getStringSetFromYjs (which expects the
2979
+ * raw Y.Map / legacy plain-object shape — #561 / #601), so they pass
2980
+ * through untouched. For any other field, a composite Yjs primitive
2981
+ * (Y.Map / Y.Array / Y.Text) must never reach user code or the DB layer
2982
+ * raw: normalize it to its plain-JS projection (toJSON / toString) and
2983
+ * warn, rather than throwing. Scalars pass through unchanged.
2984
+ */
2985
+ normalizeNonStringSetValue(fieldKey, value, schema, modelName) {
2986
+ const fieldOptions = schema?.fields?.get(fieldKey);
2987
+ if (fieldOptions?.type === "stringset") {
2988
+ return value;
2989
+ }
2990
+ if (isYjsComposite(value)) {
2991
+ Logger.warn(
2992
+ `[${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.`
2993
+ );
2994
+ return normalizeYjsValue(value);
2995
+ }
2996
+ return value;
2935
2997
  }
2936
2998
  getValue(fieldKey) {
2937
2999
  const verboseEnabled = Logger.getLogLevel() >= 5 /* VERBOSE */;
@@ -3012,7 +3074,7 @@ var init_BaseModel = __esm({
3012
3074
  validateBeforeSave() {
3013
3075
  const schema = this.constructor.getSchema();
3014
3076
  for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
3015
- const currentValue = this.getValue(fieldKey);
3077
+ const currentValue = fieldKey === "id" ? this.id : this.getValue(fieldKey);
3016
3078
  if (fieldOptions.required && (currentValue === null || currentValue === void 0)) {
3017
3079
  throw new Error(`Field ${fieldKey} is required before save`);
3018
3080
  }
@@ -3076,7 +3138,7 @@ var init_BaseModel = __esm({
3076
3138
  }
3077
3139
  getStringSetFromYjs(fieldName) {
3078
3140
  const yjsData = this.getFromYjs(fieldName);
3079
- if (yjsData instanceof Y2.Map) {
3141
+ if (yjsData instanceof Y3.Map) {
3080
3142
  return Array.from(yjsData.keys());
3081
3143
  }
3082
3144
  if (yjsData && typeof yjsData === "object") {
@@ -3184,7 +3246,7 @@ var init_BaseModel = __esm({
3184
3246
  }
3185
3247
  const extractStringSetValues = (value) => {
3186
3248
  if (!value) return [];
3187
- if (value instanceof Y2.Map) {
3249
+ if (value instanceof Y3.Map) {
3188
3250
  return Array.from(value.entries()).filter(([, isMember]) => Boolean(isMember)).map(([key]) => key);
3189
3251
  }
3190
3252
  if (Array.isArray(value)) {
@@ -3201,7 +3263,7 @@ var init_BaseModel = __esm({
3201
3263
  if (!recordId) continue;
3202
3264
  let itemData;
3203
3265
  const stringSetValuesByField = {};
3204
- if (recordData instanceof Y2.Map) {
3266
+ if (recordData instanceof Y3.Map) {
3205
3267
  itemData = {};
3206
3268
  const unknownFields = [];
3207
3269
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3320,7 +3382,7 @@ var init_BaseModel = __esm({
3320
3382
  const buildUniqueKey = (recordData, fields) => {
3321
3383
  const keyParts = [];
3322
3384
  for (const field of fields) {
3323
- const value = recordData instanceof Y2.Map ? recordData.get(field) : recordData[field];
3385
+ const value = recordData instanceof Y3.Map ? recordData.get(field) : recordData[field];
3324
3386
  if (value === null || value === void 0) {
3325
3387
  return null;
3326
3388
  }
@@ -3330,7 +3392,7 @@ var init_BaseModel = __esm({
3330
3392
  };
3331
3393
  const extractItemData = (key, recordData) => {
3332
3394
  let itemData;
3333
- if (recordData instanceof Y2.Map) {
3395
+ if (recordData instanceof Y3.Map) {
3334
3396
  itemData = {};
3335
3397
  const unknownFields = [];
3336
3398
  for (const [fieldKey, value] of recordData.entries()) {
@@ -3343,7 +3405,14 @@ var init_BaseModel = __esm({
3343
3405
  continue;
3344
3406
  }
3345
3407
  if (value !== void 0) {
3346
- itemData[fieldKey] = value;
3408
+ if (isYjsComposite(value)) {
3409
+ Logger.warn(
3410
+ `[${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.`
3411
+ );
3412
+ itemData[fieldKey] = normalizeYjsValueForStorage(value);
3413
+ } else {
3414
+ itemData[fieldKey] = value;
3415
+ }
3347
3416
  }
3348
3417
  }
3349
3418
  if (unknownFields.length > 0) {
@@ -3366,7 +3435,14 @@ var init_BaseModel = __esm({
3366
3435
  continue;
3367
3436
  }
3368
3437
  if (value !== void 0) {
3369
- filteredData[fieldKey] = value;
3438
+ if (isYjsComposite(value)) {
3439
+ Logger.warn(
3440
+ `[${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.`
3441
+ );
3442
+ filteredData[fieldKey] = normalizeYjsValueForStorage(value);
3443
+ } else {
3444
+ filteredData[fieldKey] = value;
3445
+ }
3370
3446
  }
3371
3447
  }
3372
3448
  if (unknownFields.length > 0) {
@@ -3446,7 +3522,7 @@ var init_BaseModel = __esm({
3446
3522
  if (!recordData || !key) continue;
3447
3523
  const itemData = extractItemData(key, recordData);
3448
3524
  if (!itemData) continue;
3449
- if (change.action === "add" && recordData instanceof Y2.Map) {
3525
+ if (change.action === "add" && recordData instanceof Y3.Map) {
3450
3526
  Logger.verbose(
3451
3527
  `[${this.name}] Setting up observer on newly added nested YMap for record ${key} in document ${docId}`
3452
3528
  );
@@ -3591,7 +3667,7 @@ var init_BaseModel = __esm({
3591
3667
  `[${this.name}] Setting up observers on existing nested YMaps for ${modelName}/${docId}...`
3592
3668
  );
3593
3669
  for (const [recordId, recordData] of documentYMap.entries()) {
3594
- if (recordData instanceof Y2.Map) {
3670
+ if (recordData instanceof Y3.Map) {
3595
3671
  Logger.verbose(
3596
3672
  `[${this.name}] Setting up observer on existing nested YMap for record ${recordId} in document ${docId}`
3597
3673
  );
@@ -3776,6 +3852,11 @@ var init_BaseModel = __esm({
3776
3852
  Logger.debug(
3777
3853
  `[_diffWithYjsData] Field '${key}' - Y.js value: ${yjsValue}, local value: ${localValue}`
3778
3854
  );
3855
+ if (isYjsComposite(yjsValue)) {
3856
+ Logger.warn(
3857
+ `[${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).`
3858
+ );
3859
+ }
3779
3860
  if (yjsValue === void 0) {
3780
3861
  Logger.debug(
3781
3862
  `[_diffWithYjsData] Field '${key}' not in Y.js, adding to 'added'`
@@ -3813,8 +3894,8 @@ var init_BaseModel = __esm({
3813
3894
  */
3814
3895
  applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3815
3896
  let nested = recordYMap.get(fieldName);
3816
- if (!(nested instanceof Y2.Map)) {
3817
- const migrated = new Y2.Map();
3897
+ if (!(nested instanceof Y3.Map)) {
3898
+ const migrated = new Y3.Map();
3818
3899
  if (nested && typeof nested === "object") {
3819
3900
  for (const [member, marker] of Object.entries(
3820
3901
  nested
@@ -4001,6 +4082,29 @@ var init_BaseModel = __esm({
4001
4082
  );
4002
4083
  throw new Error("Cannot save item without an id. Ensure id is set.");
4003
4084
  }
4085
+ const preTransactCreateStamps = /* @__PURE__ */ new Set();
4086
+ {
4087
+ const existingRecord = targetYMap.get(this.id);
4088
+ const isNew = !existingRecord;
4089
+ for (const [fieldKey, fieldOptions] of schema.fields.entries()) {
4090
+ const stamp = fieldOptions.autoStamp;
4091
+ if (!stamp) continue;
4092
+ const callerExplicit = this.hasLocalChange(fieldKey);
4093
+ if (callerExplicit) {
4094
+ const localValue = this._localChanges[fieldKey];
4095
+ if (localValue !== null && localValue !== void 0) continue;
4096
+ }
4097
+ if (stamp === "create") {
4098
+ if (!isNew) continue;
4099
+ const persisted = fieldKey === "id" ? this.id : this.getValue(fieldKey);
4100
+ if (persisted !== null && persisted !== void 0) continue;
4101
+ }
4102
+ this[fieldKey] = Date.now();
4103
+ if (stamp === "create") {
4104
+ preTransactCreateStamps.add(fieldKey);
4105
+ }
4106
+ }
4107
+ }
4004
4108
  this.validateBeforeSave();
4005
4109
  Logger.debug(`[${modelName}.save] About to get current JS state`);
4006
4110
  Logger.debug(`[${modelName}.save] _localChanges:`, this._localChanges);
@@ -4031,6 +4135,14 @@ var init_BaseModel = __esm({
4031
4135
  }
4032
4136
  }
4033
4137
  }
4138
+ for (const fieldKey of preTransactCreateStamps) {
4139
+ if (this._localChanges && fieldKey in this._localChanges) {
4140
+ delete this._localChanges[fieldKey];
4141
+ }
4142
+ if (fieldKey in dataToSave) {
4143
+ delete dataToSave[fieldKey];
4144
+ }
4145
+ }
4034
4146
  }
4035
4147
  }
4036
4148
  if (!targetYMap) {
@@ -4043,7 +4155,7 @@ var init_BaseModel = __esm({
4043
4155
  Logger.debug(`[${modelName}.save] isUpdate: ${isUpdate}`);
4044
4156
  Logger.debug(`[${modelName}.save] recordYMap exists: ${!!recordYMap}`);
4045
4157
  if (!recordYMap) {
4046
- recordYMap = new Y2.Map();
4158
+ recordYMap = new Y3.Map();
4047
4159
  Logger.verbose(
4048
4160
  `[${modelName}] Creating new nested YMap for record ${this.id} in document ${targetDocId || "legacy"}`
4049
4161
  );
@@ -5070,7 +5182,7 @@ var init_BaseModel = __esm({
5070
5182
  );
5071
5183
  }
5072
5184
  for (const [recordId, recordYMap] of documentYMap.entries()) {
5073
- if (recordYMap instanceof Y2.Map) {
5185
+ if (recordYMap instanceof Y3.Map) {
5074
5186
  const instance = new this({ id: recordId });
5075
5187
  instance._metaDocId = docId;
5076
5188
  const connectedDoc = modelConstructor.connectedDocuments.get(docId);
@@ -5141,7 +5253,7 @@ var init_BaseModel = __esm({
5141
5253
  const documentYMap = modelConstructor.documentYMaps.get(documentYMapKey);
5142
5254
  if (documentYMap) {
5143
5255
  const recordYMap2 = documentYMap.get(recordId2);
5144
- if (recordYMap2 && recordYMap2 instanceof Y2.Map) {
5256
+ if (recordYMap2 && recordYMap2 instanceof Y3.Map) {
5145
5257
  const instance = new modelConstructor({
5146
5258
  id: recordId2
5147
5259
  });
@@ -5178,7 +5290,7 @@ var init_BaseModel = __esm({
5178
5290
  if (!recordYMap) {
5179
5291
  return null;
5180
5292
  }
5181
- if (recordYMap instanceof Y2.Map) {
5293
+ if (recordYMap instanceof Y3.Map) {
5182
5294
  const instance = new modelConstructor({
5183
5295
  id: recordId
5184
5296
  });
@@ -5410,7 +5522,7 @@ var init_BaseModel = __esm({
5410
5522
  for (const [fieldKey, fieldOptions] of schema.fields) {
5411
5523
  if (fieldOptions?.type !== "stringset") continue;
5412
5524
  const value = recordYMap.get(fieldKey);
5413
- if (value instanceof Y2.Map) {
5525
+ if (value instanceof Y3.Map) {
5414
5526
  this.observeStringSetMapOnce(value);
5415
5527
  }
5416
5528
  }
@@ -5457,7 +5569,14 @@ var init_BaseModel = __esm({
5457
5569
  continue;
5458
5570
  }
5459
5571
  if (value !== void 0) {
5460
- updatedData[key] = value;
5572
+ if (isYjsComposite(value)) {
5573
+ Logger.warn(
5574
+ `[${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.`
5575
+ );
5576
+ updatedData[key] = normalizeYjsValueForStorage(value);
5577
+ } else {
5578
+ updatedData[key] = value;
5579
+ }
5461
5580
  }
5462
5581
  }
5463
5582
  if (unknownFields.length > 0) {
@@ -5537,7 +5656,14 @@ var init_BaseModel = __esm({
5537
5656
  continue;
5538
5657
  }
5539
5658
  if (value !== void 0) {
5540
- updatedData[key] = value;
5659
+ if (isYjsComposite(value)) {
5660
+ Logger.warn(
5661
+ `[${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.`
5662
+ );
5663
+ updatedData[key] = normalizeYjsValueForStorage(value);
5664
+ } else {
5665
+ updatedData[key] = value;
5666
+ }
5541
5667
  }
5542
5668
  }
5543
5669
  if (unknownFields.length > 0) {
@@ -6489,7 +6615,8 @@ function attachAndRegisterModel(modelClass, schema) {
6489
6615
  }
6490
6616
 
6491
6617
  // src/utils/yDocSchema.ts
6492
- import * as Y3 from "yjs";
6618
+ init_yjsValues();
6619
+ import * as Y4 from "yjs";
6493
6620
  function discoverSchema(yDoc) {
6494
6621
  const models = {};
6495
6622
  const metaNames = /* @__PURE__ */ new Set();
@@ -6523,7 +6650,7 @@ function discoverModelNames(yDoc) {
6523
6650
  function materializeMap(yDoc, key) {
6524
6651
  try {
6525
6652
  const map = yDoc.getMap(key);
6526
- return map instanceof Y3.Map ? map : null;
6653
+ return map instanceof Y4.Map ? map : null;
6527
6654
  } catch {
6528
6655
  return null;
6529
6656
  }
@@ -6533,11 +6660,11 @@ function readModelMeta(metaMap) {
6533
6660
  let constraints;
6534
6661
  let relationships;
6535
6662
  for (const [key, value] of metaMap.entries()) {
6536
- if (key === "_constraints" && value instanceof Y3.Map) {
6663
+ if (key === "_constraints" && value instanceof Y4.Map) {
6537
6664
  constraints = readConstraints(value);
6538
- } else if (key === "_relationships" && value instanceof Y3.Map) {
6665
+ } else if (key === "_relationships" && value instanceof Y4.Map) {
6539
6666
  relationships = readRelationships(value);
6540
- } else if (value instanceof Y3.Map) {
6667
+ } else if (value instanceof Y4.Map) {
6541
6668
  fields[key] = readFieldMeta(value);
6542
6669
  }
6543
6670
  }
@@ -6556,6 +6683,10 @@ function readFieldMeta(fieldMap) {
6556
6683
  if (fieldMap.get("unique") === true) field.unique = true;
6557
6684
  if (fieldMap.get("required") === true) field.required = true;
6558
6685
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
6686
+ const autoStamp = fieldMap.get("autoStamp");
6687
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
6688
+ field.autoStamp = autoStamp;
6689
+ }
6559
6690
  const def = fieldMap.get("default");
6560
6691
  if (def !== void 0) field.default = def;
6561
6692
  const maxLength = fieldMap.get("maxLength");
@@ -6568,7 +6699,7 @@ function inferModelFromData(dataMap) {
6568
6699
  const fields = {};
6569
6700
  let sampled = 0;
6570
6701
  for (const [_recordId, recordValue] of dataMap.entries()) {
6571
- if (!(recordValue instanceof Y3.Map)) continue;
6702
+ if (!(recordValue instanceof Y4.Map)) continue;
6572
6703
  if (++sampled > 5) break;
6573
6704
  for (const [fieldName, value] of recordValue.entries()) {
6574
6705
  if (fieldName.startsWith("_")) continue;
@@ -6581,7 +6712,7 @@ function inferModelFromData(dataMap) {
6581
6712
  return { fields };
6582
6713
  }
6583
6714
  function inferTypeFromValue(value) {
6584
- if (value instanceof Y3.Map) return "stringset";
6715
+ if (isStringSetYMap(value)) return "stringset";
6585
6716
  switch (typeof value) {
6586
6717
  case "string":
6587
6718
  return "string";
@@ -6596,7 +6727,7 @@ function inferTypeFromValue(value) {
6596
6727
  function readConstraints(constraintsMap) {
6597
6728
  const out = {};
6598
6729
  for (const [name, value] of constraintsMap.entries()) {
6599
- if (!(value instanceof Y3.Map)) continue;
6730
+ if (!(value instanceof Y4.Map)) continue;
6600
6731
  let fields = [];
6601
6732
  const rawFields = value.get("fields");
6602
6733
  if (typeof rawFields === "string") {
@@ -6615,7 +6746,7 @@ function readConstraints(constraintsMap) {
6615
6746
  function readRelationships(relsMap) {
6616
6747
  const out = {};
6617
6748
  for (const [name, value] of relsMap.entries()) {
6618
- if (!(value instanceof Y3.Map)) continue;
6749
+ if (!(value instanceof Y4.Map)) continue;
6619
6750
  const rel = {};
6620
6751
  for (const [k, v] of value.entries()) {
6621
6752
  rel[k] = v;
@@ -6626,6 +6757,7 @@ function readRelationships(relsMap) {
6626
6757
  }
6627
6758
  var CAMEL_TO_SNAKE = {
6628
6759
  autoAssign: "auto_assign",
6760
+ autoStamp: "auto_stamp",
6629
6761
  maxLength: "max_length",
6630
6762
  maxCount: "max_count",
6631
6763
  relatedIdField: "related_id_field",
@@ -6656,6 +6788,7 @@ function schemaToToml(schema) {
6656
6788
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
6657
6789
  lines.push(`type = ${tomlValue(field.type)}`);
6658
6790
  if (field.autoAssign) lines.push("auto_assign = true");
6791
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
6659
6792
  if (field.indexed) lines.push("indexed = true");
6660
6793
  if (field.unique) lines.push("unique = true");
6661
6794
  if (field.required) lines.push("required = true");
@@ -6702,10 +6835,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
6702
6835
  "unique",
6703
6836
  "required",
6704
6837
  "auto_assign",
6838
+ "auto_stamp",
6705
6839
  "max_length",
6706
6840
  "max_count",
6707
- "default"
6841
+ "default",
6842
+ "enum"
6708
6843
  ]);
6844
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
6709
6845
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
6710
6846
  "fields",
6711
6847
  "relationships",
@@ -6747,9 +6883,37 @@ function parseFieldOptions(raw, context, strict) {
6747
6883
  if (raw.unique === true) opts.unique = true;
6748
6884
  if (raw.required === true) opts.required = true;
6749
6885
  if (raw.auto_assign === true) opts.autoAssign = true;
6886
+ if (raw.auto_stamp !== void 0) {
6887
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
6888
+ throw new Error(
6889
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
6890
+ ...VALID_AUTO_STAMP_VALUES
6891
+ ].join(", ")}`
6892
+ );
6893
+ }
6894
+ opts.autoStamp = raw.auto_stamp;
6895
+ }
6750
6896
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
6751
6897
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
6752
6898
  if (raw.default !== void 0) opts.default = raw.default;
6899
+ if (raw.enum !== void 0) {
6900
+ if (raw.type !== "string") {
6901
+ throw new Error(
6902
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
6903
+ );
6904
+ }
6905
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
6906
+ throw new Error(
6907
+ `${context}: \`enum\` must be a non-empty array of strings.`
6908
+ );
6909
+ }
6910
+ if (!raw.enum.every((v) => typeof v === "string")) {
6911
+ throw new Error(
6912
+ `${context}: \`enum\` values must all be strings.`
6913
+ );
6914
+ }
6915
+ opts.enum = [...raw.enum];
6916
+ }
6753
6917
  return opts;
6754
6918
  }
6755
6919
  function requireField(raw, field, context) {
package/dist/client.cjs CHANGED
@@ -84,15 +84,25 @@ var init_DocumentQueryTranslator = __esm({
84
84
  }
85
85
  });
86
86
 
87
+ // src/utils/yjsValues.ts
88
+ var Y;
89
+ var init_yjsValues = __esm({
90
+ "src/utils/yjsValues.ts"() {
91
+ "use strict";
92
+ Y = __toESM(require("yjs"), 1);
93
+ }
94
+ });
95
+
87
96
  // src/models/metaSync.ts
88
97
  function registerFunctionDefault(fn, name) {
89
98
  KNOWN_FUNCTION_DEFAULTS.set(fn, name);
90
99
  }
91
- var Y, KNOWN_FUNCTION_DEFAULTS;
100
+ var Y2, KNOWN_FUNCTION_DEFAULTS;
92
101
  var init_metaSync = __esm({
93
102
  "src/models/metaSync.ts"() {
94
103
  "use strict";
95
- Y = __toESM(require("yjs"), 1);
104
+ Y2 = __toESM(require("yjs"), 1);
105
+ init_yjsValues();
96
106
  KNOWN_FUNCTION_DEFAULTS = /* @__PURE__ */ new WeakMap();
97
107
  }
98
108
  });
@@ -101,11 +111,11 @@ var init_metaSync = __esm({
101
111
  function generateULID() {
102
112
  return (0, import_ulid.ulid)();
103
113
  }
104
- var Y2, import_ulid, Logger;
114
+ var Y3, import_ulid, Logger;
105
115
  var init_BaseModel = __esm({
106
116
  "src/models/BaseModel.ts"() {
107
117
  "use strict";
108
- Y2 = __toESM(require("yjs"), 1);
118
+ Y3 = __toESM(require("yjs"), 1);
109
119
  import_ulid = require("ulid");
110
120
  init_StringSet();
111
121
  init_documentTypes();
@@ -113,6 +123,7 @@ var init_BaseModel = __esm({
113
123
  init_CursorManager();
114
124
  init_sql();
115
125
  init_metaSync();
126
+ init_yjsValues();
116
127
  Logger = class {
117
128
  static _logLevel = 1 /* ERROR */;
118
129
  static _logCallback = null;