@vibesdotdev/storage 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (423) hide show
  1. package/README.md +75 -0
  2. package/SPEC.md +60 -0
  3. package/dist/adapters/index.d.ts +14 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +15 -0
  6. package/dist/adapters/index.js.map +1 -0
  7. package/dist/bridge/index.d.ts +3 -0
  8. package/dist/bridge/index.d.ts.map +1 -0
  9. package/dist/bridge/index.js +2 -0
  10. package/dist/bridge/index.js.map +1 -0
  11. package/dist/bridge/storage-kind-bridge.d.ts +27 -0
  12. package/dist/bridge/storage-kind-bridge.d.ts.map +1 -0
  13. package/dist/bridge/storage-kind-bridge.js +110 -0
  14. package/dist/bridge/storage-kind-bridge.js.map +1 -0
  15. package/dist/bridge/types.d.ts +45 -0
  16. package/dist/bridge/types.d.ts.map +1 -0
  17. package/dist/bridge/types.js +8 -0
  18. package/dist/bridge/types.js.map +1 -0
  19. package/dist/cli/commands/list/storage.list.cli-command.descriptor.d.ts +4 -0
  20. package/dist/cli/commands/list/storage.list.cli-command.descriptor.d.ts.map +1 -0
  21. package/dist/cli/commands/list/storage.list.cli-command.descriptor.js +18 -0
  22. package/dist/cli/commands/list/storage.list.cli-command.descriptor.js.map +1 -0
  23. package/dist/cli/commands/list/storage.list.cli-command.impl.d.ts +11 -0
  24. package/dist/cli/commands/list/storage.list.cli-command.impl.d.ts.map +1 -0
  25. package/dist/cli/commands/list/storage.list.cli-command.impl.js +48 -0
  26. package/dist/cli/commands/list/storage.list.cli-command.impl.js.map +1 -0
  27. package/dist/cli/commands/sync/pull/storage.sync.pull.cli-command.descriptor.d.ts +4 -0
  28. package/dist/cli/commands/sync/pull/storage.sync.pull.cli-command.descriptor.d.ts.map +1 -0
  29. package/dist/cli/commands/sync/pull/storage.sync.pull.cli-command.descriptor.js +26 -0
  30. package/dist/cli/commands/sync/pull/storage.sync.pull.cli-command.descriptor.js.map +1 -0
  31. package/dist/cli/commands/sync/pull/storage.sync.pull.cli-command.impl.d.ts +13 -0
  32. package/dist/cli/commands/sync/pull/storage.sync.pull.cli-command.impl.d.ts.map +1 -0
  33. package/dist/cli/commands/sync/pull/storage.sync.pull.cli-command.impl.js +136 -0
  34. package/dist/cli/commands/sync/pull/storage.sync.pull.cli-command.impl.js.map +1 -0
  35. package/dist/cli/commands/sync/push/storage.sync.push.cli-command.descriptor.d.ts +4 -0
  36. package/dist/cli/commands/sync/push/storage.sync.push.cli-command.descriptor.d.ts.map +1 -0
  37. package/dist/cli/commands/sync/push/storage.sync.push.cli-command.descriptor.js +26 -0
  38. package/dist/cli/commands/sync/push/storage.sync.push.cli-command.descriptor.js.map +1 -0
  39. package/dist/cli/commands/sync/push/storage.sync.push.cli-command.impl.d.ts +13 -0
  40. package/dist/cli/commands/sync/push/storage.sync.push.cli-command.impl.d.ts.map +1 -0
  41. package/dist/cli/commands/sync/push/storage.sync.push.cli-command.impl.js +165 -0
  42. package/dist/cli/commands/sync/push/storage.sync.push.cli-command.impl.js.map +1 -0
  43. package/dist/cli/commands/sync/resolve/storage.sync.resolve.cli-command.descriptor.d.ts +4 -0
  44. package/dist/cli/commands/sync/resolve/storage.sync.resolve.cli-command.descriptor.d.ts.map +1 -0
  45. package/dist/cli/commands/sync/resolve/storage.sync.resolve.cli-command.descriptor.js +20 -0
  46. package/dist/cli/commands/sync/resolve/storage.sync.resolve.cli-command.descriptor.js.map +1 -0
  47. package/dist/cli/commands/sync/resolve/storage.sync.resolve.cli-command.impl.d.ts +12 -0
  48. package/dist/cli/commands/sync/resolve/storage.sync.resolve.cli-command.impl.d.ts.map +1 -0
  49. package/dist/cli/commands/sync/resolve/storage.sync.resolve.cli-command.impl.js +95 -0
  50. package/dist/cli/commands/sync/resolve/storage.sync.resolve.cli-command.impl.js.map +1 -0
  51. package/dist/cli/commands/sync/status/storage.sync.status.cli-command.descriptor.d.ts +4 -0
  52. package/dist/cli/commands/sync/status/storage.sync.status.cli-command.descriptor.d.ts.map +1 -0
  53. package/dist/cli/commands/sync/status/storage.sync.status.cli-command.descriptor.js +15 -0
  54. package/dist/cli/commands/sync/status/storage.sync.status.cli-command.descriptor.js.map +1 -0
  55. package/dist/cli/commands/sync/status/storage.sync.status.cli-command.impl.d.ts +13 -0
  56. package/dist/cli/commands/sync/status/storage.sync.status.cli-command.impl.d.ts.map +1 -0
  57. package/dist/cli/commands/sync/status/storage.sync.status.cli-command.impl.js +150 -0
  58. package/dist/cli/commands/sync/status/storage.sync.status.cli-command.impl.js.map +1 -0
  59. package/dist/cli/context.consumer.d.ts +26 -0
  60. package/dist/cli/context.consumer.d.ts.map +1 -0
  61. package/dist/cli/context.consumer.js +164 -0
  62. package/dist/cli/context.consumer.js.map +1 -0
  63. package/dist/cli/storage.cli-group.descriptor.d.ts +4 -0
  64. package/dist/cli/storage.cli-group.descriptor.d.ts.map +1 -0
  65. package/dist/cli/storage.cli-group.descriptor.js +11 -0
  66. package/dist/cli/storage.cli-group.descriptor.js.map +1 -0
  67. package/dist/cli/storage.sync.cli-group.descriptor.d.ts +4 -0
  68. package/dist/cli/storage.sync.cli-group.descriptor.d.ts.map +1 -0
  69. package/dist/cli/storage.sync.cli-group.descriptor.js +12 -0
  70. package/dist/cli/storage.sync.cli-group.descriptor.js.map +1 -0
  71. package/dist/cli/types.d.ts +26 -0
  72. package/dist/cli/types.d.ts.map +1 -0
  73. package/dist/cli/types.js +11 -0
  74. package/dist/cli/types.js.map +1 -0
  75. package/dist/client/index.d.ts +10 -0
  76. package/dist/client/index.d.ts.map +1 -0
  77. package/dist/client/index.js +10 -0
  78. package/dist/client/index.js.map +1 -0
  79. package/dist/client/list-browsable-adapters.d.ts +31 -0
  80. package/dist/client/list-browsable-adapters.d.ts.map +1 -0
  81. package/dist/client/list-browsable-adapters.js +40 -0
  82. package/dist/client/list-browsable-adapters.js.map +1 -0
  83. package/dist/daemon/commands/sync.d.ts +23 -0
  84. package/dist/daemon/commands/sync.d.ts.map +1 -0
  85. package/dist/daemon/commands/sync.js +169 -0
  86. package/dist/daemon/commands/sync.js.map +1 -0
  87. package/dist/docs/adapters.docs.descriptor.d.ts +4 -0
  88. package/dist/docs/adapters.docs.descriptor.d.ts.map +1 -0
  89. package/dist/docs/adapters.docs.descriptor.js +223 -0
  90. package/dist/docs/adapters.docs.descriptor.js.map +1 -0
  91. package/dist/docs/manifests.docs.descriptor.d.ts +4 -0
  92. package/dist/docs/manifests.docs.descriptor.d.ts.map +1 -0
  93. package/dist/docs/manifests.docs.descriptor.js +316 -0
  94. package/dist/docs/manifests.docs.descriptor.js.map +1 -0
  95. package/dist/docs/records.docs.descriptor.d.ts +4 -0
  96. package/dist/docs/records.docs.descriptor.d.ts.map +1 -0
  97. package/dist/docs/records.docs.descriptor.js +269 -0
  98. package/dist/docs/records.docs.descriptor.js.map +1 -0
  99. package/dist/docs/sync.docs.descriptor.d.ts +4 -0
  100. package/dist/docs/sync.docs.descriptor.d.ts.map +1 -0
  101. package/dist/docs/sync.docs.descriptor.js +279 -0
  102. package/dist/docs/sync.docs.descriptor.js.map +1 -0
  103. package/dist/index.d.ts +27 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +29 -0
  106. package/dist/index.js.map +1 -0
  107. package/dist/interfaces/adapter.d.ts +382 -0
  108. package/dist/interfaces/adapter.d.ts.map +1 -0
  109. package/dist/interfaces/adapter.js +11 -0
  110. package/dist/interfaces/adapter.js.map +1 -0
  111. package/dist/interfaces/document-store.d.ts +62 -0
  112. package/dist/interfaces/document-store.d.ts.map +1 -0
  113. package/dist/interfaces/document-store.js +8 -0
  114. package/dist/interfaces/document-store.js.map +1 -0
  115. package/dist/interfaces/index.d.ts +17 -0
  116. package/dist/interfaces/index.d.ts.map +1 -0
  117. package/dist/interfaces/index.js +24 -0
  118. package/dist/interfaces/index.js.map +1 -0
  119. package/dist/interfaces/key-value.d.ts +95 -0
  120. package/dist/interfaces/key-value.d.ts.map +1 -0
  121. package/dist/interfaces/key-value.js +8 -0
  122. package/dist/interfaces/key-value.js.map +1 -0
  123. package/dist/interfaces/query-builder.d.ts +31 -0
  124. package/dist/interfaces/query-builder.d.ts.map +1 -0
  125. package/dist/interfaces/query-builder.js +11 -0
  126. package/dist/interfaces/query-builder.js.map +1 -0
  127. package/dist/interfaces/record/index.d.ts +7 -0
  128. package/dist/interfaces/record/index.d.ts.map +1 -0
  129. package/dist/interfaces/record/index.js +7 -0
  130. package/dist/interfaces/record/index.js.map +1 -0
  131. package/dist/interfaces/record/interface.d.ts +593 -0
  132. package/dist/interfaces/record/interface.d.ts.map +1 -0
  133. package/dist/interfaces/record/interface.js +49 -0
  134. package/dist/interfaces/record/interface.js.map +1 -0
  135. package/dist/interfaces/repository.d.ts +262 -0
  136. package/dist/interfaces/repository.d.ts.map +1 -0
  137. package/dist/interfaces/repository.js +8 -0
  138. package/dist/interfaces/repository.js.map +1 -0
  139. package/dist/interfaces/schema/index.d.ts +8 -0
  140. package/dist/interfaces/schema/index.d.ts.map +1 -0
  141. package/dist/interfaces/schema/index.js +10 -0
  142. package/dist/interfaces/schema/index.js.map +1 -0
  143. package/dist/interfaces/schema/schema-interface.d.ts +516 -0
  144. package/dist/interfaces/schema/schema-interface.d.ts.map +1 -0
  145. package/dist/interfaces/schema/schema-interface.js +31 -0
  146. package/dist/interfaces/schema/schema-interface.js.map +1 -0
  147. package/dist/interfaces/schema/table-interface.d.ts +250 -0
  148. package/dist/interfaces/schema/table-interface.d.ts.map +1 -0
  149. package/dist/interfaces/schema/table-interface.js +31 -0
  150. package/dist/interfaces/schema/table-interface.js.map +1 -0
  151. package/dist/interfaces/storage-consumer-fs.d.ts +40 -0
  152. package/dist/interfaces/storage-consumer-fs.d.ts.map +1 -0
  153. package/dist/interfaces/storage-consumer-fs.js +42 -0
  154. package/dist/interfaces/storage-consumer-fs.js.map +1 -0
  155. package/dist/manifest/base.d.ts +187 -0
  156. package/dist/manifest/base.d.ts.map +1 -0
  157. package/dist/manifest/base.js +516 -0
  158. package/dist/manifest/base.js.map +1 -0
  159. package/dist/manifest/cloud-sync.consumer.d.ts +7 -0
  160. package/dist/manifest/cloud-sync.consumer.d.ts.map +1 -0
  161. package/dist/manifest/cloud-sync.consumer.js +82 -0
  162. package/dist/manifest/cloud-sync.consumer.js.map +1 -0
  163. package/dist/manifest/index.d.ts +7 -0
  164. package/dist/manifest/index.d.ts.map +1 -0
  165. package/dist/manifest/index.js +7 -0
  166. package/dist/manifest/index.js.map +1 -0
  167. package/dist/manifest/source-policy.d.ts +8 -0
  168. package/dist/manifest/source-policy.d.ts.map +1 -0
  169. package/dist/manifest/source-policy.js +62 -0
  170. package/dist/manifest/source-policy.js.map +1 -0
  171. package/dist/paths.d.ts +6 -0
  172. package/dist/paths.d.ts.map +1 -0
  173. package/dist/paths.js +152 -0
  174. package/dist/paths.js.map +1 -0
  175. package/dist/policy/context.d.ts +9 -0
  176. package/dist/policy/context.d.ts.map +1 -0
  177. package/dist/policy/context.js +29 -0
  178. package/dist/policy/context.js.map +1 -0
  179. package/dist/policy/index.d.ts +4 -0
  180. package/dist/policy/index.d.ts.map +1 -0
  181. package/dist/policy/index.js +4 -0
  182. package/dist/policy/index.js.map +1 -0
  183. package/dist/policy/overrides.d.ts +40 -0
  184. package/dist/policy/overrides.d.ts.map +1 -0
  185. package/dist/policy/overrides.js +37 -0
  186. package/dist/policy/overrides.js.map +1 -0
  187. package/dist/policy/persistence.d.ts +38 -0
  188. package/dist/policy/persistence.d.ts.map +1 -0
  189. package/dist/policy/persistence.js +65 -0
  190. package/dist/policy/persistence.js.map +1 -0
  191. package/dist/policy/types.d.ts +21 -0
  192. package/dist/policy/types.d.ts.map +1 -0
  193. package/dist/policy/types.js +7 -0
  194. package/dist/policy/types.js.map +1 -0
  195. package/dist/record/base.d.ts +186 -0
  196. package/dist/record/base.d.ts.map +1 -0
  197. package/dist/record/base.js +1437 -0
  198. package/dist/record/base.js.map +1 -0
  199. package/dist/record/events.d.ts +71 -0
  200. package/dist/record/events.d.ts.map +1 -0
  201. package/dist/record/events.js +52 -0
  202. package/dist/record/events.js.map +1 -0
  203. package/dist/record/extensions.d.ts +51 -0
  204. package/dist/record/extensions.d.ts.map +1 -0
  205. package/dist/record/extensions.js +222 -0
  206. package/dist/record/extensions.js.map +1 -0
  207. package/dist/record/index.d.ts +7 -0
  208. package/dist/record/index.d.ts.map +1 -0
  209. package/dist/record/index.js +7 -0
  210. package/dist/record/index.js.map +1 -0
  211. package/dist/resolver/storage-backed-resolver.d.ts +23 -0
  212. package/dist/resolver/storage-backed-resolver.d.ts.map +1 -0
  213. package/dist/resolver/storage-backed-resolver.js +187 -0
  214. package/dist/resolver/storage-backed-resolver.js.map +1 -0
  215. package/dist/schemas/adapter-descriptor.d.ts +108 -0
  216. package/dist/schemas/adapter-descriptor.d.ts.map +1 -0
  217. package/dist/schemas/adapter-descriptor.js +101 -0
  218. package/dist/schemas/adapter-descriptor.js.map +1 -0
  219. package/dist/schemas/index.d.ts +14 -0
  220. package/dist/schemas/index.d.ts.map +1 -0
  221. package/dist/schemas/index.js +12 -0
  222. package/dist/schemas/index.js.map +1 -0
  223. package/dist/schemas/manifest-descriptor.d.ts +110 -0
  224. package/dist/schemas/manifest-descriptor.d.ts.map +1 -0
  225. package/dist/schemas/manifest-descriptor.js +114 -0
  226. package/dist/schemas/manifest-descriptor.js.map +1 -0
  227. package/dist/schemas/record/descriptor.d.ts +570 -0
  228. package/dist/schemas/record/descriptor.d.ts.map +1 -0
  229. package/dist/schemas/record/descriptor.js +389 -0
  230. package/dist/schemas/record/descriptor.js.map +1 -0
  231. package/dist/schemas/record/index.d.ts +8 -0
  232. package/dist/schemas/record/index.d.ts.map +1 -0
  233. package/dist/schemas/record/index.js +7 -0
  234. package/dist/schemas/record/index.js.map +1 -0
  235. package/dist/schemas/record/relation.d.ts +36 -0
  236. package/dist/schemas/record/relation.d.ts.map +1 -0
  237. package/dist/schemas/record/relation.js +37 -0
  238. package/dist/schemas/record/relation.js.map +1 -0
  239. package/dist/schemas/schema/descriptor.d.ts +231 -0
  240. package/dist/schemas/schema/descriptor.d.ts.map +1 -0
  241. package/dist/schemas/schema/descriptor.js +205 -0
  242. package/dist/schemas/schema/descriptor.js.map +1 -0
  243. package/dist/schemas/schema/index.d.ts +7 -0
  244. package/dist/schemas/schema/index.d.ts.map +1 -0
  245. package/dist/schemas/schema/index.js +7 -0
  246. package/dist/schemas/schema/index.js.map +1 -0
  247. package/dist/schemas/sync-bridge-descriptor.d.ts +124 -0
  248. package/dist/schemas/sync-bridge-descriptor.d.ts.map +1 -0
  249. package/dist/schemas/sync-bridge-descriptor.js +67 -0
  250. package/dist/schemas/sync-bridge-descriptor.js.map +1 -0
  251. package/dist/server/manifest-route.d.ts +92 -0
  252. package/dist/server/manifest-route.d.ts.map +1 -0
  253. package/dist/server/manifest-route.js +226 -0
  254. package/dist/server/manifest-route.js.map +1 -0
  255. package/dist/storage-policy.context.descriptor.d.ts +16 -0
  256. package/dist/storage-policy.context.descriptor.d.ts.map +1 -0
  257. package/dist/storage-policy.context.descriptor.js +16 -0
  258. package/dist/storage-policy.context.descriptor.js.map +1 -0
  259. package/dist/storage-policy.context.server.d.ts +14 -0
  260. package/dist/storage-policy.context.server.d.ts.map +1 -0
  261. package/dist/storage-policy.context.server.js +27 -0
  262. package/dist/storage-policy.context.server.js.map +1 -0
  263. package/dist/storage.plugin.d.ts +38 -0
  264. package/dist/storage.plugin.d.ts.map +1 -0
  265. package/dist/storage.plugin.js +204 -0
  266. package/dist/storage.plugin.js.map +1 -0
  267. package/dist/sync/cloud-pull.d.ts +15 -0
  268. package/dist/sync/cloud-pull.d.ts.map +1 -0
  269. package/dist/sync/cloud-pull.js +130 -0
  270. package/dist/sync/cloud-pull.js.map +1 -0
  271. package/dist/sync/cloud-record-crud-config.d.ts +19 -0
  272. package/dist/sync/cloud-record-crud-config.d.ts.map +1 -0
  273. package/dist/sync/cloud-record-crud-config.js +22 -0
  274. package/dist/sync/cloud-record-crud-config.js.map +1 -0
  275. package/dist/sync/record-sync-bridge.d.ts +5 -0
  276. package/dist/sync/record-sync-bridge.d.ts.map +1 -0
  277. package/dist/sync/record-sync-bridge.js +121 -0
  278. package/dist/sync/record-sync-bridge.js.map +1 -0
  279. package/dist/sync/resolve-sync-target.d.ts +23 -0
  280. package/dist/sync/resolve-sync-target.d.ts.map +1 -0
  281. package/dist/sync/resolve-sync-target.js +45 -0
  282. package/dist/sync/resolve-sync-target.js.map +1 -0
  283. package/dist/sync/schemas/cloud-pull.d.ts +25 -0
  284. package/dist/sync/schemas/cloud-pull.d.ts.map +1 -0
  285. package/dist/sync/schemas/cloud-pull.js +7 -0
  286. package/dist/sync/schemas/cloud-pull.js.map +1 -0
  287. package/dist/sync/schemas/index.d.ts +9 -0
  288. package/dist/sync/schemas/index.d.ts.map +1 -0
  289. package/dist/sync/schemas/index.js +9 -0
  290. package/dist/sync/schemas/index.js.map +1 -0
  291. package/dist/sync/schemas/sync-helpers.d.ts +17 -0
  292. package/dist/sync/schemas/sync-helpers.d.ts.map +1 -0
  293. package/dist/sync/schemas/sync-helpers.js +7 -0
  294. package/dist/sync/schemas/sync-helpers.js.map +1 -0
  295. package/dist/sync/schemas/sync-state.d.ts +65 -0
  296. package/dist/sync/schemas/sync-state.d.ts.map +1 -0
  297. package/dist/sync/schemas/sync-state.js +32 -0
  298. package/dist/sync/schemas/sync-state.js.map +1 -0
  299. package/dist/sync/sync-bridge.d.ts +80 -0
  300. package/dist/sync/sync-bridge.d.ts.map +1 -0
  301. package/dist/sync/sync-bridge.js +617 -0
  302. package/dist/sync/sync-bridge.js.map +1 -0
  303. package/dist/sync/sync-manifest.d.ts +20 -0
  304. package/dist/sync/sync-manifest.d.ts.map +1 -0
  305. package/dist/sync/sync-manifest.js +140 -0
  306. package/dist/sync/sync-manifest.js.map +1 -0
  307. package/dist/sync/sync-state.consumer.d.ts +23 -0
  308. package/dist/sync/sync-state.consumer.d.ts.map +1 -0
  309. package/dist/sync/sync-state.consumer.js +111 -0
  310. package/dist/sync/sync-state.consumer.js.map +1 -0
  311. package/dist/transform/index.d.ts +75 -0
  312. package/dist/transform/index.d.ts.map +1 -0
  313. package/dist/transform/index.js +81 -0
  314. package/dist/transform/index.js.map +1 -0
  315. package/dist/transform/transform-descriptor.d.ts +51 -0
  316. package/dist/transform/transform-descriptor.d.ts.map +1 -0
  317. package/dist/transform/transform-descriptor.js +30 -0
  318. package/dist/transform/transform-descriptor.js.map +1 -0
  319. package/dist/tui/storage-explorer.descriptor.d.ts +14 -0
  320. package/dist/tui/storage-explorer.descriptor.d.ts.map +1 -0
  321. package/dist/tui/storage-explorer.descriptor.js +29 -0
  322. package/dist/tui/storage-explorer.descriptor.js.map +1 -0
  323. package/dist/tui/storage-explorer.impl.consumer.d.ts +11 -0
  324. package/dist/tui/storage-explorer.impl.consumer.d.ts.map +1 -0
  325. package/dist/tui/storage-explorer.impl.consumer.js +13 -0
  326. package/dist/tui/storage-explorer.impl.consumer.js.map +1 -0
  327. package/dist/tui/storage-open.descriptor.d.ts +14 -0
  328. package/dist/tui/storage-open.descriptor.d.ts.map +1 -0
  329. package/dist/tui/storage-open.descriptor.js +28 -0
  330. package/dist/tui/storage-open.descriptor.js.map +1 -0
  331. package/dist/tui/storage-open.impl.consumer.d.ts +11 -0
  332. package/dist/tui/storage-open.impl.consumer.d.ts.map +1 -0
  333. package/dist/tui/storage-open.impl.consumer.js +13 -0
  334. package/dist/tui/storage-open.impl.consumer.js.map +1 -0
  335. package/dist/utils/error-context.d.ts +2 -0
  336. package/dist/utils/error-context.d.ts.map +1 -0
  337. package/dist/utils/error-context.js +17 -0
  338. package/dist/utils/error-context.js.map +1 -0
  339. package/package.json +241 -0
  340. package/src/adapters/index.ts +13 -0
  341. package/src/bridge/index.ts +2 -0
  342. package/src/bridge/storage-kind-bridge.ts +131 -0
  343. package/src/bridge/types.ts +46 -0
  344. package/src/cli/commands/list/storage.list.cli-command.descriptor.ts +20 -0
  345. package/src/cli/commands/list/storage.list.cli-command.impl.ts +72 -0
  346. package/src/cli/commands/sync/pull/storage.sync.pull.cli-command.descriptor.ts +28 -0
  347. package/src/cli/commands/sync/pull/storage.sync.pull.cli-command.impl.ts +171 -0
  348. package/src/cli/commands/sync/push/storage.sync.push.cli-command.descriptor.ts +28 -0
  349. package/src/cli/commands/sync/push/storage.sync.push.cli-command.impl.ts +213 -0
  350. package/src/cli/commands/sync/resolve/storage.sync.resolve.cli-command.descriptor.ts +22 -0
  351. package/src/cli/commands/sync/resolve/storage.sync.resolve.cli-command.impl.ts +129 -0
  352. package/src/cli/commands/sync/status/storage.sync.status.cli-command.descriptor.ts +17 -0
  353. package/src/cli/commands/sync/status/storage.sync.status.cli-command.impl.ts +192 -0
  354. package/src/cli/context.consumer.ts +195 -0
  355. package/src/cli/storage.cli-group.descriptor.ts +13 -0
  356. package/src/cli/storage.sync.cli-group.descriptor.ts +14 -0
  357. package/src/cli/types.ts +25 -0
  358. package/src/client/index.ts +10 -0
  359. package/src/client/list-browsable-adapters.ts +42 -0
  360. package/src/daemon/commands/sync.ts +225 -0
  361. package/src/docs/adapters.docs.descriptor.ts +225 -0
  362. package/src/docs/manifests.docs.descriptor.ts +318 -0
  363. package/src/docs/records.docs.descriptor.ts +271 -0
  364. package/src/docs/sync.docs.descriptor.ts +281 -0
  365. package/src/index.ts +109 -0
  366. package/src/interfaces/adapter.ts +451 -0
  367. package/src/interfaces/document-store.ts +74 -0
  368. package/src/interfaces/index.ts +117 -0
  369. package/src/interfaces/key-value.ts +112 -0
  370. package/src/interfaces/query-builder.ts +31 -0
  371. package/src/interfaces/record/index.ts +58 -0
  372. package/src/interfaces/record/interface.ts +723 -0
  373. package/src/interfaces/repository.ts +300 -0
  374. package/src/interfaces/schema/index.ts +37 -0
  375. package/src/interfaces/schema/schema-interface.ts +586 -0
  376. package/src/interfaces/schema/table-interface.ts +314 -0
  377. package/src/interfaces/storage-consumer-fs.ts +66 -0
  378. package/src/manifest/base.ts +594 -0
  379. package/src/manifest/cloud-sync.consumer.ts +120 -0
  380. package/src/manifest/index.ts +7 -0
  381. package/src/manifest/source-policy.ts +74 -0
  382. package/src/paths.ts +171 -0
  383. package/src/policy/context.ts +30 -0
  384. package/src/policy/index.ts +20 -0
  385. package/src/policy/overrides.ts +46 -0
  386. package/src/policy/persistence.ts +117 -0
  387. package/src/policy/types.ts +28 -0
  388. package/src/record/base.ts +1872 -0
  389. package/src/record/events.ts +56 -0
  390. package/src/record/extensions.ts +308 -0
  391. package/src/record/index.ts +7 -0
  392. package/src/resolver/storage-backed-resolver.ts +255 -0
  393. package/src/schemas/adapter-descriptor.ts +128 -0
  394. package/src/schemas/index.ts +104 -0
  395. package/src/schemas/manifest-descriptor.ts +143 -0
  396. package/src/schemas/record/descriptor.ts +621 -0
  397. package/src/schemas/record/index.ts +60 -0
  398. package/src/schemas/record/relation.ts +49 -0
  399. package/src/schemas/schema/descriptor.ts +278 -0
  400. package/src/schemas/schema/index.ts +14 -0
  401. package/src/schemas/sync-bridge-descriptor.ts +143 -0
  402. package/src/server/manifest-route.ts +319 -0
  403. package/src/storage-policy.context.descriptor.ts +17 -0
  404. package/src/storage-policy.context.server.ts +34 -0
  405. package/src/storage.plugin.ts +264 -0
  406. package/src/sync/cloud-pull.ts +202 -0
  407. package/src/sync/cloud-record-crud-config.ts +41 -0
  408. package/src/sync/record-sync-bridge.ts +183 -0
  409. package/src/sync/resolve-sync-target.ts +57 -0
  410. package/src/sync/schemas/cloud-pull.ts +26 -0
  411. package/src/sync/schemas/index.ts +18 -0
  412. package/src/sync/schemas/sync-helpers.ts +18 -0
  413. package/src/sync/schemas/sync-state.ts +41 -0
  414. package/src/sync/sync-bridge.ts +763 -0
  415. package/src/sync/sync-manifest.ts +212 -0
  416. package/src/sync/sync-state.consumer.ts +144 -0
  417. package/src/transform/index.ts +130 -0
  418. package/src/transform/transform-descriptor.ts +53 -0
  419. package/src/tui/storage-explorer.descriptor.ts +32 -0
  420. package/src/tui/storage-explorer.impl.consumer.ts +14 -0
  421. package/src/tui/storage-open.descriptor.ts +31 -0
  422. package/src/tui/storage-open.impl.consumer.ts +14 -0
  423. package/src/utils/error-context.ts +19 -0
