@kelpie/server 0.9.0 → 0.11.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 (449) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +2 -0
  3. package/dist/app.js.map +1 -1
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +1 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/lib/actor.d.ts +3 -1
  9. package/dist/lib/actor.d.ts.map +1 -1
  10. package/dist/lib/actor.js.map +1 -1
  11. package/dist/lib/apiKeyScopes.d.ts +8 -0
  12. package/dist/lib/apiKeyScopes.d.ts.map +1 -0
  13. package/dist/lib/apiKeyScopes.js +166 -0
  14. package/dist/lib/apiKeyScopes.js.map +1 -0
  15. package/dist/modules/activities/repository.d.ts +2 -0
  16. package/dist/modules/activities/repository.d.ts.map +1 -1
  17. package/dist/modules/activities/repository.js +9 -0
  18. package/dist/modules/activities/repository.js.map +1 -1
  19. package/dist/modules/agent-tasks/repository.d.ts +2 -0
  20. package/dist/modules/agent-tasks/repository.d.ts.map +1 -1
  21. package/dist/modules/agent-tasks/repository.js +9 -0
  22. package/dist/modules/agent-tasks/repository.js.map +1 -1
  23. package/dist/modules/api-keys/routes.d.ts.map +1 -1
  24. package/dist/modules/api-keys/routes.js +4 -1
  25. package/dist/modules/api-keys/routes.js.map +1 -1
  26. package/dist/modules/api-keys/schema.d.ts +43 -0
  27. package/dist/modules/api-keys/schema.d.ts.map +1 -1
  28. package/dist/modules/api-keys/schema.js +2 -0
  29. package/dist/modules/api-keys/schema.js.map +1 -1
  30. package/dist/modules/api-keys/scopeMiddleware.d.ts +8 -0
  31. package/dist/modules/api-keys/scopeMiddleware.d.ts.map +1 -0
  32. package/dist/modules/api-keys/scopeMiddleware.js +21 -0
  33. package/dist/modules/api-keys/scopeMiddleware.js.map +1 -0
  34. package/dist/modules/api-keys/service.d.ts +3 -1
  35. package/dist/modules/api-keys/service.d.ts.map +1 -1
  36. package/dist/modules/api-keys/service.js +26 -1
  37. package/dist/modules/api-keys/service.js.map +1 -1
  38. package/dist/modules/auth/credentials.d.ts.map +1 -1
  39. package/dist/modules/auth/credentials.js +2 -0
  40. package/dist/modules/auth/credentials.js.map +1 -1
  41. package/dist/modules/consent-purposes/index.d.ts +1 -1
  42. package/dist/modules/consent-purposes/index.js +1 -1
  43. package/dist/modules/consent-purposes/schema.d.ts +3 -3
  44. package/dist/modules/consent-purposes/schema.js +3 -3
  45. package/dist/modules/consent-purposes/service.d.ts +1 -1
  46. package/dist/modules/consent-purposes/service.d.ts.map +1 -1
  47. package/dist/modules/consent-purposes/service.js +7 -16
  48. package/dist/modules/consent-purposes/service.js.map +1 -1
  49. package/dist/modules/consent-purposes/starters.d.ts +1 -1
  50. package/dist/modules/consent-purposes/tools.js +1 -1
  51. package/dist/modules/consent-purposes/tools.js.map +1 -1
  52. package/dist/modules/conversions/clearPointers.d.ts +10 -0
  53. package/dist/modules/conversions/clearPointers.d.ts.map +1 -0
  54. package/dist/modules/conversions/clearPointers.js +40 -0
  55. package/dist/modules/conversions/clearPointers.js.map +1 -0
  56. package/dist/modules/conversions/factory.d.ts +5 -0
  57. package/dist/modules/conversions/factory.d.ts.map +1 -0
  58. package/dist/modules/conversions/factory.js +18 -0
  59. package/dist/modules/conversions/factory.js.map +1 -0
  60. package/dist/modules/conversions/fieldMap.d.ts +55 -0
  61. package/dist/modules/conversions/fieldMap.d.ts.map +1 -0
  62. package/dist/modules/conversions/fieldMap.js +398 -0
  63. package/dist/modules/conversions/fieldMap.js.map +1 -0
  64. package/dist/modules/conversions/index.d.ts +7 -0
  65. package/dist/modules/conversions/index.d.ts.map +1 -0
  66. package/dist/modules/conversions/index.js +6 -0
  67. package/dist/modules/conversions/index.js.map +1 -0
  68. package/dist/modules/conversions/mcp.d.ts +7 -0
  69. package/dist/modules/conversions/mcp.d.ts.map +1 -0
  70. package/dist/modules/conversions/mcp.js +47 -0
  71. package/dist/modules/conversions/mcp.js.map +1 -0
  72. package/dist/modules/conversions/repoint.d.ts +19 -0
  73. package/dist/modules/conversions/repoint.d.ts.map +1 -0
  74. package/dist/modules/conversions/repoint.js +51 -0
  75. package/dist/modules/conversions/repoint.js.map +1 -0
  76. package/dist/modules/conversions/routes.d.ts +26 -0
  77. package/dist/modules/conversions/routes.d.ts.map +1 -0
  78. package/dist/modules/conversions/routes.js +23 -0
  79. package/dist/modules/conversions/routes.js.map +1 -0
  80. package/dist/modules/conversions/service.d.ts +28 -0
  81. package/dist/modules/conversions/service.d.ts.map +1 -0
  82. package/dist/modules/conversions/service.js +170 -0
  83. package/dist/modules/conversions/service.js.map +1 -0
  84. package/dist/modules/conversions/wire.d.ts +4 -0
  85. package/dist/modules/conversions/wire.d.ts.map +1 -0
  86. package/dist/modules/conversions/wire.js +28 -0
  87. package/dist/modules/conversions/wire.js.map +1 -0
  88. package/dist/modules/deals/events.d.ts +9 -0
  89. package/dist/modules/deals/events.d.ts.map +1 -1
  90. package/dist/modules/deals/events.js +4 -0
  91. package/dist/modules/deals/events.js.map +1 -1
  92. package/dist/modules/deals/index.d.ts.map +1 -1
  93. package/dist/modules/deals/index.js +4 -2
  94. package/dist/modules/deals/index.js.map +1 -1
  95. package/dist/modules/deals/routes.d.ts +2 -0
  96. package/dist/modules/deals/routes.d.ts.map +1 -1
  97. package/dist/modules/deals/routes.js +8 -1
  98. package/dist/modules/deals/routes.js.map +1 -1
  99. package/dist/modules/deals/schema.d.ts +34 -0
  100. package/dist/modules/deals/schema.d.ts.map +1 -1
  101. package/dist/modules/deals/schema.js +7 -2
  102. package/dist/modules/deals/schema.js.map +1 -1
  103. package/dist/modules/deals/service.d.ts.map +1 -1
  104. package/dist/modules/deals/service.js +2 -0
  105. package/dist/modules/deals/service.js.map +1 -1
  106. package/dist/modules/deals/tools.d.ts +2 -1
  107. package/dist/modules/deals/tools.d.ts.map +1 -1
  108. package/dist/modules/deals/tools.js +3 -1
  109. package/dist/modules/deals/tools.js.map +1 -1
  110. package/dist/modules/decisions/repository.d.ts +2 -1
  111. package/dist/modules/decisions/repository.d.ts.map +1 -1
  112. package/dist/modules/decisions/repository.js +9 -1
  113. package/dist/modules/decisions/repository.js.map +1 -1
  114. package/dist/modules/enquiries/events.d.ts +6 -2
  115. package/dist/modules/enquiries/events.d.ts.map +1 -1
  116. package/dist/modules/enquiries/events.js +5 -1
  117. package/dist/modules/enquiries/events.js.map +1 -1
  118. package/dist/modules/enquiries/index.d.ts.map +1 -1
  119. package/dist/modules/enquiries/index.js +4 -2
  120. package/dist/modules/enquiries/index.js.map +1 -1
  121. package/dist/modules/enquiries/routes.d.ts +2 -0
  122. package/dist/modules/enquiries/routes.d.ts.map +1 -1
  123. package/dist/modules/enquiries/routes.js +9 -9
  124. package/dist/modules/enquiries/routes.js.map +1 -1
  125. package/dist/modules/enquiries/schema.d.ts +36 -1
  126. package/dist/modules/enquiries/schema.d.ts.map +1 -1
  127. package/dist/modules/enquiries/schema.js +9 -3
  128. package/dist/modules/enquiries/schema.js.map +1 -1
  129. package/dist/modules/enquiries/service.d.ts +0 -8
  130. package/dist/modules/enquiries/service.d.ts.map +1 -1
  131. package/dist/modules/enquiries/service.js +3 -95
  132. package/dist/modules/enquiries/service.js.map +1 -1
  133. package/dist/modules/enquiries/tools.d.ts +2 -1
  134. package/dist/modules/enquiries/tools.d.ts.map +1 -1
  135. package/dist/modules/enquiries/tools.js +4 -16
  136. package/dist/modules/enquiries/tools.js.map +1 -1
  137. package/dist/modules/forms/applyMappedFields.d.ts +20 -0
  138. package/dist/modules/forms/applyMappedFields.d.ts.map +1 -0
  139. package/dist/modules/forms/applyMappedFields.js +169 -0
  140. package/dist/modules/forms/applyMappedFields.js.map +1 -0
  141. package/dist/modules/forms/applySubmissionFields.d.ts +18 -0
  142. package/dist/modules/forms/applySubmissionFields.d.ts.map +1 -0
  143. package/dist/modules/forms/applySubmissionFields.js +230 -0
  144. package/dist/modules/forms/applySubmissionFields.js.map +1 -0
  145. package/dist/modules/forms/fields.d.ts +7 -2
  146. package/dist/modules/forms/fields.d.ts.map +1 -1
  147. package/dist/modules/forms/fields.js +17 -6
  148. package/dist/modules/forms/fields.js.map +1 -1
  149. package/dist/modules/forms/mapping.d.ts +8 -2
  150. package/dist/modules/forms/mapping.d.ts.map +1 -1
  151. package/dist/modules/forms/mapping.js +11 -0
  152. package/dist/modules/forms/mapping.js.map +1 -1
  153. package/dist/modules/forms/repository.d.ts +0 -5
  154. package/dist/modules/forms/repository.d.ts.map +1 -1
  155. package/dist/modules/forms/repository.js +0 -2
  156. package/dist/modules/forms/repository.js.map +1 -1
  157. package/dist/modules/forms/routes.d.ts +2 -30
  158. package/dist/modules/forms/routes.d.ts.map +1 -1
  159. package/dist/modules/forms/routes.js +2 -2
  160. package/dist/modules/forms/routes.js.map +1 -1
  161. package/dist/modules/forms/schema.d.ts.map +1 -1
  162. package/dist/modules/forms/schema.js +1 -2
  163. package/dist/modules/forms/schema.js.map +1 -1
  164. package/dist/modules/forms/service.d.ts.map +1 -1
  165. package/dist/modules/forms/service.js +18 -3
  166. package/dist/modules/forms/service.js.map +1 -1
  167. package/dist/modules/forms/starters.d.ts +31 -0
  168. package/dist/modules/forms/starters.d.ts.map +1 -0
  169. package/dist/modules/forms/starters.js +94 -0
  170. package/dist/modules/forms/starters.js.map +1 -0
  171. package/dist/modules/forms/submission.d.ts +1 -0
  172. package/dist/modules/forms/submission.d.ts.map +1 -1
  173. package/dist/modules/forms/submission.js +58 -37
  174. package/dist/modules/forms/submission.js.map +1 -1
  175. package/dist/modules/import-export/customFieldCsv.d.ts +9 -0
  176. package/dist/modules/import-export/customFieldCsv.d.ts.map +1 -0
  177. package/dist/modules/import-export/customFieldCsv.js +39 -0
  178. package/dist/modules/import-export/customFieldCsv.js.map +1 -0
  179. package/dist/modules/import-export/customFieldImport.d.ts +19 -0
  180. package/dist/modules/import-export/customFieldImport.d.ts.map +1 -0
  181. package/dist/modules/import-export/customFieldImport.js +76 -0
  182. package/dist/modules/import-export/customFieldImport.js.map +1 -0
  183. package/dist/modules/import-export/drafts.d.ts +57 -3
  184. package/dist/modules/import-export/drafts.d.ts.map +1 -1
  185. package/dist/modules/import-export/drafts.js +65 -3
  186. package/dist/modules/import-export/drafts.js.map +1 -1
  187. package/dist/modules/import-export/exportRows.d.ts +15 -13
  188. package/dist/modules/import-export/exportRows.d.ts.map +1 -1
  189. package/dist/modules/import-export/exportRows.js +99 -26
  190. package/dist/modules/import-export/exportRows.js.map +1 -1
  191. package/dist/modules/import-export/plan.d.ts +42 -11
  192. package/dist/modules/import-export/plan.d.ts.map +1 -1
  193. package/dist/modules/import-export/plan.js +198 -56
  194. package/dist/modules/import-export/plan.js.map +1 -1
  195. package/dist/modules/import-export/repository.d.ts +58 -2
  196. package/dist/modules/import-export/repository.d.ts.map +1 -1
  197. package/dist/modules/import-export/repository.js +201 -12
  198. package/dist/modules/import-export/repository.js.map +1 -1
  199. package/dist/modules/import-export/routes.d.ts.map +1 -1
  200. package/dist/modules/import-export/routes.js +5 -5
  201. package/dist/modules/import-export/routes.js.map +1 -1
  202. package/dist/modules/import-export/service.d.ts +8 -3
  203. package/dist/modules/import-export/service.d.ts.map +1 -1
  204. package/dist/modules/import-export/service.js +133 -40
  205. package/dist/modules/import-export/service.js.map +1 -1
  206. package/dist/modules/import-export/streams.d.ts +2 -2
  207. package/dist/modules/import-export/streams.d.ts.map +1 -1
  208. package/dist/modules/import-export/streams.js +50 -9
  209. package/dist/modules/import-export/streams.js.map +1 -1
  210. package/dist/modules/import-export/tools.d.ts.map +1 -1
  211. package/dist/modules/import-export/tools.js +13 -7
  212. package/dist/modules/import-export/tools.js.map +1 -1
  213. package/dist/modules/import-export/validation.d.ts +5 -1
  214. package/dist/modules/import-export/validation.d.ts.map +1 -1
  215. package/dist/modules/import-export/validation.js +89 -2
  216. package/dist/modules/import-export/validation.js.map +1 -1
  217. package/dist/modules/import-export/writes.d.ts +2 -1
  218. package/dist/modules/import-export/writes.d.ts.map +1 -1
  219. package/dist/modules/import-export/writes.js +358 -25
  220. package/dist/modules/import-export/writes.js.map +1 -1
  221. package/dist/modules/lists/index.d.ts +38 -0
  222. package/dist/modules/lists/index.d.ts.map +1 -1
  223. package/dist/modules/lists/index.js +27 -0
  224. package/dist/modules/lists/index.js.map +1 -1
  225. package/dist/modules/lists/repository.d.ts +15 -1
  226. package/dist/modules/lists/repository.d.ts.map +1 -1
  227. package/dist/modules/lists/repository.js +19 -1
  228. package/dist/modules/lists/repository.js.map +1 -1
  229. package/dist/modules/lists/routes.d.ts +0 -2
  230. package/dist/modules/lists/routes.d.ts.map +1 -1
  231. package/dist/modules/lists/routes.js +0 -8
  232. package/dist/modules/lists/routes.js.map +1 -1
  233. package/dist/modules/lists/schema.d.ts +0 -17
  234. package/dist/modules/lists/schema.d.ts.map +1 -1
  235. package/dist/modules/lists/schema.js +0 -10
  236. package/dist/modules/lists/schema.js.map +1 -1
  237. package/dist/modules/lists/service.d.ts +2 -14
  238. package/dist/modules/lists/service.d.ts.map +1 -1
  239. package/dist/modules/lists/service.js +7 -53
  240. package/dist/modules/lists/service.js.map +1 -1
  241. package/dist/modules/lists/starters.d.ts +12 -0
  242. package/dist/modules/lists/starters.d.ts.map +1 -0
  243. package/dist/modules/lists/starters.js +4 -0
  244. package/dist/modules/lists/starters.js.map +1 -0
  245. package/dist/modules/mcp/protocol.d.ts.map +1 -1
  246. package/dist/modules/mcp/protocol.js +13 -0
  247. package/dist/modules/mcp/protocol.js.map +1 -1
  248. package/dist/modules/notes/repository.d.ts +2 -0
  249. package/dist/modules/notes/repository.d.ts.map +1 -1
  250. package/dist/modules/notes/repository.js +9 -0
  251. package/dist/modules/notes/repository.js.map +1 -1
  252. package/dist/modules/opportunities/events.d.ts +9 -0
  253. package/dist/modules/opportunities/events.d.ts.map +1 -1
  254. package/dist/modules/opportunities/events.js +4 -0
  255. package/dist/modules/opportunities/events.js.map +1 -1
  256. package/dist/modules/opportunities/index.d.ts.map +1 -1
  257. package/dist/modules/opportunities/index.js +4 -2
  258. package/dist/modules/opportunities/index.js.map +1 -1
  259. package/dist/modules/opportunities/routes.d.ts +2 -0
  260. package/dist/modules/opportunities/routes.d.ts.map +1 -1
  261. package/dist/modules/opportunities/routes.js +8 -1
  262. package/dist/modules/opportunities/routes.js.map +1 -1
  263. package/dist/modules/opportunities/schema.d.ts +34 -0
  264. package/dist/modules/opportunities/schema.d.ts.map +1 -1
  265. package/dist/modules/opportunities/schema.js +7 -2
  266. package/dist/modules/opportunities/schema.js.map +1 -1
  267. package/dist/modules/opportunities/service.d.ts.map +1 -1
  268. package/dist/modules/opportunities/service.js +2 -0
  269. package/dist/modules/opportunities/service.js.map +1 -1
  270. package/dist/modules/opportunities/tools.d.ts +2 -1
  271. package/dist/modules/opportunities/tools.d.ts.map +1 -1
  272. package/dist/modules/opportunities/tools.js +3 -1
  273. package/dist/modules/opportunities/tools.js.map +1 -1
  274. package/dist/modules/partnerships/events.d.ts +9 -0
  275. package/dist/modules/partnerships/events.d.ts.map +1 -1
  276. package/dist/modules/partnerships/events.js +4 -0
  277. package/dist/modules/partnerships/events.js.map +1 -1
  278. package/dist/modules/partnerships/index.d.ts.map +1 -1
  279. package/dist/modules/partnerships/index.js +4 -2
  280. package/dist/modules/partnerships/index.js.map +1 -1
  281. package/dist/modules/partnerships/routes.d.ts +2 -0
  282. package/dist/modules/partnerships/routes.d.ts.map +1 -1
  283. package/dist/modules/partnerships/routes.js +8 -1
  284. package/dist/modules/partnerships/routes.js.map +1 -1
  285. package/dist/modules/partnerships/schema.d.ts +34 -0
  286. package/dist/modules/partnerships/schema.d.ts.map +1 -1
  287. package/dist/modules/partnerships/schema.js +7 -2
  288. package/dist/modules/partnerships/schema.js.map +1 -1
  289. package/dist/modules/partnerships/service.d.ts.map +1 -1
  290. package/dist/modules/partnerships/service.js +2 -0
  291. package/dist/modules/partnerships/service.js.map +1 -1
  292. package/dist/modules/partnerships/tools.d.ts +2 -1
  293. package/dist/modules/partnerships/tools.d.ts.map +1 -1
  294. package/dist/modules/partnerships/tools.js +3 -1
  295. package/dist/modules/partnerships/tools.js.map +1 -1
  296. package/dist/modules/people/personConsentWrites.d.ts +1 -1
  297. package/dist/modules/people/schema.js +1 -1
  298. package/dist/modules/people/schema.js.map +1 -1
  299. package/dist/modules/plans/repository.d.ts +2 -0
  300. package/dist/modules/plans/repository.d.ts.map +1 -1
  301. package/dist/modules/plans/repository.js +9 -0
  302. package/dist/modules/plans/repository.js.map +1 -1
  303. package/dist/modules/raises/events.d.ts +9 -0
  304. package/dist/modules/raises/events.d.ts.map +1 -1
  305. package/dist/modules/raises/events.js +4 -0
  306. package/dist/modules/raises/events.js.map +1 -1
  307. package/dist/modules/raises/index.d.ts.map +1 -1
  308. package/dist/modules/raises/index.js +4 -2
  309. package/dist/modules/raises/index.js.map +1 -1
  310. package/dist/modules/raises/routes.d.ts +2 -0
  311. package/dist/modules/raises/routes.d.ts.map +1 -1
  312. package/dist/modules/raises/routes.js +8 -1
  313. package/dist/modules/raises/routes.js.map +1 -1
  314. package/dist/modules/raises/schema.d.ts +34 -0
  315. package/dist/modules/raises/schema.d.ts.map +1 -1
  316. package/dist/modules/raises/schema.js +7 -2
  317. package/dist/modules/raises/schema.js.map +1 -1
  318. package/dist/modules/raises/service.d.ts.map +1 -1
  319. package/dist/modules/raises/service.js +2 -0
  320. package/dist/modules/raises/service.js.map +1 -1
  321. package/dist/modules/raises/tools.d.ts +2 -1
  322. package/dist/modules/raises/tools.d.ts.map +1 -1
  323. package/dist/modules/raises/tools.js +3 -1
  324. package/dist/modules/raises/tools.js.map +1 -1
  325. package/dist/modules/workspace/routes.d.ts +0 -2
  326. package/dist/modules/workspace/routes.d.ts.map +1 -1
  327. package/dist/modules/workspace/routes.js +0 -6
  328. package/dist/modules/workspace/routes.js.map +1 -1
  329. package/dist/modules/workspace/schema.d.ts +0 -34
  330. package/dist/modules/workspace/schema.d.ts.map +1 -1
  331. package/dist/modules/workspace/schema.js +0 -2
  332. package/dist/modules/workspace/schema.js.map +1 -1
  333. package/dist/modules/workspace/seedStarterForms.d.ts +17 -0
  334. package/dist/modules/workspace/seedStarterForms.d.ts.map +1 -0
  335. package/dist/modules/workspace/seedStarterForms.js +71 -0
  336. package/dist/modules/workspace/seedStarterForms.js.map +1 -0
  337. package/dist/modules/workspace/service.d.ts +0 -4
  338. package/dist/modules/workspace/service.d.ts.map +1 -1
  339. package/dist/modules/workspace/service.js +20 -10
  340. package/dist/modules/workspace/service.js.map +1 -1
  341. package/dist/modules/workspace/tools.js +1 -1
  342. package/dist/modules/workspace/tools.js.map +1 -1
  343. package/dist/testing/fixtures.d.ts.map +1 -1
  344. package/dist/testing/fixtures.js +1 -0
  345. package/dist/testing/fixtures.js.map +1 -1
  346. package/migrations/0037_drop_lists_consent_purpose_id.sql +2 -0
  347. package/migrations/0038_smart_white_queen.sql +2 -0
  348. package/migrations/0039_form_map_to_open.sql +1 -0
  349. package/migrations/0040_pipeline_converted_to.sql +20 -0
  350. package/migrations/0041_api_key_scopes.sql +1 -0
  351. package/migrations/0042_drop_workspace_identity.sql +2 -0
  352. package/migrations/meta/0038_snapshot.json +6554 -0
  353. package/migrations/meta/_journal.json +42 -0
  354. package/package.json +2 -2
  355. package/src/app.ts +3 -0
  356. package/src/index.ts +6 -0
  357. package/src/lib/actor.ts +3 -1
  358. package/src/lib/apiKeyScopes.ts +213 -0
  359. package/src/modules/activities/repository.ts +24 -0
  360. package/src/modules/agent-tasks/repository.ts +24 -0
  361. package/src/modules/api-keys/routes.ts +5 -0
  362. package/src/modules/api-keys/schema.ts +2 -0
  363. package/src/modules/api-keys/scopeMiddleware.ts +30 -0
  364. package/src/modules/api-keys/service.ts +36 -2
  365. package/src/modules/auth/credentials.ts +4 -0
  366. package/src/modules/consent-purposes/index.ts +1 -1
  367. package/src/modules/consent-purposes/schema.ts +3 -3
  368. package/src/modules/consent-purposes/service.ts +7 -16
  369. package/src/modules/consent-purposes/starters.ts +1 -1
  370. package/src/modules/consent-purposes/tools.ts +1 -1
  371. package/src/modules/conversions/clearPointers.ts +81 -0
  372. package/src/modules/conversions/factory.ts +20 -0
  373. package/src/modules/conversions/fieldMap.ts +548 -0
  374. package/src/modules/conversions/index.ts +6 -0
  375. package/src/modules/conversions/mcp.ts +71 -0
  376. package/src/modules/conversions/repoint.ts +146 -0
  377. package/src/modules/conversions/routes.ts +68 -0
  378. package/src/modules/conversions/service.ts +310 -0
  379. package/src/modules/conversions/wire.ts +42 -0
  380. package/src/modules/deals/events.ts +9 -0
  381. package/src/modules/deals/index.ts +4 -2
  382. package/src/modules/deals/routes.ts +11 -1
  383. package/src/modules/deals/schema.ts +10 -2
  384. package/src/modules/deals/service.ts +3 -0
  385. package/src/modules/deals/tools.ts +9 -1
  386. package/src/modules/decisions/repository.ts +23 -1
  387. package/src/modules/enquiries/events.ts +8 -2
  388. package/src/modules/enquiries/index.ts +4 -2
  389. package/src/modules/enquiries/routes.ts +11 -13
  390. package/src/modules/enquiries/schema.ts +12 -3
  391. package/src/modules/enquiries/service.ts +3 -150
  392. package/src/modules/enquiries/tools.ts +9 -18
  393. package/src/modules/forms/applyMappedFields.ts +248 -0
  394. package/src/modules/forms/applySubmissionFields.ts +435 -0
  395. package/src/modules/forms/fields.ts +31 -7
  396. package/src/modules/forms/mapping.ts +20 -3
  397. package/src/modules/forms/repository.ts +0 -7
  398. package/src/modules/forms/routes.ts +1 -2
  399. package/src/modules/forms/schema.ts +0 -2
  400. package/src/modules/forms/service.ts +31 -4
  401. package/src/modules/forms/starters.ts +129 -0
  402. package/src/modules/forms/submission.ts +135 -69
  403. package/src/modules/import-export/customFieldCsv.ts +55 -0
  404. package/src/modules/import-export/customFieldImport.ts +111 -0
  405. package/src/modules/import-export/drafts.ts +151 -3
  406. package/src/modules/import-export/exportRows.ts +222 -68
  407. package/src/modules/import-export/plan.ts +321 -67
  408. package/src/modules/import-export/repository.ts +356 -14
  409. package/src/modules/import-export/routes.ts +8 -8
  410. package/src/modules/import-export/service.ts +208 -45
  411. package/src/modules/import-export/streams.ts +95 -13
  412. package/src/modules/import-export/tools.ts +14 -6
  413. package/src/modules/import-export/validation.ts +128 -0
  414. package/src/modules/import-export/writes.ts +559 -27
  415. package/src/modules/lists/index.ts +71 -0
  416. package/src/modules/lists/repository.ts +34 -2
  417. package/src/modules/lists/routes.ts +0 -8
  418. package/src/modules/lists/schema.ts +0 -10
  419. package/src/modules/lists/service.ts +9 -107
  420. package/src/modules/lists/starters.ts +16 -0
  421. package/src/modules/mcp/protocol.ts +18 -0
  422. package/src/modules/notes/repository.ts +24 -0
  423. package/src/modules/opportunities/events.ts +9 -0
  424. package/src/modules/opportunities/index.ts +4 -2
  425. package/src/modules/opportunities/routes.ts +11 -1
  426. package/src/modules/opportunities/schema.ts +10 -2
  427. package/src/modules/opportunities/service.ts +3 -0
  428. package/src/modules/opportunities/tools.ts +9 -1
  429. package/src/modules/partnerships/events.ts +9 -0
  430. package/src/modules/partnerships/index.ts +4 -2
  431. package/src/modules/partnerships/routes.ts +11 -1
  432. package/src/modules/partnerships/schema.ts +10 -2
  433. package/src/modules/partnerships/service.ts +3 -0
  434. package/src/modules/partnerships/tools.ts +9 -1
  435. package/src/modules/people/personConsentWrites.ts +1 -1
  436. package/src/modules/people/schema.ts +1 -1
  437. package/src/modules/plans/repository.ts +24 -0
  438. package/src/modules/raises/events.ts +9 -0
  439. package/src/modules/raises/index.ts +4 -2
  440. package/src/modules/raises/routes.ts +11 -1
  441. package/src/modules/raises/schema.ts +10 -2
  442. package/src/modules/raises/service.ts +3 -0
  443. package/src/modules/raises/tools.ts +9 -1
  444. package/src/modules/workspace/routes.ts +0 -6
  445. package/src/modules/workspace/schema.ts +0 -2
  446. package/src/modules/workspace/seedStarterForms.ts +98 -0
  447. package/src/modules/workspace/service.ts +23 -15
  448. package/src/modules/workspace/tools.ts +1 -1
  449. package/src/testing/fixtures.ts +1 -0
