@happyvertical/smrt-core 0.30.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 (631) hide show
  1. package/AGENTS.md +124 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +265 -0
  5. package/bin/smrt-prebuild.js +26 -0
  6. package/dist/__tests__/fixtures/advisor-test-classes.d.ts +28 -0
  7. package/dist/__tests__/fixtures/advisor-test-classes.d.ts.map +1 -0
  8. package/dist/__tests__/fixtures/collection-coverage-fixtures.d.ts +12 -0
  9. package/dist/__tests__/fixtures/collection-coverage-fixtures.d.ts.map +1 -0
  10. package/dist/__tests__/fixtures/inheritance-resolver-fixtures.d.ts +28 -0
  11. package/dist/__tests__/fixtures/inheritance-resolver-fixtures.d.ts.map +1 -0
  12. package/dist/__tests__/fixtures/inheritance-test-classes.d.ts +43 -0
  13. package/dist/__tests__/fixtures/inheritance-test-classes.d.ts.map +1 -0
  14. package/dist/__tests__/fixtures/mcp-integration-test-classes.d.ts +18 -0
  15. package/dist/__tests__/fixtures/mcp-integration-test-classes.d.ts.map +1 -0
  16. package/dist/__tests__/fixtures/object-ai-memory-fixtures.d.ts +15 -0
  17. package/dist/__tests__/fixtures/object-ai-memory-fixtures.d.ts.map +1 -0
  18. package/dist/__tests__/fixtures/object-spec-test-classes.d.ts +13 -0
  19. package/dist/__tests__/fixtures/object-spec-test-classes.d.ts.map +1 -0
  20. package/dist/__tests__/fixtures/registry-test-classes.d.ts +23 -0
  21. package/dist/__tests__/fixtures/registry-test-classes.d.ts.map +1 -0
  22. package/dist/adapters/ai-usage.d.ts +23 -0
  23. package/dist/adapters/ai-usage.d.ts.map +1 -0
  24. package/dist/adapters/ai-usage.js +105 -0
  25. package/dist/adapters/ai-usage.js.map +1 -0
  26. package/dist/adapters/cost-rates.d.ts +20 -0
  27. package/dist/adapters/cost-rates.d.ts.map +1 -0
  28. package/dist/adapters/cost-rates.js +40 -0
  29. package/dist/adapters/cost-rates.js.map +1 -0
  30. package/dist/adapters/index.d.ts +19 -0
  31. package/dist/adapters/index.d.ts.map +1 -0
  32. package/dist/adapters/metrics.d.ts +111 -0
  33. package/dist/adapters/metrics.d.ts.map +1 -0
  34. package/dist/adapters/metrics.js +169 -0
  35. package/dist/adapters/metrics.js.map +1 -0
  36. package/dist/adapters/pubsub.d.ts +124 -0
  37. package/dist/adapters/pubsub.d.ts.map +1 -0
  38. package/dist/adapters/pubsub.js +121 -0
  39. package/dist/adapters/pubsub.js.map +1 -0
  40. package/dist/browser.d.ts +32 -0
  41. package/dist/browser.d.ts.map +1 -0
  42. package/dist/browser.js +68 -0
  43. package/dist/browser.js.map +1 -0
  44. package/dist/child-accessors.d.ts +27 -0
  45. package/dist/child-accessors.d.ts.map +1 -0
  46. package/dist/child-accessors.js +35 -0
  47. package/dist/child-accessors.js.map +1 -0
  48. package/dist/class.d.ts +313 -0
  49. package/dist/class.d.ts.map +1 -0
  50. package/dist/class.js +896 -0
  51. package/dist/class.js.map +1 -0
  52. package/dist/collection-cache.d.ts +110 -0
  53. package/dist/collection-cache.d.ts.map +1 -0
  54. package/dist/collection-cache.js +187 -0
  55. package/dist/collection-cache.js.map +1 -0
  56. package/dist/collection.d.ts +894 -0
  57. package/dist/collection.d.ts.map +1 -0
  58. package/dist/collection.js +1987 -0
  59. package/dist/collection.js.map +1 -0
  60. package/dist/config/global-config.d.ts +3 -0
  61. package/dist/config/global-config.d.ts.map +1 -0
  62. package/dist/config/global-config.js +19 -0
  63. package/dist/config/global-config.js.map +1 -0
  64. package/dist/config.d.ts +145 -0
  65. package/dist/config.d.ts.map +1 -0
  66. package/dist/config.js +57 -0
  67. package/dist/config.js.map +1 -0
  68. package/dist/consumer-plugin/index.d.ts +22 -0
  69. package/dist/consumer-plugin/index.d.ts.map +1 -0
  70. package/dist/consumer-plugin/index.js +452 -0
  71. package/dist/consumer-plugin/index.js.map +1 -0
  72. package/dist/consumer-plugin.d.ts +8 -0
  73. package/dist/consumer-plugin.d.ts.map +1 -0
  74. package/dist/consumer-plugin.js +5 -0
  75. package/dist/consumer-plugin.js.map +1 -0
  76. package/dist/database.d.ts +95 -0
  77. package/dist/database.d.ts.map +1 -0
  78. package/dist/database.js +32 -0
  79. package/dist/database.js.map +1 -0
  80. package/dist/decorators/compatibility.d.ts +14 -0
  81. package/dist/decorators/compatibility.d.ts.map +1 -0
  82. package/dist/decorators/compatibility.js +111 -0
  83. package/dist/decorators/compatibility.js.map +1 -0
  84. package/dist/decorators/index.d.ts +381 -0
  85. package/dist/decorators/index.d.ts.map +1 -0
  86. package/dist/decorators/index.js +104 -0
  87. package/dist/decorators/index.js.map +1 -0
  88. package/dist/dispatch/bus.d.ts +306 -0
  89. package/dist/dispatch/bus.d.ts.map +1 -0
  90. package/dist/dispatch/bus.js +583 -0
  91. package/dist/dispatch/bus.js.map +1 -0
  92. package/dist/dispatch/collections/DispatchSubscriptions.d.ts +79 -0
  93. package/dist/dispatch/collections/DispatchSubscriptions.d.ts.map +1 -0
  94. package/dist/dispatch/collections/DispatchSubscriptions.js +243 -0
  95. package/dist/dispatch/collections/DispatchSubscriptions.js.map +1 -0
  96. package/dist/dispatch/collections/Dispatches.d.ts +98 -0
  97. package/dist/dispatch/collections/Dispatches.d.ts.map +1 -0
  98. package/dist/dispatch/collections/Dispatches.js +358 -0
  99. package/dist/dispatch/collections/Dispatches.js.map +1 -0
  100. package/dist/dispatch/index.d.ts +47 -0
  101. package/dist/dispatch/index.d.ts.map +1 -0
  102. package/dist/dispatch/models/Dispatch.d.ts +101 -0
  103. package/dist/dispatch/models/Dispatch.d.ts.map +1 -0
  104. package/dist/dispatch/models/Dispatch.js +162 -0
  105. package/dist/dispatch/models/Dispatch.js.map +1 -0
  106. package/dist/dispatch/models/DispatchSubscription.d.ts +83 -0
  107. package/dist/dispatch/models/DispatchSubscription.d.ts.map +1 -0
  108. package/dist/dispatch/models/DispatchSubscription.js +112 -0
  109. package/dist/dispatch/models/DispatchSubscription.js.map +1 -0
  110. package/dist/dispatch/tenant-resolver.d.ts +98 -0
  111. package/dist/dispatch/tenant-resolver.d.ts.map +1 -0
  112. package/dist/dispatch/tenant-resolver.js +32 -0
  113. package/dist/dispatch/tenant-resolver.js.map +1 -0
  114. package/dist/dispatch/types.d.ts +149 -0
  115. package/dist/dispatch/types.d.ts.map +1 -0
  116. package/dist/embeddings/hash.d.ts +33 -0
  117. package/dist/embeddings/hash.d.ts.map +1 -0
  118. package/dist/embeddings/hash.js +37 -0
  119. package/dist/embeddings/hash.js.map +1 -0
  120. package/dist/embeddings/index.d.ts +36 -0
  121. package/dist/embeddings/index.d.ts.map +1 -0
  122. package/dist/embeddings/provider.d.ts +75 -0
  123. package/dist/embeddings/provider.d.ts.map +1 -0
  124. package/dist/embeddings/provider.js +170 -0
  125. package/dist/embeddings/provider.js.map +1 -0
  126. package/dist/embeddings/similarity.d.ts +47 -0
  127. package/dist/embeddings/similarity.d.ts.map +1 -0
  128. package/dist/embeddings/similarity.js +64 -0
  129. package/dist/embeddings/similarity.js.map +1 -0
  130. package/dist/embeddings/storage.d.ts +125 -0
  131. package/dist/embeddings/storage.d.ts.map +1 -0
  132. package/dist/embeddings/storage.js +283 -0
  133. package/dist/embeddings/storage.js.map +1 -0
  134. package/dist/embeddings/types.d.ts +250 -0
  135. package/dist/embeddings/types.d.ts.map +1 -0
  136. package/dist/errors.d.ts +363 -0
  137. package/dist/errors.d.ts.map +1 -0
  138. package/dist/errors.js +669 -0
  139. package/dist/errors.js.map +1 -0
  140. package/dist/generators/cli.d.ts +162 -0
  141. package/dist/generators/cli.d.ts.map +1 -0
  142. package/dist/generators/cli.js +462 -0
  143. package/dist/generators/cli.js.map +1 -0
  144. package/dist/generators/index.d.ts +13 -0
  145. package/dist/generators/index.d.ts.map +1 -0
  146. package/dist/generators/mcp-runtime-template.d.ts +60 -0
  147. package/dist/generators/mcp-runtime-template.d.ts.map +1 -0
  148. package/dist/generators/mcp-runtime-template.js +509 -0
  149. package/dist/generators/mcp-runtime-template.js.map +1 -0
  150. package/dist/generators/mcp.d.ts +231 -0
  151. package/dist/generators/mcp.d.ts.map +1 -0
  152. package/dist/generators/mcp.js +1220 -0
  153. package/dist/generators/mcp.js.map +1 -0
  154. package/dist/generators/rest.d.ts +171 -0
  155. package/dist/generators/rest.d.ts.map +1 -0
  156. package/dist/generators/rest.js +591 -0
  157. package/dist/generators/rest.js.map +1 -0
  158. package/dist/generators/swagger.d.ts +21 -0
  159. package/dist/generators/swagger.d.ts.map +1 -0
  160. package/dist/generators/swagger.js +307 -0
  161. package/dist/generators/swagger.js.map +1 -0
  162. package/dist/generators/tenant-gate.d.ts +74 -0
  163. package/dist/generators/tenant-gate.d.ts.map +1 -0
  164. package/dist/generators/tenant-gate.js +15 -0
  165. package/dist/generators/tenant-gate.js.map +1 -0
  166. package/dist/generators.d.ts +8 -0
  167. package/dist/generators.d.ts.map +1 -0
  168. package/dist/generators.js +19 -0
  169. package/dist/generators.js.map +1 -0
  170. package/dist/hierarchical.d.ts +103 -0
  171. package/dist/hierarchical.d.ts.map +1 -0
  172. package/dist/hierarchical.js +184 -0
  173. package/dist/hierarchical.js.map +1 -0
  174. package/dist/index.d.ts +57 -0
  175. package/dist/index.d.ts.map +1 -0
  176. package/dist/index.js +202 -0
  177. package/dist/index.js.map +1 -0
  178. package/dist/interceptors.d.ts +251 -0
  179. package/dist/interceptors.d.ts.map +1 -0
  180. package/dist/interceptors.js +259 -0
  181. package/dist/interceptors.js.map +1 -0
  182. package/dist/junction.d.ts +99 -0
  183. package/dist/junction.d.ts.map +1 -0
  184. package/dist/junction.js +136 -0
  185. package/dist/junction.js.map +1 -0
  186. package/dist/knowledge.d.ts +11 -0
  187. package/dist/knowledge.d.ts.map +1 -0
  188. package/dist/knowledge.js +310 -0
  189. package/dist/knowledge.js.map +1 -0
  190. package/dist/lazy-config.d.ts +160 -0
  191. package/dist/lazy-config.d.ts.map +1 -0
  192. package/dist/lazy-config.js +146 -0
  193. package/dist/lazy-config.js.map +1 -0
  194. package/dist/manifest/discover-base-classes.d.ts +78 -0
  195. package/dist/manifest/discover-base-classes.d.ts.map +1 -0
  196. package/dist/manifest/discover-base-classes.js +85 -0
  197. package/dist/manifest/discover-base-classes.js.map +1 -0
  198. package/dist/manifest/discover-smrt-packages.d.ts +48 -0
  199. package/dist/manifest/discover-smrt-packages.d.ts.map +1 -0
  200. package/dist/manifest/discover-smrt-packages.js +361 -0
  201. package/dist/manifest/discover-smrt-packages.js.map +1 -0
  202. package/dist/manifest/generator.d.ts +93 -0
  203. package/dist/manifest/generator.d.ts.map +1 -0
  204. package/dist/manifest/generator.js +380 -0
  205. package/dist/manifest/generator.js.map +1 -0
  206. package/dist/manifest/index.d.ts +16 -0
  207. package/dist/manifest/index.d.ts.map +1 -0
  208. package/dist/manifest/index.js +51 -0
  209. package/dist/manifest/index.js.map +1 -0
  210. package/dist/manifest/manager.d.ts +51 -0
  211. package/dist/manifest/manager.d.ts.map +1 -0
  212. package/dist/manifest/manager.js +89 -0
  213. package/dist/manifest/manager.js.map +1 -0
  214. package/dist/manifest/manifest-loader.d.ts +187 -0
  215. package/dist/manifest/manifest-loader.d.ts.map +1 -0
  216. package/dist/manifest/manifest-loader.js +847 -0
  217. package/dist/manifest/manifest-loader.js.map +1 -0
  218. package/dist/manifest/sources/composite.d.ts +22 -0
  219. package/dist/manifest/sources/composite.d.ts.map +1 -0
  220. package/dist/manifest/sources/composite.js +60 -0
  221. package/dist/manifest/sources/composite.js.map +1 -0
  222. package/dist/manifest/sources/embedded.d.ts +7 -0
  223. package/dist/manifest/sources/embedded.d.ts.map +1 -0
  224. package/dist/manifest/sources/embedded.js +30 -0
  225. package/dist/manifest/sources/embedded.js.map +1 -0
  226. package/dist/manifest/sources/explicit-paths.d.ts +17 -0
  227. package/dist/manifest/sources/explicit-paths.d.ts.map +1 -0
  228. package/dist/manifest/sources/explicit-paths.js +35 -0
  229. package/dist/manifest/sources/explicit-paths.js.map +1 -0
  230. package/dist/manifest/sources/fallback.d.ts +25 -0
  231. package/dist/manifest/sources/fallback.d.ts.map +1 -0
  232. package/dist/manifest/sources/fallback.js +63 -0
  233. package/dist/manifest/sources/fallback.js.map +1 -0
  234. package/dist/manifest/sources/index.d.ts +17 -0
  235. package/dist/manifest/sources/index.d.ts.map +1 -0
  236. package/dist/manifest/sources/local-test.d.ts +7 -0
  237. package/dist/manifest/sources/local-test.d.ts.map +1 -0
  238. package/dist/manifest/sources/local-test.js +21 -0
  239. package/dist/manifest/sources/local-test.js.map +1 -0
  240. package/dist/manifest/sources/static.d.ts +7 -0
  241. package/dist/manifest/sources/static.d.ts.map +1 -0
  242. package/dist/manifest/sources/static.js +19 -0
  243. package/dist/manifest/sources/static.js.map +1 -0
  244. package/dist/manifest/sources/test.d.ts +7 -0
  245. package/dist/manifest/sources/test.d.ts.map +1 -0
  246. package/dist/manifest/sources/test.js +21 -0
  247. package/dist/manifest/sources/test.js.map +1 -0
  248. package/dist/manifest/sources/types.d.ts +79 -0
  249. package/dist/manifest/sources/types.d.ts.map +1 -0
  250. package/dist/manifest/sources/types.js +61 -0
  251. package/dist/manifest/sources/types.js.map +1 -0
  252. package/dist/manifest/static-manifest.d.ts +4 -0
  253. package/dist/manifest/static-manifest.d.ts.map +1 -0
  254. package/dist/manifest/static-manifest.js +1535 -0
  255. package/dist/manifest/static-manifest.js.map +1 -0
  256. package/dist/manifest/store.d.ts +111 -0
  257. package/dist/manifest/store.d.ts.map +1 -0
  258. package/dist/manifest/store.js +431 -0
  259. package/dist/manifest/store.js.map +1 -0
  260. package/dist/manifest/test-manifest-loader.d.ts +3 -0
  261. package/dist/manifest/test-manifest-loader.d.ts.map +1 -0
  262. package/dist/manifest/test-manifest-stub.d.ts +4 -0
  263. package/dist/manifest/test-manifest-stub.d.ts.map +1 -0
  264. package/dist/manifest/test-manifest-stub.js +80013 -0
  265. package/dist/manifest/test-manifest-stub.js.map +1 -0
  266. package/dist/manifest.d.ts +8 -0
  267. package/dist/manifest.d.ts.map +1 -0
  268. package/dist/manifest.js +20 -0
  269. package/dist/manifest.js.map +1 -0
  270. package/dist/manifest.json +1489 -0
  271. package/dist/mcp-advisor/index.d.ts +499 -0
  272. package/dist/mcp-advisor/index.d.ts.map +1 -0
  273. package/dist/mcp-advisor/tools/add-ai-methods.d.ts +6 -0
  274. package/dist/mcp-advisor/tools/add-ai-methods.d.ts.map +1 -0
  275. package/dist/mcp-advisor/tools/configure-decorators.d.ts +6 -0
  276. package/dist/mcp-advisor/tools/configure-decorators.d.ts.map +1 -0
  277. package/dist/mcp-advisor/tools/generate-collection.d.ts +6 -0
  278. package/dist/mcp-advisor/tools/generate-collection.d.ts.map +1 -0
  279. package/dist/mcp-advisor/tools/generate-field-definitions.d.ts +6 -0
  280. package/dist/mcp-advisor/tools/generate-field-definitions.d.ts.map +1 -0
  281. package/dist/mcp-advisor/tools/generate-smrt-class.d.ts +6 -0
  282. package/dist/mcp-advisor/tools/generate-smrt-class.d.ts.map +1 -0
  283. package/dist/mcp-advisor/tools/get-object-config.d.ts +6 -0
  284. package/dist/mcp-advisor/tools/get-object-config.d.ts.map +1 -0
  285. package/dist/mcp-advisor/tools/get-object-schema.d.ts +10 -0
  286. package/dist/mcp-advisor/tools/get-object-schema.d.ts.map +1 -0
  287. package/dist/mcp-advisor/tools/list-registered-objects.d.ts +9 -0
  288. package/dist/mcp-advisor/tools/list-registered-objects.d.ts.map +1 -0
  289. package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts +9 -0
  290. package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts.map +1 -0
  291. package/dist/mcp-advisor/tools/preview-mcp-tools.d.ts +9 -0
  292. package/dist/mcp-advisor/tools/preview-mcp-tools.d.ts.map +1 -0
  293. package/dist/mcp-advisor/tools/validate-smrt-object.d.ts +6 -0
  294. package/dist/mcp-advisor/tools/validate-smrt-object.d.ts.map +1 -0
  295. package/dist/mcp-advisor/types.d.ts +209 -0
  296. package/dist/mcp-advisor/types.d.ts.map +1 -0
  297. package/dist/migrations/backfill-tracker.d.ts +84 -0
  298. package/dist/migrations/backfill-tracker.d.ts.map +1 -0
  299. package/dist/migrations/backfill-tracker.js +118 -0
  300. package/dist/migrations/backfill-tracker.js.map +1 -0
  301. package/dist/migrations/checksum.d.ts +43 -0
  302. package/dist/migrations/checksum.d.ts.map +1 -0
  303. package/dist/migrations/checksum.js +32 -0
  304. package/dist/migrations/checksum.js.map +1 -0
  305. package/dist/migrations/differ.d.ts +186 -0
  306. package/dist/migrations/differ.d.ts.map +1 -0
  307. package/dist/migrations/differ.js +601 -0
  308. package/dist/migrations/differ.js.map +1 -0
  309. package/dist/migrations/generator.d.ts +133 -0
  310. package/dist/migrations/generator.d.ts.map +1 -0
  311. package/dist/migrations/generator.js +328 -0
  312. package/dist/migrations/generator.js.map +1 -0
  313. package/dist/migrations/index.d.ts +20 -0
  314. package/dist/migrations/index.d.ts.map +1 -0
  315. package/dist/migrations/orchestrate.d.ts +148 -0
  316. package/dist/migrations/orchestrate.d.ts.map +1 -0
  317. package/dist/migrations/orchestrate.js +118 -0
  318. package/dist/migrations/orchestrate.js.map +1 -0
  319. package/dist/migrations/tracker.d.ts +134 -0
  320. package/dist/migrations/tracker.d.ts.map +1 -0
  321. package/dist/migrations/tracker.js +624 -0
  322. package/dist/migrations/tracker.js.map +1 -0
  323. package/dist/migrations/types.d.ts +221 -0
  324. package/dist/migrations/types.d.ts.map +1 -0
  325. package/dist/migrations.d.ts +7 -0
  326. package/dist/migrations.d.ts.map +1 -0
  327. package/dist/migrations.js +26 -0
  328. package/dist/migrations.js.map +1 -0
  329. package/dist/node_modules/.pnpm/balanced-match@4.0.4/node_modules/balanced-match/dist/esm/index.js +56 -0
  330. package/dist/node_modules/.pnpm/balanced-match@4.0.4/node_modules/balanced-match/dist/esm/index.js.map +1 -0
  331. package/dist/node_modules/.pnpm/brace-expansion@5.0.5/node_modules/brace-expansion/dist/esm/index.js +163 -0
  332. package/dist/node_modules/.pnpm/brace-expansion@5.0.5/node_modules/brace-expansion/dist/esm/index.js.map +1 -0
  333. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/assert-valid-pattern.js +13 -0
  334. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/assert-valid-pattern.js.map +1 -0
  335. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/ast.js +654 -0
  336. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/ast.js.map +1 -0
  337. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/brace-expressions.js +111 -0
  338. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/brace-expressions.js.map +1 -0
  339. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/escape.js +10 -0
  340. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/escape.js.map +1 -0
  341. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/index.js +824 -0
  342. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/index.js.map +1 -0
  343. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/unescape.js +10 -0
  344. package/dist/node_modules/.pnpm/minimatch@10.2.3/node_modules/minimatch/dist/esm/unescape.js.map +1 -0
  345. package/dist/object.d.ts +1202 -0
  346. package/dist/object.d.ts.map +1 -0
  347. package/dist/object.js +2731 -0
  348. package/dist/object.js.map +1 -0
  349. package/dist/polymorphic-association.d.ts +69 -0
  350. package/dist/polymorphic-association.d.ts.map +1 -0
  351. package/dist/polymorphic-association.js +96 -0
  352. package/dist/polymorphic-association.js.map +1 -0
  353. package/dist/prebuild/cli.d.ts +7 -0
  354. package/dist/prebuild/cli.d.ts.map +1 -0
  355. package/dist/prebuild/cli.js +29 -0
  356. package/dist/prebuild/cli.js.map +1 -0
  357. package/dist/prebuild/index.d.ts +22 -0
  358. package/dist/prebuild/index.d.ts.map +1 -0
  359. package/dist/prebuild/index.js +239 -0
  360. package/dist/prebuild/index.js.map +1 -0
  361. package/dist/prebuild.d.ts +8 -0
  362. package/dist/prebuild.d.ts.map +1 -0
  363. package/dist/prebuild.js +6 -0
  364. package/dist/prebuild.js.map +1 -0
  365. package/dist/registry/cache-config.d.ts +13 -0
  366. package/dist/registry/cache-config.d.ts.map +1 -0
  367. package/dist/registry/cache-config.js +17 -0
  368. package/dist/registry/cache-config.js.map +1 -0
  369. package/dist/registry/class-registration.d.ts +31 -0
  370. package/dist/registry/class-registration.d.ts.map +1 -0
  371. package/dist/registry/class-registration.js +1074 -0
  372. package/dist/registry/class-registration.js.map +1 -0
  373. package/dist/registry/collection-resolution.d.ts +45 -0
  374. package/dist/registry/collection-resolution.d.ts.map +1 -0
  375. package/dist/registry/collection-resolution.js +121 -0
  376. package/dist/registry/collection-resolution.js.map +1 -0
  377. package/dist/registry/collision-policy.d.ts +179 -0
  378. package/dist/registry/collision-policy.d.ts.map +1 -0
  379. package/dist/registry/collision-policy.js +153 -0
  380. package/dist/registry/collision-policy.js.map +1 -0
  381. package/dist/registry/diagnostics.d.ts +58 -0
  382. package/dist/registry/diagnostics.d.ts.map +1 -0
  383. package/dist/registry/diagnostics.js +54 -0
  384. package/dist/registry/diagnostics.js.map +1 -0
  385. package/dist/registry/embedding-manager.d.ts +23 -0
  386. package/dist/registry/embedding-manager.d.ts.map +1 -0
  387. package/dist/registry/embedding-manager.js +62 -0
  388. package/dist/registry/embedding-manager.js.map +1 -0
  389. package/dist/registry/index.d.ts +13 -0
  390. package/dist/registry/index.d.ts.map +1 -0
  391. package/dist/registry/inheritance-resolver.d.ts +13 -0
  392. package/dist/registry/inheritance-resolver.d.ts.map +1 -0
  393. package/dist/registry/inheritance-resolver.js +244 -0
  394. package/dist/registry/inheritance-resolver.js.map +1 -0
  395. package/dist/registry/manifest-field-merge.d.ts +4 -0
  396. package/dist/registry/manifest-field-merge.d.ts.map +1 -0
  397. package/dist/registry/manifest-field-merge.js +82 -0
  398. package/dist/registry/manifest-field-merge.js.map +1 -0
  399. package/dist/registry/name-resolver.d.ts +102 -0
  400. package/dist/registry/name-resolver.d.ts.map +1 -0
  401. package/dist/registry/name-resolver.js +241 -0
  402. package/dist/registry/name-resolver.js.map +1 -0
  403. package/dist/registry/relationship-graph.d.ts +16 -0
  404. package/dist/registry/relationship-graph.d.ts.map +1 -0
  405. package/dist/registry/relationship-graph.js +79 -0
  406. package/dist/registry/relationship-graph.js.map +1 -0
  407. package/dist/registry/schema-builder.d.ts +113 -0
  408. package/dist/registry/schema-builder.d.ts.map +1 -0
  409. package/dist/registry/schema-builder.js +474 -0
  410. package/dist/registry/schema-builder.js.map +1 -0
  411. package/dist/registry/shared-state.d.ts +62 -0
  412. package/dist/registry/shared-state.d.ts.map +1 -0
  413. package/dist/registry/shared-state.js +135 -0
  414. package/dist/registry/shared-state.js.map +1 -0
  415. package/dist/registry/types.d.ts +667 -0
  416. package/dist/registry/types.d.ts.map +1 -0
  417. package/dist/registry/validator.d.ts +13 -0
  418. package/dist/registry/validator.d.ts.map +1 -0
  419. package/dist/registry/validator.js +138 -0
  420. package/dist/registry/validator.js.map +1 -0
  421. package/dist/registry.d.ts +1358 -0
  422. package/dist/registry.d.ts.map +1 -0
  423. package/dist/registry.js +2301 -0
  424. package/dist/registry.js.map +1 -0
  425. package/dist/runtime/client.d.ts +34 -0
  426. package/dist/runtime/client.d.ts.map +1 -0
  427. package/dist/runtime/client.js +104 -0
  428. package/dist/runtime/client.js.map +1 -0
  429. package/dist/runtime/index.d.ts +10 -0
  430. package/dist/runtime/index.d.ts.map +1 -0
  431. package/dist/runtime/mcp.d.ts +47 -0
  432. package/dist/runtime/mcp.d.ts.map +1 -0
  433. package/dist/runtime/mcp.js +72 -0
  434. package/dist/runtime/mcp.js.map +1 -0
  435. package/dist/runtime/server.d.ts +92 -0
  436. package/dist/runtime/server.d.ts.map +1 -0
  437. package/dist/runtime/server.js +390 -0
  438. package/dist/runtime/server.js.map +1 -0
  439. package/dist/runtime/types.d.ts +58 -0
  440. package/dist/runtime/types.d.ts.map +1 -0
  441. package/dist/runtime.d.ts +8 -0
  442. package/dist/runtime.d.ts.map +1 -0
  443. package/dist/runtime.js +10 -0
  444. package/dist/runtime.js.map +1 -0
  445. package/dist/scanner/import-scanner.d.ts +7 -0
  446. package/dist/scanner/import-scanner.d.ts.map +1 -0
  447. package/dist/scanner/index.d.ts +12 -0
  448. package/dist/scanner/index.d.ts.map +1 -0
  449. package/dist/scanner/manifest-generator.d.ts +304 -0
  450. package/dist/scanner/manifest-generator.d.ts.map +1 -0
  451. package/dist/scanner/manifest-generator.js +1707 -0
  452. package/dist/scanner/manifest-generator.js.map +1 -0
  453. package/dist/scanner/test-file-patterns.d.ts +18 -0
  454. package/dist/scanner/test-file-patterns.d.ts.map +1 -0
  455. package/dist/scanner/test-file-patterns.js +16 -0
  456. package/dist/scanner/test-file-patterns.js.map +1 -0
  457. package/dist/scanner/types.d.ts +313 -0
  458. package/dist/scanner/types.d.ts.map +1 -0
  459. package/dist/scanner/types.js +2 -0
  460. package/dist/scanner/types.js.map +1 -0
  461. package/dist/scanner.d.ts +6 -0
  462. package/dist/scanner.d.ts.map +1 -0
  463. package/dist/scanner.js +6 -0
  464. package/dist/scanner.js.map +1 -0
  465. package/dist/schema/code-generator.d.ts +53 -0
  466. package/dist/schema/code-generator.d.ts.map +1 -0
  467. package/dist/schema/ddl/base-strategy.d.ts +80 -0
  468. package/dist/schema/ddl/base-strategy.d.ts.map +1 -0
  469. package/dist/schema/ddl/base-strategy.js +240 -0
  470. package/dist/schema/ddl/base-strategy.js.map +1 -0
  471. package/dist/schema/ddl/duckdb-strategy.d.ts +33 -0
  472. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -0
  473. package/dist/schema/ddl/duckdb-strategy.js +74 -0
  474. package/dist/schema/ddl/duckdb-strategy.js.map +1 -0
  475. package/dist/schema/ddl/index.d.ts +53 -0
  476. package/dist/schema/ddl/index.d.ts.map +1 -0
  477. package/dist/schema/ddl/index.js +80 -0
  478. package/dist/schema/ddl/index.js.map +1 -0
  479. package/dist/schema/ddl/json-duckdb-strategy.d.ts +8 -0
  480. package/dist/schema/ddl/json-duckdb-strategy.d.ts.map +1 -0
  481. package/dist/schema/ddl/json-duckdb-strategy.js +14 -0
  482. package/dist/schema/ddl/json-duckdb-strategy.js.map +1 -0
  483. package/dist/schema/ddl/postgres-strategy.d.ts +29 -0
  484. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -0
  485. package/dist/schema/ddl/postgres-strategy.js +102 -0
  486. package/dist/schema/ddl/postgres-strategy.js.map +1 -0
  487. package/dist/schema/ddl/sqlite-strategy.d.ts +38 -0
  488. package/dist/schema/ddl/sqlite-strategy.d.ts.map +1 -0
  489. package/dist/schema/ddl/sqlite-strategy.js +74 -0
  490. package/dist/schema/ddl/sqlite-strategy.js.map +1 -0
  491. package/dist/schema/ddl/types.d.ts +114 -0
  492. package/dist/schema/ddl/types.d.ts.map +1 -0
  493. package/dist/schema/generator.d.ts +176 -0
  494. package/dist/schema/generator.d.ts.map +1 -0
  495. package/dist/schema/generator.js +1076 -0
  496. package/dist/schema/generator.js.map +1 -0
  497. package/dist/schema/index-utils.d.ts +19 -0
  498. package/dist/schema/index-utils.d.ts.map +1 -0
  499. package/dist/schema/index-utils.js +32 -0
  500. package/dist/schema/index-utils.js.map +1 -0
  501. package/dist/schema/index.d.ts +13 -0
  502. package/dist/schema/index.d.ts.map +1 -0
  503. package/dist/schema/override-system.d.ts +43 -0
  504. package/dist/schema/override-system.d.ts.map +1 -0
  505. package/dist/schema/schema-aggregator.d.ts +112 -0
  506. package/dist/schema/schema-aggregator.d.ts.map +1 -0
  507. package/dist/schema/schema-manager.d.ts +95 -0
  508. package/dist/schema/schema-manager.d.ts.map +1 -0
  509. package/dist/schema/schema-manager.js +283 -0
  510. package/dist/schema/schema-manager.js.map +1 -0
  511. package/dist/schema/sql-identifiers.d.ts +107 -0
  512. package/dist/schema/sql-identifiers.d.ts.map +1 -0
  513. package/dist/schema/sql-identifiers.js +190 -0
  514. package/dist/schema/sql-identifiers.js.map +1 -0
  515. package/dist/schema/table-verifier.d.ts +10 -0
  516. package/dist/schema/table-verifier.d.ts.map +1 -0
  517. package/dist/schema/table-verifier.js +37 -0
  518. package/dist/schema/table-verifier.js.map +1 -0
  519. package/dist/schema/types.d.ts +241 -0
  520. package/dist/schema/types.d.ts.map +1 -0
  521. package/dist/schema/utils.d.ts +32 -0
  522. package/dist/schema/utils.d.ts.map +1 -0
  523. package/dist/schema/utils.js +134 -0
  524. package/dist/schema/utils.js.map +1 -0
  525. package/dist/scripts/create-wrappers.js +89 -0
  526. package/dist/scripts/generate-manifest.js +155 -0
  527. package/dist/scripts/generate-test-manifest.js +77 -0
  528. package/dist/scripts/migrate-datetime-to-timestamp.ts +310 -0
  529. package/dist/scripts/prepack.js +49 -0
  530. package/dist/signals/bus.d.ts +64 -0
  531. package/dist/signals/bus.d.ts.map +1 -0
  532. package/dist/signals/bus.js +102 -0
  533. package/dist/signals/bus.js.map +1 -0
  534. package/dist/signals/index.d.ts +11 -0
  535. package/dist/signals/index.d.ts.map +1 -0
  536. package/dist/signals/sanitizer.d.ts +54 -0
  537. package/dist/signals/sanitizer.d.ts.map +1 -0
  538. package/dist/signals/sanitizer.js +111 -0
  539. package/dist/signals/sanitizer.js.map +1 -0
  540. package/dist/smrt-knowledge.json +335 -0
  541. package/dist/system/compatibility.d.ts +8 -0
  542. package/dist/system/compatibility.d.ts.map +1 -0
  543. package/dist/system/compatibility.js +409 -0
  544. package/dist/system/compatibility.js.map +1 -0
  545. package/dist/system/index.d.ts +9 -0
  546. package/dist/system/index.d.ts.map +1 -0
  547. package/dist/system/schema.d.ts +69 -0
  548. package/dist/system/schema.d.ts.map +1 -0
  549. package/dist/system/schema.js +271 -0
  550. package/dist/system/schema.js.map +1 -0
  551. package/dist/system/types.d.ts +135 -0
  552. package/dist/system/types.d.ts.map +1 -0
  553. package/dist/system-fields.d.ts +44 -0
  554. package/dist/system-fields.d.ts.map +1 -0
  555. package/dist/system-fields.js +55 -0
  556. package/dist/system-fields.js.map +1 -0
  557. package/dist/table-cache.d.ts +28 -0
  558. package/dist/table-cache.d.ts.map +1 -0
  559. package/dist/table-cache.js +21 -0
  560. package/dist/table-cache.js.map +1 -0
  561. package/dist/test-utils.d.ts +140 -0
  562. package/dist/test-utils.d.ts.map +1 -0
  563. package/dist/testing/database.d.ts +73 -0
  564. package/dist/testing/database.d.ts.map +1 -0
  565. package/dist/testing/database.js +204 -0
  566. package/dist/testing/database.js.map +1 -0
  567. package/dist/testing/index.d.ts +21 -0
  568. package/dist/testing/index.d.ts.map +1 -0
  569. package/dist/testing.d.ts +6 -0
  570. package/dist/testing.d.ts.map +1 -0
  571. package/dist/testing.js +5 -0
  572. package/dist/testing.js.map +1 -0
  573. package/dist/tools/index.d.ts +8 -0
  574. package/dist/tools/index.d.ts.map +1 -0
  575. package/dist/tools/tool-executor.d.ts +101 -0
  576. package/dist/tools/tool-executor.d.ts.map +1 -0
  577. package/dist/tools/tool-executor.js +142 -0
  578. package/dist/tools/tool-executor.js.map +1 -0
  579. package/dist/tools/tool-generator.d.ts +54 -0
  580. package/dist/tools/tool-generator.d.ts.map +1 -0
  581. package/dist/tools/tool-generator.js +121 -0
  582. package/dist/tools/tool-generator.js.map +1 -0
  583. package/dist/utils/chunk.d.ts +32 -0
  584. package/dist/utils/chunk.d.ts.map +1 -0
  585. package/dist/utils/chunk.js +14 -0
  586. package/dist/utils/chunk.js.map +1 -0
  587. package/dist/utils/import-workspace-module.d.ts +8 -0
  588. package/dist/utils/import-workspace-module.d.ts.map +1 -0
  589. package/dist/utils/import-workspace-module.js +81 -0
  590. package/dist/utils/import-workspace-module.js.map +1 -0
  591. package/dist/utils/json.d.ts +102 -0
  592. package/dist/utils/json.d.ts.map +1 -0
  593. package/dist/utils/json.js +43 -0
  594. package/dist/utils/json.js.map +1 -0
  595. package/dist/utils/lru-cache.d.ts +69 -0
  596. package/dist/utils/lru-cache.d.ts.map +1 -0
  597. package/dist/utils/lru-cache.js +100 -0
  598. package/dist/utils/lru-cache.js.map +1 -0
  599. package/dist/utils/naming.d.ts +16 -0
  600. package/dist/utils/naming.d.ts.map +1 -0
  601. package/dist/utils/naming.js +23 -0
  602. package/dist/utils/naming.js.map +1 -0
  603. package/dist/utils/qualified-names.d.ts +122 -0
  604. package/dist/utils/qualified-names.d.ts.map +1 -0
  605. package/dist/utils/qualified-names.js +82 -0
  606. package/dist/utils/qualified-names.js.map +1 -0
  607. package/dist/utils/scanner-module.d.ts +37 -0
  608. package/dist/utils/scanner-module.d.ts.map +1 -0
  609. package/dist/utils.d.ts +177 -0
  610. package/dist/utils.d.ts.map +1 -0
  611. package/dist/utils.js +185 -0
  612. package/dist/utils.js.map +1 -0
  613. package/dist/vite-plugin/import-build-aware.d.ts +68 -0
  614. package/dist/vite-plugin/import-build-aware.d.ts.map +1 -0
  615. package/dist/vite-plugin/import-build-aware.js +72 -0
  616. package/dist/vite-plugin/import-build-aware.js.map +1 -0
  617. package/dist/vite-plugin/index.d.ts +59 -0
  618. package/dist/vite-plugin/index.d.ts.map +1 -0
  619. package/dist/vite-plugin/index.js +1400 -0
  620. package/dist/vite-plugin/index.js.map +1 -0
  621. package/dist/vite-plugin/sveltekit-generator.d.ts +66 -0
  622. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -0
  623. package/dist/vite-plugin/sveltekit-generator.js +1375 -0
  624. package/dist/vite-plugin/sveltekit-generator.js.map +1 -0
  625. package/dist/vite-plugin/templates/default-ui.ts +432 -0
  626. package/dist/vite-plugin/templates/default.html +206 -0
  627. package/dist/vite-plugin.d.ts +8 -0
  628. package/dist/vite-plugin.d.ts.map +1 -0
  629. package/dist/vite-plugin.js +11 -0
  630. package/dist/vite-plugin.js.map +1 -0
  631. package/package.json +208 -0