@@ -0,0 +1,1437 @@
1
+ /**
2
+ * StorageRecord Base Implementation
3
+ *
4
+ * Provides CRUD operations on a single table with relations.
5
+ * This is what gets resolved when you query for a storage/record descriptor.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const todos = new StorageRecord(todoDescriptor, adapter);
10
+ * await todos.initialize();
11
+ *
12
+ * const todo = await todos.create({
13
+ * sessionId: 'session-123',
14
+ * title: 'Implement storage',
15
+ * status: 'in_progress'
16
+ * });
17
+ * ```
18
+ */
19
+ import { getLogger } from '@vibesdotdev/logging';
20
+ import { toErrorContext } from '../utils/error-context';
21
+ const logger = getLogger('storage:record');
22
+ import { resolveHomePath, resolveScopePathTemplate, resolveWorkspacePath } from '../paths';
23
+ import { getVibesRuntime } from '@vibesdotdev/runtime/api/runtime';
24
+ import { createTracer } from '@vibesdotdev/runtime/debug';
25
+ function resolveTimestampField(timestamps, field) {
26
+ return typeof timestamps === 'object' && typeof timestamps?.[field] === 'string'
27
+ ? timestamps[field]
28
+ : field;
29
+ }
30
+ export function applyCreateTimestamps(record, timestamps, now) {
31
+ if (!timestamps) {
32
+ return;
33
+ }
34
+ const createdAtField = resolveTimestampField(timestamps, 'createdAt');
35
+ const updatedAtField = resolveTimestampField(timestamps, 'updatedAt');
36
+ if (!record[createdAtField]) {
37
+ record[createdAtField] = now;
38
+ }
39
+ if (!record[updatedAtField]) {
40
+ record[updatedAtField] = now;
41
+ }
42
+ }
43
+ export function applyUpdateTimestamp(record, update, timestamps, now) {
44
+ if (!timestamps) {
45
+ return;
46
+ }
47
+ const updatedAtField = resolveTimestampField(timestamps, 'updatedAt');
48
+ if (!(updatedAtField in update)) {
49
+ record[updatedAtField] = now;
50
+ }
51
+ }
52
+ /**
53
+ * StorageRecord Implementation
54
+ *
55
+ * Implements StorageRecordInterface for single-table CRUD operations.
56
+ * Works with SQLite via the localdb adapter.
57
+ *
58
+ * Uses runtime query API to resolve the appropriate adapter based on
59
+ * the descriptor's allowedAdapters and defaultAdapter settings.
60
+ */
61
+ export class StorageRecord {
62
+ tableName;
63
+ primaryKey;
64
+ schema;
65
+ relations;
66
+ descriptor;
67
+ context;
68
+ adapterCache = null;
69
+ db = null;
70
+ initialized = false;
71
+ asMutableRecord(record) {
72
+ return record;
73
+ }
74
+ readStringField(record, field) {
75
+ const value = this.asMutableRecord(record)[field];
76
+ return typeof value === 'string' ? value : null;
77
+ }
78
+ readPrimaryKey(record) {
79
+ const id = this.readStringField(record, this.primaryKey);
80
+ if (!id) {
81
+ throw new Error(`Record in ${this.tableName} is missing primary key '${this.primaryKey}'`);
82
+ }
83
+ return id;
84
+ }
85
+ /**
86
+ * Emit a lifecycle event via the runtime HookBus.
87
+ * Fire-and-forget: never blocks CRUD, silently catches errors.
88
+ */
89
+ emitLifecycle(phase, record) {
90
+ try {
91
+ const runtime = getVibesRuntime();
92
+ const id = record[this.primaryKey];
93
+ runtime.hooks('storage/record').emit(phase, {
94
+ recordId: id,
95
+ recordKind: this.descriptor.id,
96
+ tableName: this.tableName,
97
+ data: record
98
+ });
99
+ }
100
+ catch {
101
+ // Runtime may not be bootstrapped in tests — never block CRUD
102
+ }
103
+ }
104
+ constructor(descriptor, context) {
105
+ this.descriptor = descriptor;
106
+ this.context = context;
107
+ this.tableName = descriptor.tableName;
108
+ this.primaryKey = descriptor.primaryKey || 'id';
109
+ this.schema = descriptor.schema;
110
+ this.relations = descriptor.relations || {};
111
+ }
112
+ /**
113
+ * Resolve the adapter for this record using runtime query API.
114
+ *
115
+ * Uses the record's allowed adapters list and current scope to
116
+ * select the best available adapter.
117
+ */
118
+ async getAdapter() {
119
+ if (this.adapterCache) {
120
+ return this.adapterCache;
121
+ }
122
+ const adapterTracer = createTracer(`adapter:${this.descriptor.id}`);
123
+ adapterTracer.start();
124
+ const runtime = getVibesRuntime();
125
+ let adapter = null;
126
+ let adapterId = '';
127
+ if (this.descriptor.defaultAdapter &&
128
+ this.descriptor.allowedAdapters.includes(this.descriptor.defaultAdapter)) {
129
+ adapterId = this.descriptor.defaultAdapter;
130
+ adapter = await runtime
131
+ .query('storage/adapter')
132
+ .withId(adapterId)
133
+ .enabled()
134
+ .resolve()
135
+ .catch(() => null);
136
+ }
137
+ if (!adapter) {
138
+ for (const allowedId of this.descriptor.allowedAdapters) {
139
+ adapter = await runtime
140
+ .query('storage/adapter')
141
+ .withId(allowedId)
142
+ .enabled()
143
+ .resolve()
144
+ .catch(() => null);
145
+ if (adapter) {
146
+ adapterId = allowedId;
147
+ break;
148
+ }
149
+ }
150
+ }
151
+ if (!adapter || !adapterId) {
152
+ throw new Error(`No available adapter for record '${this.descriptor.id}'. Checked: ${this.descriptor.allowedAdapters.join(', ')}`);
153
+ }
154
+ if (!adapter.isConfigured()) {
155
+ const config = this.getAdapterConfig(adapterId);
156
+ adapter.configure(config);
157
+ }
158
+ this.adapterCache = adapter;
159
+ adapterTracer.end('resolved');
160
+ return adapter;
161
+ }
162
+ /**
163
+ * Get adapter configuration based on record scope.
164
+ */
165
+ getAdapterConfig(adapterId) {
166
+ const workspacePath = resolveWorkspacePath(this.context);
167
+ const config = {
168
+ basePath: this.getBasePath(workspacePath),
169
+ workspacePath
170
+ };
171
+ if (this.descriptor.adapterConfig) {
172
+ const selectedAdapterId = adapterId || this.descriptor.defaultAdapter || this.descriptor.allowedAdapters[0];
173
+ const adapterSpecificConfig = this.descriptor.adapterConfig[selectedAdapterId];
174
+ if (adapterSpecificConfig && typeof adapterSpecificConfig === 'object') {
175
+ Object.assign(config, adapterSpecificConfig);
176
+ }
177
+ }
178
+ return config;
179
+ }
180
+ /**
181
+ * Get base path for storage based on scope.
182
+ *
183
+ * Scope mapping:
184
+ * - global: ~/.vibes/ (user-level, shared across projects)
185
+ * - project: ./.vibes/ (project-level, within .vibes dir)
186
+ * - local: ./.vibes/ (alias for project)
187
+ * - auto: ./.vibes/ (defaults to project scope)
188
+ */
189
+ getBasePath(workspacePath = resolveWorkspacePath(this.context)) {
190
+ const scope = this.descriptor.scope || 'project';
191
+ const homePath = resolveHomePath();
192
+ const descriptorWithScopePaths = this.descriptor;
193
+ if (descriptorWithScopePaths.scopePaths) {
194
+ const customPath = descriptorWithScopePaths.scopePaths[scope];
195
+ if (customPath) {
196
+ return resolveScopePathTemplate(customPath, workspacePath);
197
+ }
198
+ }
199
+ switch (scope) {
200
+ case 'global':
201
+ return `${homePath}/.vibes`;
202
+ case 'local':
203
+ case 'project':
204
+ case 'auto':
205
+ default:
206
+ return `${workspacePath}/.vibes`;
207
+ }
208
+ }
209
+ /**
210
+ * Initialize the storage record
211
+ * - Creates table if it doesn't exist
212
+ * - Runs migrations
213
+ * - Creates indexes
214
+ */
215
+ async initialize() {
216
+ if (this.initialized)
217
+ return;
218
+ // Get database connection from adapter
219
+ this.db = await this.getDatabase();
220
+ // Create table if using inline migrations
221
+ if (this.descriptor.migrations && this.descriptor.migrations.length > 0) {
222
+ await this.runMigrations();
223
+ // Inline migrations may create normalized column schemas.
224
+ // StorageRecord CRUD operations expect a `data` TEXT column for JSON storage.
225
+ // If the migrated table lacks a `data` column and is empty, recreate it
226
+ // with the minimal (primaryKey, data) schema that StorageRecord requires.
227
+ await this.ensureDataColumn();
228
+ }
229
+ else if (this.descriptor.migrationKey) {
230
+ // Migrations should be pre-registered by the module
231
+ // The adapter will handle them
232
+ }
233
+ else {
234
+ // Auto-create table based on schema
235
+ await this.createTableFromSchema();
236
+ }
237
+ // Create indexes
238
+ if (this.descriptor.indexes && this.descriptor.indexes.length > 0) {
239
+ await this.createIndexes();
240
+ }
241
+ this.initialized = true;
242
+ }
243
+ /**
244
+ * Ensure the table has a `data` TEXT column required by StorageRecord CRUD operations.
245
+ *
246
+ * Inline migrations may create normalized column schemas that don't include a `data`
247
+ * column. StorageRecord's create/get/list/update/delete all use json_extract(data, ...)
248
+ * and INSERT INTO ... (primaryKey, data). If the table is empty and lacks `data`,
249
+ * it is safe to drop and recreate with the minimal (primaryKey, data) schema.
250
+ */
251
+ async ensureDataColumn() {
252
+ if (!this.db)
253
+ throw new Error('Database not initialized');
254
+ const cols = this.db.query(`PRAGMA table_info(${this.tableName})`).all();
255
+ if (cols.some((c) => c.name === 'data'))
256
+ return;
257
+ // Table lacks a data column. Only recreate if it is empty (no data to lose).
258
+ const row = this.db
259
+ .query(`SELECT COUNT(*) as n FROM ${this.tableName}`)
260
+ .get();
261
+ if (!row || row.n > 0) {
262
+ logger.warn(`[StorageRecord] Table '${this.tableName}' has no 'data' column and contains rows. ` +
263
+ `CRUD operations may fail. Consider adding a migration to add a 'data TEXT' column.`);
264
+ return;
265
+ }
266
+ // Safe to recreate: drop and replace with minimal schema
267
+ this.db.exec(`DROP TABLE IF EXISTS ${this.tableName}`);
268
+ this.db.exec(`CREATE TABLE ${this.tableName} (${this.primaryKey} TEXT PRIMARY KEY, data TEXT NOT NULL)`);
269
+ }
270
+ /**
271
+ * Get database connection from adapter.
272
+ * Uses the typed ensureReady() / getDatabaseConnection() interface.
273
+ */
274
+ async getDatabase() {
275
+ const adapter = await this.getAdapter();
276
+ await adapter.ensureReady();
277
+ const db = adapter.getDatabaseConnection();
278
+ if (!db) {
279
+ throw new Error(`Adapter '${adapter.id}' returned null from getDatabaseConnection() for record '${this.descriptor.id}'`);
280
+ }
281
+ return db;
282
+ }
283
+ /**
284
+ * Create table from schema definition
285
+ */
286
+ async createTableFromSchema() {
287
+ if (!this.db)
288
+ throw new Error('Database not initialized');
289
+ // Build column definitions from schema
290
+ const columns = this.buildColumnDefinitions();
291
+ const columnDefs = Object.entries(columns)
292
+ .map(([name, def]) => `${name} ${def}`)
293
+ .join(',\n ');
294
+ const sql = `
295
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
296
+ ${columnDefs}
297
+ )
298
+ `;
299
+ this.db.exec(sql);
300
+ }
301
+ /**
302
+ * Build column definitions from schema and descriptor
303
+ */
304
+ buildColumnDefinitions() {
305
+ const columns = {};
306
+ // Primary key
307
+ const pkType = this.descriptor.fieldConfig?.[this.primaryKey]?.type || 'TEXT';
308
+ columns[this.primaryKey] = `${pkType} PRIMARY KEY`;
309
+ // Auto-managed fields
310
+ if (this.descriptor.timestamps) {
311
+ const createdAt = typeof this.descriptor.timestamps === 'object'
312
+ ? this.descriptor.timestamps.createdAt
313
+ : 'createdAt';
314
+ const updatedAt = typeof this.descriptor.timestamps === 'object'
315
+ ? this.descriptor.timestamps.updatedAt
316
+ : 'updatedAt';
317
+ if (createdAt) {
318
+ const createdAtField = typeof createdAt === 'string' ? createdAt : 'createdAt';
319
+ columns[createdAtField] = 'TEXT DEFAULT CURRENT_TIMESTAMP';
320
+ }
321
+ if (updatedAt) {
322
+ const updatedAtField = typeof updatedAt === 'string' ? updatedAt : 'updatedAt';
323
+ columns[updatedAtField] = 'TEXT DEFAULT CURRENT_TIMESTAMP';
324
+ }
325
+ }
326
+ if (this.descriptor.softDelete) {
327
+ const deletedAtField = typeof this.descriptor.softDelete === 'string' ? this.descriptor.softDelete : 'deletedAt';
328
+ columns[deletedAtField] = 'TEXT';
329
+ }
330
+ // Schema introspection is not yet wired; all non-meta fields live in a JSON `data` blob.
331
+ columns['data'] = 'TEXT';
332
+ return columns;
333
+ }
334
+ /**
335
+ * Create indexes from descriptor
336
+ */
337
+ async createIndexes() {
338
+ if (!this.db)
339
+ throw new Error('Database not initialized');
340
+ // Detect whether the table uses JSON blob storage (has a 'data' column).
341
+ // ensureDataColumn() may have recreated the table as (primaryKey, data TEXT),
342
+ // so direct column references from the descriptor won't exist. Use json_extract
343
+ // expressions instead so indexes still function and unique constraints are enforced.
344
+ const tableInfo = this.db
345
+ .query(`PRAGMA table_info(${this.tableName})`)
346
+ .all();
347
+ const hasDataColumn = tableInfo.some((c) => c.name === 'data');
348
+ const toSnakeCase = (value) => value
349
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
350
+ .replace(/-/g, '_')
351
+ .toLowerCase();
352
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
353
+ for (const index of this.descriptor.indexes || []) {
354
+ const indexName = index.name || `idx_${this.tableName}_${index.fields.join('_')}`;
355
+ const unique = index.unique ? 'UNIQUE' : '';
356
+ let fieldExpr;
357
+ let whereExpr = '';
358
+ if (hasDataColumn) {
359
+ // JSON blob mode: reference fields via json_extract so indexes work
360
+ fieldExpr = index.fields.map((f) => `json_extract(data, '$.${f}')`).join(', ');
361
+ if (index.where) {
362
+ // Replace field name references in the WHERE clause with json_extract.
363
+ // Some descriptors still use snake_case predicates (legacy migration SQL style).
364
+ let w = index.where;
365
+ for (const f of index.fields) {
366
+ // Dedupe aliases so identical camel/snake names don't double-replace,
367
+ // which would corrupt an already-substituted json_extract expression.
368
+ const aliases = [...new Set([f, toSnakeCase(f)])];
369
+ for (const alias of aliases) {
370
+ w = w.replace(new RegExp(`\\b${escapeRegExp(alias)}\\b`, 'g'), `json_extract(data, '$.${f}')`);
371
+ }
372
+ }
373
+ whereExpr = `WHERE ${w}`;
374
+ }
375
+ }
376
+ else {
377
+ // Normalized column mode: use direct column references
378
+ fieldExpr = index.fields.join(', ');
379
+ whereExpr = index.where ? `WHERE ${index.where}` : '';
380
+ }
381
+ const sql = `CREATE ${unique} INDEX IF NOT EXISTS ${indexName} ON ${this.tableName} (${fieldExpr}) ${whereExpr}`;
382
+ try {
383
+ this.db.query(sql).run();
384
+ }
385
+ catch (error) {
386
+ if (index.unique) {
387
+ // Unique index failures must be fatal — they protect data integrity
388
+ throw error;
389
+ }
390
+ // Non-unique index creation can fail if descriptor field names don't match
391
+ // actual column names (e.g. camelCase vs snake_case mismatch). Log and continue.
392
+ logger.warn(`[StorageRecord] Skipping index '${indexName}' on '${this.tableName}': ${error instanceof Error ? error.message : String(error)}`);
393
+ }
394
+ }
395
+ }
396
+ /**
397
+ * Execute one or more SQL statements, splitting on `;` and running each
398
+ * via the prepared-statement path so bun:sqlite does not misinterpret `$`
399
+ * tokens inside string literals (e.g. json_extract paths).
400
+ */
401
+ execStatements(sql) {
402
+ if (!this.db)
403
+ throw new Error('Database not initialized');
404
+ for (const stmt of sql.split(';').map((s) => s.trim()).filter(Boolean)) {
405
+ this.db.query(stmt).run();
406
+ }
407
+ }
408
+ /**
409
+ * Run migrations from descriptor
410
+ */
411
+ async runMigrations() {
412
+ if (!this.db)
413
+ throw new Error('Database not initialized');
414
+ const migrations = this.descriptor.migrations || [];
415
+ // Create migrations tracking table if not exists
416
+ this.db.exec(`
417
+ CREATE TABLE IF NOT EXISTS _migrations_${this.tableName} (
418
+ version INTEGER PRIMARY KEY,
419
+ name TEXT,
420
+ applied_at TEXT DEFAULT CURRENT_TIMESTAMP
421
+ )
422
+ `);
423
+ // Get applied migrations
424
+ const applied = this.db
425
+ .query(`SELECT version FROM _migrations_${this.tableName}`)
426
+ .all()
427
+ .map((r) => r.version);
428
+ // Run pending migrations
429
+ for (const migration of migrations) {
430
+ if (applied.includes(migration.version))
431
+ continue;
432
+ try {
433
+ if (typeof migration.up === 'string') {
434
+ this.execStatements(migration.up);
435
+ }
436
+ else if (typeof migration.up === 'function') {
437
+ await migration.up(this.db);
438
+ }
439
+ // Record migration
440
+ this.db
441
+ .query(`INSERT INTO _migrations_${this.tableName} (version, name) VALUES (?, ?)`)
442
+ .run(migration.version, migration.name || '');
443
+ logger.debug(`[StorageRecord] Applied migration ${migration.version} for ${this.tableName}`);
444
+ }
445
+ catch (error) {
446
+ // A "no such table" failure typically means the consumer is
447
+ // querying before its initial seed has run (e.g. `knowledge
448
+ // status` before any knowledge base exists). The application
449
+ // layer catches the throw and renders empty state; demote the
450
+ // log so the spurious noise doesn't reach the user. Real
451
+ // migration failures still surface at warn level + the throw.
452
+ const message = error instanceof Error ? error.message : String(error);
453
+ const isTableMissing = /no such table/i.test(message);
454
+ if (isTableMissing) {
455
+ logger.debug(`[StorageRecord] Migration ${migration.version} skipped for ${this.tableName} (table not yet initialized)`);
456
+ }
457
+ else {
458
+ logger.error(`[StorageRecord] Migration ${migration.version} failed for ${this.tableName}:`, toErrorContext(error));
459
+ }
460
+ throw error;
461
+ }
462
+ }
463
+ }
464
+ // CREATE
465
+ async create(data) {
466
+ await this.ensureInitialized();
467
+ // Prepare record
468
+ const record = { ...data };
469
+ // Auto-generate ID (must happen before validation so the schema check passes)
470
+ if (this.descriptor.autoId) {
471
+ const idField = typeof this.descriptor.autoId === 'object' && this.descriptor.autoId.field
472
+ ? this.descriptor.autoId.field
473
+ : this.primaryKey;
474
+ if (!record[idField]) {
475
+ record[idField] = this.generateId();
476
+ }
477
+ }
478
+ // Auto-manage timestamps (before validation so timestamps are present)
479
+ if (this.descriptor.timestamps) {
480
+ const now = new Date().toISOString();
481
+ applyCreateTimestamps(record, this.descriptor.timestamps, now);
482
+ }
483
+ // Execute beforeCreate hook (may set additional required fields)
484
+ if (this.descriptor.beforeCreate) {
485
+ await this.descriptor.beforeCreate(record);
486
+ }
487
+ // Validate after all auto-fields are populated
488
+ if (this.descriptor.validateOnWrite !== false) {
489
+ const validation = await this.validate(record);
490
+ if (!validation.success) {
491
+ throw new Error(`Validation failed: ${validation.errors?.message}`);
492
+ }
493
+ }
494
+ // Insert into database
495
+ const id = record[this.primaryKey];
496
+ const dataJson = JSON.stringify(record);
497
+ this.db.query(`INSERT INTO ${this.tableName} (${this.primaryKey}, data) VALUES (?, ?)`).run(id, dataJson);
498
+ // Execute afterCreate hook
499
+ if (this.descriptor.afterCreate) {
500
+ await this.descriptor.afterCreate(record);
501
+ }
502
+ this.emitLifecycle('afterCreate', record);
503
+ return record;
504
+ }
505
+ async createMany(data, options) {
506
+ const results = [];
507
+ const batchSize = options?.batchSize || 100;
508
+ for (let i = 0; i < data.length; i += batchSize) {
509
+ const batch = data.slice(i, i + batchSize);
510
+ const created = await Promise.all(batch.map((item) => this.create(item)));
511
+ results.push(...created);
512
+ }
513
+ return results;
514
+ }
515
+ // READ
516
+ async get(id, options) {
517
+ await this.ensureInitialized();
518
+ const row = this.db.query(`SELECT data FROM ${this.tableName} WHERE ${this.primaryKey} = ?`).get(id);
519
+ if (!row)
520
+ return null;
521
+ const record = JSON.parse(row.data);
522
+ // Load relations if requested
523
+ if (options?.include) {
524
+ await this.loadRelations(record, options.include);
525
+ }
526
+ // Validate on read if configured
527
+ if (this.descriptor.validateOnRead) {
528
+ const validation = await this.validate(record);
529
+ if (!validation.success) {
530
+ logger.warn(`[StorageRecord] Validation failed on read:`, toErrorContext(validation.errors));
531
+ }
532
+ }
533
+ return record;
534
+ }
535
+ async getOrThrow(id, options) {
536
+ const record = await this.get(id, options);
537
+ if (!record) {
538
+ throw new Error(`Record with ${this.primaryKey}=${id} not found in ${this.tableName}`);
539
+ }
540
+ return record;
541
+ }
542
+ async getMany(ids, options) {
543
+ await this.ensureInitialized();
544
+ if (ids.length === 0)
545
+ return [];
546
+ const placeholders = ids.map(() => '?').join(', ');
547
+ const rows = this.db.query(`SELECT data FROM ${this.tableName} WHERE ${this.primaryKey} IN (${placeholders})`).all(...ids);
548
+ let records = rows.map((row) => JSON.parse(row.data));
549
+ // Load relations if requested
550
+ if (options?.include) {
551
+ records = await Promise.all(records.map((record) => this.loadRelations(record, options.include)));
552
+ }
553
+ return records;
554
+ }
555
+ async list(options) {
556
+ await this.ensureInitialized();
557
+ // Build query
558
+ let sql = `SELECT data FROM ${this.tableName}`;
559
+ const params = [];
560
+ // WHERE clause
561
+ let hasWhereConditions = false;
562
+ if (options?.where) {
563
+ const { clause, values } = this.buildWhereClause(options.where);
564
+ if (clause) {
565
+ sql += ` WHERE ${clause}`;
566
+ params.push(...values);
567
+ hasWhereConditions = true;
568
+ }
569
+ }
570
+ // Soft delete filter
571
+ if (this.descriptor.softDelete && !options?.includeDeleted) {
572
+ const deletedAtField = typeof this.descriptor.softDelete === 'string' ? this.descriptor.softDelete : 'deletedAt';
573
+ sql += hasWhereConditions ? ` AND ` : ` WHERE `;
574
+ sql += `json_extract(data, '$.${deletedAtField}') IS NULL`;
575
+ }
576
+ // ORDER BY
577
+ if (options?.orderBy) {
578
+ sql += ` ORDER BY ${this.buildOrderBy(options.orderBy)}`;
579
+ }
580
+ // COUNT query for total
581
+ const countSql = sql.replace('SELECT data', 'SELECT COUNT(*) as count');
582
+ const countRow = this.db.query(countSql).get(...params);
583
+ const total = countRow?.count || 0;
584
+ // LIMIT and OFFSET
585
+ if (options?.limit) {
586
+ sql += ` LIMIT ?`;
587
+ params.push(options.limit);
588
+ }
589
+ if (options?.offset) {
590
+ sql += ` OFFSET ?`;
591
+ params.push(options.offset);
592
+ }
593
+ // Execute query
594
+ const rows = this.db.query(sql).all(...params);
595
+ let items = rows.map((row) => JSON.parse(row.data));
596
+ // Load relations if requested
597
+ if (options?.include) {
598
+ items = await Promise.all(items.map((item) => this.loadRelations(item, options.include)));
599
+ }
600
+ return {
601
+ items,
602
+ total,
603
+ hasMore: options?.limit ? items.length === options.limit : false
604
+ };
605
+ }
606
+ async findFirst(options) {
607
+ const result = await this.list({ ...options, limit: 1 });
608
+ return result.items[0] || null;
609
+ }
610
+ async findFirstOrThrow(options) {
611
+ const record = await this.findFirst(options);
612
+ if (!record) {
613
+ throw new Error(`No record found in ${this.tableName} matching criteria`);
614
+ }
615
+ return record;
616
+ }
617
+ async exists(id) {
618
+ await this.ensureInitialized();
619
+ const row = this.db.query(`SELECT COUNT(*) as count FROM ${this.tableName} WHERE ${this.primaryKey} = ?`).get(id);
620
+ return (row?.count || 0) > 0;
621
+ }
622
+ async count(where) {
623
+ await this.ensureInitialized();
624
+ let sql = `SELECT COUNT(*) as count FROM ${this.tableName}`;
625
+ const params = [];
626
+ let hasWhereConditions = false;
627
+ if (where) {
628
+ const { clause, values } = this.buildWhereClause(where);
629
+ if (clause) {
630
+ sql += ` WHERE ${clause}`;
631
+ params.push(...values);
632
+ hasWhereConditions = true;
633
+ }
634
+ }
635
+ // Soft delete filter
636
+ if (this.descriptor.softDelete) {
637
+ const deletedAtField = typeof this.descriptor.softDelete === 'string' ? this.descriptor.softDelete : 'deletedAt';
638
+ sql += hasWhereConditions ? ` AND ` : ` WHERE `;
639
+ sql += `json_extract(data, '$.${deletedAtField}') IS NULL`;
640
+ }
641
+ const row = this.db.query(sql).get(...params);
642
+ return row?.count || 0;
643
+ }
644
+ // UPDATE
645
+ async update(id, data, _options) {
646
+ await this.ensureInitialized();
647
+ // Get existing record
648
+ const existing = await this.get(id);
649
+ if (!existing) {
650
+ throw new Error(`Record with ${this.primaryKey}=${id} not found in ${this.tableName}`);
651
+ }
652
+ // Merge updates
653
+ const updated = { ...existing, ...data };
654
+ // Auto-manage timestamps
655
+ if (this.descriptor.timestamps) {
656
+ applyUpdateTimestamp(updated, data, this.descriptor.timestamps, new Date().toISOString());
657
+ }
658
+ // Validate
659
+ if (this.descriptor.validateOnWrite !== false) {
660
+ const validation = await this.validate(updated);
661
+ if (!validation.success) {
662
+ throw new Error(`Validation failed: ${validation.errors?.message}`);
663
+ }
664
+ }
665
+ // Execute beforeUpdate hook
666
+ if (this.descriptor.beforeUpdate) {
667
+ await this.descriptor.beforeUpdate(updated);
668
+ }
669
+ // Update in database
670
+ const dataJson = JSON.stringify(updated);
671
+ this.db.query(`UPDATE ${this.tableName} SET data = ? WHERE ${this.primaryKey} = ?`).run(dataJson, id);
672
+ // Execute afterUpdate hook
673
+ if (this.descriptor.afterUpdate) {
674
+ await this.descriptor.afterUpdate(updated);
675
+ }
676
+ this.emitLifecycle('afterUpdate', updated);
677
+ return updated;
678
+ }
679
+ async updateMany(where, data, options) {
680
+ await this.ensureInitialized();
681
+ // Find matching records
682
+ const result = await this.list({ where, select: [this.primaryKey] });
683
+ const ids = result.items.map((item) => this.readPrimaryKey(item));
684
+ // Update each record
685
+ let count = 0;
686
+ for (const id of ids) {
687
+ try {
688
+ await this.update(id, data, options);
689
+ count++;
690
+ }
691
+ catch (error) {
692
+ logger.error(`[StorageRecord] Failed to update record ${id}:`, toErrorContext(error));
693
+ }
694
+ }
695
+ return { count, success: count > 0 };
696
+ }
697
+ async upsert(id, create, update) {
698
+ const existing = await this.get(id);
699
+ if (existing) {
700
+ return this.update(id, update);
701
+ }
702
+ else {
703
+ return this.create({ ...create, [this.primaryKey]: id });
704
+ }
705
+ }
706
+ // DELETE
707
+ async delete(id, options) {
708
+ await this.ensureInitialized();
709
+ // Check if exists
710
+ let existing = await this.get(id, { include: undefined });
711
+ if (!existing && this.descriptor.softDelete && options?.force) {
712
+ const row = this.db.query(`SELECT data FROM ${this.tableName} WHERE ${this.primaryKey} = ?`).get(id);
713
+ existing = row ? JSON.parse(row.data) : null;
714
+ }
715
+ if (!existing) {
716
+ return { count: 0, success: false };
717
+ }
718
+ // Execute beforeDelete hook
719
+ if (this.descriptor.beforeDelete) {
720
+ await this.descriptor.beforeDelete(existing);
721
+ }
722
+ // Soft delete or hard delete
723
+ if (this.descriptor.softDelete && !options?.force) {
724
+ const deletedAtField = typeof this.descriptor.softDelete === 'string' ? this.descriptor.softDelete : 'deletedAt';
725
+ const deleted = {
726
+ ...existing,
727
+ [deletedAtField]: new Date().toISOString()
728
+ };
729
+ const dataJson = JSON.stringify(deleted);
730
+ this.db.query(`UPDATE ${this.tableName} SET data = ? WHERE ${this.primaryKey} = ?`).run(dataJson, id);
731
+ }
732
+ else {
733
+ this.db.query(`DELETE FROM ${this.tableName} WHERE ${this.primaryKey} = ?`).run(id);
734
+ }
735
+ // Execute afterDelete hook
736
+ if (this.descriptor.afterDelete) {
737
+ await this.descriptor.afterDelete(existing);
738
+ }
739
+ this.emitLifecycle('afterDelete', existing);
740
+ return { count: 1, success: true };
741
+ }
742
+ async deleteMany(where, options) {
743
+ await this.ensureInitialized();
744
+ // Find matching records
745
+ const result = await this.list({ where, select: [this.primaryKey] });
746
+ const ids = result.items.map((item) => this.readPrimaryKey(item));
747
+ // Delete each record
748
+ let count = 0;
749
+ for (const id of ids) {
750
+ try {
751
+ const deleteResult = await this.delete(id, options);
752
+ count += deleteResult.count;
753
+ }
754
+ catch (error) {
755
+ logger.error(`[StorageRecord] Failed to delete record ${id}:`, toErrorContext(error));
756
+ }
757
+ }
758
+ return { count, success: count > 0 };
759
+ }
760
+ async restore(id) {
761
+ await this.ensureInitialized();
762
+ if (!this.descriptor.softDelete) {
763
+ throw new Error(`Soft delete is not enabled for ${this.tableName}`);
764
+ }
765
+ const deletedAtField = typeof this.descriptor.softDelete === 'string' ? this.descriptor.softDelete : 'deletedAt';
766
+ // Get record (including soft-deleted)
767
+ const row = this.db.query(`SELECT data FROM ${this.tableName} WHERE ${this.primaryKey} = ?`).get(id);
768
+ if (!row) {
769
+ throw new Error(`Record with ${this.primaryKey}=${id} not found in ${this.tableName}`);
770
+ }
771
+ const record = JSON.parse(row.data);
772
+ if (!record[deletedAtField]) {
773
+ throw new Error(`Record ${id} is not soft-deleted`);
774
+ }
775
+ // Restore by clearing deletedAt
776
+ delete record[deletedAtField];
777
+ // Update timestamp
778
+ if (this.descriptor.timestamps) {
779
+ const updatedAtField = typeof this.descriptor.timestamps === 'object' &&
780
+ typeof this.descriptor.timestamps.updatedAt === 'string'
781
+ ? this.descriptor.timestamps.updatedAt
782
+ : 'updatedAt';
783
+ record[updatedAtField] = new Date().toISOString();
784
+ }
785
+ const dataJson = JSON.stringify(record);
786
+ this.db.query(`UPDATE ${this.tableName} SET data = ? WHERE ${this.primaryKey} = ?`).run(dataJson, id);
787
+ return record;
788
+ }
789
+ // AGGREGATION
790
+ async aggregate(options) {
791
+ await this.ensureInitialized();
792
+ const func = options.function;
793
+ const field = options.field;
794
+ let sql = `SELECT `;
795
+ // Aggregation function
796
+ if (func === 'count' && !field) {
797
+ sql += `COUNT(*) as value`;
798
+ }
799
+ else if (field) {
800
+ const fieldName = String(field);
801
+ switch (func) {
802
+ case 'count':
803
+ sql += `COUNT(${fieldName}) as value`;
804
+ break;
805
+ case 'sum':
806
+ sql += `SUM(CAST(json_extract(data, '$.${fieldName}') AS REAL)) as value`;
807
+ break;
808
+ case 'avg':
809
+ sql += `AVG(CAST(json_extract(data, '$.${fieldName}') AS REAL)) as value`;
810
+ break;
811
+ case 'min':
812
+ sql += `MIN(json_extract(data, '$.${fieldName}')) as value`;
813
+ break;
814
+ case 'max':
815
+ sql += `MAX(json_extract(data, '$.${fieldName}')) as value`;
816
+ break;
817
+ }
818
+ }
819
+ sql += ` FROM ${this.tableName}`;
820
+ // WHERE clause
821
+ const params = [];
822
+ if (options.where) {
823
+ const { clause, values } = this.buildWhereClause(options.where);
824
+ if (clause) {
825
+ sql += ` WHERE ${clause}`;
826
+ params.push(...values);
827
+ }
828
+ }
829
+ // GROUP BY
830
+ if (options.groupBy) {
831
+ const groupByFields = Array.isArray(options.groupBy) ? options.groupBy : [options.groupBy];
832
+ const groupByExpr = groupByFields
833
+ .map((f) => `json_extract(data, '$.${String(f)}')`)
834
+ .join(', ');
835
+ sql += ` GROUP BY ${groupByExpr}`;
836
+ }
837
+ const rows = this.db.query(sql).all(...params);
838
+ return rows.map((row) => ({
839
+ value: row.value || 0
840
+ }));
841
+ }
842
+ // TRANSACTIONS
843
+ async transaction(fn) {
844
+ await this.ensureInitialized();
845
+ if (!this.db) {
846
+ throw new Error('Database not initialized');
847
+ }
848
+ // Begin transaction
849
+ this.db.exec('BEGIN TRANSACTION');
850
+ const tx = {
851
+ id: `tx-${Date.now()}-${Math.random().toString(36).slice(2)}`,
852
+ startTime: Date.now(),
853
+ isActive: true
854
+ };
855
+ try {
856
+ const result = await fn(tx);
857
+ // Commit transaction
858
+ this.db.exec('COMMIT');
859
+ tx.isActive = false;
860
+ return result;
861
+ }
862
+ catch (error) {
863
+ // Rollback transaction
864
+ this.db.exec('ROLLBACK');
865
+ tx.isActive = false;
866
+ throw error;
867
+ }
868
+ }
869
+ // STREAMING
870
+ async *stream(options) {
871
+ await this.ensureInitialized();
872
+ const batchSize = options?.batchSize || 100;
873
+ let offset = options?.offset || 0;
874
+ while (true) {
875
+ // StreamOptions extends QueryOptions but adds batchSize/offset; both belong on QueryOptions
876
+ // for the list() call below, but `batchSize` is stream-only — strip it before passing along.
877
+ const { batchSize: _strip, ...listOptions } = (options ?? {});
878
+ const result = await this.list({
879
+ ...listOptions,
880
+ limit: batchSize,
881
+ offset
882
+ });
883
+ if (result.items.length === 0) {
884
+ break;
885
+ }
886
+ for (const item of result.items) {
887
+ yield item;
888
+ }
889
+ offset += batchSize;
890
+ if (!result.hasMore) {
891
+ break;
892
+ }
893
+ }
894
+ }
895
+ // BATCH OPERATIONS
896
+ batch(operations) {
897
+ return this.transaction(async () => {
898
+ const results = [];
899
+ for (const op of operations) {
900
+ let result;
901
+ switch (op.type) {
902
+ case 'create':
903
+ result = await this.create(op.data);
904
+ break;
905
+ case 'update':
906
+ if (!op.id)
907
+ throw new Error('ID required for update');
908
+ result = await this.update(op.id, op.data);
909
+ break;
910
+ case 'delete':
911
+ if (!op.id)
912
+ throw new Error('ID required for delete');
913
+ result = await this.delete(op.id);
914
+ break;
915
+ }
916
+ results.push(result);
917
+ }
918
+ return results;
919
+ });
920
+ }
921
+ // UTILITIES
922
+ async validate(data) {
923
+ try {
924
+ const result = this.schema.parse(data);
925
+ return { success: true, data: result };
926
+ }
927
+ catch (error) {
928
+ if (error instanceof Error && 'issues' in error) {
929
+ return { success: false, errors: error };
930
+ }
931
+ return { success: false, errors: undefined };
932
+ }
933
+ }
934
+ getAdapterId() {
935
+ if (this.adapterCache) {
936
+ return this.adapterCache.id;
937
+ }
938
+ return this.descriptor.defaultAdapter || this.descriptor.allowedAdapters[0] || '';
939
+ }
940
+ async close() {
941
+ // Database connection is managed by the adapter
942
+ this.initialized = false;
943
+ }
944
+ // PRIVATE HELPERS
945
+ async ensureInitialized() {
946
+ if (!this.initialized) {
947
+ await this.initialize();
948
+ }
949
+ }
950
+ generateId() {
951
+ const strategy = typeof this.descriptor.autoId === 'object' ? this.descriptor.autoId.strategy : 'uuid';
952
+ if (strategy === 'uuid') {
953
+ return crypto.randomUUID();
954
+ }
955
+ else if (strategy === 'nanoid') {
956
+ // Simple nanoid implementation
957
+ const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
958
+ let id = '';
959
+ for (let i = 0; i < 21; i++) {
960
+ id += alphabet[Math.floor(Math.random() * alphabet.length)];
961
+ }
962
+ return id;
963
+ }
964
+ else if (strategy === 'custom' && typeof this.descriptor.autoId === 'object') {
965
+ if (this.descriptor.autoId.generator) {
966
+ return this.descriptor.autoId.generator();
967
+ }
968
+ }
969
+ return crypto.randomUUID();
970
+ }
971
+ buildWhereClause(where) {
972
+ const conditions = [];
973
+ const values = [];
974
+ for (const [field, condition] of Object.entries(where)) {
975
+ if (typeof condition === 'object' && condition !== null) {
976
+ // Complex condition
977
+ const cond = condition;
978
+ if ('eq' in cond) {
979
+ conditions.push(`json_extract(data, '$.${field}') = ?`);
980
+ values.push(cond.eq);
981
+ }
982
+ if ('ne' in cond) {
983
+ conditions.push(`json_extract(data, '$.${field}') != ?`);
984
+ values.push(cond.ne);
985
+ }
986
+ if ('gt' in cond) {
987
+ conditions.push(`json_extract(data, '$.${field}') > ?`);
988
+ values.push(cond.gt);
989
+ }
990
+ if ('gte' in cond) {
991
+ conditions.push(`json_extract(data, '$.${field}') >= ?`);
992
+ values.push(cond.gte);
993
+ }
994
+ if ('lt' in cond) {
995
+ conditions.push(`json_extract(data, '$.${field}') < ?`);
996
+ values.push(cond.lt);
997
+ }
998
+ if ('lte' in cond) {
999
+ conditions.push(`json_extract(data, '$.${field}') <= ?`);
1000
+ values.push(cond.lte);
1001
+ }
1002
+ if ('in' in cond && Array.isArray(cond.in)) {
1003
+ const placeholders = cond.in.map(() => '?').join(', ');
1004
+ conditions.push(`json_extract(data, '$.${field}') IN (${placeholders})`);
1005
+ values.push(...cond.in);
1006
+ }
1007
+ if ('like' in cond) {
1008
+ conditions.push(`json_extract(data, '$.${field}') LIKE ?`);
1009
+ values.push(cond.like);
1010
+ }
1011
+ if ('isNull' in cond) {
1012
+ if (cond.isNull) {
1013
+ conditions.push(`json_extract(data, '$.${field}') IS NULL`);
1014
+ }
1015
+ else {
1016
+ conditions.push(`json_extract(data, '$.${field}') IS NOT NULL`);
1017
+ }
1018
+ }
1019
+ }
1020
+ else {
1021
+ // Simple equality
1022
+ conditions.push(`json_extract(data, '$.${field}') = ?`);
1023
+ values.push(condition);
1024
+ }
1025
+ }
1026
+ return {
1027
+ clause: conditions.join(' AND '),
1028
+ values
1029
+ };
1030
+ }
1031
+ buildOrderBy(orderBy) {
1032
+ if (Array.isArray(orderBy)) {
1033
+ return orderBy
1034
+ .map((o) => `json_extract(data, '$.${String(o.field)}') ${o.direction}`)
1035
+ .join(', ');
1036
+ }
1037
+ else {
1038
+ const fields = Object.entries(orderBy).map(([field, direction]) => {
1039
+ return `json_extract(data, '$.${field}') ${direction}`;
1040
+ });
1041
+ return fields.join(', ');
1042
+ }
1043
+ }
1044
+ async loadRelations(record, include) {
1045
+ if (!include || Object.keys(this.relations).length === 0) {
1046
+ return record;
1047
+ }
1048
+ const relationNames = Array.isArray(include) ? include : Object.keys(include);
1049
+ for (const relationName of relationNames) {
1050
+ const relation = this.relations[relationName];
1051
+ if (!relation)
1052
+ continue;
1053
+ const related = await this.loadRelation(record, relation);
1054
+ this.asMutableRecord(record)[relationName] = related;
1055
+ }
1056
+ return record;
1057
+ }
1058
+ async loadRelation(record, relation) {
1059
+ // For now, we'll implement basic relation loading
1060
+ // This would be enhanced with proper relation resolution
1061
+ switch (relation.type) {
1062
+ case 'belongs_to': {
1063
+ const foreignKey = relation.foreignKey;
1064
+ if (!foreignKey)
1065
+ return null;
1066
+ const foreignId = this.readStringField(record, foreignKey);
1067
+ if (!foreignId)
1068
+ return null;
1069
+ // Resolving belongs_to requires runtime access to the target storage; unimplemented.
1070
+ return null;
1071
+ }
1072
+ case 'has_many': {
1073
+ // Resolving has_many requires runtime access to the target storage; unimplemented.
1074
+ return [];
1075
+ }
1076
+ case 'has_one': {
1077
+ // Resolving has_one requires runtime access to the target storage; unimplemented.
1078
+ return null;
1079
+ }
1080
+ case 'many_to_many': {
1081
+ // Implement many-to-many through junction table
1082
+ return await this.loadManyToManyRelation(record, relation);
1083
+ }
1084
+ default:
1085
+ return null;
1086
+ }
1087
+ }
1088
+ /**
1089
+ * Load many-to-many relation through junction table
1090
+ */
1091
+ async loadManyToManyRelation(record, relation) {
1092
+ if (!relation.through) {
1093
+ logger.warn(`Many-to-many relation '${relation.as || 'unknown'}' has no 'through' property`);
1094
+ return [];
1095
+ }
1096
+ try {
1097
+ const runtime = getVibesRuntime();
1098
+ if (!runtime) {
1099
+ logger.warn('Runtime not available for relation loading');
1100
+ return [];
1101
+ }
1102
+ // Resolve the junction table storage interface
1103
+ const junctionStorage = await runtime
1104
+ .query('storage/record')
1105
+ .withId(relation.through)
1106
+ .enabled()
1107
+ .resolve()
1108
+ .catch((error) => {
1109
+ logger.warn(`Failed to resolve junction table '${relation.through}':`, toErrorContext(error));
1110
+ return null;
1111
+ });
1112
+ if (!junctionStorage) {
1113
+ return [];
1114
+ }
1115
+ // Resolve the target table storage interface
1116
+ const targetStorage = await runtime
1117
+ .query('storage/record')
1118
+ .withId(relation.target)
1119
+ .enabled()
1120
+ .resolve()
1121
+ .catch((error) => {
1122
+ logger.warn(`Failed to resolve target table '${relation.target}':`, toErrorContext(error));
1123
+ return null;
1124
+ });
1125
+ if (!targetStorage) {
1126
+ return [];
1127
+ }
1128
+ // Get the primary key for this record
1129
+ const thisPrimaryKey = this.readStringField(record, this.primaryKey);
1130
+ if (!thisPrimaryKey) {
1131
+ return [];
1132
+ }
1133
+ const junctionTable = this.quoteIdentifier(junctionStorage.tableName);
1134
+ const thisFkColumn = await this.findJunctionForeignKeyColumn(junctionStorage, thisPrimaryKey, junctionTable);
1135
+ if (!thisFkColumn) {
1136
+ return [];
1137
+ }
1138
+ // Query junction rows by this record id
1139
+ const junctionResults = await junctionStorage.raw(`SELECT * FROM ${junctionTable} WHERE ${this.quoteIdentifier(thisFkColumn)} = ?`, thisPrimaryKey);
1140
+ if (junctionResults.length === 0) {
1141
+ return [];
1142
+ }
1143
+ // Extract target foreign keys from junction results
1144
+ const targetIds = this.extractJunctionTargetIds(junctionResults, thisFkColumn, targetStorage.primaryKey);
1145
+ if (targetIds.length === 0) {
1146
+ return [];
1147
+ }
1148
+ return await targetStorage.getMany(targetIds);
1149
+ }
1150
+ catch (error) {
1151
+ logger.error(`Error loading many-to-many relation: ${error}`);
1152
+ return [];
1153
+ }
1154
+ }
1155
+ /**
1156
+ * Find a junction table column that stores this record's primary key.
1157
+ */
1158
+ async findJunctionForeignKeyColumn(junctionStorage, recordId, quotedJunctionTable) {
1159
+ let tableInfo;
1160
+ try {
1161
+ tableInfo = await junctionStorage.raw(`PRAGMA table_info(${quotedJunctionTable})`);
1162
+ }
1163
+ catch {
1164
+ return null;
1165
+ }
1166
+ const candidateColumns = tableInfo
1167
+ .map((column) => column.name)
1168
+ .filter((name) => this.isLikelyIdColumn(name))
1169
+ .filter((name) => name !== 'id' && name !== junctionStorage.primaryKey)
1170
+ .filter((name) => name !== 'data');
1171
+ for (const column of candidateColumns) {
1172
+ const rows = await junctionStorage.raw(`SELECT COUNT(*) as count FROM ${quotedJunctionTable} WHERE ${this.quoteIdentifier(column)} = ?`, recordId);
1173
+ if ((rows[0]?.count ?? 0) > 0) {
1174
+ return column;
1175
+ }
1176
+ }
1177
+ return null;
1178
+ }
1179
+ /**
1180
+ * Extract target IDs from junction rows.
1181
+ */
1182
+ extractJunctionTargetIds(junctionRows, thisFkColumn, targetPrimaryKey) {
1183
+ const targetIds = new Set();
1184
+ for (const row of junctionRows) {
1185
+ for (const [field, value] of Object.entries(row)) {
1186
+ if (field === thisFkColumn || field === targetPrimaryKey) {
1187
+ continue;
1188
+ }
1189
+ if (!this.isLikelyIdColumn(field) || typeof value !== 'string') {
1190
+ continue;
1191
+ }
1192
+ if (value.length > 0) {
1193
+ targetIds.add(value);
1194
+ }
1195
+ }
1196
+ }
1197
+ return [...targetIds];
1198
+ }
1199
+ /**
1200
+ * True when a field name is likely an identifier / foreign key field.
1201
+ */
1202
+ isLikelyIdColumn(name) {
1203
+ const lower = name.toLowerCase();
1204
+ return (lower === 'id' ||
1205
+ lower === 'data' ||
1206
+ /(^|[._])id$/i.test(name) ||
1207
+ /_id$/.test(lower) ||
1208
+ /Id$/.test(name));
1209
+ }
1210
+ /**
1211
+ * Escape SQL identifier safely for query fragments.
1212
+ */
1213
+ quoteIdentifier(name) {
1214
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1215
+ throw new Error(`Invalid SQL identifier: ${name}`);
1216
+ }
1217
+ return `"${name}"`;
1218
+ }
1219
+ // FULL-TEXT SEARCH
1220
+ async search(query, options) {
1221
+ await this.ensureInitialized();
1222
+ if (!this.descriptor.fts) {
1223
+ throw new Error(`FTS is not configured for ${this.tableName}`);
1224
+ }
1225
+ const ftsTable = this.descriptor.fts.tableName || `${this.tableName}_fts`;
1226
+ const ftsFields = this.descriptor.fts.fields;
1227
+ // Split into words and AND them for multi-word queries.
1228
+ // Each word is quoted to escape FTS5 special characters.
1229
+ const words = query.split(/\s+/).filter((w) => w.length > 0);
1230
+ const ftsQuery = words.length <= 1
1231
+ ? `"${query.replace(/"/g, '""')}"`
1232
+ : words.map((w) => `"${w.replace(/"/g, '""')}"`).join(' AND ');
1233
+ const params = [ftsQuery];
1234
+ let sql = `
1235
+ SELECT
1236
+ ${this.tableName}.${this.primaryKey},
1237
+ ${this.tableName}.data,
1238
+ ${ftsTable}.rank as _rank
1239
+ FROM ${this.tableName}
1240
+ JOIN ${ftsTable} ON ${ftsTable}.rowid = ${this.tableName}.rowid
1241
+ WHERE ${ftsTable} MATCH ?
1242
+ `;
1243
+ if (options?.where) {
1244
+ const { clause, values } = this.buildWhereClause(options.where);
1245
+ if (clause) {
1246
+ sql += ` AND ${clause}`;
1247
+ params.push(...values);
1248
+ }
1249
+ }
1250
+ if (options?.orderBy) {
1251
+ sql += ` ORDER BY ${this.buildOrderBy(options.orderBy)}`;
1252
+ }
1253
+ else if (options?.rankByRelevance !== false) {
1254
+ sql += ` ORDER BY _rank`;
1255
+ }
1256
+ if (options?.limit) {
1257
+ sql += ` LIMIT ?`;
1258
+ params.push(options.limit);
1259
+ }
1260
+ if (options?.offset) {
1261
+ sql += ` OFFSET ?`;
1262
+ params.push(options.offset);
1263
+ }
1264
+ const rows = this.db.query(sql).all(...params);
1265
+ const items = [];
1266
+ const scores = [];
1267
+ const highlights = new Map();
1268
+ for (const row of rows) {
1269
+ const record = JSON.parse(row.data);
1270
+ items.push(record);
1271
+ scores.push(row._rank);
1272
+ if (options?.highlight) {
1273
+ const prefix = options.highlightPrefix || '<mark>';
1274
+ const suffix = options.highlightSuffix || '</mark>';
1275
+ const id = this.readPrimaryKey(record);
1276
+ const fieldHighlights = [];
1277
+ for (const field of ftsFields) {
1278
+ const highlightSql = `
1279
+ SELECT highlight(${ftsTable}, ?, ?, ?) as highlighted
1280
+ WHERE ${ftsTable} MATCH ? AND rowid = ?
1281
+ `;
1282
+ const fieldIndex = ftsFields.indexOf(field);
1283
+ const highlightRow = this.db.query(highlightSql).get(fieldIndex, prefix, suffix, ftsQuery, rows.indexOf(row) + 1);
1284
+ if (highlightRow?.highlighted) {
1285
+ fieldHighlights.push(highlightRow.highlighted);
1286
+ }
1287
+ }
1288
+ highlights.set(id, fieldHighlights);
1289
+ }
1290
+ if (options?.minScore !== undefined && row._rank < options.minScore) {
1291
+ items.pop();
1292
+ scores.pop();
1293
+ }
1294
+ }
1295
+ return {
1296
+ items,
1297
+ total: items.length,
1298
+ scores,
1299
+ highlights: highlights.size > 0 ? highlights : undefined,
1300
+ hasMore: options?.limit ? items.length === options.limit : false
1301
+ };
1302
+ }
1303
+ // HIERARCHY
1304
+ async getDescendants(id, options) {
1305
+ await this.ensureInitialized();
1306
+ if (!this.descriptor.hierarchy) {
1307
+ throw new Error(`Hierarchy is not configured for ${this.tableName}`);
1308
+ }
1309
+ const parentField = this.descriptor.hierarchy.parentField;
1310
+ const maxDepth = options?.maxDepth || 1000;
1311
+ const includePath = options?.includePath !== false;
1312
+ const sql = `
1313
+ WITH RECURSIVE hierarchy AS (
1314
+ SELECT
1315
+ *,
1316
+ 0 as depth
1317
+ ${includePath ? `, json_array(id) as path` : ''}
1318
+ FROM ${this.tableName}
1319
+ WHERE ${this.primaryKey} = ?
1320
+
1321
+ UNION ALL
1322
+
1323
+ SELECT
1324
+ t.*,
1325
+ h.depth + 1 as depth
1326
+ ${includePath ? `, CASE WHEN h.depth = 0 THEN json_array(t.id) ELSE json_insert(h.path, '$[#]', t.id) END as path` : ''}
1327
+ FROM ${this.tableName} t
1328
+ INNER JOIN hierarchy h ON json_extract(t.data, '$.${parentField}') = h.${this.primaryKey}
1329
+ WHERE h.depth < ?
1330
+ )
1331
+ SELECT * FROM hierarchy ORDER BY depth, ${this.primaryKey}
1332
+ `;
1333
+ const rows = this.db.query(sql).all(id, maxDepth);
1334
+ const items = rows.map((row) => {
1335
+ const record = JSON.parse(row.data);
1336
+ record.depth = row.depth;
1337
+ if (includePath && row.path) {
1338
+ record.path = JSON.parse(row.path);
1339
+ }
1340
+ return record;
1341
+ });
1342
+ return {
1343
+ items,
1344
+ total: items.length,
1345
+ hasMore: false
1346
+ };
1347
+ }
1348
+ async getAncestors(id, _options) {
1349
+ await this.ensureInitialized();
1350
+ if (!this.descriptor.hierarchy) {
1351
+ throw new Error(`Hierarchy is not configured for ${this.tableName}`);
1352
+ }
1353
+ const parentField = this.descriptor.hierarchy.parentField;
1354
+ const sql = `
1355
+ WITH RECURSIVE ancestors AS (
1356
+ SELECT *, 0 as depth FROM ${this.tableName} WHERE ${this.primaryKey} = ?
1357
+
1358
+ UNION ALL
1359
+
1360
+ SELECT t.*, a.depth + 1 as depth
1361
+ FROM ${this.tableName} t
1362
+ INNER JOIN ancestors a ON json_extract(a.data, '$.${parentField}') = t.${this.primaryKey}
1363
+ )
1364
+ SELECT * FROM ancestors ORDER BY depth DESC
1365
+ `;
1366
+ const rows = this.db.query(sql).all(id);
1367
+ return rows.map((row) => JSON.parse(row.data));
1368
+ }
1369
+ async getTree(options) {
1370
+ await this.ensureInitialized();
1371
+ if (!this.descriptor.hierarchy) {
1372
+ throw new Error(`Hierarchy is not configured for ${this.tableName}`);
1373
+ }
1374
+ const parentField = this.descriptor.hierarchy.parentField;
1375
+ const maxDepth = options?.maxDepth || 1000;
1376
+ const whereClause = options?.where
1377
+ ? this.buildWhereClause(options.where)
1378
+ : { clause: `json_extract(data, '$.${parentField}') IS NULL`, values: [] };
1379
+ const sql = `
1380
+ WITH RECURSIVE tree AS (
1381
+ SELECT
1382
+ *,
1383
+ 0 as depth,
1384
+ json_array(id) as path
1385
+ FROM ${this.tableName}
1386
+ WHERE ${whereClause.clause}
1387
+
1388
+ UNION ALL
1389
+
1390
+ SELECT
1391
+ t.*,
1392
+ tree.depth + 1 as depth,
1393
+ json_insert(tree.path, '$[#]', t.id) as path
1394
+ FROM ${this.tableName} t
1395
+ INNER JOIN tree ON json_extract(t.data, '$.${parentField}') = tree.${this.primaryKey}
1396
+ WHERE tree.depth < ?
1397
+ )
1398
+ SELECT * FROM tree ORDER BY depth, ${this.primaryKey}
1399
+ `;
1400
+ const rows = this.db.query(sql).all(...whereClause.values, maxDepth);
1401
+ const nodeMap = new Map();
1402
+ const rootNodes = [];
1403
+ for (const row of rows) {
1404
+ const record = JSON.parse(row.data);
1405
+ const recordId = this.readPrimaryKey(record);
1406
+ const parentId = this.readStringField(record, parentField);
1407
+ const node = {
1408
+ record,
1409
+ children: [],
1410
+ depth: row.depth,
1411
+ path: JSON.parse(row.path)
1412
+ };
1413
+ nodeMap.set(recordId, node);
1414
+ if (parentId && nodeMap.has(parentId)) {
1415
+ nodeMap.get(parentId).children.push(node);
1416
+ }
1417
+ else {
1418
+ rootNodes.push(node);
1419
+ }
1420
+ }
1421
+ return rootNodes;
1422
+ }
1423
+ // RAW QUERIES
1424
+ async raw(sql, ...params) {
1425
+ await this.ensureInitialized();
1426
+ return this.db.query(sql).all(...params);
1427
+ }
1428
+ async queryRaw(options) {
1429
+ await this.ensureInitialized();
1430
+ const rows = this.db.query(options.sql).all(...(options.params || []));
1431
+ if (options.schema) {
1432
+ return rows.map((row) => options.schema.parse(row));
1433
+ }
1434
+ return rows;
1435
+ }
1436
+ }
1437
+ //# sourceMappingURL=base.js.map