@@ -1,5 +1,11 @@
1
+ import type { EventActor } from '@kelpie/schemas'
2
+
3
+ import type { Database } from '../../lib/database.ts'
1
4
  import type { KelpieModule } from '../../runtime/module.ts'
5
+ import type { TransactionScope } from '../../runtime/transaction.ts'
6
+ import type { RecordTargetType } from '../recordTargets.ts'
2
7
  import { listsEvents } from './events.ts'
8
+ import * as repository from './repository.ts'
3
9
  import { mountListsRoutes } from './routes.ts'
4
10
  import * as schema from './schema.ts'
5
11
  import { createListsService } from './service.ts'
@@ -39,3 +45,68 @@ export function createListsModule(migrationsDirectory: string): KelpieModule {
39
45
  },
40
46
  }
41
47
  }
48
+
49
+ /** Dependencies a caller outside the lists module already holds. */
50
+ export interface RemoveListMemberByTargetDependencies {
51
+ readonly db: Database
52
+ readonly transaction: TransactionScope
53
+ }
54
+
55
+ export interface RemoveListMemberByTargetInput {
56
+ readonly workspaceId: string
57
+ readonly listId: string
58
+ readonly targetType: RecordTargetType
59
+ readonly targetId: string
60
+ /**
61
+ * The event actor to stamp on `lists.member.removed`. An integration webhook
62
+ * that has no signed-in user passes `{ kind: 'system' }`; a module acting on
63
+ * behalf of a user passes `{ kind: 'user', id: userId }`.
64
+ */
65
+ readonly actor: EventActor
66
+ }
67
+
68
+ /**
69
+ * Removes a person (or any record) from a list by target, from outside the lists module.
70
+ *
71
+ * The path an integration takes when a signal from an external system removes
72
+ * membership — a Resend unsubscribe removing the person from every mapped list,
73
+ * a compliance sweep evicting a record — and there is no signed-in user to
74
+ * hang a normal `removeMember(actor, listId, id)` call on. Every safeguard the
75
+ * REST route enforces (workspace membership, ownership of the list, presence
76
+ * of the member row) is skipped here on purpose: the caller has already
77
+ * decided, and the input is trusted because it comes from another module in
78
+ * the same assembly rather than a request body.
79
+ *
80
+ * @returns `true` if the row was on the list and is now gone (emits
81
+ * `lists.member.removed`), `false` if the target was not a member — a no-op
82
+ * so a delivery redelivered by an at-least-once webhook is safe.
83
+ */
84
+ export async function removeListMemberByTarget(
85
+ dependencies: RemoveListMemberByTargetDependencies,
86
+ input: RemoveListMemberByTargetInput,
87
+ ): Promise<boolean> {
88
+ return dependencies.transaction(
89
+ async ({ tx, events }) => {
90
+ const deleted = await repository.deleteListMemberByTarget(
91
+ tx,
92
+ input.workspaceId,
93
+ input.listId,
94
+ input.targetType,
95
+ input.targetId,
96
+ )
97
+
98
+ if (deleted === undefined) {
99
+ return false
100
+ }
101
+
102
+ events.emit(
103
+ 'lists.member.removed',
104
+ { type: input.targetType, id: input.targetId },
105
+ { listId: input.listId },
106
+ )
107
+
108
+ return true
109
+ },
110
+ { workspaceId: input.workspaceId, actor: input.actor },
111
+ )
112
+ }
@@ -111,7 +111,6 @@ export async function findList(
111
111
  export interface ListIdRow {
112
112
  readonly id: string
113
113
  readonly targetType: string
114
- readonly consentPurposeId: string | null
115
114
  }
116
115
 
117
116
  export async function listListsById(
@@ -127,7 +126,6 @@ export async function listListsById(
127
126
  .select({
128
127
  id: lists.id,
129
128
  targetType: lists.targetType,
130
- consentPurposeId: lists.consentPurposeId,
131
129
  })
132
130
  .from(lists)
133
131
  .where(and(eq(lists.workspaceId, workspaceId), inArray(lists.id, ids)))
@@ -270,6 +268,40 @@ export async function deleteListMember(
270
268
  return deleted.length
271
269
  }
272
270
 
271
+ /**
272
+ * Removes the membership that points at a given target, if one is on the list.
273
+ *
274
+ * The addressed-by-target counterpart to `deleteListMember`, which addresses a
275
+ * row by its own id. An inbound integration webhook (Resend unsubscribe, for
276
+ * example) knows the person, not the list_member row that ties them to a list,
277
+ * and reading the row just to delete it is a round trip that adds nothing.
278
+ *
279
+ * @returns The removed row's id, or undefined if no member of that list points
280
+ * at that target — the same shape a caller wanting to decide whether to emit
281
+ * `lists.member.removed` needs.
282
+ */
283
+ export async function deleteListMemberByTarget(
284
+ db: Queryable,
285
+ workspaceId: string,
286
+ listId: string,
287
+ targetType: string,
288
+ targetId: string,
289
+ ): Promise<{ id: string } | undefined> {
290
+ const [deleted] = await db
291
+ .delete(listMembers)
292
+ .where(
293
+ and(
294
+ eq(listMembers.workspaceId, workspaceId),
295
+ eq(listMembers.listId, listId),
296
+ eq(listMembers.targetType, targetType),
297
+ eq(listMembers.targetId, targetId),
298
+ ),
299
+ )
300
+ .returning({ id: listMembers.id })
301
+
302
+ return deleted
303
+ }
304
+
273
305
  /** A membership row enriched with the list it points at. */
274
306
  export interface MembershipWithList {
275
307
  readonly id: string
@@ -31,14 +31,12 @@ export const createBody = z.strictObject({
31
31
  name: z.string().min(1),
32
32
  target_type: z.enum(RECORD_TARGET_TYPES),
33
33
  description: z.string().nullable().default(null),
34
- consent_purpose_id: z.string().min(1).nullable().default(null),
35
34
  })
36
35
 
37
36
  export const updateBody = z
38
37
  .strictObject({
39
38
  name: z.string().min(1),
40
39
  description: z.string().nullable(),
41
- consent_purpose_id: z.string().min(1).nullable(),
42
40
  })
43
41
  .partial()
44
42
 
@@ -56,7 +54,6 @@ export function toCreateInput(body: z.infer<typeof createBody>): CreateListInput
56
54
  name: body.name,
57
55
  targetType: body.target_type,
58
56
  description: body.description,
59
- consentPurposeId: body.consent_purpose_id,
60
57
  }
61
58
  }
62
59
 
@@ -64,9 +61,6 @@ export function toUpdateInput(body: z.infer<typeof updateBody>): UpdateListInput
64
61
  return {
65
62
  ...(body.name === undefined ? {} : { name: body.name }),
66
63
  ...(body.description === undefined ? {} : { description: body.description }),
67
- ...(body.consent_purpose_id === undefined
68
- ? {}
69
- : { consentPurposeId: body.consent_purpose_id }),
70
64
  }
71
65
  }
72
66
 
@@ -84,8 +78,6 @@ export function listResponse(list: ListView): Record<string, unknown> {
84
78
  description: list.description,
85
79
  target_type: list.targetType,
86
80
  member_count: list.memberCount,
87
- consent_purpose_id: list.consentPurposeId,
88
- consent_purpose_slug: list.consentPurposeSlug,
89
81
  created_at: list.createdAt.toISOString(),
90
82
  updated_at: list.updatedAt.toISOString(),
91
83
  }
@@ -2,7 +2,6 @@ import { RECORD_TARGET_TYPES } from '@kelpie/schemas'
2
2
  import { foreignKey, index, pgTable, text, timestamp, unique } from 'drizzle-orm/pg-core'
3
3
 
4
4
  import { checkOneOf, createdAt, primaryId, updatedAt } from '../../lib/columns.ts'
5
- import { consentPurposes } from '../consent-purposes/schema.ts'
6
5
  import { workspaces } from '../workspace/schema.ts'
7
6
 
8
7
  /**
@@ -35,15 +34,6 @@ export const lists = pgTable(
35
34
  name: text('name').notNull(),
36
35
  description: text('description'),
37
36
  targetType: text('target_type').notNull(),
38
- /**
39
- * The consent purpose captured when a form submit adds a person to this
40
- * list. Person lists only — the service refuses a non-null value on any
41
- * other target type. `restrict` on delete so a purpose in use is not
42
- * silently unset behind the list's back.
43
- */
44
- consentPurposeId: text('consent_purpose_id').references(() => consentPurposes.id, {
45
- onDelete: 'restrict',
46
- }),
47
37
  createdAt: createdAt(),
48
38
  updatedAt: updatedAt(),
49
39
  },
