js-bao 0.4.0 → 0.4.2

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.js CHANGED
@@ -1836,6 +1836,9 @@ var init_BaseModel = __esm({
1836
1836
  static connectedDocuments = /* @__PURE__ */ new Map();
1837
1837
  static documentYMaps = /* @__PURE__ */ new Map();
1838
1838
  // Maps docId to YMap for that document
1839
+ // Tracks nested-Y.Map stringset fields we've already attached observers to,
1840
+ // so attachStringSetObserversToRecord() is idempotent across re-entry.
1841
+ static _observedStringSetMaps = /* @__PURE__ */ new WeakSet();
1839
1842
  // Copy-on-write state management
1840
1843
  _localChanges = null;
1841
1844
  _isDirty = false;
@@ -2355,16 +2358,15 @@ var init_BaseModel = __esm({
2355
2358
  this._isDirty = true;
2356
2359
  }
2357
2360
  getStringSetCurrentValues(fieldName) {
2358
- const yjsData = this.getFromYjs(fieldName);
2359
- if (yjsData && typeof yjsData === "object") {
2360
- return Object.keys(yjsData);
2361
- }
2362
- return [];
2361
+ return this.getStringSetFromYjs(fieldName);
2363
2362
  }
2364
2363
  getStringSetFromYjs(fieldName) {
2365
2364
  const yjsData = this.getFromYjs(fieldName);
2365
+ if (yjsData instanceof Y2.Map) {
2366
+ return Array.from(yjsData.keys());
2367
+ }
2366
2368
  if (yjsData && typeof yjsData === "object") {
2367
- return Object.keys(yjsData);
2369
+ return Object.keys(yjsData).filter((k) => Boolean(yjsData[k]));
2368
2370
  }
2369
2371
  return [];
2370
2372
  }
@@ -2975,7 +2977,7 @@ var init_BaseModel = __esm({
2975
2977
  Logger.debug(
2976
2978
  `[_diffWithYjsData] No unsaved changes, returning empty diff`
2977
2979
  );
2978
- return { added: {}, modified: {}, removed: [] };
2980
+ return { added: {}, modified: {}, removed: [], stringSetChanges: {} };
2979
2981
  }
2980
2982
  const modelConstructor = this.constructor;
2981
2983
  const schema = modelConstructor.getSchema();
@@ -3006,6 +3008,7 @@ var init_BaseModel = __esm({
3006
3008
  const added = {};
3007
3009
  const modified = {};
3008
3010
  const removed = [];
3011
+ const stringSetChanges = {};
3009
3012
  if (!recordYMap) {
3010
3013
  Logger.debug(
3011
3014
  `[_diffWithYjsData] No existing recordYMap, treating all local changes as 'added'`
@@ -3014,11 +3017,12 @@ var init_BaseModel = __esm({
3014
3017
  for (const [key, value] of Object.entries(this._localChanges)) {
3015
3018
  Logger.debug(`[_diffWithYjsData] Adding field '${key}': ${value}`);
3016
3019
  if (value && value.type === "stringset") {
3017
- const stringSetData = {};
3018
- for (const addition of value.additions) {
3019
- stringSetData[addition] = true;
3020
+ if (value.additions.size > 0 || value.removals.size > 0) {
3021
+ stringSetChanges[key] = {
3022
+ additions: value.additions,
3023
+ removals: value.removals
3024
+ };
3020
3025
  }
3021
- added[key] = stringSetData;
3022
3026
  } else {
3023
3027
  added[key] = value;
3024
3028
  }
@@ -3027,9 +3031,10 @@ var init_BaseModel = __esm({
3027
3031
  Logger.debug(`[_diffWithYjsData] Final diff for new record:`, {
3028
3032
  added,
3029
3033
  modified: {},
3030
- removed: []
3034
+ removed: [],
3035
+ stringSetChanges
3031
3036
  });
3032
- return { added, modified, removed: [] };
3037
+ return { added, modified, removed: [], stringSetChanges };
3033
3038
  }
3034
3039
  Logger.debug(
3035
3040
  `[_diffWithYjsData] Existing record found, comparing local changes with Y.js data`
@@ -3046,21 +3051,11 @@ var init_BaseModel = __esm({
3046
3051
  `[_diffWithYjsData] Processing field '${key}' with local value: ${localValue}`
3047
3052
  );
3048
3053
  if (localValue && localValue.type === "stringset") {
3049
- const currentYjsData = recordYMap.get(key) || {};
3050
- const newStringSetData = {
3051
- ...currentYjsData
3052
- };
3053
- for (const addition of localValue.additions) {
3054
- newStringSetData[addition] = true;
3055
- }
3056
- for (const removal of localValue.removals) {
3057
- delete newStringSetData[removal];
3058
- }
3059
- const yjsValue = recordYMap.get(key);
3060
- if (yjsValue === void 0) {
3061
- added[key] = newStringSetData;
3062
- } else if (!this._deepEqual(yjsValue, newStringSetData)) {
3063
- modified[key] = newStringSetData;
3054
+ if (localValue.additions.size > 0 || localValue.removals.size > 0) {
3055
+ stringSetChanges[key] = {
3056
+ additions: localValue.additions,
3057
+ removals: localValue.removals
3058
+ };
3064
3059
  }
3065
3060
  } else {
3066
3061
  const yjsValue = recordYMap.get(key);
@@ -3086,14 +3081,43 @@ var init_BaseModel = __esm({
3086
3081
  Logger.debug(`[_diffWithYjsData] Final diff result:`, {
3087
3082
  added,
3088
3083
  modified,
3089
- removed
3084
+ removed,
3085
+ stringSetChanges
3090
3086
  });
3091
3087
  Logger.verbose(`[${modelConstructor.name}] Diff for ${this.id}:`, {
3092
3088
  added: Object.keys(added),
3093
3089
  modified: Object.keys(modified),
3094
- removed
3090
+ removed,
3091
+ stringSetChanges: Object.keys(stringSetChanges)
3095
3092
  });
3096
- return { added, modified, removed };
3093
+ return { added, modified, removed, stringSetChanges };
3094
+ }
3095
+ /**
3096
+ * Apply a stringset change set to the parent record's Y.Map by mutating a
3097
+ * nested Y.Map keyed by member. Migrates from a legacy plain-object value
3098
+ * if the field hasn't been touched since the wire-format change in #561.
3099
+ */
3100
+ applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3101
+ let nested = recordYMap.get(fieldName);
3102
+ if (!(nested instanceof Y2.Map)) {
3103
+ const migrated = new Y2.Map();
3104
+ if (nested && typeof nested === "object") {
3105
+ for (const [member, marker] of Object.entries(
3106
+ nested
3107
+ )) {
3108
+ if (Boolean(marker)) migrated.set(member, true);
3109
+ }
3110
+ }
3111
+ recordYMap.set(fieldName, migrated);
3112
+ nested = migrated;
3113
+ }
3114
+ const target = nested;
3115
+ for (const member of change.additions) {
3116
+ target.set(member, true);
3117
+ }
3118
+ for (const member of change.removals) {
3119
+ target.delete(member);
3120
+ }
3097
3121
  }
3098
3122
  /**
3099
3123
  * Deep equality check for comparing field values
@@ -3332,7 +3356,7 @@ var init_BaseModel = __esm({
3332
3356
  Logger.debug(`[${modelName}.save] About to calculate diff`);
3333
3357
  const diff = this._diffWithYjsData();
3334
3358
  Logger.debug(`[${modelName}.save] Diff result:`, diff);
3335
- const hasChanges = Object.keys(diff.added).length > 0 || Object.keys(diff.modified).length > 0 || diff.removed.length > 0;
3359
+ const hasChanges = Object.keys(diff.added).length > 0 || Object.keys(diff.modified).length > 0 || diff.removed.length > 0 || Object.keys(diff.stringSetChanges).length > 0;
3336
3360
  Logger.debug(`[${modelName}.save] hasChanges: ${hasChanges}`);
3337
3361
  if (!hasChanges && isUpdate) {
3338
3362
  Logger.verbose(
@@ -3450,6 +3474,16 @@ var init_BaseModel = __esm({
3450
3474
  Logger.debug(`[${modelName}.save] Removing field '${key}'`);
3451
3475
  recordYMap.delete(key);
3452
3476
  }
3477
+ for (const [fieldName, change] of Object.entries(diff.stringSetChanges)) {
3478
+ Logger.debug(
3479
+ `[${modelName}.save] Applying stringset change to field '${fieldName}':`,
3480
+ {
3481
+ additions: Array.from(change.additions),
3482
+ removals: Array.from(change.removals)
3483
+ }
3484
+ );
3485
+ this.applyStringSetChangeToYMap(recordYMap, fieldName, change);
3486
+ }
3453
3487
  Logger.debug(
3454
3488
  `[${modelName}.save] After applying changes, recordYMap contents:`,
3455
3489
  Object.fromEntries(recordYMap.entries())
@@ -4634,6 +4668,39 @@ var init_BaseModel = __esm({
4634
4668
  _BaseModel.prototype.setValue = originalSetValue;
4635
4669
  }
4636
4670
  }
4671
+ /**
4672
+ * Attach a one-shot observer to a nested-Y.Map stringset field. Calls
4673
+ * notifyListeners() when the nested map's keys change (i.e. when a remote
4674
+ * member arrives via Y.applyUpdate, or a local per-member set/delete).
4675
+ * Idempotent — re-calling on the same Y.Map is a no-op.
4676
+ */
4677
+ static observeStringSetMapOnce(nestedMap) {
4678
+ if (_BaseModel._observedStringSetMaps.has(nestedMap)) return;
4679
+ _BaseModel._observedStringSetMaps.add(nestedMap);
4680
+ const modelConstructor = this;
4681
+ nestedMap.observe(() => {
4682
+ Logger.verbose(
4683
+ `[${modelConstructor.name}] Nested stringset Y.Map changed; notifying listeners`
4684
+ );
4685
+ modelConstructor.notifyListeners();
4686
+ });
4687
+ }
4688
+ /**
4689
+ * Walk the schema's stringset fields on `recordYMap` and observe any nested
4690
+ * Y.Map values. Called from observer setup and from the parent observer body
4691
+ * so newly-arrived stringset Y.Maps (local migration or remote create) get
4692
+ * an observer too.
4693
+ */
4694
+ static attachStringSetObserversToRecord(recordYMap, schema) {
4695
+ if (!schema?.fields) return;
4696
+ for (const [fieldKey, fieldOptions] of schema.fields) {
4697
+ if (fieldOptions?.type !== "stringset") continue;
4698
+ const value = recordYMap.get(fieldKey);
4699
+ if (value instanceof Y2.Map) {
4700
+ this.observeStringSetMapOnce(value);
4701
+ }
4702
+ }
4703
+ }
4637
4704
  /**
4638
4705
  * Sets up deep observation on a nested YMap to sync field-level changes to the database
4639
4706
  */
@@ -4650,11 +4717,13 @@ var init_BaseModel = __esm({
4650
4717
  Logger.verbose(
4651
4718
  `[${modelName}] Setting up nested YMap observer for record ${recordId}`
4652
4719
  );
4720
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
4653
4721
  recordYMap.observe(async (event) => {
4654
4722
  Logger.verbose(
4655
4723
  `[${modelName}] Nested YMap change detected for record ${recordId}:`,
4656
4724
  event
4657
4725
  );
4726
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
4658
4727
  const currentDbInstance = _BaseModel.dbInstance;
4659
4728
  if (!currentDbInstance) {
4660
4729
  Logger.error(
@@ -4728,11 +4797,13 @@ var init_BaseModel = __esm({
4728
4797
  Logger.verbose(
4729
4798
  `[${modelName}] Setting up nested YMap observer for record ${recordId} in document ${docId}`
4730
4799
  );
4800
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
4731
4801
  recordYMap.observe(async (event) => {
4732
4802
  Logger.verbose(
4733
4803
  `[${modelName}] Nested YMap change detected for record ${recordId} in document ${docId}:`,
4734
4804
  event
4735
4805
  );
4806
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
4736
4807
  const currentDbInstance = _BaseModel.dbInstance;
4737
4808
  if (!currentDbInstance) {
4738
4809
  Logger.error(
@@ -6843,6 +6914,7 @@ function defineModelSchema(input) {
6843
6914
  const { name, fields } = input;
6844
6915
  const options = {
6845
6916
  name,
6917
+ className: input.options?.className,
6846
6918
  uniqueConstraints: input.options?.uniqueConstraints ? [...input.options.uniqueConstraints] : void 0,
6847
6919
  relationships: input.options?.relationships
6848
6920
  };
@@ -6942,18 +7014,13 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
6942
7014
  function attachSchemaToClass(modelClass, schema) {
6943
7015
  modelClass.schema = schema;
6944
7016
  modelClass.modelName = schema.options.name;
6945
- if (!modelClass.getSchema) {
6946
- Object.defineProperty(modelClass, "getSchema", {
6947
- value: function() {
6948
- return schema.buildRuntimeShape(modelClass);
6949
- },
6950
- writable: false
6951
- });
6952
- } else {
6953
- modelClass.getSchema = function() {
7017
+ Object.defineProperty(modelClass, "getSchema", {
7018
+ value: function() {
6954
7019
  return schema.buildRuntimeShape(modelClass);
6955
- };
6956
- }
7020
+ },
7021
+ writable: false,
7022
+ configurable: true
7023
+ });
6957
7024
  const runtimeShape = schema.buildRuntimeShape(modelClass);
6958
7025
  BaseModel.attachFieldAccessors(modelClass, runtimeShape.fields);
6959
7026
  return runtimeShape;
@@ -7030,12 +7097,52 @@ var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
7030
7097
  "id",
7031
7098
  "stringset"
7032
7099
  ]);
7033
- function parseFieldOptions(raw) {
7100
+ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
7101
+ "type",
7102
+ "indexed",
7103
+ "unique",
7104
+ "required",
7105
+ "auto_assign",
7106
+ "max_length",
7107
+ "max_count",
7108
+ "default"
7109
+ ]);
7110
+ var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
7111
+ "fields",
7112
+ "relationships",
7113
+ "unique_constraints",
7114
+ "class_name"
7115
+ ]);
7116
+ var KNOWN_RELATIONSHIP_KEYS = /* @__PURE__ */ new Set([
7117
+ "type",
7118
+ "model",
7119
+ "related_id_field",
7120
+ "join_model",
7121
+ "join_model_local_field",
7122
+ "join_model_related_field",
7123
+ "order_by_field",
7124
+ "order_direction",
7125
+ "join_model_order_by_field",
7126
+ "join_model_order_direction"
7127
+ ]);
7128
+ var KNOWN_UNIQUE_CONSTRAINT_KEYS = /* @__PURE__ */ new Set(["name", "fields"]);
7129
+ function checkUnknownKeys(raw, known, context, strict) {
7130
+ if (!strict) return;
7131
+ for (const key of Object.keys(raw)) {
7132
+ if (!known.has(key)) {
7133
+ throw new Error(
7134
+ `${context}: unknown key "${key}". Allowed: ${[...known].join(", ")}`
7135
+ );
7136
+ }
7137
+ }
7138
+ }
7139
+ function parseFieldOptions(raw, context, strict) {
7034
7140
  if (!raw.type || !VALID_FIELD_TYPES.has(raw.type)) {
7035
7141
  throw new Error(
7036
7142
  `Invalid field type "${raw.type}". Must be one of: ${[...VALID_FIELD_TYPES].join(", ")}`
7037
7143
  );
7038
7144
  }
7145
+ checkUnknownKeys(raw, KNOWN_FIELD_KEYS, context, strict);
7039
7146
  const opts = { type: raw.type };
7040
7147
  if (raw.indexed === true) opts.indexed = true;
7041
7148
  if (raw.unique === true) opts.unique = true;
@@ -7051,8 +7158,9 @@ function requireField(raw, field, context) {
7051
7158
  throw new Error(`Relationship ${context}: missing required field "${field}"`);
7052
7159
  }
7053
7160
  }
7054
- function parseRelationship(raw) {
7161
+ function parseRelationship(raw, context, strict) {
7055
7162
  const type = raw.type;
7163
+ checkUnknownKeys(raw, KNOWN_RELATIONSHIP_KEYS, context, strict);
7056
7164
  if (type === "refersTo") {
7057
7165
  requireField(raw, "model", "refersTo");
7058
7166
  requireField(raw, "related_id_field", "refersTo");
@@ -7094,7 +7202,8 @@ function parseRelationship(raw) {
7094
7202
  }
7095
7203
  throw new Error(`Unknown relationship type: ${type}`);
7096
7204
  }
7097
- function loadSchemaFromTomlString(tomlString) {
7205
+ function loadSchemaFromTomlString(tomlString, options = {}) {
7206
+ const strict = options.strict !== false;
7098
7207
  const parsed = parseToml(tomlString);
7099
7208
  const models = parsed.models;
7100
7209
  if (!models || typeof models !== "object") {
@@ -7102,10 +7211,20 @@ function loadSchemaFromTomlString(tomlString) {
7102
7211
  }
7103
7212
  const schemas = [];
7104
7213
  for (const [modelName, modelDef] of Object.entries(models)) {
7214
+ checkUnknownKeys(
7215
+ modelDef,
7216
+ KNOWN_MODEL_KEYS,
7217
+ `[models.${modelName}]`,
7218
+ strict
7219
+ );
7105
7220
  const fields = {};
7106
7221
  if (modelDef.fields) {
7107
7222
  for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
7108
- fields[fieldName] = parseFieldOptions(fieldDef);
7223
+ fields[fieldName] = parseFieldOptions(
7224
+ fieldDef,
7225
+ `[models.${modelName}.fields.${fieldName}]`,
7226
+ strict
7227
+ );
7109
7228
  }
7110
7229
  }
7111
7230
  let relationships;
@@ -7114,24 +7233,36 @@ function loadSchemaFromTomlString(tomlString) {
7114
7233
  for (const [relName, relDef] of Object.entries(
7115
7234
  modelDef.relationships
7116
7235
  )) {
7117
- relationships[relName] = parseRelationship(relDef);
7236
+ relationships[relName] = parseRelationship(
7237
+ relDef,
7238
+ `[models.${modelName}.relationships.${relName}]`,
7239
+ strict
7240
+ );
7118
7241
  }
7119
7242
  }
7120
7243
  let uniqueConstraints;
7121
7244
  if (modelDef.unique_constraints) {
7122
7245
  uniqueConstraints = [];
7123
7246
  for (const raw of modelDef.unique_constraints) {
7247
+ checkUnknownKeys(
7248
+ raw,
7249
+ KNOWN_UNIQUE_CONSTRAINT_KEYS,
7250
+ `[[models.${modelName}.unique_constraints]]`,
7251
+ strict
7252
+ );
7124
7253
  uniqueConstraints.push({
7125
7254
  name: raw.name,
7126
7255
  fields: [...raw.fields]
7127
7256
  });
7128
7257
  }
7129
7258
  }
7259
+ const className = typeof modelDef.class_name === "string" ? modelDef.class_name : void 0;
7130
7260
  schemas.push(
7131
7261
  defineModelSchema({
7132
7262
  name: modelName,
7133
7263
  fields,
7134
7264
  options: {
7265
+ className,
7135
7266
  uniqueConstraints,
7136
7267
  relationships
7137
7268
  }
@@ -7140,10 +7271,10 @@ function loadSchemaFromTomlString(tomlString) {
7140
7271
  }
7141
7272
  return schemas;
7142
7273
  }
7143
- async function loadSchemaFromToml(filePath) {
7274
+ async function loadSchemaFromToml(filePath, options = {}) {
7144
7275
  const { readFile } = await import("fs/promises");
7145
7276
  const content = await readFile(filePath, "utf-8");
7146
- return loadSchemaFromTomlString(content);
7277
+ return loadSchemaFromTomlString(content, options);
7147
7278
  }
7148
7279
 
7149
7280
  // src/utils/yDocSchema.ts