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.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;
@@ -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
  /**
package/dist/node.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) {
@@ -7105,7 +7231,8 @@ function attachAndRegisterModel(modelClass, schema) {
7105
7231
  }
7106
7232
 
7107
7233
  // src/utils/yDocSchema.ts
7108
- import * as Y3 from "yjs";
7234
+ init_yjsValues();
7235
+ import * as Y4 from "yjs";
7109
7236
  function discoverSchema(yDoc) {
7110
7237
  const models = {};
7111
7238
  const metaNames = /* @__PURE__ */ new Set();
@@ -7139,7 +7266,7 @@ function discoverModelNames(yDoc) {
7139
7266
  function materializeMap(yDoc, key) {
7140
7267
  try {
7141
7268
  const map = yDoc.getMap(key);
7142
- return map instanceof Y3.Map ? map : null;
7269
+ return map instanceof Y4.Map ? map : null;
7143
7270
  } catch {
7144
7271
  return null;
7145
7272
  }
@@ -7149,11 +7276,11 @@ function readModelMeta(metaMap) {
7149
7276
  let constraints;
7150
7277
  let relationships;
7151
7278
  for (const [key, value] of metaMap.entries()) {
7152
- if (key === "_constraints" && value instanceof Y3.Map) {
7279
+ if (key === "_constraints" && value instanceof Y4.Map) {
7153
7280
  constraints = readConstraints(value);
7154
- } else if (key === "_relationships" && value instanceof Y3.Map) {
7281
+ } else if (key === "_relationships" && value instanceof Y4.Map) {
7155
7282
  relationships = readRelationships(value);
7156
- } else if (value instanceof Y3.Map) {
7283
+ } else if (value instanceof Y4.Map) {
7157
7284
  fields[key] = readFieldMeta(value);
7158
7285
  }
7159
7286
  }
@@ -7172,6 +7299,10 @@ function readFieldMeta(fieldMap) {
7172
7299
  if (fieldMap.get("unique") === true) field.unique = true;
7173
7300
  if (fieldMap.get("required") === true) field.required = true;
7174
7301
  if (fieldMap.get("autoAssign") === true) field.autoAssign = true;
7302
+ const autoStamp = fieldMap.get("autoStamp");
7303
+ if (typeof autoStamp === "string" && (autoStamp === "create" || autoStamp === "update" || autoStamp === "both")) {
7304
+ field.autoStamp = autoStamp;
7305
+ }
7175
7306
  const def = fieldMap.get("default");
7176
7307
  if (def !== void 0) field.default = def;
7177
7308
  const maxLength = fieldMap.get("maxLength");
@@ -7184,7 +7315,7 @@ function inferModelFromData(dataMap) {
7184
7315
  const fields = {};
7185
7316
  let sampled = 0;
7186
7317
  for (const [_recordId, recordValue] of dataMap.entries()) {
7187
- if (!(recordValue instanceof Y3.Map)) continue;
7318
+ if (!(recordValue instanceof Y4.Map)) continue;
7188
7319
  if (++sampled > 5) break;
7189
7320
  for (const [fieldName, value] of recordValue.entries()) {
7190
7321
  if (fieldName.startsWith("_")) continue;
@@ -7197,7 +7328,7 @@ function inferModelFromData(dataMap) {
7197
7328
  return { fields };
7198
7329
  }
7199
7330
  function inferTypeFromValue(value) {
7200
- if (value instanceof Y3.Map) return "stringset";
7331
+ if (isStringSetYMap(value)) return "stringset";
7201
7332
  switch (typeof value) {
7202
7333
  case "string":
7203
7334
  return "string";
@@ -7212,7 +7343,7 @@ function inferTypeFromValue(value) {
7212
7343
  function readConstraints(constraintsMap) {
7213
7344
  const out = {};
7214
7345
  for (const [name, value] of constraintsMap.entries()) {
7215
- if (!(value instanceof Y3.Map)) continue;
7346
+ if (!(value instanceof Y4.Map)) continue;
7216
7347
  let fields = [];
7217
7348
  const rawFields = value.get("fields");
7218
7349
  if (typeof rawFields === "string") {
@@ -7231,7 +7362,7 @@ function readConstraints(constraintsMap) {
7231
7362
  function readRelationships(relsMap) {
7232
7363
  const out = {};
7233
7364
  for (const [name, value] of relsMap.entries()) {
7234
- if (!(value instanceof Y3.Map)) continue;
7365
+ if (!(value instanceof Y4.Map)) continue;
7235
7366
  const rel = {};
7236
7367
  for (const [k, v] of value.entries()) {
7237
7368
  rel[k] = v;
@@ -7242,6 +7373,7 @@ function readRelationships(relsMap) {
7242
7373
  }
7243
7374
  var CAMEL_TO_SNAKE = {
7244
7375
  autoAssign: "auto_assign",
7376
+ autoStamp: "auto_stamp",
7245
7377
  maxLength: "max_length",
7246
7378
  maxCount: "max_count",
7247
7379
  relatedIdField: "related_id_field",
@@ -7272,6 +7404,7 @@ function schemaToToml(schema) {
7272
7404
  lines.push(`[models.${modelName}.fields.${fieldName}]`);
7273
7405
  lines.push(`type = ${tomlValue(field.type)}`);
7274
7406
  if (field.autoAssign) lines.push("auto_assign = true");
7407
+ if (field.autoStamp) lines.push(`auto_stamp = ${tomlValue(field.autoStamp)}`);
7275
7408
  if (field.indexed) lines.push("indexed = true");
7276
7409
  if (field.unique) lines.push("unique = true");
7277
7410
  if (field.required) lines.push("required = true");
@@ -7318,10 +7451,13 @@ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
7318
7451
  "unique",
7319
7452
  "required",
7320
7453
  "auto_assign",
7454
+ "auto_stamp",
7321
7455
  "max_length",
7322
7456
  "max_count",
7323
- "default"
7457
+ "default",
7458
+ "enum"
7324
7459
  ]);
7460
+ var VALID_AUTO_STAMP_VALUES = /* @__PURE__ */ new Set(["create", "update", "both"]);
7325
7461
  var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
7326
7462
  "fields",
7327
7463
  "relationships",
@@ -7363,9 +7499,37 @@ function parseFieldOptions(raw, context, strict) {
7363
7499
  if (raw.unique === true) opts.unique = true;
7364
7500
  if (raw.required === true) opts.required = true;
7365
7501
  if (raw.auto_assign === true) opts.autoAssign = true;
7502
+ if (raw.auto_stamp !== void 0) {
7503
+ if (typeof raw.auto_stamp !== "string" || !VALID_AUTO_STAMP_VALUES.has(raw.auto_stamp)) {
7504
+ throw new Error(
7505
+ `${context}: invalid auto_stamp value "${raw.auto_stamp}". Must be one of: ${[
7506
+ ...VALID_AUTO_STAMP_VALUES
7507
+ ].join(", ")}`
7508
+ );
7509
+ }
7510
+ opts.autoStamp = raw.auto_stamp;
7511
+ }
7366
7512
  if (raw.max_length !== void 0) opts.maxLength = raw.max_length;
7367
7513
  if (raw.max_count !== void 0) opts.maxCount = raw.max_count;
7368
7514
  if (raw.default !== void 0) opts.default = raw.default;
7515
+ if (raw.enum !== void 0) {
7516
+ if (raw.type !== "string") {
7517
+ throw new Error(
7518
+ `${context}: \`enum\` is only valid on a "string" field, not "${raw.type}".`
7519
+ );
7520
+ }
7521
+ if (!Array.isArray(raw.enum) || raw.enum.length === 0) {
7522
+ throw new Error(
7523
+ `${context}: \`enum\` must be a non-empty array of strings.`
7524
+ );
7525
+ }
7526
+ if (!raw.enum.every((v) => typeof v === "string")) {
7527
+ throw new Error(
7528
+ `${context}: \`enum\` values must all be strings.`
7529
+ );
7530
+ }
7531
+ opts.enum = [...raw.enum];
7532
+ }
7369
7533
  return opts;
7370
7534
  }
7371
7535
  function requireField(raw, field, context) {
@@ -7496,16 +7660,16 @@ async function loadSchemaFromToml(filePath, options = {}) {
7496
7660
  init_metaSync();
7497
7661
 
7498
7662
  // src/utils/yDocDump.ts
7499
- import * as Y4 from "yjs";
7663
+ import * as Y5 from "yjs";
7500
7664
  function toPlain(value) {
7501
- if (value instanceof Y4.Map) {
7665
+ if (value instanceof Y5.Map) {
7502
7666
  const obj = {};
7503
7667
  value.forEach((v, k) => {
7504
7668
  obj[k] = toPlain(v);
7505
7669
  });
7506
7670
  return obj;
7507
7671
  }
7508
- if (value instanceof Y4.Array) {
7672
+ if (value instanceof Y5.Array) {
7509
7673
  return value.toArray().map((item) => toPlain(item));
7510
7674
  }
7511
7675
  return value;
@@ -7514,7 +7678,7 @@ function dumpYDocToPlain(yDoc, options = {}) {
7514
7678
  const result = {};
7515
7679
  const includeIndexes = !!options.includeIndexes;
7516
7680
  yDoc.share.forEach((type, name) => {
7517
- if (!(type instanceof Y4.Map)) return;
7681
+ if (!(type instanceof Y5.Map)) return;
7518
7682
  if (!includeIndexes && name.startsWith("_uniqueIdx_")) return;
7519
7683
  const records = {};
7520
7684
  type.forEach((recordValue, recordId) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-bao",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "A library providing data modeling capabilities which support live updates and queries.",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",