@@ -9,7 +9,6 @@ import type { TransactionScope } from '../../runtime/transaction.ts'
9
9
  import { toEventActor } from '../../lib/actor.ts'
10
10
  import type { Actor } from '../auth/actor.ts'
11
11
  import { requireWorkspaceId } from '../auth/actor.ts'
12
- import * as consentPurposesRepository from '../consent-purposes/repository.ts'
13
12
  import { missingTargets, resolveTargetNames, targetKey } from '../recordTargets.ts'
14
13
  import type { RecordTargetType } from '../recordTargets.ts'
15
14
  import './events.ts'
@@ -44,14 +43,8 @@ export interface ListsDependencies {
44
43
  readonly now: () => Date
45
44
  }
46
45
 
47
- /**
48
- * A list as the API returns one: the stored row minus the tenancy column,
49
- * plus the denormalised slug of the consent purpose (when set) so a reader
50
- * needs no second call.
51
- */
52
- export type ListView = Omit<ListWithCount, 'workspaceId'> & {
53
- readonly consentPurposeSlug: string | null
54
- }
46
+ /** A list as the API returns one: the stored row minus the tenancy column. */
47
+ export type ListView = Omit<ListWithCount, 'workspaceId'>
55
48
 
56
49
  /** A membership plus the list it points at, as `/v1/list-memberships` returns one. */
