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/node.cjs CHANGED
@@ -2572,6 +2572,9 @@ var init_BaseModel = __esm({
2572
2572
  static connectedDocuments = /* @__PURE__ */ new Map();
2573
2573
  static documentYMaps = /* @__PURE__ */ new Map();
2574
2574
  // Maps docId to YMap for that document
2575
+ // Tracks nested-Y.Map stringset fields we've already attached observers to,
2576
+ // so attachStringSetObserversToRecord() is idempotent across re-entry.
2577
+ static _observedStringSetMaps = /* @__PURE__ */ new WeakSet();
2575
2578
  // Copy-on-write state management
2576
2579
  _localChanges = null;
2577
2580
  _isDirty = false;
@@ -3091,16 +3094,15 @@ var init_BaseModel = __esm({
3091
3094
  this._isDirty = true;
3092
3095
  }
3093
3096
  getStringSetCurrentValues(fieldName) {
3094
- const yjsData = this.getFromYjs(fieldName);
3095
- if (yjsData && typeof yjsData === "object") {
3096
- return Object.keys(yjsData);
3097
- }
3098
- return [];
3097
+ return this.getStringSetFromYjs(fieldName);
3099
3098
  }
3100
3099
  getStringSetFromYjs(fieldName) {
3101
3100
  const yjsData = this.getFromYjs(fieldName);
3101
+ if (yjsData instanceof Y2.Map) {
3102
+ return Array.from(yjsData.keys());
3103
+ }
3102
3104
  if (yjsData && typeof yjsData === "object") {
3103
- return Object.keys(yjsData);
3105
+ return Object.keys(yjsData).filter((k) => Boolean(yjsData[k]));
3104
3106
  }
3105
3107
  return [];
3106
3108
  }
@@ -3711,7 +3713,7 @@ var init_BaseModel = __esm({
3711
3713
  Logger.debug(
3712
3714
  `[_diffWithYjsData] No unsaved changes, returning empty diff`
3713
3715
  );
3714
- return { added: {}, modified: {}, removed: [] };
3716
+ return { added: {}, modified: {}, removed: [], stringSetChanges: {} };
3715
3717
  }
3716
3718
  const modelConstructor = this.constructor;
3717
3719
  const schema = modelConstructor.getSchema();
@@ -3742,6 +3744,7 @@ var init_BaseModel = __esm({
3742
3744
  const added = {};
3743
3745
  const modified = {};
3744
3746
  const removed = [];
3747
+ const stringSetChanges = {};
3745
3748
  if (!recordYMap) {
3746
3749
  Logger.debug(
3747
3750
  `[_diffWithYjsData] No existing recordYMap, treating all local changes as 'added'`
@@ -3750,11 +3753,12 @@ var init_BaseModel = __esm({
3750
3753
  for (const [key, value] of Object.entries(this._localChanges)) {
3751
3754
  Logger.debug(`[_diffWithYjsData] Adding field '${key}': ${value}`);
3752
3755
  if (value && value.type === "stringset") {
3753
- const stringSetData = {};
3754
- for (const addition of value.additions) {
3755
- stringSetData[addition] = true;
3756
+ if (value.additions.size > 0 || value.removals.size > 0) {
3757
+ stringSetChanges[key] = {
3758
+ additions: value.additions,
3759
+ removals: value.removals
3760
+ };
3756
3761
  }
3757
- added[key] = stringSetData;
3758
3762
  } else {
3759
3763
  added[key] = value;
3760
3764
  }
@@ -3763,9 +3767,10 @@ var init_BaseModel = __esm({
3763
3767
  Logger.debug(`[_diffWithYjsData] Final diff for new record:`, {
3764
3768
  added,
3765
3769
  modified: {},
3766
- removed: []
3770
+ removed: [],
3771
+ stringSetChanges
3767
3772
  });
3768
- return { added, modified, removed: [] };
3773
+ return { added, modified, removed: [], stringSetChanges };
3769
3774
  }
3770
3775
  Logger.debug(
3771
3776
  `[_diffWithYjsData] Existing record found, comparing local changes with Y.js data`
@@ -3782,21 +3787,11 @@ var init_BaseModel = __esm({
3782
3787
  `[_diffWithYjsData] Processing field '${key}' with local value: ${localValue}`
3783
3788
  );
3784
3789
  if (localValue && localValue.type === "stringset") {
3785
- const currentYjsData = recordYMap.get(key) || {};
3786
- const newStringSetData = {
3787
- ...currentYjsData
3788
- };
3789
- for (const addition of localValue.additions) {
3790
- newStringSetData[addition] = true;
3791
- }
3792
- for (const removal of localValue.removals) {
3793
- delete newStringSetData[removal];
3794
- }
3795
- const yjsValue = recordYMap.get(key);
3796
- if (yjsValue === void 0) {
3797
- added[key] = newStringSetData;
3798
- } else if (!this._deepEqual(yjsValue, newStringSetData)) {
3799
- modified[key] = newStringSetData;
3790
+ if (localValue.additions.size > 0 || localValue.removals.size > 0) {
3791
+ stringSetChanges[key] = {
3792
+ additions: localValue.additions,
3793
+ removals: localValue.removals
3794
+ };
3800
3795
  }
3801
3796
  } else {
3802
3797
  const yjsValue = recordYMap.get(key);
@@ -3822,14 +3817,43 @@ var init_BaseModel = __esm({
3822
3817
  Logger.debug(`[_diffWithYjsData] Final diff result:`, {
3823
3818
  added,
3824
3819
  modified,
3825
- removed
3820
+ removed,
3821
+ stringSetChanges
3826
3822
  });
3827
3823
  Logger.verbose(`[${modelConstructor.name}] Diff for ${this.id}:`, {
3828
3824
  added: Object.keys(added),
3829
3825
  modified: Object.keys(modified),
3830
- removed
3826
+ removed,
3827
+ stringSetChanges: Object.keys(stringSetChanges)
3831
3828
  });
3832
- return { added, modified, removed };
3829
+ return { added, modified, removed, stringSetChanges };
3830
+ }
3831
+ /**
3832
+ * Apply a stringset change set to the parent record's Y.Map by mutating a
3833
+ * nested Y.Map keyed by member. Migrates from a legacy plain-object value
3834
+ * if the field hasn't been touched since the wire-format change in #561.
3835
+ */
3836
+ applyStringSetChangeToYMap(recordYMap, fieldName, change) {
3837
+ let nested = recordYMap.get(fieldName);
3838
+ if (!(nested instanceof Y2.Map)) {
3839
+ const migrated = new Y2.Map();
3840
+ if (nested && typeof nested === "object") {
3841
+ for (const [member, marker] of Object.entries(
3842
+ nested
3843
+ )) {
3844
+ if (Boolean(marker)) migrated.set(member, true);
3845
+ }
3846
+ }
3847
+ recordYMap.set(fieldName, migrated);
3848
+ nested = migrated;
3849
+ }
3850
+ const target = nested;
3851
+ for (const member of change.additions) {
3852
+ target.set(member, true);
3853
+ }
3854
+ for (const member of change.removals) {
3855
+ target.delete(member);
3856
+ }
3833
3857
  }
3834
3858
  /**
3835
3859
  * Deep equality check for comparing field values
@@ -4068,7 +4092,7 @@ var init_BaseModel = __esm({
4068
4092
  Logger.debug(`[${modelName}.save] About to calculate diff`);
4069
4093
  const diff = this._diffWithYjsData();
4070
4094
  Logger.debug(`[${modelName}.save] Diff result:`, diff);
4071
- const hasChanges = Object.keys(diff.added).length > 0 || Object.keys(diff.modified).length > 0 || diff.removed.length > 0;
4095
+ const hasChanges = Object.keys(diff.added).length > 0 || Object.keys(diff.modified).length > 0 || diff.removed.length > 0 || Object.keys(diff.stringSetChanges).length > 0;
4072
4096
  Logger.debug(`[${modelName}.save] hasChanges: ${hasChanges}`);
4073
4097
  if (!hasChanges && isUpdate) {
4074
4098
  Logger.verbose(
@@ -4186,6 +4210,16 @@ var init_BaseModel = __esm({
4186
4210
  Logger.debug(`[${modelName}.save] Removing field '${key}'`);
4187
4211
  recordYMap.delete(key);
4188
4212
  }
4213
+ for (const [fieldName, change] of Object.entries(diff.stringSetChanges)) {
4214
+ Logger.debug(
4215
+ `[${modelName}.save] Applying stringset change to field '${fieldName}':`,
4216
+ {
4217
+ additions: Array.from(change.additions),
4218
+ removals: Array.from(change.removals)
4219
+ }
4220
+ );
4221
+ this.applyStringSetChangeToYMap(recordYMap, fieldName, change);
4222
+ }
4189
4223
  Logger.debug(
4190
4224
  `[${modelName}.save] After applying changes, recordYMap contents:`,
4191
4225
  Object.fromEntries(recordYMap.entries())
@@ -5370,6 +5404,39 @@ var init_BaseModel = __esm({
5370
5404
  _BaseModel.prototype.setValue = originalSetValue;
5371
5405
  }
5372
5406
  }
5407
+ /**
5408
+ * Attach a one-shot observer to a nested-Y.Map stringset field. Calls
5409
+ * notifyListeners() when the nested map's keys change (i.e. when a remote
5410
+ * member arrives via Y.applyUpdate, or a local per-member set/delete).
5411
+ * Idempotent — re-calling on the same Y.Map is a no-op.
5412
+ */
5413
+ static observeStringSetMapOnce(nestedMap) {
5414
+ if (_BaseModel._observedStringSetMaps.has(nestedMap)) return;
5415
+ _BaseModel._observedStringSetMaps.add(nestedMap);
5416
+ const modelConstructor = this;
5417
+ nestedMap.observe(() => {
5418
+ Logger.verbose(
5419
+ `[${modelConstructor.name}] Nested stringset Y.Map changed; notifying listeners`
5420
+ );
5421
+ modelConstructor.notifyListeners();
5422
+ });
5423
+ }
5424
+ /**
5425
+ * Walk the schema's stringset fields on `recordYMap` and observe any nested
5426
+ * Y.Map values. Called from observer setup and from the parent observer body
5427
+ * so newly-arrived stringset Y.Maps (local migration or remote create) get
5428
+ * an observer too.
5429
+ */
5430
+ static attachStringSetObserversToRecord(recordYMap, schema) {
5431
+ if (!schema?.fields) return;
5432
+ for (const [fieldKey, fieldOptions] of schema.fields) {
5433
+ if (fieldOptions?.type !== "stringset") continue;
5434
+ const value = recordYMap.get(fieldKey);
5435
+ if (value instanceof Y2.Map) {
5436
+ this.observeStringSetMapOnce(value);
5437
+ }
5438
+ }
5439
+ }
5373
5440
  /**
5374
5441
  * Sets up deep observation on a nested YMap to sync field-level changes to the database
5375
5442
  */
@@ -5386,11 +5453,13 @@ var init_BaseModel = __esm({
5386
5453
  Logger.verbose(
5387
5454
  `[${modelName}] Setting up nested YMap observer for record ${recordId}`
5388
5455
  );
5456
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
5389
5457
  recordYMap.observe(async (event) => {
5390
5458
  Logger.verbose(
5391
5459
  `[${modelName}] Nested YMap change detected for record ${recordId}:`,
5392
5460
  event
5393
5461
  );
5462
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
5394
5463
  const currentDbInstance = _BaseModel.dbInstance;
5395
5464
  if (!currentDbInstance) {
5396
5465
  Logger.error(
@@ -5464,11 +5533,13 @@ var init_BaseModel = __esm({
5464
5533
  Logger.verbose(
5465
5534
  `[${modelName}] Setting up nested YMap observer for record ${recordId} in document ${docId}`
5466
5535
  );
5536
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
5467
5537
  recordYMap.observe(async (event) => {
5468
5538
  Logger.verbose(
5469
5539
  `[${modelName}] Nested YMap change detected for record ${recordId} in document ${docId}:`,
5470
5540
  event
5471
5541
  );
5542
+ modelConstructor.attachStringSetObserversToRecord(recordYMap, schema);
5472
5543
  const currentDbInstance = _BaseModel.dbInstance;
5473
5544
  if (!currentDbInstance) {
5474
5545
  Logger.error(
@@ -6965,6 +7036,7 @@ function defineModelSchema(input) {
6965
7036
  const { name, fields } = input;
6966
7037
  const options = {
6967
7038
  name,
7039
+ className: input.options?.className,
6968
7040
  uniqueConstraints: input.options?.uniqueConstraints ? [...input.options.uniqueConstraints] : void 0,
6969
7041
  relationships: input.options?.relationships
6970
7042
  };
@@ -7064,18 +7136,13 @@ function resolveUniqueConstraints(modelName, fields, customConstraints) {
7064
7136
  function attachSchemaToClass(modelClass, schema) {
7065
7137
  modelClass.schema = schema;
7066
7138
  modelClass.modelName = schema.options.name;
7067
- if (!modelClass.getSchema) {
7068
- Object.defineProperty(modelClass, "getSchema", {
7069
- value: function() {
7070
- return schema.buildRuntimeShape(modelClass);
7071
- },
7072
- writable: false
7073
- });
7074
- } else {
7075
- modelClass.getSchema = function() {
7139
+ Object.defineProperty(modelClass, "getSchema", {
7140
+ value: function() {
7076
7141
  return schema.buildRuntimeShape(modelClass);
7077
- };
7078
- }
7142
+ },
7143
+ writable: false,
7144
+ configurable: true
7145
+ });
7079
7146
  const runtimeShape = schema.buildRuntimeShape(modelClass);
7080
7147
  BaseModel2.attachFieldAccessors(modelClass, runtimeShape.fields);
7081
7148
  return runtimeShape;
@@ -7308,12 +7375,52 @@ var VALID_FIELD_TYPES = /* @__PURE__ */ new Set([
7308
7375
  "id",
7309
7376
  "stringset"
7310
7377
  ]);
7311
- function parseFieldOptions(raw) {
7378
+ var KNOWN_FIELD_KEYS = /* @__PURE__ */ new Set([
7379
+ "type",
7380
+ "indexed",
7381
+ "unique",
7382
+ "required",
7383
+ "auto_assign",
7384
+ "max_length",
7385
+ "max_count",
7386
+ "default"
7387
+ ]);
7388
+ var KNOWN_MODEL_KEYS = /* @__PURE__ */ new Set([
7389
+ "fields",
7390
+ "relationships",
7391
+ "unique_constraints",
7392
+ "class_name"
7393
+ ]);
7394
+ var KNOWN_RELATIONSHIP_KEYS = /* @__PURE__ */ new Set([
7395
+ "type",
7396
+ "model",
7397
+ "related_id_field",
7398
+ "join_model",
7399
+ "join_model_local_field",
7400
+ "join_model_related_field",
7401
+ "order_by_field",
7402
+ "order_direction",
7403
+ "join_model_order_by_field",
7404
+ "join_model_order_direction"
7405
+ ]);
7406
+ var KNOWN_UNIQUE_CONSTRAINT_KEYS = /* @__PURE__ */ new Set(["name", "fields"]);
7407
+ function checkUnknownKeys(raw, known, context, strict) {
7408
+ if (!strict) return;
7409
+ for (const key of Object.keys(raw)) {
7410
+ if (!known.has(key)) {
7411
+ throw new Error(
7412
+ `${context}: unknown key "${key}". Allowed: ${[...known].join(", ")}`
7413
+ );
7414
+ }
7415
+ }
7416
+ }
7417
+ function parseFieldOptions(raw, context, strict) {
7312
7418
  if (!raw.type || !VALID_FIELD_TYPES.has(raw.type)) {
7313
7419
  throw new Error(
7314
7420
  `Invalid field type "${raw.type}". Must be one of: ${[...VALID_FIELD_TYPES].join(", ")}`
7315
7421
  );
7316
7422
  }
7423
+ checkUnknownKeys(raw, KNOWN_FIELD_KEYS, context, strict);
7317
7424
  const opts = { type: raw.type };
7318
7425
  if (raw.indexed === true) opts.indexed = true;
7319
7426
  if (raw.unique === true) opts.unique = true;
@@ -7329,8 +7436,9 @@ function requireField(raw, field, context) {
7329
7436
  throw new Error(`Relationship ${context}: missing required field "${field}"`);
7330
7437
  }
7331
7438
  }
7332
- function parseRelationship(raw) {
7439
+ function parseRelationship(raw, context, strict) {
7333
7440
  const type = raw.type;
7441
+ checkUnknownKeys(raw, KNOWN_RELATIONSHIP_KEYS, context, strict);
7334
7442
  if (type === "refersTo") {
7335
7443
  requireField(raw, "model", "refersTo");
7336
7444
  requireField(raw, "related_id_field", "refersTo");
@@ -7372,7 +7480,8 @@ function parseRelationship(raw) {
7372
7480
  }
7373
7481
  throw new Error(`Unknown relationship type: ${type}`);
7374
7482
  }
7375
- function loadSchemaFromTomlString(tomlString) {
7483
+ function loadSchemaFromTomlString(tomlString, options = {}) {
7484
+ const strict = options.strict !== false;
7376
7485
  const parsed = (0, import_smol_toml.parse)(tomlString);
7377
7486
  const models = parsed.models;
7378
7487
  if (!models || typeof models !== "object") {
@@ -7380,10 +7489,20 @@ function loadSchemaFromTomlString(tomlString) {
7380
7489
  }
7381
7490
  const schemas = [];
7382
7491
  for (const [modelName, modelDef] of Object.entries(models)) {
7492
+ checkUnknownKeys(
7493
+ modelDef,
7494
+ KNOWN_MODEL_KEYS,
7495
+ `[models.${modelName}]`,
7496
+ strict
7497
+ );
7383
7498
  const fields = {};
7384
7499
  if (modelDef.fields) {
7385
7500
  for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) {
7386
- fields[fieldName] = parseFieldOptions(fieldDef);
7501
+ fields[fieldName] = parseFieldOptions(
7502
+ fieldDef,
7503
+ `[models.${modelName}.fields.${fieldName}]`,
7504
+ strict
7505
+ );
7387
7506
  }
7388
7507
  }
7389
7508
  let relationships;
@@ -7392,24 +7511,36 @@ function loadSchemaFromTomlString(tomlString) {
7392
7511
  for (const [relName, relDef] of Object.entries(
7393
7512
  modelDef.relationships
7394
7513
  )) {
7395
- relationships[relName] = parseRelationship(relDef);
7514
+ relationships[relName] = parseRelationship(
7515
+ relDef,
7516
+ `[models.${modelName}.relationships.${relName}]`,
7517
+ strict
7518
+ );
7396
7519
  }
7397
7520
  }
7398
7521
  let uniqueConstraints;
7399
7522
  if (modelDef.unique_constraints) {
7400
7523
  uniqueConstraints = [];
7401
7524
  for (const raw of modelDef.unique_constraints) {
7525
+ checkUnknownKeys(
7526
+ raw,
7527
+ KNOWN_UNIQUE_CONSTRAINT_KEYS,
7528
+ `[[models.${modelName}.unique_constraints]]`,
7529
+ strict
7530
+ );
7402
7531
  uniqueConstraints.push({
7403
7532
  name: raw.name,
7404
7533
  fields: [...raw.fields]
7405
7534
  });
7406
7535
  }
7407
7536
  }
7537
+ const className = typeof modelDef.class_name === "string" ? modelDef.class_name : void 0;
7408
7538
  schemas.push(
7409
7539
  defineModelSchema({
7410
7540
  name: modelName,
7411
7541
  fields,
7412
7542
  options: {
7543
+ className,
7413
7544
  uniqueConstraints,
7414
7545
  relationships
7415
7546
  }
@@ -7418,10 +7549,10 @@ function loadSchemaFromTomlString(tomlString) {
7418
7549
  }
7419
7550
  return schemas;
7420
7551
  }
7421
- async function loadSchemaFromToml(filePath) {
7552
+ async function loadSchemaFromToml(filePath, options = {}) {
7422
7553
  const { readFile } = await import("fs/promises");
7423
7554
  const content = await readFile(filePath, "utf-8");
7424
- return loadSchemaFromTomlString(content);
7555
+ return loadSchemaFromTomlString(content, options);
7425
7556
  }
7426
7557
 
7427
7558
  // src/node.ts
package/dist/node.d.cts CHANGED
@@ -40,6 +40,13 @@ interface UniqueConstraintConfig {
40
40
  }
41
41
  interface ModelOptions {
42
42
  name: string;
43
+ /**
44
+ * Optional PascalCase class name. Used by the v2 codegen to drive
45
+ * generated TypeScript class names (and relationship method names that
46
+ * derive from a target's class name). When absent, the v2 codegen
47
+ * falls back to suffix-based singularization of `name`.
48
+ */
49
+ className?: string;
43
50
  uniqueConstraints?: UniqueConstraintConfig[];
44
51
  relationships?: Record<string, RelationshipConfig>;
45
52
  }
@@ -447,6 +454,7 @@ declare class BaseModel implements StringSetChangeTracker {
447
454
  permissionHint: DocumentPermissionHint;
448
455
  }>;
449
456
  protected static documentYMaps: Map<string, Y.Map<any>>;
457
+ private static _observedStringSetMaps;
450
458
  private _localChanges;
451
459
  private _isDirty;
452
460
  private _isLoadingFromYjs;
@@ -521,7 +529,17 @@ declare class BaseModel implements StringSetChangeTracker {
521
529
  added: Record<string, any>;
522
530
  modified: Record<string, any>;
523
531
  removed: string[];
532
+ stringSetChanges: Record<string, {
533
+ additions: Set<string>;
534
+ removals: Set<string>;
535
+ }>;
524
536
  };
537
+ /**
538
+ * Apply a stringset change set to the parent record's Y.Map by mutating a
539
+ * nested Y.Map keyed by member. Migrates from a legacy plain-object value
540
+ * if the field hasn't been touched since the wire-format change in #561.
541
+ */
542
+ private applyStringSetChangeToYMap;
525
543
  /**
526
544
  * Deep equality check for comparing field values
527
545
  */
@@ -623,6 +641,20 @@ declare class BaseModel implements StringSetChangeTracker {
623
641
  * Execute a callback with automatic transaction handling for all modified models
624
642
  */
625
643
  static withTransaction<T>(callback: () => Promise<T> | T): Promise<T>;
644
+ /**
645
+ * Attach a one-shot observer to a nested-Y.Map stringset field. Calls
646
+ * notifyListeners() when the nested map's keys change (i.e. when a remote
647
+ * member arrives via Y.applyUpdate, or a local per-member set/delete).
648
+ * Idempotent — re-calling on the same Y.Map is a no-op.
649
+ */
650
+ protected static observeStringSetMapOnce(nestedMap: Y.Map<any>): void;
651
+ /**
652
+ * Walk the schema's stringset fields on `recordYMap` and observe any nested
653
+ * Y.Map values. Called from observer setup and from the parent observer body
654
+ * so newly-arrived stringset Y.Maps (local migration or remote create) get
655
+ * an observer too.
656
+ */
657
+ protected static attachStringSetObserversToRecord(recordYMap: Y.Map<any>, schema: any): void;
626
658
  /**
627
659
  * Sets up deep observation on a nested YMap to sync field-level changes to the database
628
660
  */
@@ -974,15 +1006,27 @@ declare function schemaToToml(schema: DiscoveredSchema): string;
974
1006
  * - Compound unique constraints are [[models.*.unique_constraints]]
975
1007
  */
976
1008
 
1009
+ interface LoadSchemaOptions {
1010
+ /**
1011
+ * When true (default), throw on unknown keys at the model, field,
1012
+ * relationship, and unique-constraint level. When false, unknown
1013
+ * keys are silently ignored (legacy behavior).
1014
+ */
1015
+ strict?: boolean;
1016
+ }
977
1017
  /**
978
1018
  * Parse a TOML string and return an array of DefinedModelSchema objects.
1019
+ *
1020
+ * By default operates in strict mode: unknown keys at the model, field,
1021
+ * relationship, or unique-constraint level cause an error. Pass
1022
+ * `{ strict: false }` to silently ignore unknown keys (legacy behavior).
979
1023
  */
980
- declare function loadSchemaFromTomlString(tomlString: string): DefinedModelSchema[];
1024
+ declare function loadSchemaFromTomlString(tomlString: string, options?: LoadSchemaOptions): DefinedModelSchema[];
981
1025
  /**
982
1026
  * Read a TOML file from disk and return an array of DefinedModelSchema objects.
983
1027
  * Only available in Node.js environments.
984
1028
  */
985
- declare function loadSchemaFromToml(filePath: string): Promise<DefinedModelSchema[]>;
1029
+ declare function loadSchemaFromToml(filePath: string, options?: LoadSchemaOptions): Promise<DefinedModelSchema[]>;
986
1030
 
987
1031
  /**
988
1032
  * Meta Sync — writes _meta_* YMaps into a YDoc.
package/dist/node.d.ts CHANGED
@@ -40,6 +40,13 @@ interface UniqueConstraintConfig {
40
40
  }
41
41
  interface ModelOptions {
42
42
  name: string;
43
+ /**
44
+ * Optional PascalCase class name. Used by the v2 codegen to drive
45
+ * generated TypeScript class names (and relationship method names that
46
+ * derive from a target's class name). When absent, the v2 codegen
47
+ * falls back to suffix-based singularization of `name`.
48
+ */
49
+ className?: string;
43
50
  uniqueConstraints?: UniqueConstraintConfig[];
44
51
  relationships?: Record<string, RelationshipConfig>;
45
52
  }
@@ -447,6 +454,7 @@ declare class BaseModel implements StringSetChangeTracker {
447
454
  permissionHint: DocumentPermissionHint;
448
455
  }>;
449
456
  protected static documentYMaps: Map<string, Y.Map<any>>;
457
+ private static _observedStringSetMaps;
450
458
  private _localChanges;
451
459
  private _isDirty;
452
460
  private _isLoadingFromYjs;
@@ -521,7 +529,17 @@ declare class BaseModel implements StringSetChangeTracker {
521
529
  added: Record<string, any>;
522
530
  modified: Record<string, any>;
523
531
  removed: string[];
532
+ stringSetChanges: Record<string, {
533
+ additions: Set<string>;
534
+ removals: Set<string>;
535
+ }>;
524
536
  };
537
+ /**
538
+ * Apply a stringset change set to the parent record's Y.Map by mutating a
539
+ * nested Y.Map keyed by member. Migrates from a legacy plain-object value
540
+ * if the field hasn't been touched since the wire-format change in #561.
541
+ */
542
+ private applyStringSetChangeToYMap;
525
543
  /**
526
544
  * Deep equality check for comparing field values
527
545
  */
@@ -623,6 +641,20 @@ declare class BaseModel implements StringSetChangeTracker {
623
641
  * Execute a callback with automatic transaction handling for all modified models
624
642
  */
625
643
  static withTransaction<T>(callback: () => Promise<T> | T): Promise<T>;
644
+ /**
645
+ * Attach a one-shot observer to a nested-Y.Map stringset field. Calls
646
+ * notifyListeners() when the nested map's keys change (i.e. when a remote
647
+ * member arrives via Y.applyUpdate, or a local per-member set/delete).
648
+ * Idempotent — re-calling on the same Y.Map is a no-op.
649
+ */
650
+ protected static observeStringSetMapOnce(nestedMap: Y.Map<any>): void;
651
+ /**
652
+ * Walk the schema's stringset fields on `recordYMap` and observe any nested
653
+ * Y.Map values. Called from observer setup and from the parent observer body
654
+ * so newly-arrived stringset Y.Maps (local migration or remote create) get
655
+ * an observer too.
656
+ */
657
+ protected static attachStringSetObserversToRecord(recordYMap: Y.Map<any>, schema: any): void;
626
658
  /**
627
659
  * Sets up deep observation on a nested YMap to sync field-level changes to the database
628
660
  */
@@ -974,15 +1006,27 @@ declare function schemaToToml(schema: DiscoveredSchema): string;
974
1006
  * - Compound unique constraints are [[models.*.unique_constraints]]
975
1007
  */
976
1008
 
1009
+ interface LoadSchemaOptions {
1010
+ /**
1011
+ * When true (default), throw on unknown keys at the model, field,
1012
+ * relationship, and unique-constraint level. When false, unknown
1013
+ * keys are silently ignored (legacy behavior).
1014
+ */
1015
+ strict?: boolean;
1016
+ }
977
1017
  /**
978
1018
  * Parse a TOML string and return an array of DefinedModelSchema objects.
1019
+ *
1020
+ * By default operates in strict mode: unknown keys at the model, field,
1021
+ * relationship, or unique-constraint level cause an error. Pass
1022
+ * `{ strict: false }` to silently ignore unknown keys (legacy behavior).
979
1023
  */
980
- declare function loadSchemaFromTomlString(tomlString: string): DefinedModelSchema[];
1024
+ declare function loadSchemaFromTomlString(tomlString: string, options?: LoadSchemaOptions): DefinedModelSchema[];
981
1025
  /**
982
1026
  * Read a TOML file from disk and return an array of DefinedModelSchema objects.
983
1027
  * Only available in Node.js environments.
984
1028
  */
985
- declare function loadSchemaFromToml(filePath: string): Promise<DefinedModelSchema[]>;
1029
+ declare function loadSchemaFromToml(filePath: string, options?: LoadSchemaOptions): Promise<DefinedModelSchema[]>;
986
1030
 
987
1031
  /**
988
1032
  * Meta Sync — writes _meta_* YMaps into a YDoc.