@happyvertical/smrt-core 0.36.8 → 0.37.0

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.
Files changed (70) hide show
  1. package/dist/child-accessors.d.ts +1 -1
  2. package/dist/child-accessors.d.ts.map +1 -1
  3. package/dist/child-accessors.js +1 -1
  4. package/dist/child-accessors.js.map +1 -1
  5. package/dist/class.d.ts.map +1 -1
  6. package/dist/class.js +3 -1
  7. package/dist/class.js.map +1 -1
  8. package/dist/collection-cache.d.ts.map +1 -1
  9. package/dist/collection-cache.js +5 -3
  10. package/dist/collection-cache.js.map +1 -1
  11. package/dist/collection.d.ts +39 -16
  12. package/dist/collection.d.ts.map +1 -1
  13. package/dist/collection.js +40 -19
  14. package/dist/collection.js.map +1 -1
  15. package/dist/decorators/compatibility.d.ts +1 -1
  16. package/dist/decorators/compatibility.d.ts.map +1 -1
  17. package/dist/decorators/compatibility.js.map +1 -1
  18. package/dist/decorators/index.d.ts +4 -4
  19. package/dist/decorators/index.d.ts.map +1 -1
  20. package/dist/decorators/index.js.map +1 -1
  21. package/dist/hierarchical.d.ts.map +1 -1
  22. package/dist/hierarchical.js.map +1 -1
  23. package/dist/junction.d.ts.map +1 -1
  24. package/dist/junction.js.map +1 -1
  25. package/dist/manifest/static-manifest.d.ts.map +1 -1
  26. package/dist/manifest/static-manifest.js +39 -20
  27. package/dist/manifest/static-manifest.js.map +1 -1
  28. package/dist/manifest/store.js +2 -2
  29. package/dist/manifest/store.js.map +1 -1
  30. package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
  31. package/dist/manifest/test-manifest-stub.js +2301 -629
  32. package/dist/manifest/test-manifest-stub.js.map +1 -1
  33. package/dist/manifest.json +39 -20
  34. package/dist/object.d.ts +64 -17
  35. package/dist/object.d.ts.map +1 -1
  36. package/dist/object.js +76 -30
  37. package/dist/object.js.map +1 -1
  38. package/dist/registry/class-registration.d.ts +3 -3
  39. package/dist/registry/class-registration.d.ts.map +1 -1
  40. package/dist/registry/class-registration.js +39 -42
  41. package/dist/registry/class-registration.js.map +1 -1
  42. package/dist/registry/inheritance-resolver.d.ts +17 -3
  43. package/dist/registry/inheritance-resolver.d.ts.map +1 -1
  44. package/dist/registry/inheritance-resolver.js +1 -1
  45. package/dist/registry/inheritance-resolver.js.map +1 -1
  46. package/dist/registry/manifest-field-merge.d.ts +17 -3
  47. package/dist/registry/manifest-field-merge.d.ts.map +1 -1
  48. package/dist/registry/manifest-field-merge.js +8 -6
  49. package/dist/registry/manifest-field-merge.js.map +1 -1
  50. package/dist/registry/schema-builder.d.ts +1 -1
  51. package/dist/registry/schema-builder.d.ts.map +1 -1
  52. package/dist/registry/schema-builder.js.map +1 -1
  53. package/dist/registry/shared-state.d.ts +3 -3
  54. package/dist/registry/shared-state.d.ts.map +1 -1
  55. package/dist/registry/shared-state.js.map +1 -1
  56. package/dist/registry/types.d.ts +78 -19
  57. package/dist/registry/types.d.ts.map +1 -1
  58. package/dist/registry/validator.d.ts +2 -1
  59. package/dist/registry/validator.d.ts.map +1 -1
  60. package/dist/registry/validator.js +38 -39
  61. package/dist/registry/validator.js.map +1 -1
  62. package/dist/registry.d.ts +84 -57
  63. package/dist/registry.d.ts.map +1 -1
  64. package/dist/registry.js +31 -25
  65. package/dist/registry.js.map +1 -1
  66. package/dist/smrt-knowledge.json +5 -4
  67. package/dist/system-fields.d.ts +1 -1
  68. package/dist/system-fields.d.ts.map +1 -1
  69. package/dist/system-fields.js.map +1 -1
  70. package/package.json +4 -4