57
50
  export interface ListMembershipView {
@@ -78,18 +71,12 @@ export interface CreateListInput {
78
71
  readonly name: string
79
72
  readonly targetType: RecordTargetType
80
73
  readonly description: string | null
81
- /**
82
- * Optional consent purpose captured on form-driven adds. Person lists only.
83
- * A non-null value on any other target type is refused at 422.
84
- */
85
- readonly consentPurposeId: string | null
86
74
  }
87
75
 
88
76
  /** PATCH semantics: an absent field is left alone. The type never moves. */
89
77
  export interface UpdateListInput {
90
78
  readonly name?: string | undefined
91
79
  readonly description?: string | null | undefined
92
- readonly consentPurposeId?: string | null | undefined
93
80
  }
94
81
 
95
82
  export interface AddListMemberInput {
@@ -117,10 +104,10 @@ export interface ListsService {
117
104
  ): Promise<readonly ListMembershipView[]>
118
105
  }
119
106
 
120
- function toView(record: ListWithCount, consentPurposeSlug: string | null): ListView {
107
+ function toView(record: ListWithCount): ListView {
121
108
  const { workspaceId: _workspaceId, ...view } = record
122
109
 
123
- return { ...view, consentPurposeSlug }
110
+ return view
124
111
  }
125
112
 
126
113
  function toMembershipView(row: MembershipWithList): ListMembershipView {
@@ -170,13 +157,6 @@ function alreadyMember(): AppError {
170
157
  ])
171
158
  }
172
159
 
173
- function purposeNotPersonList(): AppError {
174
- return AppError.validationFailed(
175
- 'Only person lists capture consent — remove consent_purpose_id or change the list type',
176
- [{ field: 'consent_purpose_id', message: 'Person lists only' }],
177
- )
178
- }
179
-
180
160
  export function createListsService(dependencies: ListsDependencies): ListsService {
181
161
  async function require(workspaceId: string, id: string): Promise<ListWithCount> {
182
162
  const list = await repository.findList(dependencies.db, workspaceId, id)
@@ -200,77 +180,25 @@ export function createListsService(dependencies: ListsDependencies): ListsServic
200
180
  }
201
181
  }
202
182
 
203
- async function resolveConsentPurposeSlug(
204
- workspaceId: string,
205
- purposeId: string | null,
206
- ): Promise<string | null> {
207
- if (purposeId === null) return null
208
- const [row] = await consentPurposesRepository.listPurposesByIds(
209
- dependencies.db,
210
- workspaceId,
211
- [purposeId],
212
- )
213
- return row?.slug ?? null
214
- }
215
-
216
- async function requireConsentPurposeExists(
217
- workspaceId: string,
218
- purposeId: string,
219
- ): Promise<void> {
220
- const rows = await consentPurposesRepository.listPurposesByIds(
221
- dependencies.db,
222
- workspaceId,
223
- [purposeId],
224
- )
225
- if (rows.length === 0) {
226
- throw AppError.validationFailed('That consent purpose was not found', [
227
- { field: 'consent_purpose_id', message: `No purpose ${purposeId} here` },
228
- ])
229
- }
230
- }
231
-
232
183
  return {
233
184
  async list(actor, filters, query) {
234
185
  const workspaceId = requireWorkspaceId(actor)
235
186
  const window = readListWindow(query, LIST_SORTS, DEFAULT_LIST_SORT)
236
187
  const rows = await repository.listLists(dependencies.db, workspaceId, filters, window)
237
- const purposeIds = Array.from(
238
- new Set(
239
- rows
240
- .map((row) => row.consentPurposeId)
241
- .filter((id): id is string => id !== null),
242
- ),
243
- )
244
- const purposes = await consentPurposesRepository.listPurposesByIds(
245
- dependencies.db,
246
- workspaceId,
247
- purposeIds,
248
- )
249
- const slugByPurpose = new Map(purposes.map((purpose) => [purpose.id, purpose.slug]))
250
188
 
251
- return mapPage(toPage(rows, window, (row) => row.id), (row) =>
252
- toView(row, row.consentPurposeId === null ? null : (slugByPurpose.get(row.consentPurposeId) ?? null)),
253
- )
189
+ return mapPage(toPage(rows, window, (row) => row.id), (row) => toView(row))
254
190
  },
255
191
 
256
192
  async get(actor, id) {
257
193
  const workspaceId = requireWorkspaceId(actor)
258
194
  const record = await require(workspaceId, id)
259
- const slug = await resolveConsentPurposeSlug(workspaceId, record.consentPurposeId)
260
- return toView(record, slug)
195
+ return toView(record)
261
196
  },
262
197
 
263
198
  async create(actor, input) {
264
199
  const workspaceId = requireWorkspaceId(actor)
265
200
  const id = dependencies.createId('list')
266
201
 
267
- if (input.consentPurposeId !== null) {
268
- if (input.targetType !== 'person') {
269
- throw purposeNotPersonList()
270
- }
271
- await requireConsentPurposeExists(workspaceId, input.consentPurposeId)
272
- }
273
-
274
202
  return dependencies.transaction(
275
203
  async ({ tx, events }) => {
276
204
  let created
@@ -281,7 +209,6 @@ export function createListsService(dependencies: ListsDependencies): ListsServic
281
209
  name: input.name,
282
210
  description: input.description,
283
211
  targetType: input.targetType,
284
- consentPurposeId: input.consentPurposeId,
285
212
  })
286
213
  } catch (error: unknown) {
287
214
  if (postgresErrorCode(error) === UNIQUE_VIOLATION) {
@@ -293,12 +220,7 @@ export function createListsService(dependencies: ListsDependencies): ListsServic
293
220
 
294
221
  events.emit('lists.list.created', { type: 'list', id: created.id }, {})
295
222
 
296
- const slug = await resolveConsentPurposeSlug(
297
- workspaceId,
298
- created.consentPurposeId,
299
- )
300
-
301
- return toView({ ...created, memberCount: 0 }, slug)
223
+ return toView({ ...created, memberCount: 0 })
302
224
  },
303
225
  { workspaceId, actor: toEventActor(actor) },
304
226
  )
@@ -308,32 +230,14 @@ export function createListsService(dependencies: ListsDependencies): ListsServic
308
230
  const workspaceId = requireWorkspaceId(actor)
309
231
  const existing = await require(workspaceId, id)
310
232
 
311
- if (
312
- changes.consentPurposeId !== undefined &&
313
- changes.consentPurposeId !== null &&
314
- existing.targetType !== 'person'
315
- ) {
316
- throw purposeNotPersonList()
317
- }
318
- if (
319
- changes.consentPurposeId !== undefined &&
320
- changes.consentPurposeId !== null
321
- ) {
322
- await requireConsentPurposeExists(workspaceId, changes.consentPurposeId)
323
- }
324
-
325
233
  const columns: Partial<repository.ListColumns> = {
326
234
  ...(changes.name === undefined ? {} : { name: changes.name }),
327
235
  ...(changes.description === undefined ? {} : { description: changes.description }),
328
- ...(changes.consentPurposeId === undefined
329
- ? {}
330
- : { consentPurposeId: changes.consentPurposeId }),
331
236
  }
332
237
  const changed = changedKeys(existing, columns)
333
238
 
334
239
  if (changed.length === 0) {
335
- const slug = await resolveConsentPurposeSlug(workspaceId, existing.consentPurposeId)
336
- return toView(existing, slug)
240
+ return toView(existing)
337
241
  }
338
242
 
339
243
  return dependencies.transaction(
@@ -358,9 +262,7 @@ export function createListsService(dependencies: ListsDependencies): ListsServic
358
262
 
359
263
  events.emit('lists.list.updated', { type: 'list', id }, { changed })
360
264
 
361
- const slug = await resolveConsentPurposeSlug(workspaceId, updated.consentPurposeId)
362
-
363
- return toView({ ...updated, memberCount: existing.memberCount }, slug)
265
+ return toView({ ...updated, memberCount: existing.memberCount })
364
266
  },
365
267
  { workspaceId, actor: toEventActor(actor) },
366
268
  )
@@ -0,0 +1,16 @@
1
+ import type { RecordTargetType } from '@kelpie/schemas'
2
+
3
+ /**
4
+ * The lists a new workspace starts with. Data, not migrations: a workspace
5
+ * owner may rename or delete them, and add any others they need.
6
+ */
7
+
8
+ export interface StarterList {
9
+ readonly slug: string
10
+ readonly name: string
11
+ readonly targetType: RecordTargetType
12
+ }
13
+
14
+ export const STARTER_LISTS: readonly StarterList[] = [
15
+ { slug: 'newsletter', name: 'Newsletter', targetType: 'person' },
16
+ ]
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  import type { Actor } from '../../lib/actor.ts'
4
+ import { requireApiKeyScope, resolveMcpScope } from '../../lib/apiKeyScopes.ts'
4
5
  import { AppError, describeThrown, internalErrorBody, toErrorBody } from '../../lib/errors.ts'
5
6
  import type { Logger } from '../../lib/logger.ts'
6
7
  import type { McpTool } from '../../runtime/module.ts'
@@ -317,6 +318,23 @@ async function callTool(
317
318
  return failure(id, INVALID_PARAMS, `Unknown tool "${parsed.data.name}"`)
318
319
  }
319
320
 
321
+ const requiredScope = resolveMcpScope(parsed.data.name)
322
+
323
+ if (requiredScope !== null) {
324
+ try {
325
+ requireApiKeyScope(actor, requiredScope)
326
+ } catch (error: unknown) {
327
+ if (error instanceof AppError && error.code === 'forbidden') {
328
+ return success(
329
+ id,
330
+ completeResult(dependencies, era, toolFailure(dependencies.logger, tool.name, error)),
331
+ )
332
+ }
333
+
334
+ throw error
335
+ }
336
+ }
337
+
320
338
  try {
321
339
  // `arguments` is optional on the wire and every tool schema is an object, so
322
340
  // an absent one is an empty object rather than a validation failure the
@@ -128,3 +128,27 @@ export async function deleteForTarget(
128
128
 
129
129
  return deleted.length
130
130
  }
131
+
132
+ /** Moves every note from one target to another inside a conversion transaction. */
133
+ export async function repointForTarget(
134
+ db: Queryable,
135
+ workspaceId: string,
136
+ fromType: string,
137
+ fromId: string,
138
+ toType: string,
139
+ toId: string,
140
+ ): Promise<number> {
141
+ const updated = await db
142
+ .update(notes)
143
+ .set({ targetType: toType, targetId: toId })
144
+ .where(
145
+ and(
146
+ eq(notes.workspaceId, workspaceId),
147
+ eq(notes.targetType, fromType),
148
+ eq(notes.targetId, fromId),
149
+ ),
150
+ )
151
+ .returning({ id: notes.id })
152
+
153
+ return updated.length
154
+ }
@@ -12,6 +12,10 @@ export const opportunitiesEvents = {
12
12
  fromStageId: z.string().nullable(),
13
13
  toStageId: z.string(),
14
14
  }),
15
+ 'opportunities.opportunity.converted': z.object({
16
+ targetType: z.string(),
17
+ targetId: z.string(),
18
+ }),
15
19
  } satisfies ModuleEventCatalog