package/dist/object.js ADDED
@@ -0,0 +1,2731 @@
1
+ import { createLogger } from "@happyvertical/logger";
2
+ import { SmrtClass } from "./class.js";
3
+ import { resolveDbCacheKey, invalidateCollectionCache, broadcastCacheInvalidation, hasCrossProcessCacheInterest } from "./collection-cache.js";
4
+ import { ContentHasher } from "./embeddings/hash.js";
5
+ import { EmbeddingProvider } from "./embeddings/provider.js";
6
+ import { EmbeddingStorage } from "./embeddings/storage.js";
7
+ import { ErrorUtils, DatabaseError, ValidationError, SmrtError, RuntimeError, TenantIsolationError } from "./errors.js";
8
+ import { createInterceptorContext, GlobalInterceptors } from "./interceptors.js";
9
+ import { ObjectRegistry } from "./registry.js";
10
+ import { verifyPersistenceTable } from "./schema/table-verifier.js";
11
+ import { executeToolCall } from "./tools/tool-executor.js";
12
+ import { fieldsFromClass, tableNameFromClass } from "./utils.js";
13
+ import { toSnakeCase } from "./utils/naming.js";
14
+ const logger = createLogger({
15
+ level: process.env.DEBUG_STI ? "debug" : "info"
16
+ });
17
+ const AI_PROMPT_DATA_MAX_LENGTH = 1e5;
18
+ function isValidMetaType(actualMetaType, className) {
19
+ if (typeof actualMetaType !== "string") {
20
+ return false;
21
+ }
22
+ if (actualMetaType === className) {
23
+ return true;
24
+ }
25
+ const registeredClass = ObjectRegistry.getClass(className);
26
+ if (registeredClass?.qualifiedName && actualMetaType === registeredClass.qualifiedName) {
27
+ return true;
28
+ }
29
+ return false;
30
+ }
31
+ function getExpectedMetaType(className) {
32
+ const registeredClass = ObjectRegistry.getClass(className);
33
+ return registeredClass?.qualifiedName || className;
34
+ }
35
+ function getSTIHierarchyMembers(qualifiedOrSimpleName) {
36
+ const registered = ObjectRegistry.getClass(qualifiedOrSimpleName);
37
+ const lookupKey = registered?.qualifiedName ?? registered?.name ?? qualifiedOrSimpleName;
38
+ const stiBase = ObjectRegistry.getSTIBase(lookupKey);
39
+ if (!stiBase) {
40
+ return [];
41
+ }
42
+ return Array.from(
43
+ /* @__PURE__ */ new Set([stiBase, ...ObjectRegistry.getDescendants(stiBase)])
44
+ );
45
+ }
46
+ let didWarnSkipRehydrate = false;
47
+ function warnIfSkipRehydrateSet() {
48
+ if (didWarnSkipRehydrate) return;
49
+ didWarnSkipRehydrate = true;
50
+ if (process.env.SMRT_SKIP_STI_REHYDRATE !== void 0) {
51
+ logger.warn(
52
+ "[smrt] SMRT_SKIP_STI_REHYDRATE is set but has no effect since Release C (#1134). The flag is retired; unconditional STI descendant rehydration is the only behavior. Remove the env variable from your configuration. See issue #1139 for the perf follow-up."
53
+ );
54
+ }
55
+ }
56
+ class SmrtObject extends SmrtClass {
57
+ /**
58
+ * Database table name for this object
59
+ */
60
+ _tableName;
61
+ /**
62
+ * Cache for loaded relationships to avoid repeated database queries
63
+ * Maps fieldName to loaded object(s)
64
+ */
65
+ _loadedRelationships = /* @__PURE__ */ new Map();
66
+ /**
67
+ * Whether this object is backed by an existing database row.
68
+ *
69
+ * Set when the object is hydrated from the database (collection
70
+ * get/list/query, `loadFromId()`, `loadFromSlug()`) and after a successful
71
+ * `save()`. `save()` uses it to target the primary key on upsert instead
72
+ * of the natural-key conflict columns, so natural-key edits (e.g. renaming
73
+ * a slug) update the existing row rather than colliding on `*_pkey`
74
+ * (issue #1472).
75
+ */
76
+ _persisted = false;
77
+ /**
78
+ * Unique identifier for the object
79
+ */
80
+ _id;
81
+ /**
82
+ * URL-friendly identifier
83
+ */
84
+ _slug;
85
+ /**
86
+ * Optional context to scope the slug
87
+ */
88
+ _context;
89
+ /**
90
+ * Creation timestamp
91
+ */
92
+ created_at = null;
93
+ /**
94
+ * Last update timestamp
95
+ */
96
+ updated_at = null;
97
+ /**
98
+ * Creates a new SmrtObject instance
99
+ *
100
+ * @param options - Configuration options including identifiers and metadata
101
+ * @throws Error if options is null
102
+ */
103
+ constructor(options = {}) {
104
+ super(options);
105
+ this.options = options;
106
+ if (options === null) {
107
+ throw new Error("options cant be null");
108
+ }
109
+ this._id = options.id || null;
110
+ this._slug = options.slug || null;
111
+ this._context = options.context || "";
112
+ if (this.constructor !== SmrtObject && !ObjectRegistry.getClassByConstructor(
113
+ this.constructor
114
+ ) && !options?._skipRegistration) {
115
+ ObjectRegistry.register(this.constructor, {});
116
+ }
117
+ }
118
+ getRegisteredClassInfo() {
119
+ return ObjectRegistry.getClassByConstructor(
120
+ this.constructor
121
+ );
122
+ }
123
+ getResolvedClassName() {
124
+ return this.getRegisteredClassInfo()?.name || this.constructor.name;
125
+ }
126
+ getResolvedQualifiedName() {
127
+ const registered = this.getRegisteredClassInfo();
128
+ return registered?.qualifiedName || registered?.name || this.constructor.name;
129
+ }
130
+ getCurrentMetaType() {
131
+ const metaType = this._meta_type;
132
+ return typeof metaType === "string" ? metaType : void 0;
133
+ }
134
+ isLegacySTIDiscriminatorUpgrade(currentMetaType, nextMetaType, className) {
135
+ return typeof nextMetaType === "string" && typeof currentMetaType === "string" && currentMetaType !== nextMetaType && !currentMetaType.includes(":") && isValidMetaType(currentMetaType, className) && isValidMetaType(nextMetaType, className);
136
+ }
137
+ async planPersistenceWrite(className, tableStrategy, data, conflictColumns) {
138
+ const upsertPlan = {
139
+ type: "upsert",
140
+ conflictColumns
141
+ };
142
+ if (tableStrategy !== "sti" || !data.id) {
143
+ return upsertPlan;
144
+ }
145
+ const currentMetaType = this.getCurrentMetaType();
146
+ const nextMetaType = data._meta_type;
147
+ if (!this.isLegacySTIDiscriminatorUpgrade(
148
+ currentMetaType,
149
+ nextMetaType,
150
+ className
151
+ )) {
152
+ return upsertPlan;
153
+ }
154
+ const qualifiedMetaType = String(nextMetaType);
155
+ const conflictIdentity = {};
156
+ for (const column of conflictColumns.filter(
157
+ (conflictColumn) => conflictColumn !== "_meta_type"
158
+ )) {
159
+ if (!Object.hasOwn(data, column)) {
160
+ return upsertPlan;
161
+ }
162
+ conflictIdentity[column] = data[column];
163
+ }
164
+ const id = String(data.id);
165
+ const existingById = await ErrorUtils.withRetry(
166
+ async () => {
167
+ try {
168
+ return await this.db.get(this.tableName, { id });
169
+ } catch (error) {
170
+ throw DatabaseError.queryFailed(
171
+ `get(${this.tableName}, legacy STI id)`,
172
+ error instanceof Error ? error : new Error(String(error))
173
+ );
174
+ }
175
+ },
176
+ 3,
177
+ 500
178
+ );
179
+ if (!existingById || existingById._meta_type !== currentMetaType) {
180
+ return upsertPlan;
181
+ }
182
+ const slug = String(data.slug ?? "");
183
+ const context = String(data.context ?? "");
184
+ const existingQualified = await ErrorUtils.withRetry(
185
+ async () => {
186
+ try {
187
+ return await this.db.get(this.tableName, {
188
+ ...conflictIdentity,
189
+ _meta_type: qualifiedMetaType
190
+ });
191
+ } catch (error) {
192
+ throw DatabaseError.queryFailed(
193
+ `get(${this.tableName}, qualified STI conflict identity)`,
194
+ error instanceof Error ? error : new Error(String(error))
195
+ );
196
+ }
197
+ },
198
+ 3,
199
+ 500
200
+ );
201
+ if (existingQualified && existingQualified.id !== id) {
202
+ throw DatabaseError.stiDiscriminatorConflict({
203
+ className,
204
+ tableName: this.tableName,
205
+ id,
206
+ slug,
207
+ context,
208
+ conflictIdentity,
209
+ legacyMetaType: currentMetaType,
210
+ qualifiedMetaType,
211
+ duplicateId: String(existingQualified.id)
212
+ });
213
+ }
214
+ return { type: "updateById", qualifiedMetaType };
215
+ }
216
+ /**
217
+ * Protected setter for ID to maintain type safety
218
+ * Used internally by collection.create() and other framework code
219
+ * @internal
220
+ */
221
+ setId(id) {
222
+ this._id = id;
223
+ }
224
+ /**
225
+ * Whether this object is backed by an existing database row.
226
+ *
227
+ * True after hydration from the database (collection get/list/query,
228
+ * `loadFromId()`, `loadFromSlug()`) or after a successful `save()`.
229
+ */
230
+ get isPersisted() {
231
+ return this._persisted;
232
+ }
233
+ /**
234
+ * Marks this object as backed by an existing database row, so `save()`
235
+ * targets the primary key instead of the natural-key conflict columns.
236
+ * Called by framework hydration paths (issue #1472).
237
+ * @internal
238
+ */
239
+ markAsPersisted() {
240
+ this._persisted = true;
241
+ }
242
+ async verifyStorageReady() {
243
+ await verifyPersistenceTable(
244
+ this.db,
245
+ this.tableName,
246
+ this.constructor.name
247
+ );
248
+ }
249
+ /**
250
+ * Protected setter for STI discriminator to maintain type safety
251
+ * Used internally for Single Table Inheritance support
252
+ * @internal
253
+ */
254
+ setMetaType(metaType) {
255
+ this._meta_type = metaType;
256
+ }
257
+ /**
258
+ * Smart clone helper to avoid array/object aliasing (Issue #22)
259
+ *
260
+ * Clones values to prevent shared references between options and instance properties:
261
+ * - Primitives (string, number, boolean, null, undefined): No clone needed
262
+ * - Dates: No clone needed (immutable)
263
+ * - Field instances: No clone needed (framework objects)
264
+ * - Arrays: Shallow clone
265
+ * - Plain objects: Shallow clone
266
+ * - Other objects: Pass through (class instances, etc.)
267
+ *
268
+ * @param value - Value to clone
269
+ * @returns Cloned value or original if no cloning needed
270
+ * @private
271
+ */
272
+ cloneValue(value) {
273
+ if (value === null || value === void 0) return value;
274
+ if (typeof value !== "object") return value;
275
+ if (value instanceof Date) return value;
276
+ if (Array.isArray(value)) return [...value];
277
+ const proto = Object.getPrototypeOf(value);
278
+ if (proto === null || proto === Object.prototype) {
279
+ return { ...value };
280
+ }
281
+ return value;
282
+ }
283
+ /**
284
+ * Initialize properties from options after field initializers have run
285
+ * This ensures option values take precedence over default field initializer values
286
+ * Uses smart cloning to prevent array/object aliasing (Issue #22)
287
+ */
288
+ async initializePropertiesFromOptions() {
289
+ const options = this.options;
290
+ if (options.created_at !== void 0) this.created_at = options.created_at;
291
+ if (options.updated_at !== void 0) this.updated_at = options.updated_at;
292
+ if (options._meta_type !== void 0) {
293
+ this.setMetaType(options._meta_type);
294
+ }
295
+ const fields = await fieldsFromClass(
296
+ this.constructor
297
+ );
298
+ for (const [key, field] of Object.entries(fields)) {
299
+ if (options[key] !== void 0) {
300
+ const clonedValue = this.cloneValue(options[key]);
301
+ let descriptor = Object.getOwnPropertyDescriptor(this, key);
302
+ if (!descriptor) {
303
+ let proto = Object.getPrototypeOf(this);
304
+ while (proto && !descriptor) {
305
+ descriptor = Object.getOwnPropertyDescriptor(proto, key);
306
+ proto = Object.getPrototypeOf(proto);
307
+ }
308
+ }
309
+ if (!descriptor || descriptor.set || descriptor.writable === true) {
310
+ this[key] = clonedValue;
311
+ }
312
+ }
313
+ }
314
+ }
315
+ /**
316
+ * Gets the unique identifier for this object
317
+ */
318
+ get id() {
319
+ return this._id;
320
+ }
321
+ /**
322
+ * Sets the unique identifier for this object
323
+ *
324
+ * @param value - The ID to set
325
+ * @throws Error if the value is invalid
326
+ */
327
+ set id(value) {
328
+ if (!value || value === "undefined" || value === "null") {
329
+ throw new Error(`id is required, ${value} given`);
330
+ }
331
+ this._id = value;
332
+ }
333
+ /**
334
+ * Gets the URL-friendly slug for this object
335
+ */
336
+ get slug() {
337
+ return this._slug;
338
+ }
339
+ /**
340
+ * Sets the URL-friendly slug for this object
341
+ *
342
+ * @param value - The slug to set
343
+ * @throws Error if the value is invalid
344
+ */
345
+ set slug(value) {
346
+ if (!value || value === "undefined" || value === "null") {
347
+ throw new Error(`slug is invalid, ${value} given`);
348
+ }
349
+ this._slug = value;
350
+ }
351
+ /**
352
+ * Gets the context that scopes this object's slug
353
+ */
354
+ get context() {
355
+ return this._context || "";
356
+ }
357
+ /**
358
+ * Sets the context that scopes this object's slug
359
+ *
360
+ * @param value - The context to set
361
+ * @throws Error if the value is invalid
362
+ */
363
+ set context(value) {
364
+ if (value !== "" && !value) {
365
+ throw new Error(`context is invalid, ${value} given`);
366
+ }
367
+ this._context = value;
368
+ }
369
+ /**
370
+ * Initializes this object and optionally loads its data from the database.
371
+ *
372
+ * Initialization order:
373
+ * 1. Runs TypeScript field initializers (class property defaults)
374
+ * 2. Applies `options` values on top of defaults (options win)
375
+ * 3. If `options.id` is set, calls `loadFromId()` to hydrate from DB
376
+ * 4. If `options.slug` is set (and no id), calls `loadFromSlug()` instead
377
+ *
378
+ * Runtime schema verification is deferred until the first actual DB
379
+ * operation, which keeps initialization safe for SSR and prerendering
380
+ * without mutating application schema.
381
+ *
382
+ * Called automatically by collection methods. Call manually only when
383
+ * constructing objects outside of a collection.
384
+ *
385
+ * @returns This instance, ready to use (enables chaining)
386
+ *
387
+ * @example
388
+ * ```typescript
389
+ * const product = new Product({ db: myDb, id: 'existing-uuid' });
390
+ * await product.initialize(); // loads product data from DB
391
+ * console.log(product.name);
392
+ * ```
393
+ */
394
+ async initialize() {
395
+ await super.initialize();
396
+ if (!this.options._extractingFields) {
397
+ await this.initializePropertiesFromOptions();
398
+ }
399
+ if (this._id && !this.options._skipLoad) {
400
+ await this.loadFromId();
401
+ } else if (this._slug && !this.options._skipLoad) {
402
+ await this.loadFromSlug();
403
+ }
404
+ return this;
405
+ }
406
+ /**
407
+ * Determines if this class needs automatic property initialization from options
408
+ *
409
+ * Decorator-based classes handle property assignment in their constructor,
410
+ * so they don't need the automatic property initialization logic.
411
+ * This optimization avoids redundant work and Field instance handling.
412
+ *
413
+ * @returns True if property initialization is needed, false otherwise
414
+ * @private
415
+ */
416
+ needsPropertyInitialization() {
417
+ const className = this.getResolvedClassName();
418
+ if (ObjectRegistry.hasClass(className)) {
419
+ const hasDecorators = ObjectRegistry.hasFieldDecorators(className);
420
+ if (hasDecorators) {
421
+ return false;
422
+ }
423
+ }
424
+ return true;
425
+ }
426
+ /**
427
+ * Loads data from a database row into this object's properties
428
+ *
429
+ * STI Support: If using Single Table Inheritance (tableStrategy: 'sti'):
430
+ * - Merges _meta_data JSONB fields into the main data object
431
+ * - Validates _meta_type discriminator matches class name
432
+ *
433
+ * @param data - Database row data (with snake_case column names)
434
+ * @throws Error if STI validation fails
435
+ */
436
+ async loadDataFromDb(data) {
437
+ const className = this.getResolvedClassName();
438
+ if (process.env.DEBUG_STI) {
439
+ logger.debug("[loadDataFromDb] Loading", {
440
+ class: className,
441
+ dataKeys: Object.keys(data),
442
+ metaType: data._meta_type
443
+ });
444
+ }
445
+ const fields = await this.getFields();
446
+ if (process.env.DEBUG_STI) {
447
+ logger.debug("[loadDataFromDb] Field definitions", {
448
+ class: className,
449
+ fieldKeys: Object.keys(fields)
450
+ });
451
+ }
452
+ const { formatDataJs } = await import("./utils.js");
453
+ const formattedData = formatDataJs(data, fields);
454
+ const tableStrategy = ObjectRegistry.getTableStrategy(
455
+ this.getResolvedQualifiedName()
456
+ );
457
+ const isSTI = tableStrategy === "sti";
458
+ if (process.env.DEBUG_STI) {
459
+ logger.debug("[loadDataFromDb] After formatDataJs", {
460
+ class: className,
461
+ isSTI,
462
+ formattedDataKeys: Object.keys(formattedData)
463
+ });
464
+ }
465
+ if (isSTI) {
466
+ if (!formattedData._meta_type) {
467
+ throw new Error(
468
+ `STI validation failed: Missing _meta_type discriminator in database row for ${className}. Ensure the row was saved with STI support enabled.`
469
+ );
470
+ }
471
+ if (!isValidMetaType(formattedData._meta_type, className)) {
472
+ 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.`
474
+ );
475
+ }
476
+ this.setMetaType(formattedData._meta_type);
477
+ }
478
+ if (process.env.DEBUG_STI) {
479
+ logger.debug("[loadDataFromDb] Starting field hydration", {
480
+ class: className,
481
+ fieldCount: Object.keys(fields).length
482
+ });
483
+ }
484
+ let hydratedCount = 0;
485
+ let skippedCount = 0;
486
+ for (const field in fields) {
487
+ if (Object.hasOwn(fields, field)) {
488
+ let descriptor = Object.getOwnPropertyDescriptor(this, field);
489
+ if (!descriptor) {
490
+ let proto = Object.getPrototypeOf(this);
491
+ while (proto && !descriptor) {
492
+ descriptor = Object.getOwnPropertyDescriptor(proto, field);
493
+ proto = Object.getPrototypeOf(proto);
494
+ }
495
+ }
496
+ if (!descriptor || descriptor.set || descriptor.writable === true) {
497
+ const value = formattedData[field];
498
+ if (process.env.DEBUG_STI && value !== void 0) {
499
+ logger.debug(`[loadDataFromDb] Setting field '${field}'`, {
500
+ value,
501
+ valueType: typeof value
502
+ });
503
+ }
504
+ this[field] = value;
505
+ hydratedCount++;
506
+ } else {
507
+ skippedCount++;
508
+ if (process.env.DEBUG_STI) {
509
+ logger.debug(`[loadDataFromDb] Skipping readonly field '${field}'`);
510
+ }
511
+ }
512
+ }
513
+ }
514
+ if (process.env.DEBUG_STI) {
515
+ logger.debug("[loadDataFromDb] Hydration complete", {
516
+ class: className,
517
+ hydratedCount,
518
+ skippedCount,
519
+ totalFields: Object.keys(fields).length
520
+ });
521
+ }
522
+ this._persisted = true;
523
+ }
524
+ /**
525
+ * Gets the database table name for this object
526
+ */
527
+ get tableName() {
528
+ if (!this._tableName) {
529
+ const className = this.getResolvedClassName();
530
+ const qualifiedName = this.getResolvedQualifiedName();
531
+ const tableStrategy = ObjectRegistry.getTableStrategy(qualifiedName);
532
+ if (tableStrategy === "sti") {
533
+ const stiBase = ObjectRegistry.getSTIBase(qualifiedName);
534
+ if (stiBase) {
535
+ const baseSchema = ObjectRegistry.getSchema(stiBase);
536
+ if (baseSchema?.tableName) {
537
+ this._tableName = baseSchema.tableName;
538
+ } else {
539
+ const ownSchema = ObjectRegistry.getSchema(className);
540
+ this._tableName = ownSchema?.tableName || tableNameFromClass(this.constructor);
541
+ }
542
+ } else {
543
+ const ownSchema = ObjectRegistry.getSchema(className);
544
+ this._tableName = ownSchema?.tableName || tableNameFromClass(this.constructor);
545
+ }
546
+ } else {
547
+ const ownSchema = ObjectRegistry.getSchema(className);
548
+ this._tableName = ownSchema?.tableName || tableNameFromClass(this.constructor);
549
+ }
550
+ }
551
+ return this._tableName;
552
+ }
553
+ /**
554
+ * Gets field definitions and current values for this object
555
+ *
556
+ * @returns Object containing field definitions with current values
557
+ */
558
+ async getFields() {
559
+ const className = this.getResolvedClassName();
560
+ const cachedFields = await ObjectRegistry.getAllFields(className);
561
+ const fields = {};
562
+ for (const [key, field] of cachedFields.entries()) {
563
+ const meta = { ...field._meta || {} };
564
+ delete meta.__smrtSystemField;
565
+ fields[key] = {
566
+ name: key,
567
+ type: field.type || "TEXT",
568
+ _meta: meta
569
+ };
570
+ }
571
+ for (const key in fields) {
572
+ fields[key].value = this.getPropertyValue(key);
573
+ }
574
+ return fields;
575
+ }
576
+ /**
577
+ * Override hook for customizing JSON serialization output.
578
+ *
579
+ * This is the **only safe way** to customize serialization. Do **not** override
580
+ * `toJSON()` directly — it manages STI discriminator assignment (`_meta_type`),
581
+ * meta-field extraction into `_meta_data`, and manifest-driven field enumeration.
582
+ * Overriding it will silently break STI persistence.
583
+ *
584
+ * The `data` argument already contains all persisted fields. Return a new object
585
+ * (spread `data` and add/modify/remove keys) to change what is serialized.
586
+ * Non-persisted computed properties added here are excluded from `save()` because
587
+ * the DB write path uses `toJSON()` output, not this hook's additions — unless
588
+ * those keys also correspond to real schema columns.
589
+ *
590
+ * @param data - Fully serialized object produced by the framework's `toJSON()` logic
591
+ * @returns Transformed data to use as the final serialization result
592
+ *
593
+ * @example
594
+ * ```typescript
595
+ * class Article extends SmrtObject {
596
+ * body: string = '';
597
+ *
598
+ * protected transformJSON(data: any): any {
599
+ * return {
600
+ * ...data,
601
+ * wordCount: this.body.split(/\s+/).length,
602
+ * preview: this.body.substring(0, 100),
603
+ * };
604
+ * }
605
+ * }
606
+ * ```
607
+ *
608
+ * @see {@link toJSON} for the framework implementation (do not override)
609
+ */
610
+ transformJSON(data) {
611
+ return data;
612
+ }
613
+ /**
614
+ * Custom JSON serialization
615
+ * Returns a plain object with all field values for proper JSON.stringify() behavior
616
+ * Field instances automatically call their toJSON() method during serialization
617
+ *
618
+ * Issue #205: Filters out undefined values to prevent database errors
619
+ *
620
+ * Note: This method cannot be async because JSON.stringify() expects synchronous toJSON()
621
+ * It uses direct property access instead of getFields() to avoid async issues
622
+ *
623
+ * STI Support: If using Single Table Inheritance (tableStrategy: 'sti'):
624
+ * - Sets _meta_type discriminator to class name
625
+ * - Extracts meta fields to _meta_data JSONB column
626
+ *
627
+ * **Customization:** Override `transformJSON()` instead of this method.
628
+ * See transformJSON() documentation for safe customization patterns.
629
+ */
630
+ toJSON() {
631
+ const className = this.getResolvedClassName();
632
+ const data = {
633
+ id: this.id,
634
+ slug: this.slug,
635
+ context: this.context,
636
+ created_at: this.created_at,
637
+ updated_at: this.updated_at
638
+ };
639
+ const tableStrategy = ObjectRegistry.getTableStrategy(
640
+ this.getResolvedQualifiedName()
641
+ );
642
+ const isSTI = tableStrategy === "sti";
643
+ if (isSTI) {
644
+ data._meta_type = this.getResolvedQualifiedName();
645
+ data._meta_data = {};
646
+ }
647
+ const registered = ObjectRegistry.getClass(className);
648
+ let registeredFields = registered?.inheritedFields || ObjectRegistry.getFields(className);
649
+ if (isSTI) {
650
+ const descendants = getSTIHierarchyMembers(
651
+ this.getResolvedQualifiedName()
652
+ );
653
+ if (descendants.length > 0) {
654
+ const allSTIFields = new Map(registeredFields);
655
+ for (const descendant of descendants) {
656
+ const descendantFields = ObjectRegistry.getClass(descendant)?.inheritedFields || ObjectRegistry.getFields(descendant);
657
+ for (const [key, value] of descendantFields) {
658
+ if (!allSTIFields.has(key)) {
659
+ allSTIFields.set(key, value);
660
+ }
661
+ }
662
+ }
663
+ registeredFields = allSTIFields;
664
+ }
665
+ }
666
+ for (const key of registeredFields.keys()) {
667
+ 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") {
669
+ continue;
670
+ }
671
+ const fieldDef = registeredFields.get(key);
672
+ if (fieldDef && (fieldDef.transient || fieldDef._meta?.transient)) {
673
+ continue;
674
+ }
675
+ if (fieldDef && (fieldDef.type === "oneToMany" || fieldDef.type === "manyToMany")) {
676
+ continue;
677
+ }
678
+ const prop = this[key];
679
+ const value = this.getPropertyValue(key);
680
+ if (value === void 0) {
681
+ const fieldType = prop && typeof prop === "object" && "type" in prop && prop.type || fieldDef?.type;
682
+ if (fieldType === "text") {
683
+ const hasTenancyMarker = prop && typeof prop === "object" && "__tenancy" in prop && prop.__tenancy?.isTenantIdField || fieldDef?.__tenancy?.isTenantIdField || fieldDef?._meta?.__tenancy?.isTenantIdField;
684
+ if (hasTenancyMarker) {
685
+ if (isSTI && fieldDef?.type === "meta") {
686
+ data._meta_data[key] = null;
687
+ } else {
688
+ data[key] = null;
689
+ }
690
+ } else {
691
+ if (isSTI && fieldDef?.type === "meta") {
692
+ data._meta_data[key] = "";
693
+ } else {
694
+ data[key] = "";
695
+ }
696
+ }
697
+ } else if (fieldType === "json") {
698
+ const defaultValue = fieldDef?.default ?? null;
699
+ if (isSTI && fieldDef?.type === "meta") {
700
+ data._meta_data[key] = defaultValue;
701
+ } else {
702
+ data[key] = defaultValue;
703
+ }
704
+ }
705
+ continue;
706
+ }
707
+ if (isSTI && fieldDef && fieldDef.type === "meta") {
708
+ data._meta_data[key] = value;
709
+ } else {
710
+ data[key] = value;
711
+ }
712
+ }
713
+ return this.transformJSON(data);
714
+ }
715
+ /**
716
+ * Converts this object to a plain JavaScript object (POJO)
717
+ *
718
+ * Unlike toJSON() which returns an object that can still be a class instance,
719
+ * this method returns a true plain object suitable for:
720
+ * - SvelteKit's load function returns (requires serializable data)
721
+ * - Passing data through web workers
722
+ * - Any context requiring non-class objects
723
+ *
724
+ * @returns Plain JavaScript object with all field values
725
+ *
726
+ * @example
727
+ * ```typescript
728
+ * // In a SvelteKit +page.server.ts load function:
729
+ * const users = await userCollection.list();
730
+ * return {
731
+ * users: users.map(u => u.toPlainObject())
732
+ * };
733
+ * ```
734
+ */
735
+ toPlainObject() {
736
+ return JSON.parse(JSON.stringify(this));
737
+ }
738
+ /**
739
+ * Collects the property names of fields marked `@field({ sensitive: true })`
740
+ * for this instance's class (and, under STI, every member of its hierarchy,
741
+ * since `toJSON()` merges sibling fields into the serialized payload).
742
+ *
743
+ * Reads both the first-class `sensitive` flag and the `_meta.sensitive`
744
+ * mirror so it works regardless of whether the field metadata came from the
745
+ * AST manifest or a runtime `@field()` decorator registration.
746
+ */
747
+ getSensitiveFieldNames() {
748
+ const className = this.getResolvedClassName();
749
+ const registered = ObjectRegistry.getClass(className);
750
+ const fieldMaps = [
751
+ registered?.inheritedFields || ObjectRegistry.getFields(className)
752
+ ];
753
+ const tableStrategy = ObjectRegistry.getTableStrategy(
754
+ this.getResolvedQualifiedName()
755
+ );
756
+ if (tableStrategy === "sti") {
757
+ const descendants = getSTIHierarchyMembers(
758
+ this.getResolvedQualifiedName()
759
+ );
760
+ for (const descendant of descendants) {
761
+ fieldMaps.push(
762
+ ObjectRegistry.getClass(descendant)?.inheritedFields || ObjectRegistry.getFields(descendant)
763
+ );
764
+ }
765
+ }
766
+ const sensitive = /* @__PURE__ */ new Set();
767
+ for (const fields of fieldMaps) {
768
+ for (const [key, def] of fields) {
769
+ if (def && (def.sensitive === true || def._meta?.sensitive === true)) {
770
+ sensitive.add(key);
771
+ }
772
+ }
773
+ }
774
+ return sensitive;
775
+ }
776
+ /**
777
+ * Public-safe serialization for network surfaces.
778
+ *
779
+ * Returns the same shape as `toJSON()` but with every `sensitive` field
780
+ * removed (including STI meta fields nested under `_meta_data`). This is the
781
+ * serializer used by the generated REST / MCP / SvelteKit routes so that
782
+ * secret columns (API secrets, credentials, tax IDs) are never returned over
783
+ * the wire, while `toJSON()` continues to carry them for database persistence.
784
+ *
785
+ * @returns A plain object safe to send to API consumers.
786
+ */
787
+ toPublicJSON() {
788
+ const json = this.toJSON();
789
+ const sensitiveFields = this.getSensitiveFieldNames();
790
+ if (sensitiveFields.size === 0) {
791
+ return json;
792
+ }
793
+ const metaData = json._meta_data && typeof json._meta_data === "object" ? json._meta_data : null;
794
+ for (const name of sensitiveFields) {
795
+ delete json[name];
796
+ if (metaData) {
797
+ delete metaData[name];
798
+ }
799
+ }
800
+ return json;
801
+ }
802
+ /**
803
+ * Gets or generates a unique ID for this object
804
+ *
805
+ * @returns Promise resolving to the object's ID
806
+ */
807
+ async getId() {
808
+ if (this.slug) {
809
+ await this.verifyStorageReady();
810
+ const saved = await this.db.get(this.tableName, {
811
+ slug: this.slug,
812
+ context: this.context
813
+ });
814
+ if (saved) {
815
+ this.id = saved.id;
816
+ }
817
+ }
818
+ if (!this.id) {
819
+ this.id = crypto.randomUUID();
820
+ }
821
+ return this.id;
822
+ }
823
+ /**
824
+ * Returns the slug for this object, generating one if not already set.
825
+ *
826
+ * Generation falls back through the following fields in order:
827
+ * `name` → `title` → `label` → `id`
828
+ *
829
+ * The generated slug is lowercased, with non-alphanumeric characters
830
+ * replaced by hyphens and leading/trailing hyphens stripped.
831
+ *
832
+ * Called automatically by `save()` when no slug is present.
833
+ *
834
+ * @returns The existing slug or a newly generated one (may be `null` if
835
+ * none of the fallback fields and `id` are set)
836
+ *
837
+ * @example
838
+ * ```typescript
839
+ * const product = new Product({ name: 'My Widget' });
840
+ * console.log(await product.getSlug()); // 'my-widget'
841
+ * ```
842
+ */
843
+ async getSlug() {
844
+ if (!this.slug) {
845
+ let sourceField = null;
846
+ if (this.name) {
847
+ sourceField = String(this.name);
848
+ } else if (this.title) {
849
+ sourceField = String(this.title);
850
+ } else if (this.label) {
851
+ sourceField = String(this.label);
852
+ } else if (this.id) {
853
+ sourceField = String(this.id);
854
+ }
855
+ if (sourceField) {
856
+ this.slug = sourceField.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
857
+ }
858
+ }
859
+ return this.slug;
860
+ }
861
+ /**
862
+ * Gets the ID of this object if it's already saved in the database
863
+ *
864
+ * @returns Promise resolving to the saved ID or null if not saved
865
+ */
866
+ async getSavedId() {
867
+ if (!this.id && !this.slug) {
868
+ return null;
869
+ }
870
+ await this.verifyStorageReady();
871
+ if (this.id) {
872
+ const byId = await this.db.get(this.tableName, { id: this.id });
873
+ if (byId) return byId.id;
874
+ }
875
+ if (this.slug) {
876
+ const bySlug = await this.db.get(this.tableName, { slug: this.slug });
877
+ if (bySlug) return bySlug.id;
878
+ }
879
+ return null;
880
+ }
881
+ /**
882
+ * Checks if this object is already saved in the database
883
+ *
884
+ * @returns Promise resolving to true if saved, false otherwise
885
+ */
886
+ async isSaved() {
887
+ const saved = await this.getSavedId();
888
+ return !!saved;
889
+ }
890
+ /**
891
+ * Resolve the set of snake_case column names whose database type is `UUID`
892
+ * for the table this object writes to.
893
+ *
894
+ * Declared foreign keys (`@foreignKey()`) and cross-package references
895
+ * (`@crossPackageRef()`) generate native `UUID` columns (R11). Their default
896
+ * declared value in TypeScript is the empty string (`''`), which is not a
897
+ * valid UUID literal. Postgres rejects `''` on a `uuid` column with
898
+ * `invalid input syntax for type uuid`, so any optional/unset declared FK
899
+ * (a root entity's self-reference, an optional cross-package link, etc.)
900
+ * would fail to insert. We coerce `''` → `NULL` for these columns before
901
+ * binding (see {@link coerceEmptyUuidValuesToNull}); this method tells that
902
+ * coercion which columns are UUID-typed.
903
+ *
904
+ * Resolution prefers the built schema (`getSchema().columns`), which already
905
+ * reflects all reconciliation — e.g. a `@foreignKey()` whose target class
906
+ * uses `idType: 'text'` is downgraded to `TEXT` and is therefore correctly
907
+ * excluded here. For STI children, columns live on the STI base table, so we
908
+ * union the base schema's columns. When no built schema is available (pure
909
+ * runtime-registered classes without a manifest) we fall back to field
910
+ * metadata, mirroring the schema-builder's field→SQL mapping.
911
+ */
912
+ async resolveUuidColumnNames(className) {
913
+ const uuidColumns = /* @__PURE__ */ new Set();
914
+ const collectFromSchema = (schemaName) => {
915
+ if (!schemaName) return;
916
+ const schema = ObjectRegistry.getSchema(schemaName);
917
+ const columns = schema?.columns;
918
+ if (!columns) return;
919
+ for (const [columnName, columnDef] of Object.entries(columns)) {
920
+ if (columnDef?.type === "UUID") {
921
+ uuidColumns.add(columnName);
922
+ }
923
+ }
924
+ };
925
+ collectFromSchema(className);
926
+ const qualifiedName = this.getResolvedQualifiedName();
927
+ if (ObjectRegistry.getTableStrategy(qualifiedName) === "sti") {
928
+ collectFromSchema(ObjectRegistry.getSTIBase(qualifiedName));
929
+ }
930
+ if (uuidColumns.size > 0) {
931
+ return uuidColumns;
932
+ }
933
+ try {
934
+ const fields = await ObjectRegistry.getAllFields(className);
935
+ for (const [fieldName, field] of fields.entries()) {
936
+ if (!field) continue;
937
+ const type = field.type;
938
+ if (type !== "foreignKey" && type !== "crossPackageRef") {
939
+ continue;
940
+ }
941
+ const metaSqlType = field._meta?.sqlType;
942
+ if (metaSqlType) {
943
+ if (metaSqlType === "UUID") {
944
+ uuidColumns.add(toSnakeCase(fieldName));
945
+ }
946
+ continue;
947
+ }
948
+ const isTextIdCrossRef = type === "crossPackageRef" && (field._meta?.idType === "text" || field.idType === "text");
949
+ if (!isTextIdCrossRef) {
950
+ uuidColumns.add(toSnakeCase(fieldName));
951
+ }
952
+ }
953
+ } catch {
954
+ }
955
+ return uuidColumns;
956
+ }
957
+ /**
958
+ * Coerce empty-string values to `NULL` for UUID-typed columns in the
959
+ * snake_cased write payload.
960
+ *
961
+ * Framework-level fix for the whole class of bug where a declared FK field
962
+ * defaults to `''` (the natural TypeScript default for `string`) and is left
963
+ * unset on insert — e.g. creating a ROOT entity whose optional self-reference
964
+ * (`previousFactId`, `parentPartnerId`, …) is empty. On Postgres `uuid`
965
+ * columns, `''` raises `invalid input syntax for type uuid`. Coercing to
966
+ * `NULL` here fixes every such field uniformly without per-field type churn
967
+ * or consumer-facing type changes. Mutates `data` in place.
968
+ */
969
+ async coerceEmptyUuidValuesToNull(className, data) {
970
+ const uuidColumns = await this.resolveUuidColumnNames(className);
971
+ if (uuidColumns.size === 0) return;
972
+ for (const columnName of uuidColumns) {
973
+ if (data[columnName] === "") {
974
+ data[columnName] = null;
975
+ }
976
+ }
977
+ }
978
+ /**
979
+ * Persists this object to the database using an upsert (insert or update).
980
+ *
981
+ * Steps performed on every save:
982
+ * 1. Runs field-level validation (`validateBeforeSave()`)
983
+ * 2. Executes `beforeSave` interceptors (e.g. tenant injection)
984
+ * 3. Assigns a UUID `id` if not already set
985
+ * 4. Generates a `slug` via `getSlug()` if not already set
986
+ * 5. Updates `updated_at` (and sets `created_at` on first save)
987
+ * 6. Upserts the row with automatic retry (3 attempts, 500 ms backoff)
988
+ * 7. Executes `afterSave` interceptors
989
+ * 8. Triggers embedding generation in the background if configured
990
+ *
991
+ * For STI classes, validates that `_meta_type` is present and correct
992
+ * before writing to the database.
993
+ *
994
+ * @returns This instance after saving (enables chaining)
995
+ * @throws {ValidationError} If a required field is missing or a unique constraint is violated
996
+ * @throws {DatabaseError} If the table does not exist (`DB_SCHEMA_MISSING`) or the query fails
997
+ * @throws {RuntimeError} For any other unexpected failure during save
998
+ *
999
+ * @example
1000
+ * ```typescript
1001
+ * const product = new Product({ db: myDb, name: 'Widget', price: 9.99 });
1002
+ * await product.initialize();
1003
+ * await product.save();
1004
+ * console.log(product.id); // UUID assigned during save
1005
+ *
1006
+ * // Update
1007
+ * product.price = 14.99;
1008
+ * await product.save();
1009
+ * ```
1010
+ */
1011
+ async save() {
1012
+ const className = this.getResolvedClassName();
1013
+ try {
1014
+ await this.validateBeforeSave();
1015
+ await this.validateCrossPackageRefs();
1016
+ const interceptorContext = createInterceptorContext(className, "save");
1017
+ await GlobalInterceptors.executeBeforeSave(this, interceptorContext);
1018
+ if (!this.id) {
1019
+ this.id = crypto.randomUUID();
1020
+ }
1021
+ if (!this.slug) {
1022
+ this.slug = await this.getSlug();
1023
+ }
1024
+ this.updated_at = /* @__PURE__ */ new Date();
1025
+ if (!this.created_at) {
1026
+ this.created_at = /* @__PURE__ */ new Date();
1027
+ }
1028
+ await this.verifyStorageReady();
1029
+ const tableStrategy = ObjectRegistry.getTableStrategy(
1030
+ this.getResolvedQualifiedName()
1031
+ );
1032
+ if (process.env.NODE_ENV === "development") {
1033
+ const hasOverride = this.toJSON !== SmrtObject.prototype.toJSON;
1034
+ const usesSTI = tableStrategy === "sti";
1035
+ if (hasOverride && usesSTI) {
1036
+ logger.warn(
1037
+ `[SMRT STI Warning] ${this.constructor.name} overrides toJSON() but uses STI.
1038
+ Ensure super.toJSON() is called or _meta_type is set manually.
1039
+ This can cause "Missing _meta_type discriminator" errors.
1040
+ Prefer using the transformJSON() hook instead of overriding toJSON().
1041
+ See issue #377: https://github.com/happyvertical/smrt/issues/377`
1042
+ );
1043
+ }
1044
+ }
1045
+ if (tableStrategy === "sti") {
1046
+ warnIfSkipRehydrateSet();
1047
+ const qualifiedName = this.getResolvedQualifiedName();
1048
+ const classesNeedingFreshSTIFieldState = Array.from(
1049
+ /* @__PURE__ */ new Set([qualifiedName, ...getSTIHierarchyMembers(qualifiedName)])
1050
+ );
1051
+ for (const stiClassName of classesNeedingFreshSTIFieldState) {
1052
+ await ObjectRegistry.getAllFields(stiClassName);
1053
+ }
1054
+ }
1055
+ const jsonData = this.toJSON();
1056
+ if (tableStrategy === "sti") {
1057
+ if (!jsonData._meta_type) {
1058
+ throw new Error(
1059
+ `STI validation failed: Missing _meta_type discriminator when saving ${className}. This should have been set automatically by toJSON(). Please report this bug.`
1060
+ );
1061
+ }
1062
+ if (!isValidMetaType(jsonData._meta_type, className)) {
1063
+ throw new Error(
1064
+ `STI validation failed: _meta_type mismatch when saving ${className}. Expected '${getExpectedMetaType(className)}' but got '${jsonData._meta_type}'. This should not happen - please report this bug.`
1065
+ );
1066
+ }
1067
+ }
1068
+ const data = {};
1069
+ for (const [key, value] of Object.entries(jsonData)) {
1070
+ if (key.startsWith("_")) {
1071
+ data[key] = value;
1072
+ } else {
1073
+ data[toSnakeCase(key)] = value;
1074
+ }
1075
+ }
1076
+ await this.coerceEmptyUuidValuesToNull(className, data);
1077
+ const conflictColumns = ObjectRegistry.getConflictColumns(className);
1078
+ const writePlan = await this.planPersistenceWrite(
1079
+ className,
1080
+ tableStrategy,
1081
+ data,
1082
+ conflictColumns
1083
+ );
1084
+ const upsertConflictColumns = this._persisted && data.id ? ["id"] : conflictColumns;
1085
+ await ErrorUtils.withRetry(
1086
+ async () => {
1087
+ try {
1088
+ if (writePlan.type === "updateById") {
1089
+ const { id: _id, ...updateData } = data;
1090
+ await this.db.update(this.tableName, { id: data.id }, updateData);
1091
+ this.setMetaType(writePlan.qualifiedMetaType);
1092
+ } else {
1093
+ await this.db.upsert(this.tableName, upsertConflictColumns, data);
1094
+ }
1095
+ } catch (error) {
1096
+ if (error instanceof Error) {
1097
+ const kind = SmrtObject.classifyConstraintError(error.message);
1098
+ if (kind === "unique") {
1099
+ const field = this.extractConstraintField(error.message);
1100
+ throw ValidationError.uniqueConstraint(
1101
+ field,
1102
+ this.getFieldValue(field)
1103
+ );
1104
+ }
1105
+ if (kind === "not_null") {
1106
+ const field = this.extractConstraintField(error.message);
1107
+ throw ValidationError.requiredField(field, className);
1108
+ }
1109
+ const operation = writePlan.type === "updateById" ? `UPDATE ${this.tableName} (id-targeted)` : `UPSERT INTO ${this.tableName}`;
1110
+ throw DatabaseError.queryFailed(operation, error);
1111
+ }
1112
+ throw error;
1113
+ }
1114
+ },
1115
+ 3,
1116
+ 500
1117
+ );
1118
+ this._persisted = true;
1119
+ this.invalidateCollectionReadCache();
1120
+ await GlobalInterceptors.executeAfterSave(this, interceptorContext);
1121
+ const embeddingConfig = ObjectRegistry.resolveEmbeddingConfig(className);
1122
+ const skipAutoEmbeddings = this.options._skipAutoEmbeddings === true;
1123
+ if (embeddingConfig && embeddingConfig.autoGenerate !== false && !skipAutoEmbeddings) {
1124
+ const aiClient = await this.getOptionalAiClient();
1125
+ if (aiClient) {
1126
+ const isStale = await this.hasStaleEmbeddings();
1127
+ if (isStale) {
1128
+ this.generateEmbeddings().catch((error) => {
1129
+ logger.warn(
1130
+ `Failed to auto-generate embeddings for ${this.constructor.name}`,
1131
+ { error: error instanceof Error ? error.message : error }
1132
+ );
1133
+ });
1134
+ }
1135
+ }
1136
+ }
1137
+ if (skipAutoEmbeddings) {
1138
+ this.options._skipAutoEmbeddings = false;
1139
+ }
1140
+ return this;
1141
+ } catch (error) {
1142
+ if (error instanceof SmrtError) {
1143
+ throw error;
1144
+ }
1145
+ if (this.isTenantContractError(error)) {
1146
+ throw error;
1147
+ }
1148
+ throw RuntimeError.operationFailed(
1149
+ "save",
1150
+ `${className}#${this.id}`,
1151
+ error instanceof Error ? error : new Error(String(error))
1152
+ );
1153
+ }
1154
+ }
1155
+ /**
1156
+ * Detects tenant-boundary errors that must propagate from `save()` unwrapped.
1157
+ *
1158
+ * Matches both core's {@link TenantIsolationError} and the duck-typed
1159
+ * tenancy-package errors (`TenantIsolationError` / `TenantContextError`) that
1160
+ * the `beforeSave` interceptor throws. Those tenancy classes extend plain
1161
+ * `Error` — core cannot import the tenancy package (the dependency runs the
1162
+ * other way) — so they are matched by their stable `code` constants, with a
1163
+ * class-name fallback for resilience.
1164
+ */
1165
+ isTenantContractError(error) {
1166
+ if (error instanceof TenantIsolationError) {
1167
+ return true;
1168
+ }
1169
+ const candidate = error;
1170
+ const code = candidate?.code;
1171
+ if (code === "TENANT_ISOLATION_VIOLATION" || code === "TENANT_CONTEXT_REQUIRED") {
1172
+ return true;
1173
+ }
1174
+ const name = candidate?.name;
1175
+ return name === "TenantIsolationError" || name === "TenantContextError";
1176
+ }
1177
+ /**
1178
+ * Validates object state before saving
1179
+ * Override in subclasses to add custom validation logic
1180
+ */
1181
+ async validateBeforeSave() {
1182
+ const className = this.getResolvedClassName();
1183
+ const validationRules = ObjectRegistry.getValidationRules(className);
1184
+ if (validationRules && validationRules.length > 0) {
1185
+ const errors = await ObjectRegistry.validateWithRules(
1186
+ this,
1187
+ validationRules,
1188
+ className
1189
+ );
1190
+ if (errors.length > 0) {
1191
+ throw errors[0];
1192
+ }
1193
+ return;
1194
+ }
1195
+ const validators = ObjectRegistry.getValidators(className);
1196
+ if (validators && validators.length > 0) {
1197
+ const errors = [];
1198
+ for (const validator of validators) {
1199
+ const error = await validator(this);
1200
+ if (error) {
1201
+ errors.push(error);
1202
+ }
1203
+ }
1204
+ if (errors.length > 0) {
1205
+ throw errors[0];
1206
+ }
1207
+ return;
1208
+ }
1209
+ const fields = await fieldsFromClass(this.constructor);
1210
+ for (const [fieldName, field] of Object.entries(fields)) {
1211
+ if (field.options?.required) {
1212
+ const value = this.getFieldValue(fieldName);
1213
+ if (value === null || value === void 0 || value === "") {
1214
+ throw ValidationError.requiredField(fieldName, className);
1215
+ }
1216
+ }
1217
+ }
1218
+ }
1219
+ /**
1220
+ * Validates cross-package references that opted in via `validate: true`.
1221
+ *
1222
+ * Iterates registered fields of type `crossPackageRef`. For each one whose
1223
+ * field options include `validate: true` AND has a non-empty value, looks up
1224
+ * the referenced object via the target package's manifest and throws
1225
+ * `ValidationError` if the target row does not exist.
1226
+ *
1227
+ * Empty/null/undefined values are treated as "no reference set" and skipped.
1228
+ * Fields without `validate: true` are always skipped (the registered metadata
1229
+ * still powers eager loading and `loadRelated()`).
1230
+ */
1231
+ async validateCrossPackageRefs() {
1232
+ const className = this.getResolvedClassName();
1233
+ const registered = ObjectRegistry.getClass(className);
1234
+ if (!registered) return;
1235
+ const fields = registered.inheritedFields || registered.fields;
1236
+ if (!fields || fields.size === 0) return;
1237
+ const checks = [];
1238
+ for (const [fieldName, field] of fields) {
1239
+ if (field?.type !== "crossPackageRef") continue;
1240
+ const opts = field._meta || field;
1241
+ if (!opts.validate) continue;
1242
+ if (!field.related) continue;
1243
+ const value = this.getFieldValue(fieldName);
1244
+ if (value === null || value === void 0 || value === "") continue;
1245
+ checks.push({
1246
+ fieldName,
1247
+ qualifiedTarget: String(field.related),
1248
+ value: String(value)
1249
+ });
1250
+ }
1251
+ if (checks.length === 0) return;
1252
+ for (const check of checks) {
1253
+ await ObjectRegistry.ensureManifestLoaded(check.qualifiedTarget);
1254
+ const targetClass = ObjectRegistry.getClassByQualifiedName(check.qualifiedTarget) ?? ObjectRegistry.getClass(check.qualifiedTarget);
1255
+ if (!targetClass) {
1256
+ throw new ValidationError(
1257
+ `crossPackageRef target ${check.qualifiedTarget} for ${className}.${check.fieldName} is not registered. Ensure the target package's manifest is discoverable at runtime.`,
1258
+ "VALIDATION_CROSS_PACKAGE_REF_UNREGISTERED",
1259
+ { className, fieldName: check.fieldName, value: check.value }
1260
+ );
1261
+ }
1262
+ const probe = new targetClass.constructor(this.options);
1263
+ await probe.initialize();
1264
+ await probe.verifyStorageReady();
1265
+ const row = await probe.db.get(probe.tableName, { id: check.value });
1266
+ if (!row) {
1267
+ throw new ValidationError(
1268
+ `crossPackageRef validation failed: ${className}.${check.fieldName} references ${check.qualifiedTarget} id="${check.value}" but no such row exists.`,
1269
+ "VALIDATION_CROSS_PACKAGE_REF_MISSING",
1270
+ { className, fieldName: check.fieldName, value: check.value }
1271
+ );
1272
+ }
1273
+ }
1274
+ }
1275
+ /**
1276
+ * Gets the value of a field on this object
1277
+ */
1278
+ getFieldValue(fieldName) {
1279
+ return this[fieldName];
1280
+ }
1281
+ /**
1282
+ * Gets the value of a property.
1283
+ *
1284
+ * @param key - Property name to get value from
1285
+ * @returns The property value
1286
+ */
1287
+ getPropertyValue(key) {
1288
+ return this[key];
1289
+ }
1290
+ /**
1291
+ * Classifies a raw database driver error message as a unique-constraint or
1292
+ * not-null-constraint violation, across SQLite, PostgreSQL and DuckDB.
1293
+ *
1294
+ * `@happyvertical/sql` does not normalize driver errors, so each dialect's
1295
+ * native wording is matched case-insensitively. Returns `null` when the
1296
+ * message is not a recognized unique/not-null violation (the caller then
1297
+ * falls back to a generic `DatabaseError`). Kept as a pure static so the
1298
+ * cross-dialect matching can be unit-tested directly without a live PG/DuckDB
1299
+ * connection (#1378).
1300
+ *
1301
+ * @param message - The raw driver error message.
1302
+ * @returns `'unique'`, `'not_null'`, or `null`.
1303
+ */
1304
+ static classifyConstraintError(message) {
1305
+ if (!message) {
1306
+ return null;
1307
+ }
1308
+ if (/NOT NULL constraint failed/i.test(message) || /null value in column .* violates not-null/i.test(message)) {
1309
+ return "not_null";
1310
+ }
1311
+ if (/UNIQUE constraint failed/i.test(message) || /violates unique constraint/i.test(message) || /violates primary key constraint/i.test(message)) {
1312
+ return "unique";
1313
+ }
1314
+ return null;
1315
+ }
1316
+ /**
1317
+ * Extracts field name from database constraint error messages.
1318
+ *
1319
+ * Handles SQLite, PostgreSQL and DuckDB phrasings. Returns
1320
+ * `'unknown_field'` when no column name can be recovered.
1321
+ */
1322
+ extractConstraintField(errorMessage) {
1323
+ const patterns = [
1324
+ // SQLite / DuckDB: "UNIQUE constraint failed: products.slug"
1325
+ /UNIQUE constraint failed: \w+\.(\w+)/i,
1326
+ // SQLite / DuckDB: "NOT NULL constraint failed: products.name"
1327
+ /NOT NULL constraint failed: \w+\.(\w+)/i,
1328
+ // PostgreSQL not-null: 'null value in column "name" ...'
1329
+ /null value in column "([^"]+)"/i,
1330
+ // PostgreSQL unique DETAIL line: 'Key (slug)=(foo) already exists.'
1331
+ /Key \(([^)]+)\)=/i,
1332
+ // SQLite / DuckDB generic fallback: "constraint failed: slug"
1333
+ /constraint failed: (\w+)/i
1334
+ ];
1335
+ for (const pattern of patterns) {
1336
+ const match = errorMessage.match(pattern);
1337
+ if (match?.[1]) {
1338
+ return match[1];
1339
+ }
1340
+ }
1341
+ return "unknown_field";
1342
+ }
1343
+ /**
1344
+ * Hydrates this object from the database using its `id` property.
1345
+ *
1346
+ * Queries the database for a row matching `{ id: this._id }` and calls
1347
+ * `loadDataFromDb()` if found. Uses a 3-attempt retry with 250 ms initial
1348
+ * delay to handle transient failures.
1349
+ *
1350
+ * Called automatically by `initialize()` when `options.id` is provided.
1351
+ * Typically you do not need to call this directly.
1352
+ *
1353
+ * @returns Promise that resolves when loading is complete (no-op if not found)
1354
+ * @throws {ValidationError} If `this._id` is not set
1355
+ * @throws {DatabaseError} If the query fails after all retries
1356
+ *
1357
+ * @example
1358
+ * ```typescript
1359
+ * const product = new Product({ db: myDb });
1360
+ * await product.initialize();
1361
+ * product._id = 'some-uuid';
1362
+ * await product.loadFromId(); // hydrates from DB
1363
+ * ```
1364
+ */
1365
+ async loadFromId() {
1366
+ try {
1367
+ if (!this._id) {
1368
+ throw ValidationError.requiredField("id", this.constructor.name);
1369
+ }
1370
+ await this.verifyStorageReady();
1371
+ await ErrorUtils.withRetry(
1372
+ async () => {
1373
+ try {
1374
+ const existing = await this.db.get(this.tableName, {
1375
+ id: this._id
1376
+ });
1377
+ if (existing) {
1378
+ await this.loadDataFromDb(existing);
1379
+ }
1380
+ } catch (error) {
1381
+ throw DatabaseError.queryFailed(
1382
+ `get(${this.tableName}, { id: ${this._id} })`,
1383
+ error instanceof Error ? error : new Error(String(error))
1384
+ );
1385
+ }
1386
+ },
1387
+ 3,
1388
+ 250
1389
+ );
1390
+ } catch (error) {
1391
+ if (error instanceof ValidationError || error instanceof DatabaseError) {
1392
+ throw error;
1393
+ }
1394
+ throw RuntimeError.operationFailed(
1395
+ "loadFromId",
1396
+ `${this.constructor.name}#${this._id}`,
1397
+ error instanceof Error ? error : new Error(String(error))
1398
+ );
1399
+ }
1400
+ }
1401
+ /**
1402
+ * Hydrates this object from the database using its `slug` (and `context`).
1403
+ *
1404
+ * Queries for a row matching `{ slug, context }`. The `context` defaults to
1405
+ * an empty string when not provided. Calls `loadDataFromDb()` if a matching
1406
+ * row is found.
1407
+ *
1408
+ * Called automatically by `initialize()` when `options.slug` is provided and
1409
+ * no `options.id` is set. Typically you do not need to call this directly.
1410
+ *
1411
+ * @returns Promise that resolves when loading is complete (no-op if not found)
1412
+ *
1413
+ * @example
1414
+ * ```typescript
1415
+ * const product = new Product({ db: myDb, slug: 'my-widget', context: 'shop' });
1416
+ * await product.initialize(); // calls loadFromSlug() automatically
1417
+ * console.log(product.name);
1418
+ * ```
1419
+ */
1420
+ async loadFromSlug() {
1421
+ await this.verifyStorageReady();
1422
+ const existing = await this.db.get(this.tableName, {
1423
+ slug: this._slug,
1424
+ context: this._context || ""
1425
+ });
1426
+ if (existing) {
1427
+ await this.loadDataFromDb(existing);
1428
+ }
1429
+ }
1430
+ /**
1431
+ * Serializes this instance into the "content body" injected into the AI
1432
+ * prompts used by {@link is}, {@link do}, and {@link describe} so those
1433
+ * methods reason over the object's own field data, not just the caller's
1434
+ * instruction string.
1435
+ *
1436
+ * Uses {@link toPublicJSON} (never {@link toJSON}) so that
1437
+ * `@field({ sensitive: true })` values — API secrets, credentials, tax IDs —
1438
+ * are never sent to the model. The serialized payload is truncated to
1439
+ * `maxLength` characters as a coarse token-budget guard for large instances;
1440
+ * truncation appends a clear marker so the model knows the data was cut.
1441
+ *
1442
+ * @param maxLength - Maximum characters of object data to include
1443
+ * (defaults to {@link AI_PROMPT_DATA_MAX_LENGTH}). A non-positive value
1444
+ * disables truncation.
1445
+ * @returns A JSON string of the object's public (sensitive-stripped) fields
1446
+ */
1447
+ serializeForAiPrompt(maxLength = AI_PROMPT_DATA_MAX_LENGTH) {
1448
+ let serialized;
1449
+ try {
1450
+ serialized = JSON.stringify(this.toPublicJSON(), null, 2);
1451
+ } catch (error) {
1452
+ logger.warn(
1453
+ `Failed to serialize ${this.constructor.name} for AI prompt: ${error instanceof Error ? error.message : String(error)}`
1454
+ );
1455
+ serialized = "{}";
1456
+ }
1457
+ if (Number.isFinite(maxLength) && maxLength > 0 && serialized.length > maxLength) {
1458
+ const marker = `
1459
+ … [truncated: object data exceeded ${maxLength} characters]`;
1460
+ const keep = Math.max(0, maxLength - marker.length);
1461
+ serialized = `${serialized.slice(0, keep)}${marker}`;
1462
+ }
1463
+ return serialized;
1464
+ }
1465
+ /**
1466
+ * Builds the optional "content body" section prepended to the AI prompts in
1467
+ * {@link is}, {@link do}, and {@link describe}.
1468
+ *
1469
+ * Returns an empty string when the caller opts out with `includeData: false`
1470
+ * — used by callers that already curate the object's relevant fields into
1471
+ * their own instruction/criteria string (so the data is not duplicated, and
1472
+ * the prompt stays exactly as the caller authored it). Otherwise it wraps
1473
+ * {@link serializeForAiPrompt} in `--- Beginning/End of content ---`
1474
+ * delimiters so the methods reason over the instance's own data by default.
1475
+ *
1476
+ * @param includeData - `false` to omit the section; any other value (incl.
1477
+ * `undefined`) injects it
1478
+ * @param maxLength - Forwarded to {@link serializeForAiPrompt} as the
1479
+ * truncation budget
1480
+ * @returns The content-body section (with a trailing newline) or `''`
1481
+ */
1482
+ buildAiContentSection(includeData, maxLength) {
1483
+ if (includeData === false) {
1484
+ return "";
1485
+ }
1486
+ const contentBody = this.serializeForAiPrompt(maxLength);
1487
+ return `--- Beginning of content ---
1488
+ ${contentBody}
1489
+ --- End of content ---
1490
+ `;
1491
+ }
1492
+ /**
1493
+ * Evaluates whether this object satisfies the given natural-language criteria using AI.
1494
+ *
1495
+ * Injects the object's own public field data (via {@link toPublicJSON}, so
1496
+ * `@field({ sensitive: true })` values are excluded) into the prompt as the
1497
+ * "content body", then asks the AI whether that data meets the criteria and
1498
+ * to reply with a `{ result: boolean }` JSON response. Uses any AI-callable
1499
+ * tools registered on this class (via `@smrt({ ai })`) as part of the
1500
+ * function-calling context.
1501
+ *
1502
+ * @param criteria - Natural-language description of the condition to evaluate
1503
+ * @param options - AI message options passed to `ai.message()` (e.g. model
1504
+ * override). Two non-standard control keys are consumed here and not
1505
+ * forwarded to `ai.message()`:
1506
+ * - `includeData: false` — omit the injected object "content body" (for
1507
+ * callers that already curate the relevant fields into `criteria`).
1508
+ * - `maxDataLength` (number of characters) — override the injected
1509
+ * object-data truncation limit.
1510
+ * @returns `true` if the object meets the criteria, `false` otherwise
1511
+ * @throws Error if the AI returns a non-boolean or malformed JSON response
1512
+ *
1513
+ * @example
1514
+ * ```typescript
1515
+ * const article = await articles.get('article-uuid');
1516
+ * const isSuitable = await article.is('appropriate for a general audience');
1517
+ * if (isSuitable) await article.publish();
1518
+ * ```
1519
+ *
1520
+ * @see {@link do} for open-ended instructions instead of boolean checks
1521
+ */
1522
+ async is(criteria, options = {}) {
1523
+ const ai = await this.getAiClient();
1524
+ const { maxDataLength, includeData, ...aiOptions } = options ?? {};
1525
+ const contentSection = this.buildAiContentSection(
1526
+ includeData,
1527
+ maxDataLength
1528
+ );
1529
+ const prompt = `${contentSection}--- Beginning of criteria ---
1530
+ ${criteria}
1531
+ --- End of criteria ---
1532
+ Does the content meet all the given criteria? Reply with a json object with a single boolean 'result' property`;
1533
+ const tools = this.getAvailableTools();
1534
+ const message = await ai.message(prompt, {
1535
+ ...aiOptions,
1536
+ responseFormat: { type: "json_object" },
1537
+ tools: tools.length > 0 ? tools : void 0
1538
+ });
1539
+ try {
1540
+ const { result } = JSON.parse(message);
1541
+ if (result === true || result === false) {
1542
+ return result;
1543
+ }
1544
+ } catch (_e) {
1545
+ throw new Error(`Unexpected answer: ${message}`);
1546
+ }
1547
+ }
1548
+ /**
1549
+ * Performs a freeform operation on this object using AI instructions.
1550
+ *
1551
+ * Injects the object's own public field data (via {@link toPublicJSON}, so
1552
+ * `@field({ sensitive: true })` values are excluded) into the prompt as the
1553
+ * "content body" the instructions operate on, then returns the raw text
1554
+ * response. Unlike `is()`, this method does not constrain the response
1555
+ * format — use it for transformations, summaries, extractions, or any
1556
+ * open-ended AI task.
1557
+ *
1558
+ * Uses any AI-callable tools registered on this class (via `@smrt({ ai })`)
1559
+ * as part of the function-calling context.
1560
+ *
1561
+ * @param instructions - Natural-language instructions for the AI to follow
1562
+ * @param options - AI message options passed to `ai.message()` (e.g. model
1563
+ * override). Two non-standard control keys are consumed here and not
1564
+ * forwarded to `ai.message()`:
1565
+ * - `includeData: false` — omit the injected object "content body" (for
1566
+ * callers that already curate the relevant fields into `instructions`).
1567
+ * - `maxDataLength` (number of characters) — override the injected
1568
+ * object-data truncation limit.
1569
+ * @returns The raw AI response string
1570
+ *
1571
+ * @example
1572
+ * ```typescript
1573
+ * const article = await articles.get('article-uuid');
1574
+ * const summary = await article.do('summarize this article in 3 bullet points');
1575
+ * const translated = await article.do('translate the title and body to Spanish');
1576
+ * ```
1577
+ *
1578
+ * @see {@link is} for boolean criteria checks
1579
+ */
1580
+ async do(instructions, options = {}) {
1581
+ const ai = await this.getAiClient();
1582
+ const { maxDataLength, includeData, ...aiOptions } = options ?? {};
1583
+ const contentSection = this.buildAiContentSection(
1584
+ includeData,
1585
+ maxDataLength
1586
+ );
1587
+ const prompt = `${contentSection}--- Beginning of instructions ---
1588
+ ${instructions}
1589
+ --- End of instructions ---
1590
+ Based on the content body, please follow the instructions and provide a response. Never make use of codeblocks.`;
1591
+ const tools = this.getAvailableTools();
1592
+ const result = await ai.message(prompt, {
1593
+ ...aiOptions,
1594
+ tools: tools.length > 0 ? tools : void 0
1595
+ });
1596
+ return result;
1597
+ }
1598
+ /**
1599
+ * Generates a description of this object using AI (Issue #52)
1600
+ *
1601
+ * Creates a concise, human-readable description based on the object's content
1602
+ * and properties. The object's own public field data (via
1603
+ * {@link toPublicJSON}, so `@field({ sensitive: true })` values are excluded)
1604
+ * is injected into the prompt as the "content body" the description is built
1605
+ * from. Useful for summaries, previews, and documentation.
1606
+ *
1607
+ * @param options - AI message options (can include style, length, focus,
1608
+ * etc.). Two non-standard control keys are consumed here and not forwarded
1609
+ * to `ai.message()`:
1610
+ * - `includeData: false` — omit the injected object "content body".
1611
+ * - `maxDataLength` (number of characters) — override the injected
1612
+ * object-data truncation limit.
1613
+ * @returns Promise resolving to the AI-generated description
1614
+ *
1615
+ * @example
1616
+ * ```typescript
1617
+ * const product = await products.get('product-123');
1618
+ * const description = await product.describe();
1619
+ * // "A high-quality widget for home improvement..."
1620
+ *
1621
+ * // With custom options
1622
+ * const shortDesc = await product.describe({ maxTokens: 50 });
1623
+ * // "Premium widget, steel construction"
1624
+ * ```
1625
+ */
1626
+ async describe(options = {}) {
1627
+ const ai = await this.getAiClient();
1628
+ const { maxDataLength, includeData, ...aiOptions } = options ?? {};
1629
+ const contentSection = this.buildAiContentSection(
1630
+ includeData,
1631
+ maxDataLength
1632
+ );
1633
+ const prompt = `${contentSection}Generate a concise, professional description of this object based on its content and properties. The description should be clear, informative, and suitable for display to end users. Focus on the most important and distinctive characteristics.`;
1634
+ const tools = this.getAvailableTools();
1635
+ const result = await ai.message(prompt, {
1636
+ ...aiOptions,
1637
+ tools: tools.length > 0 ? tools : void 0
1638
+ });
1639
+ return result;
1640
+ }
1641
+ /**
1642
+ * Runs a lifecycle hook if it's defined in the object's configuration
1643
+ *
1644
+ * @param hookName - Name of the hook to run (e.g., 'beforeDelete', 'afterDelete')
1645
+ * @returns Promise that resolves when the hook completes
1646
+ */
1647
+ async runHook(hookName) {
1648
+ const config = ObjectRegistry.getConfig(this.constructor.name);
1649
+ const hook = config.hooks?.[hookName];
1650
+ if (!hook) {
1651
+ return;
1652
+ }
1653
+ if (typeof hook === "string") {
1654
+ const method = this[hook];
1655
+ if (typeof method === "function") {
1656
+ await method.call(this);
1657
+ } else {
1658
+ logger.warn(
1659
+ `Hook method '${hook}' not found on ${this.constructor.name}`
1660
+ );
1661
+ }
1662
+ } else if (typeof hook === "function") {
1663
+ await hook(this);
1664
+ }
1665
+ }
1666
+ /**
1667
+ * Deletes this object from the database.
1668
+ *
1669
+ * Runs the full lifecycle in order:
1670
+ * 1. `beforeDelete` interceptors (e.g. tenant validation)
1671
+ * 2. `beforeDelete` lifecycle hook (defined in `@smrt({ hooks })`)
1672
+ * 3. Database row deletion
1673
+ * 4. `afterDelete` lifecycle hook
1674
+ * 5. `afterDelete` interceptors
1675
+ *
1676
+ * Prefer `collection.delete(id)` from application code — it loads the
1677
+ * object first (returning `false` when not found) before calling this method.
1678
+ *
1679
+ * @returns Promise that resolves when deletion is complete
1680
+ *
1681
+ * @example
1682
+ * ```typescript
1683
+ * const product = await products.get('product-uuid');
1684
+ * if (product) await product.delete();
1685
+ * ```
1686
+ */
1687
+ async delete() {
1688
+ const interceptorContext = createInterceptorContext(
1689
+ this.constructor.name,
1690
+ "delete"
1691
+ );
1692
+ await GlobalInterceptors.executeBeforeDelete(this, interceptorContext);
1693
+ await this.runHook("beforeDelete");
1694
+ await this.verifyStorageReady();
1695
+ await this.db.delete(this.tableName, { id: this.id });
1696
+ this._persisted = false;
1697
+ this.invalidateCollectionReadCache();
1698
+ await this.runHook("afterDelete");
1699
+ await GlobalInterceptors.executeAfterDelete(this, interceptorContext);
1700
+ }
1701
+ /**
1702
+ * Invalidate cached collection reads after a successful mutation
1703
+ * (issue #1498).
1704
+ *
1705
+ * Always drops this table's in-process entries — a no-op when nothing
1706
+ * opted into caching. When the table is cached cross-process (see
1707
+ * {@link shouldBroadcastCacheInvalidation}), the invalidation is also
1708
+ * broadcast to peer replicas over the database adapter's notification
1709
+ * capability, fire-and-forget. Cache maintenance must never fail the
1710
+ * write that triggered it.
1711
+ */
1712
+ invalidateCollectionReadCache() {
1713
+ try {
1714
+ const dbKey = resolveDbCacheKey(this.db);
1715
+ invalidateCollectionCache(dbKey, this.tableName);
1716
+ if (this.shouldBroadcastCacheInvalidation(dbKey)) {
1717
+ void broadcastCacheInvalidation(this.db, this.tableName);
1718
+ }
1719
+ } catch (error) {
1720
+ logger.warn("Failed to invalidate collection read cache after write", {
1721
+ table: this.tableName,
1722
+ error: error instanceof Error ? error.message : error
1723
+ });
1724
+ }
1725
+ }
1726
+ /**
1727
+ * Decide whether a mutation should broadcast a cross-process cache
1728
+ * invalidation. Invalidation is table-scoped, so the decision must be too:
1729
+ *
1730
+ * 1. A per-call `crossProcess` cached read in this process registered
1731
+ * interest in the table — broadcast even without model-level config.
1732
+ * 2. This class's resolved `@smrt({ cache })` config sets `crossProcess`.
1733
+ * 3. Any other STI hierarchy member sharing the table resolves to a
1734
+ * `crossProcess` config — a child that opted out with `cache: false`
1735
+ * still mutates the shared table its base/siblings are caching.
1736
+ */
1737
+ shouldBroadcastCacheInvalidation(dbKey) {
1738
+ if (hasCrossProcessCacheInterest(dbKey, this.tableName)) {
1739
+ return true;
1740
+ }
1741
+ const qualifiedName = this.getResolvedQualifiedName();
1742
+ if (ObjectRegistry.resolveCollectionCacheConfig(qualifiedName)?.crossProcess) {
1743
+ return true;
1744
+ }
1745
+ for (const member of getSTIHierarchyMembers(qualifiedName)) {
1746
+ if (member === qualifiedName) continue;
1747
+ if (ObjectRegistry.resolveCollectionCacheConfig(member)?.crossProcess) {
1748
+ return true;
1749
+ }
1750
+ }
1751
+ return false;
1752
+ }
1753
+ /**
1754
+ * Check if a relationship has been loaded
1755
+ *
1756
+ * @param fieldName - Name of the relationship field
1757
+ * @returns True if the relationship is loaded, false otherwise
1758
+ * @example
1759
+ * ```typescript
1760
+ * if (order.isRelatedLoaded('customer')) {
1761
+ * console.log('Customer already loaded');
1762
+ * }
1763
+ * ```
1764
+ */
1765
+ isRelatedLoaded(fieldName) {
1766
+ return this._loadedRelationships.has(fieldName);
1767
+ }
1768
+ /**
1769
+ * Lazy-loads a `foreignKey` or `crossPackageRef` relationship and caches the
1770
+ * result.
1771
+ *
1772
+ * Looks up the relationship metadata in the ObjectRegistry, reads the
1773
+ * foreign key value on this object, and fetches the related object from
1774
+ * the database. Subsequent calls return the cached value without hitting
1775
+ * the database again.
1776
+ *
1777
+ * For STI relationships, the correct subclass is determined by reading
1778
+ * `_meta_type` from the database row before instantiation.
1779
+ *
1780
+ * Enforces tenant isolation: if this object and the loaded target both carry
1781
+ * a non-null `tenantId` that differ, a {@link TenantIsolationError} is thrown
1782
+ * (unless `opts.allowCrossTenant` is set). The check is a no-op for
1783
+ * global/null-tenant models and same-tenant loads, so it only catches genuine
1784
+ * cross-tenant leaks (Issue #1321).
1785
+ *
1786
+ * @param fieldName - Name of the `@foreignKey()` or `@crossPackageRef()`
1787
+ * decorated property
1788
+ * @param opts - Optional loader options; see {@link LoadRelatedOptions}
1789
+ * @returns The related object, or `null` if the foreign key is empty
1790
+ * @throws {RuntimeError} If `fieldName` is not a `foreignKey` or
1791
+ * `crossPackageRef` relationship, or the target class is not found in the
1792
+ * ObjectRegistry
1793
+ * @throws {TenantIsolationError} If the target belongs to a different,
1794
+ * non-null tenant and `opts.allowCrossTenant` is not set
1795
+ *
1796
+ * @example
1797
+ * ```typescript
1798
+ * // class Order { @foreignKey(Customer) customerId: string = ''; }
1799
+ * const customer = await order.loadRelated('customerId');
1800
+ * console.log(customer?.name);
1801
+ * ```
1802
+ *
1803
+ * @see {@link getRelated} for a convenience wrapper that auto-detects relationship type
1804
+ */
1805
+ async loadRelated(fieldName, opts) {
1806
+ if (this._loadedRelationships.has(fieldName)) {
1807
+ const cached = this._loadedRelationships.get(fieldName);
1808
+ this.assertRelatedTenant(cached, fieldName, opts?.allowCrossTenant);
1809
+ return cached;
1810
+ }
1811
+ await ObjectRegistry.ensureManifestLoaded(this.constructor.name);
1812
+ const relationships = ObjectRegistry.getRelationships(
1813
+ this.constructor.name
1814
+ );
1815
+ const relationship = relationships.find(
1816
+ (r) => r.fieldName === fieldName && (r.type === "foreignKey" || r.type === "crossPackageRef")
1817
+ );
1818
+ if (!relationship) {
1819
+ throw RuntimeError.invalidState(
1820
+ `Field ${fieldName} is not a foreignKey or crossPackageRef relationship on ${this.constructor.name}`,
1821
+ { fieldName, className: this.constructor.name }
1822
+ );
1823
+ }
1824
+ const foreignKeyValue = this[fieldName];
1825
+ if (!foreignKeyValue) {
1826
+ this._loadedRelationships.set(fieldName, null);
1827
+ return null;
1828
+ }
1829
+ if (relationship.type === "crossPackageRef") {
1830
+ await ObjectRegistry.ensureManifestLoaded(relationship.targetClass);
1831
+ }
1832
+ const targetClassInfo = relationship.type === "crossPackageRef" ? ObjectRegistry.getClassByQualifiedName(relationship.targetClass) ?? ObjectRegistry.getClass(relationship.targetClass) : ObjectRegistry.getClass(relationship.targetClass);
1833
+ if (!targetClassInfo) {
1834
+ throw RuntimeError.invalidState(
1835
+ `Target class ${relationship.targetClass} not found in ObjectRegistry`,
1836
+ { targetClass: relationship.targetClass, fieldName }
1837
+ );
1838
+ }
1839
+ const tableStrategy = ObjectRegistry.getTableStrategy(
1840
+ relationship.targetClass
1841
+ );
1842
+ const isSTI = tableStrategy === "sti";
1843
+ let actualClassInfo = targetClassInfo;
1844
+ if (isSTI) {
1845
+ const tempInstance = new targetClassInfo.constructor(this.options);
1846
+ await tempInstance.initialize();
1847
+ await tempInstance.verifyStorageReady();
1848
+ const row = await tempInstance.db.get(tempInstance.tableName, {
1849
+ id: foreignKeyValue
1850
+ });
1851
+ if (row?._meta_type) {
1852
+ let actualClass = ObjectRegistry.getClassByQualifiedName(
1853
+ row._meta_type
1854
+ );
1855
+ if (!actualClass) {
1856
+ actualClass = ObjectRegistry.getClass(row._meta_type);
1857
+ }
1858
+ if (actualClass) {
1859
+ actualClassInfo = actualClass;
1860
+ }
1861
+ }
1862
+ }
1863
+ const relatedInstance = new actualClassInfo.constructor(this.options);
1864
+ await relatedInstance.initialize();
1865
+ relatedInstance.id = foreignKeyValue;
1866
+ await relatedInstance.loadFromId();
1867
+ this.assertRelatedTenant(
1868
+ relatedInstance,
1869
+ fieldName,
1870
+ opts?.allowCrossTenant
1871
+ );
1872
+ this._loadedRelationships.set(fieldName, relatedInstance);
1873
+ return relatedInstance;
1874
+ }
1875
+ /**
1876
+ * Guards a loaded relationship target against cross-tenant access.
1877
+ *
1878
+ * Throws {@link TenantIsolationError} when this object and `loaded` both carry
1879
+ * a non-null `tenantId` that differ — the genuine cross-tenant leak. It is a
1880
+ * no-op when `allowCrossTenant === true`, when `loaded` is `null`, when either
1881
+ * side has a `null`/`undefined` tenant (global / non-tenant-scoped models),
1882
+ * and when both sides share the same tenant (Issue #1321).
1883
+ *
1884
+ * `loaded` may be a single related object or an array (oneToMany / manyToMany);
1885
+ * arrays are validated per item. This runs on both fresh loads and cache hits,
1886
+ * so a cache populated by an eager `include` load — or by a prior
1887
+ * `allowCrossTenant` load — cannot leak cross-tenant data through a later
1888
+ * guarded call.
1889
+ *
1890
+ * @param loaded - The loaded related object, or array of objects.
1891
+ * @param fieldName - The relationship field name (for error context).
1892
+ * @param allowCrossTenant - When exactly `true`, bypasses the guard entirely.
1893
+ */
1894
+ assertRelatedTenant(loaded, fieldName, allowCrossTenant) {
1895
+ if (allowCrossTenant === true || loaded == null) {
1896
+ return;
1897
+ }
1898
+ if (Array.isArray(loaded)) {
1899
+ for (const item of loaded) {
1900
+ this.assertRelatedTenant(item, fieldName, allowCrossTenant);
1901
+ }
1902
+ return;
1903
+ }
1904
+ const sourceTenantId = this.tenantId;
1905
+ const targetTenantId = loaded.tenantId;
1906
+ if (sourceTenantId != null && targetTenantId != null && sourceTenantId !== targetTenantId) {
1907
+ throw TenantIsolationError.crossTenantReference({
1908
+ sourceClass: this.constructor.name,
1909
+ fieldName,
1910
+ sourceTenantId: String(sourceTenantId),
1911
+ targetClass: loaded?.constructor?.name,
1912
+ targetTenantId: String(targetTenantId)
1913
+ });
1914
+ }
1915
+ }
1916
+ /**
1917
+ * Load related objects for oneToMany or manyToMany fields (lazy loading)
1918
+ *
1919
+ * Loads all related objects from the database. For oneToMany, queries by
1920
+ * the inverse foreign key. For manyToMany, queries through the join table.
1921
+ *
1922
+ * Enforces tenant isolation per item: if this object and a loaded target both
1923
+ * carry a non-null `tenantId` that differ, a {@link TenantIsolationError} is
1924
+ * thrown (unless `opts.allowCrossTenant` is set). The check is a no-op for
1925
+ * global/null-tenant models and same-tenant loads, so it only catches genuine
1926
+ * cross-tenant leaks (Issue #1321).
1927
+ *
1928
+ * @param fieldName - Name of the oneToMany or manyToMany field
1929
+ * @param opts - Optional loader options; see {@link LoadRelatedOptions}
1930
+ * @returns Promise resolving to array of related objects
1931
+ * @throws {RuntimeError} If the field is not a relationship or not implemented
1932
+ * @throws {TenantIsolationError} If any loaded item belongs to a different,
1933
+ * non-null tenant and `opts.allowCrossTenant` is not set
1934
+ * @example
1935
+ * ```typescript
1936
+ * // Given: class Customer with orders = oneToMany(Order)
1937
+ * const orders = await customer.loadRelatedMany('orders');
1938
+ * console.log(`${orders.length} orders found`);
1939
+ * ```
1940
+ */
1941
+ async loadRelatedMany(fieldName, opts) {
1942
+ if (this._loadedRelationships.has(fieldName)) {
1943
+ const cached = this._loadedRelationships.get(fieldName);
1944
+ this.assertRelatedTenant(cached, fieldName, opts?.allowCrossTenant);
1945
+ return cached;
1946
+ }
1947
+ await ObjectRegistry.ensureManifestLoaded(this.constructor.name);
1948
+ const relationships = ObjectRegistry.getRelationships(
1949
+ this.constructor.name
1950
+ );
1951
+ const relationship = relationships.find((r) => r.fieldName === fieldName);
1952
+ if (!relationship) {
1953
+ throw RuntimeError.invalidState(
1954
+ `Field ${fieldName} is not a relationship on ${this.constructor.name}`,
1955
+ { fieldName, className: this.constructor.name }
1956
+ );
1957
+ }
1958
+ if (relationship.type === "oneToMany") {
1959
+ const inverseRelationships = ObjectRegistry.getInverseRelationshipsForSelf(this.constructor.name);
1960
+ const inverseCandidates = inverseRelationships.filter(
1961
+ (r) => r.sourceClass === relationship.targetClass && r.type === "foreignKey"
1962
+ );
1963
+ const explicitForeignKey = relationship.options?.foreignKey;
1964
+ const matchedForeignKey = explicitForeignKey ? inverseCandidates.find((r) => r.fieldName === explicitForeignKey) : void 0;
1965
+ if (explicitForeignKey && !matchedForeignKey) {
1966
+ throw RuntimeError.invalidState(
1967
+ `oneToMany ${fieldName} on ${this.constructor.name} specifies foreignKey '${explicitForeignKey}', but ${relationship.targetClass} has no matching inverse foreignKey. Candidates: ${inverseCandidates.map((r) => r.fieldName).join(", ") || "(none)"}`,
1968
+ {
1969
+ fieldName,
1970
+ targetClass: relationship.targetClass,
1971
+ foreignKey: explicitForeignKey
1972
+ }
1973
+ );
1974
+ }
1975
+ const inverseForeignKey = matchedForeignKey ?? inverseCandidates.find(
1976
+ (r) => r.targetClass === this.constructor.name
1977
+ ) ?? inverseCandidates[0];
1978
+ if (!inverseForeignKey) {
1979
+ throw RuntimeError.invalidState(
1980
+ `Could not find inverse foreignKey on ${relationship.targetClass} for oneToMany relationship ${fieldName}`,
1981
+ { fieldName, targetClass: relationship.targetClass }
1982
+ );
1983
+ }
1984
+ const collection = await ObjectRegistry.getCollection(
1985
+ relationship.targetClass,
1986
+ this.options
1987
+ );
1988
+ const relatedObjects = await collection.list({
1989
+ where: { [inverseForeignKey.fieldName]: this.id }
1990
+ });
1991
+ this.assertRelatedTenant(
1992
+ relatedObjects,
1993
+ fieldName,
1994
+ opts?.allowCrossTenant
1995
+ );
1996
+ this._loadedRelationships.set(fieldName, relatedObjects);
1997
+ return relatedObjects;
1998
+ }
1999
+ if (relationship.type === "manyToMany") {
2000
+ const { through, sourceColumn, targetColumn, targetClassName } = await this.resolveManyToManyJoin(fieldName, relationship);
2001
+ if (!this.id) {
2002
+ this._loadedRelationships.set(fieldName, []);
2003
+ return [];
2004
+ }
2005
+ await this.verifyStorageReady();
2006
+ const junctionRows = await this.db.query(
2007
+ `SELECT "${targetColumn}" FROM "${through}" WHERE "${sourceColumn}" = ?`,
2008
+ [this.id]
2009
+ );
2010
+ const targetIds = junctionRows.rows.map((row) => row[targetColumn]).filter(
2011
+ (id) => typeof id === "string" && id.length > 0
2012
+ );
2013
+ if (targetIds.length === 0) {
2014
+ this._loadedRelationships.set(fieldName, []);
2015
+ return [];
2016
+ }
2017
+ const targetCollection = await ObjectRegistry.getCollection(
2018
+ targetClassName,
2019
+ this.options
2020
+ );
2021
+ const targetObjects = await targetCollection.list({
2022
+ where: { "id in": targetIds }
2023
+ });
2024
+ this.assertRelatedTenant(
2025
+ targetObjects,
2026
+ fieldName,
2027
+ opts?.allowCrossTenant
2028
+ );
2029
+ this._loadedRelationships.set(fieldName, targetObjects);
2030
+ return targetObjects;
2031
+ }
2032
+ throw RuntimeError.invalidState(
2033
+ `Field ${fieldName} is not a oneToMany or manyToMany relationship`,
2034
+ { fieldName, type: relationship.type }
2035
+ );
2036
+ }
2037
+ /**
2038
+ * Resolves the junction-table coordinates for a manyToMany relationship.
2039
+ *
2040
+ * Discovers `through`, source-side column, and target-side column from the
2041
+ * registered field metadata. Falls back to convention (`<class>_id`) when
2042
+ * `sourceKey` / `targetKey` are not explicitly set.
2043
+ */
2044
+ async resolveManyToManyJoin(fieldName, relationship) {
2045
+ const decorator = ObjectRegistry.getFieldDecorator(
2046
+ relationship.sourceClass,
2047
+ fieldName
2048
+ );
2049
+ const opts = relationship.options || {};
2050
+ const through = decorator?.through ?? opts.through ?? opts._meta?.through;
2051
+ if (!through) {
2052
+ throw RuntimeError.invalidState(
2053
+ `manyToMany field ${fieldName} on ${relationship.sourceClass} is missing the 'through' join table name`,
2054
+ { fieldName, type: "manyToMany" }
2055
+ );
2056
+ }
2057
+ const targetSimpleName = relationship.targetClass.includes(":") ? relationship.targetClass.split(":").pop() : relationship.targetClass;
2058
+ const sourceSimpleName = relationship.sourceClass.includes(":") ? relationship.sourceClass.split(":").pop() : relationship.sourceClass;
2059
+ const sourceColumn = decorator?.sourceKey ?? opts.sourceKey ?? opts._meta?.sourceKey ?? `${toSnakeCase(sourceSimpleName)}_id`;
2060
+ const targetColumn = decorator?.targetKey ?? opts.targetKey ?? opts._meta?.targetKey ?? `${toSnakeCase(targetSimpleName)}_id`;
2061
+ return {
2062
+ through: String(through),
2063
+ sourceColumn,
2064
+ targetColumn,
2065
+ targetClassName: relationship.targetClass
2066
+ };
2067
+ }
2068
+ /**
2069
+ * Get a related object, loading it if not already loaded
2070
+ *
2071
+ * Convenience method that checks if the relationship is loaded and
2072
+ * loads it if necessary. Automatically detects foreignKey vs oneToMany/manyToMany.
2073
+ *
2074
+ * Tenant isolation is enforced by the underlying loaders; see
2075
+ * {@link loadRelated} and {@link loadRelatedMany}.
2076
+ *
2077
+ * @param fieldName - Name of the relationship field
2078
+ * @param opts - Optional loader options; see {@link LoadRelatedOptions}
2079
+ * @returns Promise resolving to the related object(s)
2080
+ * @throws {TenantIsolationError} If a loaded target belongs to a different,
2081
+ * non-null tenant and `opts.allowCrossTenant` is not set
2082
+ * @example
2083
+ * ```typescript
2084
+ * // Loads customer if not already loaded
2085
+ * const customer = await order.getRelated('customerId');
2086
+ *
2087
+ * // Loads orders if not already loaded
2088
+ * const orders = await customer.getRelated('orders');
2089
+ * ```
2090
+ */
2091
+ async getRelated(fieldName, opts) {
2092
+ if (this._loadedRelationships.has(fieldName)) {
2093
+ const cached = this._loadedRelationships.get(fieldName);
2094
+ this.assertRelatedTenant(cached, fieldName, opts?.allowCrossTenant);
2095
+ return cached;
2096
+ }
2097
+ await ObjectRegistry.ensureManifestLoaded(this.constructor.name);
2098
+ const relationships = ObjectRegistry.getRelationships(
2099
+ this.constructor.name
2100
+ );
2101
+ const relationship = relationships.find((r) => r.fieldName === fieldName);
2102
+ if (!relationship) {
2103
+ throw RuntimeError.invalidState(
2104
+ `Field ${fieldName} is not a relationship on ${this.constructor.name}`,
2105
+ { fieldName, className: this.constructor.name }
2106
+ );
2107
+ }
2108
+ if (relationship.type === "foreignKey" || relationship.type === "crossPackageRef") {
2109
+ return this.loadRelated(fieldName, opts);
2110
+ }
2111
+ return this.loadRelatedMany(fieldName, opts);
2112
+ }
2113
+ /**
2114
+ * Get available AI-callable tools for this object
2115
+ *
2116
+ * Returns the pre-generated tool definitions from the manifest.
2117
+ * Tools are generated at build time based on the @smrt decorator's AI config.
2118
+ *
2119
+ * @returns Array of AITool definitions for LLM function calling
2120
+ * @example
2121
+ * ```typescript
2122
+ * const tools = document.getAvailableTools();
2123
+ * console.log(`${tools.length} AI-callable methods available`);
2124
+ * ```
2125
+ */
2126
+ getAvailableTools() {
2127
+ const classInfo = ObjectRegistry.getClass(this.constructor.name);
2128
+ return classInfo?.tools || [];
2129
+ }
2130
+ /**
2131
+ * Execute a tool call from AI on this object instance
2132
+ *
2133
+ * Validates the tool call against allowed methods and executes it with
2134
+ * proper error handling and timing.
2135
+ *
2136
+ * @param toolCall - Tool call from AI response
2137
+ * @returns Promise resolving to the tool call result
2138
+ * @example
2139
+ * ```typescript
2140
+ * const toolCall = {
2141
+ * id: 'call_123',
2142
+ * type: 'function',
2143
+ * function: {
2144
+ * name: 'analyze',
2145
+ * arguments: '{"type": "detailed"}'
2146
+ * }
2147
+ * };
2148
+ *
2149
+ * const result = await document.executeToolCall(toolCall);
2150
+ * console.log(result.success ? result.result : result.error);
2151
+ * ```
2152
+ */
2153
+ async executeToolCall(toolCall) {
2154
+ const tools = this.getAvailableTools();
2155
+ const allowedMethods = tools.map((tool) => tool.function.name);
2156
+ return executeToolCall(this, toolCall, allowedMethods);
2157
+ }
2158
+ /**
2159
+ * Stores a named value in the `_smrt_contexts` system table, scoped to this object.
2160
+ *
2161
+ * Context entries are keyed by `(owner_class, owner_id, scope, key, version)` and
2162
+ * support an optional `confidence` score (0–1, default 1.0) for learned patterns.
2163
+ * Calling `remember()` with the same scope+key upserts the existing entry.
2164
+ *
2165
+ * Requires `initialize()` to have been called (needs `this.systemDb`).
2166
+ *
2167
+ * @param options.id - Optional explicit ID for the context entry (auto-generated if omitted)
2168
+ * @param options.scope - Hierarchical path scoping the context (e.g. `'parser/example.com'`)
2169
+ * @param options.key - Lookup key within the scope (e.g. a normalized URL)
2170
+ * @param options.value - Any JSON-serializable value to store
2171
+ * @param options.metadata - Optional additional JSON metadata
2172
+ * @param options.confidence - Confidence score (0–1, default 1.0)
2173
+ * @param options.version - Schema version for the stored value (default 1)
2174
+ * @param options.expiresAt - Optional expiry date after which `recall()` will ignore this entry
2175
+ * @returns Promise that resolves when the context is stored
2176
+ * @throws Error if `initialize()` has not been called
2177
+ *
2178
+ * @example
2179
+ * ```typescript
2180
+ * await agent.remember({
2181
+ * scope: 'parser/example.com',
2182
+ * key: normalizedUrl,
2183
+ * value: { patterns: ['regex1', 'regex2'] },
2184
+ * confidence: 0.9,
2185
+ * });
2186
+ * ```
2187
+ *
2188
+ * @see {@link recall} to retrieve a single entry
2189
+ * @see {@link recallAll} to retrieve all entries in a scope
2190
+ * @see {@link forget} to delete a specific entry
2191
+ */
2192
+ async remember(options) {
2193
+ if (!this.systemDb) {
2194
+ throw new Error("Database not initialized. Call initialize() first.");
2195
+ }
2196
+ const id = options.id || crypto.randomUUID();
2197
+ const now = /* @__PURE__ */ new Date();
2198
+ if (!this.id) {
2199
+ this._id = crypto.randomUUID();
2200
+ }
2201
+ await this.systemDb.upsert(
2202
+ "_smrt_contexts",
2203
+ ["owner_class", "owner_id", "scope", "key", "version"],
2204
+ {
2205
+ id,
2206
+ owner_class: this._className,
2207
+ owner_id: this.id,
2208
+ scope: options.scope,
2209
+ key: options.key,
2210
+ value: JSON.stringify(options.value),
2211
+ metadata: options.metadata ? JSON.stringify(options.metadata) : null,
2212
+ version: options.version ?? 1,
2213
+ confidence: options.confidence ?? 1,
2214
+ created_at: now,
2215
+ updated_at: now,
2216
+ last_used_at: now,
2217
+ expires_at: options.expiresAt ?? null
2218
+ }
2219
+ );
2220
+ }
2221
+ /**
2222
+ * Retrieves a single remembered context value for this object.
2223
+ *
2224
+ * Looks up the most recent (highest confidence, then highest version) entry
2225
+ * matching `(owner_class, owner_id, scope, key)`. Returns the JSON-parsed value
2226
+ * or `null` if no matching entry exists.
2227
+ *
2228
+ * When `includeAncestors: true`, walks up the scope hierarchy by progressively
2229
+ * removing the last path segment (e.g. `'a/b/c'` → `'a/b'` → `'a'` → `'global'`)
2230
+ * until a match is found.
2231
+ *
2232
+ * @param options.scope - Scope path to search (e.g. `'parser/example.com/article'`)
2233
+ * @param options.key - Lookup key within the scope
2234
+ * @param options.includeAncestors - If `true`, fall back to parent scopes (default `false`)
2235
+ * @param options.minConfidence - Only return entries at or above this confidence (0–1)
2236
+ * @returns The stored value (parsed from JSON), or `null` if not found
2237
+ * @throws Error if `initialize()` has not been called
2238
+ *
2239
+ * @example
2240
+ * ```typescript
2241
+ * const strategy = await agent.recall({
2242
+ * scope: 'parser/example.com/article',
2243
+ * key: normalizedUrl,
2244
+ * includeAncestors: true,
2245
+ * minConfidence: 0.6,
2246
+ * });
2247
+ * ```
2248
+ *
2249
+ * @see {@link remember} to store a context entry
2250
+ * @see {@link recallAll} to retrieve all entries in a scope
2251
+ */
2252
+ async recall(options) {
2253
+ if (!this.systemDb) {
2254
+ throw new Error("Database not initialized. Call initialize() first.");
2255
+ }
2256
+ let result;
2257
+ if (options.minConfidence !== void 0) {
2258
+ result = await this.systemDb.single`
2259
+ SELECT value, confidence
2260
+ FROM _smrt_contexts
2261
+ WHERE owner_class = ${this._className}
2262
+ AND owner_id = ${this.id}
2263
+ AND scope = ${options.scope}
2264
+ AND key = ${options.key}
2265
+ AND confidence >= ${options.minConfidence}
2266
+ ORDER BY confidence DESC, version DESC
2267
+ LIMIT 1
2268
+ `;
2269
+ } else {
2270
+ result = await this.systemDb.single`
2271
+ SELECT value, confidence
2272
+ FROM _smrt_contexts
2273
+ WHERE owner_class = ${this._className}
2274
+ AND owner_id = ${this.id}
2275
+ AND scope = ${options.scope}
2276
+ AND key = ${options.key}
2277
+ ORDER BY confidence DESC, version DESC
2278
+ LIMIT 1
2279
+ `;
2280
+ }
2281
+ if (result) {
2282
+ try {
2283
+ return JSON.parse(result.value);
2284
+ } catch (error) {
2285
+ logger.warn("Skipping corrupted _smrt_contexts value in recall()", {
2286
+ ownerClass: this._className,
2287
+ ownerId: this.id,
2288
+ scope: options.scope,
2289
+ key: options.key,
2290
+ error: error instanceof Error ? error.message : String(error)
2291
+ });
2292
+ }
2293
+ }
2294
+ if (options.includeAncestors) {
2295
+ const scopeParts = options.scope.split("/");
2296
+ while (scopeParts.length > 0) {
2297
+ scopeParts.pop();
2298
+ const parentScope = scopeParts.join("/") || "global";
2299
+ const parentResult = await this.recall({
2300
+ ...options,
2301
+ scope: parentScope,
2302
+ includeAncestors: false
2303
+ });
2304
+ if (parentResult) return parentResult;
2305
+ }
2306
+ }
2307
+ return null;
2308
+ }
2309
+ /**
2310
+ * Retrieves all remembered context entries for this object in a scope.
2311
+ *
2312
+ * Returns a `Map<key, value>` for every entry owned by this object that matches
2313
+ * the scope and optional filters. When `includeDescendants: true`, a LIKE query
2314
+ * (`scope%`) matches the scope itself and all child scopes.
2315
+ *
2316
+ * @param options.scope - Optional scope path filter; omit to retrieve all scopes
2317
+ * @param options.includeDescendants - If `true`, match `scope` and all child scopes (default `false`)
2318
+ * @param options.minConfidence - Only include entries at or above this confidence (0–1)
2319
+ * @returns `Map<string, any>` of key → JSON-parsed value pairs
2320
+ * @throws Error if `initialize()` has not been called
2321
+ *
2322
+ * @example
2323
+ * ```typescript
2324
+ * const strategies = await agent.recallAll({
2325
+ * scope: 'parser/example.com',
2326
+ * minConfidence: 0.5,
2327
+ * });
2328
+ * for (const [url, pattern] of strategies) {
2329
+ * console.log(`Cached pattern for ${url}:`, pattern);
2330
+ * }
2331
+ * ```
2332
+ *
2333
+ * @see {@link recall} to retrieve a single entry by key
2334
+ * @see {@link forgetScope} to delete all entries in a scope
2335
+ */
2336
+ async recallAll(options = {}) {
2337
+ if (!this.systemDb) {
2338
+ throw new Error("Database not initialized. Call initialize() first.");
2339
+ }
2340
+ const results = /* @__PURE__ */ new Map();
2341
+ const where = {
2342
+ owner_class: this._className,
2343
+ owner_id: this.id
2344
+ };
2345
+ if (options.scope) {
2346
+ if (options.includeDescendants) {
2347
+ where["scope like"] = `${options.scope}%`;
2348
+ } else {
2349
+ where.scope = options.scope;
2350
+ }
2351
+ }
2352
+ if (options.minConfidence !== void 0) {
2353
+ where["confidence >="] = options.minConfidence;
2354
+ }
2355
+ const rows = await this.systemDb.list("_smrt_contexts", where);
2356
+ for (const row of rows) {
2357
+ try {
2358
+ results.set(row.key, JSON.parse(row.value));
2359
+ } catch (error) {
2360
+ logger.warn("Skipping corrupted _smrt_contexts value in recallAll()", {
2361
+ ownerClass: this._className,
2362
+ ownerId: this.id,
2363
+ scope: options.scope,
2364
+ key: row.key,
2365
+ error: error instanceof Error ? error.message : String(error)
2366
+ });
2367
+ }
2368
+ }
2369
+ return results;
2370
+ }
2371
+ /**
2372
+ * Deletes a specific remembered context entry for this object.
2373
+ *
2374
+ * Removes the entry matching `(owner_class, owner_id, scope, key)`. A no-op
2375
+ * if the entry does not exist.
2376
+ *
2377
+ * @param options.scope - Scope path of the entry to delete
2378
+ * @param options.key - Key of the entry to delete
2379
+ * @returns Promise that resolves when the entry is deleted
2380
+ * @throws Error if `initialize()` has not been called
2381
+ *
2382
+ * @example
2383
+ * ```typescript
2384
+ * await agent.forget({ scope: 'parser/example.com', key: normalizedUrl });
2385
+ * ```
2386
+ *
2387
+ * @see {@link forgetScope} to delete all entries in a scope at once
2388
+ * @see {@link remember} to store a context entry
2389
+ */
2390
+ async forget(options) {
2391
+ if (!this.systemDb) {
2392
+ throw new Error("Database not initialized. Call initialize() first.");
2393
+ }
2394
+ await this.systemDb.delete("_smrt_contexts", {
2395
+ owner_class: this._className,
2396
+ owner_id: this.id,
2397
+ scope: options.scope,
2398
+ key: options.key
2399
+ });
2400
+ }
2401
+ /**
2402
+ * Deletes all remembered context entries in a scope for this object.
2403
+ *
2404
+ * When `includeDescendants: true`, uses a LIKE query (`scope%`) that matches
2405
+ * the scope itself and all child scopes (e.g. `'parser/example.com'` also
2406
+ * deletes `'parser/example.com/article'`).
2407
+ *
2408
+ * @param options.scope - Scope path to clear
2409
+ * @param options.includeDescendants - If `true`, also delete all child scopes (default `false`)
2410
+ * @returns Number of context entries deleted
2411
+ * @throws Error if `initialize()` has not been called
2412
+ *
2413
+ * @example
2414
+ * ```typescript
2415
+ * const count = await agent.forgetScope({
2416
+ * scope: 'parser/example.com',
2417
+ * includeDescendants: true,
2418
+ * });
2419
+ * console.log(`Cleared ${count} cached strategies`);
2420
+ * ```
2421
+ *
2422
+ * @see {@link forget} to delete a single entry by key
2423
+ * @see {@link recallAll} to bulk-retrieve before clearing
2424
+ */
2425
+ async forgetScope(options) {
2426
+ if (!this.systemDb) {
2427
+ throw new Error("Database not initialized. Call initialize() first.");
2428
+ }
2429
+ const where = {
2430
+ owner_class: this._className,
2431
+ owner_id: this.id
2432
+ };
2433
+ if (options.includeDescendants) {
2434
+ where["scope like"] = `${options.scope}%`;
2435
+ } else {
2436
+ where.scope = options.scope;
2437
+ }
2438
+ const result = await this.systemDb.delete("_smrt_contexts", where);
2439
+ return result.affected || 0;
2440
+ }
2441
+ // ============================================================================
2442
+ // Embedding Methods for Semantic Search
2443
+ // ============================================================================
2444
+ /**
2445
+ * Generate embeddings for configured fields
2446
+ *
2447
+ * Creates embedding vectors for fields configured in the @smrt decorator
2448
+ * and stores them in the _smrt_embeddings system table. Uses content hashing
2449
+ * to detect changes and avoid regenerating unchanged content.
2450
+ *
2451
+ * @param options - Generation options
2452
+ * @returns Promise that resolves when embeddings are generated
2453
+ * @throws Error if no embedding configuration or database not initialized
2454
+ *
2455
+ * @example
2456
+ * ```typescript
2457
+ * // Generate embeddings for all configured fields
2458
+ * await article.generateEmbeddings();
2459
+ *
2460
+ * // Generate only for specific fields
2461
+ * await article.generateEmbeddings({ fields: ['title'] });
2462
+ *
2463
+ * // Force regeneration (ignore content hash)
2464
+ * await article.generateEmbeddings({ force: true });
2465
+ * ```
2466
+ */
2467
+ async generateEmbeddings(options = {}) {
2468
+ if (!this.systemDb) {
2469
+ throw new Error("Database not initialized. Call initialize() first.");
2470
+ }
2471
+ if (!this.id) {
2472
+ throw new Error("Object must have an ID before generating embeddings.");
2473
+ }
2474
+ const config = ObjectRegistry.resolveEmbeddingConfig(this.constructor.name);
2475
+ if (!config) {
2476
+ throw new Error(
2477
+ `No embedding configuration found for ${this.constructor.name}. Add embeddings config to the @smrt() decorator.`
2478
+ );
2479
+ }
2480
+ const fieldsToProcess = options.fields || config.fields;
2481
+ const provider = options.provider || config.provider;
2482
+ const aiClient = await this.getOptionalAiClient();
2483
+ const embeddingProvider = new EmbeddingProvider(
2484
+ {
2485
+ dimensions: config.dimensions,
2486
+ provider,
2487
+ localModel: config.localModel,
2488
+ aiModel: config.aiModel,
2489
+ fallbackToAI: config.fallbackToAI
2490
+ },
2491
+ aiClient
2492
+ );
2493
+ const projectConfig = ObjectRegistry.getProjectEmbeddingConfig();
2494
+ const vector = projectConfig?.storage === "native" ? this.systemDb.vector : void 0;
2495
+ for (const fieldName of fieldsToProcess) {
2496
+ const content = this.getPropertyValue(fieldName);
2497
+ if (!content || typeof content !== "string") {
2498
+ continue;
2499
+ }
2500
+ const contentHash = ContentHasher.hash(content);
2501
+ if (!options.force) {
2502
+ const existing = await EmbeddingStorage.get(
2503
+ this.systemDb,
2504
+ this.constructor.name,
2505
+ this.id,
2506
+ fieldName,
2507
+ embeddingProvider.getModelName()
2508
+ );
2509
+ if (existing && existing.content_hash === contentHash) {
2510
+ continue;
2511
+ }
2512
+ }
2513
+ const embeddings = await embeddingProvider.embed(content);
2514
+ const embedding = embeddings[0];
2515
+ await EmbeddingStorage.upsert(
2516
+ this.systemDb,
2517
+ {
2518
+ objectClass: this.constructor.name,
2519
+ objectId: this.id,
2520
+ fieldName,
2521
+ contentHash,
2522
+ embedding,
2523
+ model: embeddingProvider.getModelName(),
2524
+ dimensions: config.dimensions,
2525
+ provider
2526
+ },
2527
+ vector
2528
+ );
2529
+ }
2530
+ if (config.combinedField) {
2531
+ const { name, template } = config.combinedField;
2532
+ let combinedContent = template;
2533
+ for (const fieldName of config.fields) {
2534
+ const value = this.getPropertyValue(fieldName) || "";
2535
+ combinedContent = combinedContent.replace(
2536
+ new RegExp(`\\{${fieldName}\\}`, "g"),
2537
+ String(value)
2538
+ );
2539
+ }
2540
+ if (combinedContent.trim()) {
2541
+ const contentHash = ContentHasher.hash(combinedContent);
2542
+ if (!options.force) {
2543
+ const existing = await EmbeddingStorage.get(
2544
+ this.systemDb,
2545
+ this.constructor.name,
2546
+ this.id,
2547
+ name,
2548
+ embeddingProvider.getModelName()
2549
+ );
2550
+ if (existing && existing.content_hash === contentHash) {
2551
+ return;
2552
+ }
2553
+ }
2554
+ const embeddings = await embeddingProvider.embed(combinedContent);
2555
+ const embedding = embeddings[0];
2556
+ await EmbeddingStorage.upsert(
2557
+ this.systemDb,
2558
+ {
2559
+ objectClass: this.constructor.name,
2560
+ objectId: this.id,
2561
+ fieldName: name,
2562
+ contentHash,
2563
+ embedding,
2564
+ model: embeddingProvider.getModelName(),
2565
+ dimensions: config.dimensions,
2566
+ provider
2567
+ },
2568
+ vector
2569
+ );
2570
+ }
2571
+ }
2572
+ }
2573
+ /**
2574
+ * Get stored embedding for a field
2575
+ *
2576
+ * Retrieves the embedding vector for a specific field from storage.
2577
+ * Returns null if no embedding exists for the field.
2578
+ *
2579
+ * @param fieldName - Name of the field to get embedding for
2580
+ * @param model - Optional model name (defaults to configured model)
2581
+ * @returns Promise resolving to embedding vector or null
2582
+ *
2583
+ * @example
2584
+ * ```typescript
2585
+ * const embedding = await article.getEmbedding('title');
2586
+ * if (embedding) {
2587
+ * console.log(`Embedding has ${embedding.length} dimensions`);
2588
+ * }
2589
+ * ```
2590
+ */
2591
+ async getEmbedding(fieldName, model) {
2592
+ if (!this.systemDb) {
2593
+ throw new Error("Database not initialized. Call initialize() first.");
2594
+ }
2595
+ if (!this.id) {
2596
+ return null;
2597
+ }
2598
+ let modelName = model;
2599
+ if (!modelName) {
2600
+ const config = ObjectRegistry.resolveEmbeddingConfig(
2601
+ this.constructor.name
2602
+ );
2603
+ if (config) {
2604
+ const aiClient = await this.getOptionalAiClient();
2605
+ const provider = new EmbeddingProvider(
2606
+ {
2607
+ dimensions: config.dimensions,
2608
+ provider: config.provider,
2609
+ localModel: config.localModel,
2610
+ aiModel: config.aiModel,
2611
+ fallbackToAI: config.fallbackToAI
2612
+ },
2613
+ aiClient
2614
+ );
2615
+ modelName = provider.getModelName();
2616
+ } else {
2617
+ modelName = "Xenova/bge-base-en-v1.5";
2618
+ }
2619
+ }
2620
+ const stored = await EmbeddingStorage.get(
2621
+ this.systemDb,
2622
+ this.constructor.name,
2623
+ this.id,
2624
+ fieldName,
2625
+ modelName
2626
+ );
2627
+ return stored?.embedding || null;
2628
+ }
2629
+ /**
2630
+ * Check if any embeddings are stale (content has changed)
2631
+ *
2632
+ * Compares content hashes of configured fields with stored embeddings
2633
+ * to determine if regeneration is needed.
2634
+ *
2635
+ * @returns Promise resolving to true if any embeddings are stale
2636
+ *
2637
+ * @example
2638
+ * ```typescript
2639
+ * if (await article.hasStaleEmbeddings()) {
2640
+ * await article.generateEmbeddings();
2641
+ * }
2642
+ * ```
2643
+ */
2644
+ async hasStaleEmbeddings() {
2645
+ if (!this.systemDb || !this.id) {
2646
+ return false;
2647
+ }
2648
+ const config = ObjectRegistry.resolveEmbeddingConfig(this.constructor.name);
2649
+ if (!config) {
2650
+ return false;
2651
+ }
2652
+ const aiClient = await this.getOptionalAiClient();
2653
+ const embeddingProvider = new EmbeddingProvider(
2654
+ {
2655
+ dimensions: config.dimensions,
2656
+ provider: config.provider,
2657
+ localModel: config.localModel,
2658
+ aiModel: config.aiModel,
2659
+ fallbackToAI: config.fallbackToAI
2660
+ },
2661
+ aiClient
2662
+ );
2663
+ const modelName = embeddingProvider.getModelName();
2664
+ const storedEmbeddings = await EmbeddingStorage.getForObject(
2665
+ this.systemDb,
2666
+ this.constructor.name,
2667
+ this.id
2668
+ );
2669
+ for (const fieldName of config.fields) {
2670
+ const content = this.getPropertyValue(fieldName);
2671
+ if (!content || typeof content !== "string") {
2672
+ continue;
2673
+ }
2674
+ const currentHash = ContentHasher.hash(content);
2675
+ const stored = storedEmbeddings.find(
2676
+ (e) => e.field_name === fieldName && e.model === modelName
2677
+ );
2678
+ if (!stored) {
2679
+ return true;
2680
+ }
2681
+ if (stored.content_hash !== currentHash) {
2682
+ return true;
2683
+ }
2684
+ }
2685
+ if (config.combinedField) {
2686
+ let combinedContent = config.combinedField.template;
2687
+ for (const fieldName of config.fields) {
2688
+ const value = this.getPropertyValue(fieldName) || "";
2689
+ combinedContent = combinedContent.replace(
2690
+ new RegExp(`\\{${fieldName}\\}`, "g"),
2691
+ String(value)
2692
+ );
2693
+ }
2694
+ const currentHash = ContentHasher.hash(combinedContent);
2695
+ const stored = storedEmbeddings.find(
2696
+ (e) => e.field_name === config.combinedField?.name && e.model === modelName
2697
+ );
2698
+ if (!stored || stored.content_hash !== currentHash) {
2699
+ return true;
2700
+ }
2701
+ }
2702
+ return false;
2703
+ }
2704
+ /**
2705
+ * Clear all embeddings for this object
2706
+ *
2707
+ * Removes all stored embeddings from the _smrt_embeddings table.
2708
+ * Useful when deleting objects or resetting embedding state.
2709
+ *
2710
+ * @returns Promise that resolves when embeddings are cleared
2711
+ *
2712
+ * @example
2713
+ * ```typescript
2714
+ * await article.clearEmbeddings();
2715
+ * ```
2716
+ */
2717
+ async clearEmbeddings() {
2718
+ if (!this.systemDb || !this.id) {
2719
+ return;
2720
+ }
2721
+ await EmbeddingStorage.deleteForObject(
2722
+ this.systemDb,
2723
+ this.constructor.name,
2724
+ this.id
2725
+ );
2726
+ }
2727
+ }
2728
+ export {
2729
+ SmrtObject
2730
+ };
2731
+ //# sourceMappingURL=object.js.map