package/dist/object.js CHANGED
@@ -289,12 +289,13 @@ class SmrtObject extends SmrtClass {
289
289
  const options = this.options;
290
290
  if (options.created_at !== void 0) this.created_at = options.created_at;
291
291
  if (options.updated_at !== void 0) this.updated_at = options.updated_at;
292
- if (options._meta_type !== void 0) {
292
+ if (typeof options._meta_type === "string") {
293
293
  this.setMetaType(options._meta_type);
294
294
  }
295
295
  const fields = await fieldsFromClass(
296
296
  this.constructor
297
297
  );
298
+ const writable = this;
298
299
  for (const [key, field] of Object.entries(fields)) {
299
300
  if (options[key] !== void 0) {
300
301
  const clonedValue = this.cloneValue(options[key]);
@@ -307,7 +308,7 @@ class SmrtObject extends SmrtClass {
307
308
  }
308
309
  }
309
310
  if (!descriptor || descriptor.set || descriptor.writable === true) {
310
- this[key] = clonedValue;
311
+ writable[key] = clonedValue;
311
312
  }
312
313
  }
313
314
  }
@@ -463,17 +464,18 @@ class SmrtObject extends SmrtClass {
463
464
  });
464
465
  }
465
466
  if (isSTI) {
466
- if (!formattedData._meta_type) {
467
+ const metaType = formattedData._meta_type;
468
+ if (!metaType) {
467
469
  throw new Error(
468
470
  `STI validation failed: Missing _meta_type discriminator in database row for ${className}. Ensure the row was saved with STI support enabled.`
469
471
  );
470
472
  }
471
- if (!isValidMetaType(formattedData._meta_type, className)) {
473
+ if (!isValidMetaType(metaType, className)) {
472
474
  throw new Error(
473
- `STI validation failed: Type mismatch when loading ${className}. Database row has _meta_type='${formattedData._meta_type}' but expected '${getExpectedMetaType(className)}'. This usually means you're trying to load a row with the wrong class.`
475
+ `STI validation failed: Type mismatch when loading ${className}. Database row has _meta_type='${metaType}' but expected '${getExpectedMetaType(className)}'. This usually means you're trying to load a row with the wrong class.`
474
476
  );
475
477
  }
476
- this.setMetaType(formattedData._meta_type);
478
+ this.setMetaType(metaType);
477
479
  }
478
480
  if (process.env.DEBUG_STI) {
479
481
  logger.debug("[loadDataFromDb] Starting field hydration", {
@@ -483,6 +485,7 @@ class SmrtObject extends SmrtClass {
483
485
  }
484
486
  let hydratedCount = 0;
485
487
  let skippedCount = 0;
488
+ const writable = this;
486
489
  for (const field in fields) {
487
490
  if (Object.hasOwn(fields, field)) {
488
491
  let descriptor = Object.getOwnPropertyDescriptor(this, field);
@@ -501,7 +504,7 @@ class SmrtObject extends SmrtClass {
501
504
  valueType: typeof value
502
505
  });
503
506
  }
504
- this[field] = value;
507
+ writable[field] = value;
505
508
  hydratedCount++;
506
509
  } else {
507
510
  skippedCount++;
@@ -555,6 +558,11 @@ class SmrtObject extends SmrtClass {
555
558
  *
556
559
  * @returns Object containing field definitions with current values
557
560
  */
561
+ // Explicit `any` return (documented S4 #1579 leaf): subclasses legitimately
562
+ // OVERRIDE getFields() with domain-specific shapes (e.g. projects'
563
+ // `Project.getFields(): Promise<ProjectField[]>`), which a precise field-map
564
+ // return type would reject. The body below stays internally typed.
565
+ // biome-ignore lint/suspicious/noExplicitAny: subclasses OVERRIDE getFields() with domain shapes (e.g. `Project.getFields(): Promise<ProjectField[]>`); a precise base return type would reject those overrides. S4 #1579.
558
566
  async getFields() {
559
567
  const className = this.getResolvedClassName();
560
568
  const cachedFields = await ObjectRegistry.getAllFields(className);
@@ -595,7 +603,9 @@ class SmrtObject extends SmrtClass {
595
603
  * class Article extends SmrtObject {
596
604
  * body: string = '';
597
605
  *
598
- * protected transformJSON(data: any): any {
606
+ * protected transformJSON(
607
+ * data: Record<string, unknown>,
608
+ * ): Record<string, unknown> {
599
609
  * return {
600
610
  * ...data,
601
611
  * wordCount: this.body.split(/\s+/).length,
@@ -636,13 +646,15 @@ class SmrtObject extends SmrtClass {
636
646
  created_at: this.created_at,
637
647
  updated_at: this.updated_at
638
648
  };
649
+ const self = this;
639
650
  const tableStrategy = ObjectRegistry.getTableStrategy(
640
651
  this.getResolvedQualifiedName()
641
652
  );
642
653
  const isSTI = tableStrategy === "sti";
654
+ const metaData = {};
643
655
  if (isSTI) {
644
656
  data._meta_type = this.getResolvedQualifiedName();
645
- data._meta_data = {};
657
+ data._meta_data = metaData;
646
658
  }
647
659
  const registered = ObjectRegistry.getClass(className);
648
660
  let registeredFields = registered?.inheritedFields || ObjectRegistry.getFields(className);
@@ -665,7 +677,7 @@ class SmrtObject extends SmrtClass {
665
677
  }
666
678
  for (const key of registeredFields.keys()) {
667
679
  if (key.startsWith("_") || key === "id" || key === "slug" || key === "context" || key === "created_at" || key === "updated_at" || key === "options" || // Skip options object (not a database column)
668
- typeof this[key] === "function") {
680
+ typeof self[key] === "function") {
669
681
  continue;
670
682
  }
671
683
  const fieldDef = registeredFields.get(key);
@@ -675,21 +687,22 @@ class SmrtObject extends SmrtClass {
675
687
  if (fieldDef && (fieldDef.type === "oneToMany" || fieldDef.type === "manyToMany")) {
676
688
  continue;
677
689
  }
678
- const prop = this[key];
690
+ const prop = self[key];
679
691
  const value = this.getPropertyValue(key);
680
692
  if (value === void 0) {
681
693
  const fieldType = prop && typeof prop === "object" && "type" in prop && prop.type || fieldDef?.type;
682
694
  if (fieldType === "text") {
683
- const hasTenancyMarker = prop && typeof prop === "object" && "__tenancy" in prop && prop.__tenancy?.isTenantIdField || fieldDef?.__tenancy?.isTenantIdField || fieldDef?._meta?.__tenancy?.isTenantIdField;
695
+ const propTenancy = prop && typeof prop === "object" && "__tenancy" in prop ? prop.__tenancy : void 0;
696
+ const hasTenancyMarker = propTenancy?.isTenantIdField || fieldDef?.__tenancy?.isTenantIdField || fieldDef?._meta?.__tenancy?.isTenantIdField;
684
697
  if (hasTenancyMarker) {
685
698
  if (isSTI && fieldDef?.type === "meta") {
686
- data._meta_data[key] = null;
699
+ metaData[key] = null;
687
700
  } else {
688
701
  data[key] = null;
689
702
  }
690
703
  } else {
691
704
  if (isSTI && fieldDef?.type === "meta") {
692
- data._meta_data[key] = "";
705
+ metaData[key] = "";
693
706
  } else {
694
707
  data[key] = "";
695
708
  }
@@ -697,7 +710,7 @@ class SmrtObject extends SmrtClass {
697
710
  } else if (fieldType === "json") {
698
711
  const defaultValue = fieldDef?.default ?? null;
699
712
  if (isSTI && fieldDef?.type === "meta") {
700
- data._meta_data[key] = defaultValue;
713
+ metaData[key] = defaultValue;
701
714
  } else {
702
715
  data[key] = defaultValue;
703
716
  }
@@ -705,7 +718,7 @@ class SmrtObject extends SmrtClass {
705
718
  continue;
706
719
  }
707
720
  if (isSTI && fieldDef && fieldDef.type === "meta") {
708
- data._meta_data[key] = value;
721
+ metaData[key] = value;
709
722
  } else {
710
723
  data[key] = value;
711
724
  }
@@ -899,13 +912,14 @@ class SmrtObject extends SmrtClass {
899
912
  */
900
913
  async getSlug() {
901
914
  if (!this.slug) {
915
+ const self = this;
902
916
  let sourceField = null;
903
- if (this.name) {
904
- sourceField = String(this.name);
905
- } else if (this.title) {
906
- sourceField = String(this.title);
907
- } else if (this.label) {
908
- sourceField = String(this.label);
917
+ if (self.name) {
918
+ sourceField = String(self.name);
919
+ } else if (self.title) {
920
+ sourceField = String(self.title);
921
+ } else if (self.label) {
922
+ sourceField = String(self.label);
909
923
  } else if (this.id) {
910
924
  sourceField = String(this.id);
911
925
  }
@@ -1263,9 +1277,12 @@ See issue #377: https://github.com/happyvertical/smrt/issues/377`
1263
1277
  }
1264
1278
  return;
1265
1279
  }
1266
- const fields = await fieldsFromClass(this.constructor);
1280
+ const fields = await fieldsFromClass(
1281
+ this.constructor
1282
+ );
1267
1283
  for (const [fieldName, field] of Object.entries(fields)) {
1268
- if (field.options?.required) {
1284
+ const options = field?.options;
1285
+ if (options?.required) {
1269
1286
  const value = this.getFieldValue(fieldName);
1270
1287
  if (value === null || value === void 0 || value === "") {
1271
1288
  throw ValidationError.requiredField(fieldName, className);
@@ -1822,6 +1839,26 @@ Based on the content body, please follow the instructions and provide a response
1822
1839
  isRelatedLoaded(fieldName) {
1823
1840
  return this._loadedRelationships.has(fieldName);
1824
1841
  }
1842
+ /**
1843
+ * Internal: seed the lazy-load relationship cache from an external batch
1844
+ * loader.
1845
+ *
1846
+ * `SmrtCollection`'s eager `include:` and `getOrUpsert` paths resolve
1847
+ * relationships in bulk and need to prime each instance's cache so a later
1848
+ * {@link loadRelated}/{@link loadRelatedMany} call returns the already-fetched
1849
+ * value instead of re-querying. This method is the sanctioned write path into
1850
+ * the otherwise-private `_loadedRelationships` map, so collaborators never have
1851
+ * to structurally cast into the private field (which the no-private-reach-ins
1852
+ * convention forbids). The leading underscore keeps it out of the generated
1853
+ * REST/CLI/MCP surface, like other framework-internal members.
1854
+ *
1855
+ * @param fieldName - Relationship field whose value is being cached
1856
+ * @param value - The loaded relationship (a `SmrtObject`, an array of them, or
1857
+ * `null`), stored verbatim
1858
+ */
1859
+ _setLoadedRelationship(fieldName, value) {
1860
+ this._loadedRelationships.set(fieldName, value);
1861
+ }
1825
1862
  /**
1826
1863
  * Lazy-loads a `foreignKey` or `crossPackageRef` relationship and caches the
1827
1864
  * result.
@@ -1859,6 +1896,10 @@ Based on the content body, please follow the instructions and provide a response
1859
1896
  *
1860
1897
  * @see {@link getRelated} for a convenience wrapper that auto-detects relationship type
1861
1898
  */
1899
+ // Returns a polymorphic related object whose concrete subclass type only the
1900
+ // CALLER knows (e.g. `metafield.validateValue(...)`). Typed `any` so callers
1901
+ // can use the concrete API without a cast at every site; a generic with a
1902
+ // `SmrtObject` default wouldn't help untyped callers. Documented S4 #1579 leaf.
1862
1903
  async loadRelated(fieldName, opts) {
1863
1904
  if (this._loadedRelationships.has(fieldName)) {
1864
1905
  const cached = this._loadedRelationships.get(fieldName);
@@ -1995,6 +2036,8 @@ Based on the content body, please follow the instructions and provide a response
1995
2036
  * console.log(`${orders.length} orders found`);
1996
2037
  * ```
1997
2038
  */
2039
+ // Polymorphic related objects; concrete type is caller-known (see loadRelated).
2040
+ // Documented S4 #1579 leaf.
1998
2041
  async loadRelatedMany(fieldName, opts) {
1999
2042
  if (this._loadedRelationships.has(fieldName)) {
2000
2043
  const cached = this._loadedRelationships.get(fieldName);
@@ -2104,7 +2147,8 @@ Based on the content body, please follow the instructions and provide a response
2104
2147
  fieldName
2105
2148
  );
2106
2149
  const opts = relationship.options || {};
2107
- const through = decorator?.through ?? opts.through ?? opts._meta?.through;
2150
+ const optsMeta = opts._meta && typeof opts._meta === "object" ? opts._meta : void 0;
2151
+ const through = decorator?.through ?? opts.through ?? optsMeta?.through;
2108
2152
  if (!through) {
2109
2153
  throw RuntimeError.invalidState(
2110
2154
  `manyToMany field ${fieldName} on ${relationship.sourceClass} is missing the 'through' join table name`,
@@ -2113,12 +2157,12 @@ Based on the content body, please follow the instructions and provide a response
2113
2157
  }
2114
2158
  const targetSimpleName = relationship.targetClass.includes(":") ? relationship.targetClass.split(":").pop() : relationship.targetClass;
2115
2159
  const sourceSimpleName = relationship.sourceClass.includes(":") ? relationship.sourceClass.split(":").pop() : relationship.sourceClass;
2116
- const sourceColumn = decorator?.sourceKey ?? opts.sourceKey ?? opts._meta?.sourceKey ?? `${toSnakeCase(sourceSimpleName)}_id`;
2117
- const targetColumn = decorator?.targetKey ?? opts.targetKey ?? opts._meta?.targetKey ?? `${toSnakeCase(targetSimpleName)}_id`;
2160
+ const sourceColumn = decorator?.sourceKey ?? opts.sourceKey ?? optsMeta?.sourceKey ?? `${toSnakeCase(sourceSimpleName)}_id`;
2161
+ const targetColumn = decorator?.targetKey ?? opts.targetKey ?? optsMeta?.targetKey ?? `${toSnakeCase(targetSimpleName)}_id`;
2118
2162
  return {
2119
2163
  through: String(through),
2120
- sourceColumn,
2121
- targetColumn,
2164
+ sourceColumn: String(sourceColumn),
2165
+ targetColumn: String(targetColumn),
2122
2166
  targetClassName: relationship.targetClass
2123
2167
  };
2124
2168
  }
@@ -2145,6 +2189,8 @@ Based on the content body, please follow the instructions and provide a response
2145
2189
  * const orders = await customer.getRelated('orders');
2146
2190
  * ```
2147
2191
  */
2192
+ // Polymorphic related object(s); concrete type is caller-known (see
2193
+ // loadRelated). Documented S4 #1579 leaf.
2148
2194
  async getRelated(fieldName, opts) {
2149
2195
  if (this._loadedRelationships.has(fieldName)) {
2150
2196
  const cached = this._loadedRelationships.get(fieldName);
@@ -2337,7 +2383,7 @@ Based on the content body, please follow the instructions and provide a response
2337
2383
  }
2338
2384
  if (result) {
2339
2385
  try {
2340
- return JSON.parse(result.value);
2386
+ return JSON.parse(String(result.value));
2341
2387
  } catch (error) {
2342
2388
  logger.warn("Skipping corrupted _smrt_contexts value in recall()", {
2343
2389
  ownerClass: this._className,