16
20
 
17
21
  export type OpportunityCreatedData = Record<string, never>
@@ -23,6 +27,10 @@ export interface OpportunityStageChangedData {
23
27
  readonly fromStageId: string | null
24
28
  readonly toStageId: string
25
29
  }
30
+ export interface OpportunityConvertedData {
31
+ readonly targetType: string
32
+ readonly targetId: string
33
+ }
26
34
 
27
35
  declare module '../../runtime/events.ts' {
28
36
  interface KelpieEventMap {
@@ -30,5 +38,6 @@ declare module '../../runtime/events.ts' {
30
38
  'opportunities.opportunity.updated': OpportunityUpdatedData
31
39
  'opportunities.opportunity.deleted': OpportunityDeletedData
32
40
  'opportunities.opportunity.stage_changed': OpportunityStageChangedData
41
+ 'opportunities.opportunity.converted': OpportunityConvertedData
33
42
  }
34
43
  }
@@ -1,5 +1,6 @@
1
1
  import type { KelpieModule } from '../../runtime/module.ts'
2
2
  import { createActivityRecorder } from '../activities/index.ts'
3
+ import { createConversionsFromContext } from '../conversions/index.ts'
3
4
  import { createCustomFieldValues } from '../custom-fields/index.ts'
4
5
  import { opportunitiesEvents } from './events.ts'
5
6
  import { mountOpportunitiesRoutes } from './routes.ts'
@@ -22,6 +23,7 @@ export function createOpportunitiesModule(migrationsDirectory: string): KelpieMo
22
23
  events: opportunitiesEvents,
23
24
 
24
25
  register(context) {
26
+ const conversions = createConversionsFromContext(context)
25
27
  const service = createOpportunitiesService({
26
28
  db: context.db,
27
29
  transaction: context.transaction,
@@ -37,10 +39,10 @@ export function createOpportunitiesModule(migrationsDirectory: string): KelpieMo
37
39
  context.schema(schema, migrationsDirectory)
38
40
 
39
41
  context.routes((router) => {
40
- mountOpportunitiesRoutes(router, { db: context.db, now: context.now, service })
42
+ mountOpportunitiesRoutes(router, { db: context.db, now: context.now, service, conversions })
41
43
  })
42
44
 
43
- registerOpportunitiesTools(context.mcp, service)
45
+ registerOpportunitiesTools(context.mcp, service, conversions)
44
46
 
45
47
  return Promise.resolve()
46
48
  },
@@ -1,4 +1,4 @@
1
- import { customFieldsPatchShape } from '@kelpie/schemas'
1
+ import { convertPipelineRecordBody, convertedToResponse, customFieldsPatchShape } from '@kelpie/schemas'
2
2
  import type { Context, Hono } from 'hono'
3
3
  import { z } from 'zod'
4
4
 
@@ -8,6 +8,8 @@ import type { Actor } from '../auth/actor.ts'
8
8
  import { resolveActorFrom } from '../auth/credentials.ts'
9
9
  import type { CredentialDependencies } from '../auth/credentials.ts'
10
10
  import { renderCustomFieldsForWire } from '../custom-fields/wire.ts'
11
+ import { mountPipelineConvertRoute } from '../conversions/routes.ts'
12
+ import type { ConversionsService } from '../conversions/index.ts'
11
13
  import type {
12
14
  CreateOpportunityInput,
13
15
  OpportunitiesService,
@@ -55,6 +57,7 @@ export const updateBody = z.strictObject(opportunityShape).partial()
55
57
 
56
58
  export interface OpportunitiesRoutesDependencies extends CredentialDependencies {
57
59
  readonly service: OpportunitiesService
60
+ readonly conversions: ConversionsService
58
61
  }
59
62
 
60
63
  export function toCreateInput(body: z.infer<typeof createBody>): CreateOpportunityInput {
@@ -99,6 +102,7 @@ export function opportunityResponse(opportunity: OpportunityView): Record<string
99
102
  person_ids: opportunity.personIds,
100
103
  summary: opportunity.summary,
101
104
  tags: opportunity.tags,
105
+ converted_to: convertedToResponse(opportunity.convertedTargetType, opportunity.convertedTargetId),
102
106
  custom_fields: renderCustomFieldsForWire(opportunity.customFields),
103
107
  created_at: opportunity.createdAt.toISOString(),
104
108
  updated_at: opportunity.updatedAt.toISOString(),
@@ -162,4 +166,10 @@ export function mountOpportunitiesRoutes(
162
166
 
163
167
  return context.body(null, 204)
164
168
  })
169
+
170
+ mountPipelineConvertRoute(router, '/opportunities/:id/convert', {
171
+ ...dependencies,
172
+ sourceKind: 'opportunity',
173
+ bodySchema: convertPipelineRecordBody,
174
+ })
165
175
  }
@@ -1,7 +1,9 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import { check, date, index, jsonb, pgTable, text } from 'drizzle-orm/pg-core'
3
+ import { PIPELINE_KINDS } from '@kelpie/schemas'
1
4
  import type { CustomFieldValue } from '@kelpie/schemas'
2
- import { date, index, jsonb, pgTable, text } from 'drizzle-orm/pg-core'
3
5
 
4
- import { createdAt, primaryId, searchVector, updatedAt } from '../../lib/columns.ts'
6
+ import { createdAt, oneOf, primaryId, searchVector, updatedAt } from '../../lib/columns.ts'
5
7
  import type { SearchVectorPart } from '../../lib/columns.ts'
6
8
  import { companies } from '../companies/schema.ts'
7
9
  import { pipelineStages } from '../pipelines/schema.ts'
@@ -31,6 +33,8 @@ export const opportunities = pgTable(
31
33
  expectedClose: date('expected_close'),
32
34
  summary: text('summary').notNull().default(''),
33
35
  tags: text('tags').array().notNull().default([]),
36
+ convertedTargetType: text('converted_target_type'),
37
+ convertedTargetId: text('converted_target_id'),
34
38
  // Workspace-defined fields, keyed by definition key. See people/schema.ts.
35
39
  customFields: jsonb('custom_fields')
36
40
  .$type<Readonly<Record<string, CustomFieldValue>>>()
@@ -49,5 +53,9 @@ export const opportunities = pgTable(
49
53
  index('opportunities_workspace_idx').on(table.workspaceId),
50
54
  index('opportunities_stage_idx').on(table.stageId),
51
55
  index('opportunities_search_idx').using('gin', table.searchVector),
56
+ check(
57
+ 'opportunities_converted_target_type_check',
58
+ sql`${table.convertedTargetType} is null or ${oneOf('opportunities_converted_target_type_check', table.convertedTargetType, PIPELINE_KINDS)}`,
59
+ ),
52
60
  ],
53
61
  )
@@ -21,6 +21,7 @@ import type { Actor } from '../auth/actor.ts'
21
21
  import { actorMemberId, requireWorkspaceId } from '../auth/actor.ts'
22
22
  import './events.ts'
23
23
  import { deleteRecordsAttachedTo } from '../attachedRecords.ts'
24
+ import { clearConversionPointersToTarget } from '../conversions/clearPointers.ts'
24
25
  import * as companyRepository from '../companies/repository.ts'
25
26
  import type { CustomFieldValuesValidator } from '../custom-fields/values.ts'
26
27
  import * as personLinks from '../personLinks.ts'
@@ -494,6 +495,8 @@ export function createOpportunitiesService(
494
495
  await dependencies.transaction(async ({ tx, events }) => {
495
496
  await require(workspaceId, id)
496
497
 
498
+ await clearConversionPointersToTarget(tx, workspaceId, 'opportunity', id)
499
+
497
500
  // Notes, activities, decisions, plan items, and person_links have no
498
501
  // foreign key to their target, so the deleting service removes them
499
502
  // here, in the same transaction. Nothing else references an opportunity.
@@ -2,6 +2,8 @@ import { z } from 'zod'
2
2
 
3
3
  import type { McpToolRegistry } from '../../runtime/module.ts'
4
4
  import { idArg, idSetArg, listWindowShape, registerCrudTools, termArg, toSet } from '../crudTools.ts'
5
+ import type { ConversionsService } from '../conversions/index.ts'
6
+ import { registerPipelineConvertTool } from '../conversions/mcp.ts'
5
7
  import { createBody, opportunityResponse, toCreateInput, toUpdateInput, updateBody } from './routes.ts'
6
8
  import type { OpportunitiesService } from './service.ts'
7
9
 
@@ -16,7 +18,11 @@ const listArgs = z.strictObject({
16
18
  person_id: idSetArg.optional().describe('Only opportunities any of these people are on.'),
17
19
  })
18
20
 
19
- export function registerOpportunitiesTools(mcp: McpToolRegistry, service: OpportunitiesService): void {
21
+ export function registerOpportunitiesTools(
22
+ mcp: McpToolRegistry,
23
+ service: OpportunitiesService,
24
+ conversions: ConversionsService,
25
+ ): void {
20
26
  registerCrudTools(mcp, {
21
27
  resource: 'opportunities',
22
28
  subject: 'opportunity',
@@ -38,4 +44,6 @@ export function registerOpportunitiesTools(mcp: McpToolRegistry, service: Opport
38
44
  updateArgs: updateBody.extend({ id: idArg }),
39
45
  toUpdateInput,
40
46
  })
47
+
48
+ registerPipelineConvertTool(mcp, conversions, 'opportunity')
41
49
  }