@kelpie/server 0.12.0 → 0.13.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 (383) hide show
  1. package/dist/app.d.ts +13 -0
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +8 -1
  4. package/dist/app.js.map +1 -1
  5. package/dist/boot.d.ts.map +1 -1
  6. package/dist/boot.js +1 -0
  7. package/dist/boot.js.map +1 -1
  8. package/dist/lib/apiKeyScopes.d.ts.map +1 -1
  9. package/dist/lib/apiKeyScopes.js +6 -0
  10. package/dist/lib/apiKeyScopes.js.map +1 -1
  11. package/dist/lib/columns.d.ts +6 -0
  12. package/dist/lib/columns.d.ts.map +1 -1
  13. package/dist/lib/columns.js +5 -3
  14. package/dist/lib/columns.js.map +1 -1
  15. package/dist/lib/config.d.ts +17 -0
  16. package/dist/lib/config.d.ts.map +1 -1
  17. package/dist/lib/config.js +3 -0
  18. package/dist/lib/config.js.map +1 -1
  19. package/dist/lib/egress.d.ts.map +1 -1
  20. package/dist/lib/egress.js +8 -4
  21. package/dist/lib/egress.js.map +1 -1
  22. package/dist/lib/errors.d.ts +3 -3
  23. package/dist/lib/errors.d.ts.map +1 -1
  24. package/dist/lib/errors.js +13 -5
  25. package/dist/lib/errors.js.map +1 -1
  26. package/dist/lib/ids.d.ts +11 -0
  27. package/dist/lib/ids.d.ts.map +1 -1
  28. package/dist/lib/ids.js +11 -0
  29. package/dist/lib/ids.js.map +1 -1
  30. package/dist/lib/kelpieConfigFile.d.ts +15 -1
  31. package/dist/lib/kelpieConfigFile.d.ts.map +1 -1
  32. package/dist/lib/kelpieConfigFile.js +33 -1
  33. package/dist/lib/kelpieConfigFile.js.map +1 -1
  34. package/dist/modules/agent-tasks/catalog.d.ts.map +1 -1
  35. package/dist/modules/agent-tasks/catalog.js +32 -1
  36. package/dist/modules/agent-tasks/catalog.js.map +1 -1
  37. package/dist/modules/agent-tasks/index.d.ts.map +1 -1
  38. package/dist/modules/agent-tasks/index.js +1 -0
  39. package/dist/modules/agent-tasks/index.js.map +1 -1
  40. package/dist/modules/agent-tasks/resolve.d.ts.map +1 -1
  41. package/dist/modules/agent-tasks/resolve.js +5 -0
  42. package/dist/modules/agent-tasks/resolve.js.map +1 -1
  43. package/dist/modules/agent-tasks/routes.d.ts +2 -0
  44. package/dist/modules/agent-tasks/routes.d.ts.map +1 -1
  45. package/dist/modules/attachedRecords.d.ts +22 -8
  46. package/dist/modules/attachedRecords.d.ts.map +1 -1
  47. package/dist/modules/attachedRecords.js +42 -5
  48. package/dist/modules/attachedRecords.js.map +1 -1
  49. package/dist/modules/auth/index.d.ts.map +1 -1
  50. package/dist/modules/auth/index.js +1 -0
  51. package/dist/modules/auth/index.js.map +1 -1
  52. package/dist/modules/auth/repository.d.ts +14 -0
  53. package/dist/modules/auth/repository.d.ts.map +1 -1
  54. package/dist/modules/auth/repository.js +20 -2
  55. package/dist/modules/auth/repository.js.map +1 -1
  56. package/dist/modules/auth/routes.d.ts.map +1 -1
  57. package/dist/modules/auth/routes.js +30 -0
  58. package/dist/modules/auth/routes.js.map +1 -1
  59. package/dist/modules/auth/service.d.ts +28 -0
  60. package/dist/modules/auth/service.d.ts.map +1 -1
  61. package/dist/modules/auth/service.js +72 -4
  62. package/dist/modules/auth/service.js.map +1 -1
  63. package/dist/modules/companies/repository.d.ts.map +1 -1
  64. package/dist/modules/companies/repository.js +1 -1
  65. package/dist/modules/companies/repository.js.map +1 -1
  66. package/dist/modules/companies/routes.d.ts +2 -4
  67. package/dist/modules/companies/routes.d.ts.map +1 -1
  68. package/dist/modules/companies/routes.js +6 -11
  69. package/dist/modules/companies/routes.js.map +1 -1
  70. package/dist/modules/companies/schema.d.ts +13 -28
  71. package/dist/modules/companies/schema.d.ts.map +1 -1
  72. package/dist/modules/companies/schema.js +2 -2
  73. package/dist/modules/companies/schema.js.map +1 -1
  74. package/dist/modules/companies/service.d.ts +2 -3
  75. package/dist/modules/companies/service.d.ts.map +1 -1
  76. package/dist/modules/companies/service.js +4 -7
  77. package/dist/modules/companies/service.js.map +1 -1
  78. package/dist/modules/core.d.ts.map +1 -1
  79. package/dist/modules/core.js +2 -0
  80. package/dist/modules/core.js.map +1 -1
  81. package/dist/modules/custom-fields/repository.d.ts.map +1 -1
  82. package/dist/modules/custom-fields/repository.js +2 -0
  83. package/dist/modules/custom-fields/repository.js.map +1 -1
  84. package/dist/modules/custom-fields/routes.d.ts +1 -0
  85. package/dist/modules/custom-fields/routes.d.ts.map +1 -1
  86. package/dist/modules/dashboard/index.d.ts.map +1 -1
  87. package/dist/modules/dashboard/index.js +1 -0
  88. package/dist/modules/dashboard/index.js.map +1 -1
  89. package/dist/modules/dashboard/repository.d.ts +13 -0
  90. package/dist/modules/dashboard/repository.d.ts.map +1 -1
  91. package/dist/modules/dashboard/repository.js +36 -0
  92. package/dist/modules/dashboard/repository.js.map +1 -1
  93. package/dist/modules/dashboard/routes.d.ts.map +1 -1
  94. package/dist/modules/dashboard/routes.js +13 -0
  95. package/dist/modules/dashboard/routes.js.map +1 -1
  96. package/dist/modules/dashboard/service.d.ts +11 -0
  97. package/dist/modules/dashboard/service.d.ts.map +1 -1
  98. package/dist/modules/dashboard/service.js +24 -1
  99. package/dist/modules/dashboard/service.js.map +1 -1
  100. package/dist/modules/decisions/repository.d.ts.map +1 -1
  101. package/dist/modules/decisions/repository.js +2 -0
  102. package/dist/modules/decisions/repository.js.map +1 -1
  103. package/dist/modules/decisions/routes.d.ts +2 -0
  104. package/dist/modules/decisions/routes.d.ts.map +1 -1
  105. package/dist/modules/events/attendances.d.ts +35 -0
  106. package/dist/modules/events/attendances.d.ts.map +1 -0
  107. package/dist/modules/events/attendances.js +157 -0
  108. package/dist/modules/events/attendances.js.map +1 -0
  109. package/dist/modules/events/catalog.d.ts +35 -0
  110. package/dist/modules/events/catalog.d.ts.map +1 -0
  111. package/dist/modules/events/catalog.js +11 -0
  112. package/dist/modules/events/catalog.js.map +1 -0
  113. package/dist/modules/events/index.d.ts +10 -0
  114. package/dist/modules/events/index.d.ts.map +1 -0
  115. package/dist/modules/events/index.js +47 -0
  116. package/dist/modules/events/index.js.map +1 -0
  117. package/dist/modules/events/repository.d.ts +67 -0
  118. package/dist/modules/events/repository.d.ts.map +1 -0
  119. package/dist/modules/events/repository.js +212 -0
  120. package/dist/modules/events/repository.js.map +1 -0
  121. package/dist/modules/events/routes.d.ts +136 -0
  122. package/dist/modules/events/routes.d.ts.map +1 -0
  123. package/dist/modules/events/routes.js +280 -0
  124. package/dist/modules/events/routes.js.map +1 -0
  125. package/dist/modules/events/schema.d.ts +626 -0
  126. package/dist/modules/events/schema.d.ts.map +1 -0
  127. package/dist/modules/events/schema.js +93 -0
  128. package/dist/modules/events/schema.js.map +1 -0
  129. package/dist/modules/events/service.d.ts +68 -0
  130. package/dist/modules/events/service.d.ts.map +1 -0
  131. package/dist/modules/events/service.js +262 -0
  132. package/dist/modules/events/service.js.map +1 -0
  133. package/dist/modules/events/tools.d.ts +8 -0
  134. package/dist/modules/events/tools.d.ts.map +1 -0
  135. package/dist/modules/events/tools.js +80 -0
  136. package/dist/modules/events/tools.js.map +1 -0
  137. package/dist/modules/forms/applySubmissionFields.d.ts.map +1 -1
  138. package/dist/modules/forms/applySubmissionFields.js +7 -5
  139. package/dist/modules/forms/applySubmissionFields.js.map +1 -1
  140. package/dist/modules/forms/fillAddresses.d.ts +4 -0
  141. package/dist/modules/forms/fillAddresses.d.ts.map +1 -0
  142. package/dist/modules/forms/fillAddresses.js +93 -0
  143. package/dist/modules/forms/fillAddresses.js.map +1 -0
  144. package/dist/modules/forms/repository.d.ts +2 -2
  145. package/dist/modules/forms/repository.d.ts.map +1 -1
  146. package/dist/modules/forms/repository.js.map +1 -1
  147. package/dist/modules/forms/routes.d.ts +2 -0
  148. package/dist/modules/forms/routes.d.ts.map +1 -1
  149. package/dist/modules/forms/routes.js +2 -2
  150. package/dist/modules/forms/routes.js.map +1 -1
  151. package/dist/modules/forms/schema.js +2 -2
  152. package/dist/modules/forms/schema.js.map +1 -1
  153. package/dist/modules/forms/service.js +2 -2
  154. package/dist/modules/forms/service.js.map +1 -1
  155. package/dist/modules/forms/submission.d.ts +1 -0
  156. package/dist/modules/forms/submission.d.ts.map +1 -1
  157. package/dist/modules/forms/submission.js +27 -4
  158. package/dist/modules/forms/submission.js.map +1 -1
  159. package/dist/modules/hiring/roles.d.ts.map +1 -1
  160. package/dist/modules/hiring/roles.js +2 -1
  161. package/dist/modules/hiring/roles.js.map +1 -1
  162. package/dist/modules/import-export/addresses.d.ts +22 -0
  163. package/dist/modules/import-export/addresses.d.ts.map +1 -0
  164. package/dist/modules/import-export/addresses.js +83 -0
  165. package/dist/modules/import-export/addresses.js.map +1 -0
  166. package/dist/modules/import-export/drafts.d.ts +3 -4
  167. package/dist/modules/import-export/drafts.d.ts.map +1 -1
  168. package/dist/modules/import-export/drafts.js +4 -5
  169. package/dist/modules/import-export/drafts.js.map +1 -1
  170. package/dist/modules/import-export/exportRows.d.ts.map +1 -1
  171. package/dist/modules/import-export/exportRows.js +3 -3
  172. package/dist/modules/import-export/exportRows.js.map +1 -1
  173. package/dist/modules/import-export/plan.d.ts.map +1 -1
  174. package/dist/modules/import-export/plan.js +17 -2
  175. package/dist/modules/import-export/plan.js.map +1 -1
  176. package/dist/modules/import-export/presets.d.ts.map +1 -1
  177. package/dist/modules/import-export/presets.js +52 -7
  178. package/dist/modules/import-export/presets.js.map +1 -1
  179. package/dist/modules/import-export/repository.d.ts +3 -4
  180. package/dist/modules/import-export/repository.d.ts.map +1 -1
  181. package/dist/modules/import-export/repository.js +2 -3
  182. package/dist/modules/import-export/repository.js.map +1 -1
  183. package/dist/modules/import-export/writes.d.ts.map +1 -1
  184. package/dist/modules/import-export/writes.js +8 -4
  185. package/dist/modules/import-export/writes.js.map +1 -1
  186. package/dist/modules/lists/routes.d.ts +4 -0
  187. package/dist/modules/lists/routes.d.ts.map +1 -1
  188. package/dist/modules/notes/routes.d.ts +2 -0
  189. package/dist/modules/notes/routes.d.ts.map +1 -1
  190. package/dist/modules/people/repository.d.ts.map +1 -1
  191. package/dist/modules/people/repository.js +1 -1
  192. package/dist/modules/people/repository.js.map +1 -1
  193. package/dist/modules/people/routes.d.ts +2 -2
  194. package/dist/modules/people/routes.js +6 -6
  195. package/dist/modules/people/routes.js.map +1 -1
  196. package/dist/modules/people/schema.d.ts +17 -14
  197. package/dist/modules/people/schema.d.ts.map +1 -1
  198. package/dist/modules/people/schema.js +6 -4
  199. package/dist/modules/people/schema.js.map +1 -1
  200. package/dist/modules/people/service.d.ts +2 -2
  201. package/dist/modules/people/service.d.ts.map +1 -1
  202. package/dist/modules/people/service.js +5 -4
  203. package/dist/modules/people/service.js.map +1 -1
  204. package/dist/modules/plans/repository.d.ts +2 -2
  205. package/dist/modules/plans/repository.d.ts.map +1 -1
  206. package/dist/modules/plans/routes.d.ts +1 -0
  207. package/dist/modules/plans/routes.d.ts.map +1 -1
  208. package/dist/modules/plans/routes.js +4 -4
  209. package/dist/modules/plans/routes.js.map +1 -1
  210. package/dist/modules/plans/schema.d.ts +2 -2
  211. package/dist/modules/plans/schema.d.ts.map +1 -1
  212. package/dist/modules/plans/schema.js +3 -3
  213. package/dist/modules/plans/schema.js.map +1 -1
  214. package/dist/modules/plans/service.d.ts +2 -2
  215. package/dist/modules/plans/service.d.ts.map +1 -1
  216. package/dist/modules/plans/service.js.map +1 -1
  217. package/dist/modules/plans/tools.js +2 -2
  218. package/dist/modules/plans/tools.js.map +1 -1
  219. package/dist/modules/recordTargets.d.ts.map +1 -1
  220. package/dist/modules/recordTargets.js +11 -0
  221. package/dist/modules/recordTargets.js.map +1 -1
  222. package/dist/modules/references.d.ts.map +1 -1
  223. package/dist/modules/references.js +2 -0
  224. package/dist/modules/references.js.map +1 -1
  225. package/dist/modules/sample-data/fixture.d.ts +26 -4
  226. package/dist/modules/sample-data/fixture.d.ts.map +1 -1
  227. package/dist/modules/sample-data/fixture.js +157 -23
  228. package/dist/modules/sample-data/fixture.js.map +1 -1
  229. package/dist/modules/sample-data/index.d.ts +5 -2
  230. package/dist/modules/sample-data/index.d.ts.map +1 -1
  231. package/dist/modules/sample-data/index.js +7 -2
  232. package/dist/modules/sample-data/index.js.map +1 -1
  233. package/dist/modules/sample-data/routes.d.ts.map +1 -1
  234. package/dist/modules/sample-data/routes.js +2 -0
  235. package/dist/modules/sample-data/routes.js.map +1 -1
  236. package/dist/modules/sample-data/service.d.ts +11 -3
  237. package/dist/modules/sample-data/service.d.ts.map +1 -1
  238. package/dist/modules/sample-data/service.js +452 -363
  239. package/dist/modules/sample-data/service.js.map +1 -1
  240. package/dist/modules/sample-data/tools.d.ts.map +1 -1
  241. package/dist/modules/sample-data/tools.js +4 -3
  242. package/dist/modules/sample-data/tools.js.map +1 -1
  243. package/dist/modules/search/index.d.ts +1 -1
  244. package/dist/modules/search/index.d.ts.map +1 -1
  245. package/dist/modules/search/index.js +2 -1
  246. package/dist/modules/search/index.js.map +1 -1
  247. package/dist/modules/search/repository.d.ts +1 -0
  248. package/dist/modules/search/repository.d.ts.map +1 -1
  249. package/dist/modules/search/repository.js +22 -0
  250. package/dist/modules/search/repository.js.map +1 -1
  251. package/dist/modules/search/service.d.ts +2 -2
  252. package/dist/modules/search/service.d.ts.map +1 -1
  253. package/dist/modules/search/service.js +4 -3
  254. package/dist/modules/search/service.js.map +1 -1
  255. package/dist/modules/smtp-email/index.d.ts +12 -4
  256. package/dist/modules/smtp-email/index.d.ts.map +1 -1
  257. package/dist/modules/smtp-email/index.js +25 -5
  258. package/dist/modules/smtp-email/index.js.map +1 -1
  259. package/dist/modules/workspace/repository.d.ts +2 -0
  260. package/dist/modules/workspace/repository.d.ts.map +1 -1
  261. package/dist/modules/workspace/repository.js +12 -0
  262. package/dist/modules/workspace/repository.js.map +1 -1
  263. package/dist/modules/workspace/routes.d.ts.map +1 -1
  264. package/dist/modules/workspace/routes.js +13 -0
  265. package/dist/modules/workspace/routes.js.map +1 -1
  266. package/dist/modules/workspace/service.d.ts +10 -0
  267. package/dist/modules/workspace/service.d.ts.map +1 -1
  268. package/dist/modules/workspace/service.js +34 -10
  269. package/dist/modules/workspace/service.js.map +1 -1
  270. package/dist/modules/workspace/starters.d.ts +17 -2
  271. package/dist/modules/workspace/starters.d.ts.map +1 -1
  272. package/dist/modules/workspace/starters.js +141 -14
  273. package/dist/modules/workspace/starters.js.map +1 -1
  274. package/dist/modules/workspace/workspaceAccessMiddleware.d.ts.map +1 -1
  275. package/dist/modules/workspace/workspaceAccessMiddleware.js +3 -4
  276. package/dist/modules/workspace/workspaceAccessMiddleware.js.map +1 -1
  277. package/dist/runtime/events.d.ts +1 -1
  278. package/dist/runtime/events.d.ts.map +1 -1
  279. package/dist/runtime/events.js +2 -0
  280. package/dist/runtime/events.js.map +1 -1
  281. package/dist/runtime/module.d.ts +8 -0
  282. package/dist/runtime/module.d.ts.map +1 -1
  283. package/dist/runtime/registry.d.ts.map +1 -1
  284. package/dist/runtime/registry.js +102 -7
  285. package/dist/runtime/registry.js.map +1 -1
  286. package/dist/schema/index.d.ts +1 -0
  287. package/dist/schema/index.d.ts.map +1 -1
  288. package/dist/schema/index.js +1 -0
  289. package/dist/schema/index.js.map +1 -1
  290. package/dist/testing/app.d.ts +8 -0
  291. package/dist/testing/app.d.ts.map +1 -1
  292. package/dist/testing/app.js +6 -1
  293. package/dist/testing/app.js.map +1 -1
  294. package/dist/testing/client.d.ts.map +1 -1
  295. package/dist/testing/client.js +9 -1
  296. package/dist/testing/client.js.map +1 -1
  297. package/migrations/0044_events.sql +79 -0
  298. package/migrations/0045_addresses.sql +66 -0
  299. package/migrations/0046_drop_company_website.sql +66 -0
  300. package/migrations/meta/_journal.json +21 -0
  301. package/package.json +3 -3
  302. package/src/app.ts +25 -1
  303. package/src/boot.ts +1 -0
  304. package/src/lib/apiKeyScopes.ts +6 -0
  305. package/src/lib/columns.ts +12 -3
  306. package/src/lib/config.ts +21 -0
  307. package/src/lib/egress.ts +8 -4
  308. package/src/lib/errors.ts +16 -9
  309. package/src/lib/ids.ts +11 -0
  310. package/src/lib/kelpieConfigFile.ts +69 -1
  311. package/src/modules/agent-tasks/catalog.ts +33 -1
  312. package/src/modules/agent-tasks/index.ts +1 -0
  313. package/src/modules/agent-tasks/resolve.ts +11 -0
  314. package/src/modules/attachedRecords.ts +85 -13
  315. package/src/modules/auth/index.ts +1 -0
  316. package/src/modules/auth/repository.ts +34 -3
  317. package/src/modules/auth/routes.ts +44 -1
  318. package/src/modules/auth/service.ts +118 -4
  319. package/src/modules/companies/repository.ts +1 -0
  320. package/src/modules/companies/routes.ts +6 -11
  321. package/src/modules/companies/schema.ts +3 -3
  322. package/src/modules/companies/service.ts +6 -10
  323. package/src/modules/core.ts +2 -0
  324. package/src/modules/custom-fields/repository.ts +2 -0
  325. package/src/modules/dashboard/index.ts +1 -0
  326. package/src/modules/dashboard/repository.ts +64 -0
  327. package/src/modules/dashboard/routes.ts +15 -0
  328. package/src/modules/dashboard/service.ts +48 -0
  329. package/src/modules/decisions/repository.ts +2 -0
  330. package/src/modules/events/attendances.ts +245 -0
  331. package/src/modules/events/catalog.ts +36 -0
  332. package/src/modules/events/index.ts +54 -0
  333. package/src/modules/events/repository.ts +396 -0
  334. package/src/modules/events/routes.ts +405 -0
  335. package/src/modules/events/schema.ts +148 -0
  336. package/src/modules/events/service.ts +443 -0
  337. package/src/modules/events/tools.ts +118 -0
  338. package/src/modules/forms/applySubmissionFields.ts +7 -5
  339. package/src/modules/forms/fillAddresses.ts +185 -0
  340. package/src/modules/forms/repository.ts +3 -3
  341. package/src/modules/forms/routes.ts +2 -2
  342. package/src/modules/forms/schema.ts +2 -2
  343. package/src/modules/forms/service.ts +2 -2
  344. package/src/modules/forms/submission.ts +34 -4
  345. package/src/modules/hiring/roles.ts +2 -1
  346. package/src/modules/import-export/addresses.ts +150 -0
  347. package/src/modules/import-export/drafts.ts +7 -9
  348. package/src/modules/import-export/exportRows.ts +3 -3
  349. package/src/modules/import-export/plan.ts +30 -2
  350. package/src/modules/import-export/presets.ts +55 -7
  351. package/src/modules/import-export/repository.ts +5 -7
  352. package/src/modules/import-export/writes.ts +8 -4
  353. package/src/modules/people/repository.ts +1 -0
  354. package/src/modules/people/routes.ts +6 -6
  355. package/src/modules/people/schema.ts +7 -5
  356. package/src/modules/people/service.ts +7 -5
  357. package/src/modules/plans/repository.ts +2 -2
  358. package/src/modules/plans/routes.ts +6 -6
  359. package/src/modules/plans/schema.ts +4 -4
  360. package/src/modules/plans/service.ts +3 -3
  361. package/src/modules/plans/tools.ts +2 -2
  362. package/src/modules/recordTargets.ts +14 -0
  363. package/src/modules/references.ts +2 -0
  364. package/src/modules/sample-data/fixture.ts +208 -27
  365. package/src/modules/sample-data/index.ts +7 -2
  366. package/src/modules/sample-data/routes.ts +2 -0
  367. package/src/modules/sample-data/service.ts +166 -49
  368. package/src/modules/sample-data/tools.ts +4 -3
  369. package/src/modules/search/index.ts +2 -1
  370. package/src/modules/search/repository.ts +32 -1
  371. package/src/modules/search/service.ts +6 -4
  372. package/src/modules/smtp-email/index.ts +32 -10
  373. package/src/modules/workspace/repository.ts +18 -0
  374. package/src/modules/workspace/routes.ts +16 -0
  375. package/src/modules/workspace/service.ts +59 -14
  376. package/src/modules/workspace/starters.ts +275 -14
  377. package/src/modules/workspace/workspaceAccessMiddleware.ts +3 -4
  378. package/src/runtime/events.ts +2 -0
  379. package/src/runtime/module.ts +8 -0
  380. package/src/runtime/registry.ts +122 -8
  381. package/src/schema/index.ts +1 -0
  382. package/src/testing/app.ts +14 -1
  383. package/src/testing/client.ts +11 -1
@@ -1,28 +1,31 @@
1
- import { and, count, eq, inArray } from 'drizzle-orm'
1
+ import { and, eq, inArray } from 'drizzle-orm'
2
2
 
3
3
  import { toEventActor } from '../../lib/actor.ts'
4
+ import { UNIQUE_VIOLATION, postgresErrorCode } from '../../lib/database.ts'
4
5
  import type { Database } from '../../lib/database.ts'
5
6
  import { AppError } from '../../lib/errors.ts'
6
7
  import type { IdFactory } from '../../lib/ids.ts'
8
+ import type { EntitlementRegistry } from '../../runtime/entitlements.ts'
9
+ import { moduleCapabilityName } from '../../runtime/moduleConfig.ts'
7
10
  import type { TransactionScope } from '../../runtime/transaction.ts'
8
11
  import type { Actor } from '../auth/actor.ts'
9
12
  import { requireWorkspaceId } from '../auth/actor.ts'
10
13
  import * as authRepository from '../auth/repository.ts'
11
14
  import { companies } from '../companies/schema.ts'
12
15
  import { deals } from '../deals/schema.ts'
13
- import { candidates, roles } from '../hiring/schema.ts'
14
- import { notes } from '../notes/schema.ts'
15
16
  import { enquiries } from '../enquiries/schema.ts'
17
+ import { attendances, eventAssociations, events } from '../events/schema.ts'
18
+ import { candidates, roles } from '../hiring/schema.ts'
16
19
  import { opportunities } from '../opportunities/schema.ts'
17
20
  import { partnerships } from '../partnerships/schema.ts'
18
21
  import { people, personLinks } from '../people/schema.ts'
22
+ import { notes } from '../notes/schema.ts'
19
23
  import { pipelineStages } from '../pipelines/schema.ts'
20
24
  import { planItems } from '../plans/schema.ts'
21
25
  import { positions } from '../positions/schema.ts'
22
26
  import { raises } from '../raises/schema.ts'
23
27
  import { parseMemberRole, roleAllows } from '../workspace/roles.ts'
24
28
  import { SAMPLE_DATA_FIXTURE } from './fixture.ts'
25
- import type { Fixture } from './fixture.ts'
26
29
 
27
30
  /**
28
31
  * Installs the sample workspace, in one transaction.
@@ -30,9 +33,13 @@ import type { Fixture } from './fixture.ts'
30
33
  * The whole seed is one write. A failure part-way through rolls the whole
31
34
  * thing back, so a workspace never ends up with half of it.
32
35
  *
33
- * Idempotent by refusal: a workspace that already holds any companies or
34
- * people gets a 409. Installing twice would double the fixture, which is not
35
- * what an "install once" button says.
36
+ * Existing companies and people stay. The fixture is inserted next to them.
37
+ * A second install of the same fixture still fails: people emails and company
38
+ * domains are unique per workspace, and that unique violation becomes a 409.
39
+ *
40
+ * A workspace that has switched a toggleable module off (deals, opportunities,
41
+ * raises, partnerships, events) does not get fixture rows for that module.
42
+ * Companies, people, hiring, and enquiries still install.
36
43
  */
37
44
 
38
45
  export interface SampleDataDependencies {
@@ -40,6 +47,7 @@ export interface SampleDataDependencies {
40
47
  readonly transaction: TransactionScope
41
48
  readonly createId: IdFactory
42
49
  readonly now: () => Date
50
+ readonly entitlements: EntitlementRegistry
43
51
  }
44
52
 
45
53
  export interface SampleDataCounts {
@@ -55,6 +63,8 @@ export interface SampleDataCounts {
55
63
  readonly enquiries: number
56
64
  readonly roles: number
57
65
  readonly candidates: number
66
+ readonly events: number
67
+ readonly attendances: number
58
68
  }
59
69
 
60
70
  export interface SampleDataService {
@@ -97,6 +107,15 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
97
107
  }
98
108
  }
99
109
 
110
+ async function moduleIsOn(workspaceId: string, moduleId: string): Promise<boolean> {
111
+ const entitlement = await dependencies.entitlements.check(
112
+ workspaceId,
113
+ moduleCapabilityName(moduleId),
114
+ )
115
+
116
+ return entitlement.kind === 'flag' ? entitlement.granted : true
117
+ }
118
+
100
119
  return {
101
120
  async install(actor, workspaceId) {
102
121
  // The path parameter must match the actor's workspace, and it must be an
@@ -106,26 +125,15 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
106
125
  await requireAdmin(actor, workspaceId)
107
126
 
108
127
  const fixture = SAMPLE_DATA_FIXTURE
109
-
110
- return dependencies.transaction(
111
- async ({ tx }) => {
112
- const [companiesTally] = await tx
113
- .select({ value: count() })
114
- .from(companies)
115
- .where(eq(companies.workspaceId, workspaceId))
116
-
117
- const [peopleTally] = await tx
118
- .select({ value: count() })
119
- .from(people)
120
- .where(eq(people.workspaceId, workspaceId))
121
-
122
- const alreadyHasData =
123
- (companiesTally?.value ?? 0) > 0 || (peopleTally?.value ?? 0) > 0
124
-
125
- if (alreadyHasData) {
126
- throw AppError.conflict('This workspace already has CRM data')
127
- }
128
-
128
+ const includeDeals = await moduleIsOn(workspaceId, 'deals')
129
+ const includeOpportunities = await moduleIsOn(workspaceId, 'opportunities')
130
+ const includeRaises = await moduleIsOn(workspaceId, 'raises')
131
+ const includePartnerships = await moduleIsOn(workspaceId, 'partnerships')
132
+ const includeEvents = await moduleIsOn(workspaceId, 'events')
133
+
134
+ try {
135
+ return await dependencies.transaction(
136
+ async ({ tx }) => {
129
137
  // Every stage this workspace carries, grouped by kind so a deal, an
130
138
  // opportunity, a raise and a partnership all resolve their fixture
131
139
  // stage slugs against the same read.
@@ -165,6 +173,10 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
165
173
  const enquiryIds = new Map<string, string>()
166
174
  const roleIds = new Map<string, string>()
167
175
  const candidateIds = new Map<string, string>()
176
+ let planItemCount = 0
177
+ let notesWritten = 0
178
+ let eventCount = 0
179
+ let attendanceCount = 0
168
180
 
169
181
  for (const record of fixture.companies) {
170
182
  const id = dependencies.createId('company')
@@ -179,8 +191,7 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
179
191
  description: record.description,
180
192
  stage: record.stage,
181
193
  sizeBand: record.sizeBand,
182
- hq: record.hq,
183
- website: record.website,
194
+ addresses: [...record.addresses],
184
195
  accountType: record.accountType,
185
196
  icpFit: record.icpFit,
186
197
  techStack: [...record.techStack],
@@ -202,7 +213,7 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
202
213
  firstName: record.firstName,
203
214
  lastName: record.lastName,
204
215
  email: record.email,
205
- location: record.location,
216
+ addresses: [...record.addresses],
206
217
  preferredChannel: record.preferredChannel,
207
218
  influence: record.influence,
208
219
  relationship: record.relationship,
@@ -234,6 +245,7 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
234
245
  })
235
246
  }
236
247
 
248
+ if (includeDeals) {
237
249
  for (const record of fixture.deals) {
238
250
  const stageId = stageIdByKindAndSlug.get(stageLookupKey('deal', record.stageSlug))
239
251
  const companyId = companyIds.get(record.companyKey)
@@ -289,7 +301,9 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
289
301
  })
290
302
  }
291
303
  }
304
+ }
292
305
 
306
+ if (includeOpportunities) {
293
307
  for (const record of fixture.opportunities) {
294
308
  const stageId = stageIdByKindAndSlug.get(
295
309
  stageLookupKey('opportunity', record.stageSlug),
@@ -327,7 +341,9 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
327
341
  updatedAt: now,
328
342
  })
329
343
  }
344
+ }
330
345
 
346
+ if (includeRaises) {
331
347
  for (const record of fixture.raises) {
332
348
  const stageId = stageIdByKindAndSlug.get(stageLookupKey('raise', record.stageSlug))
333
349
  const companyId = companyIds.get(record.companyKey)
@@ -381,7 +397,9 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
381
397
  })
382
398
  }
383
399
  }
400
+ }
384
401
 
402
+ if (includePartnerships) {
385
403
  for (const record of fixture.partnerships) {
386
404
  const stageId = stageIdByKindAndSlug.get(
387
405
  stageLookupKey('partnership', record.stageSlug),
@@ -437,6 +455,7 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
437
455
  })
438
456
  }
439
457
  }
458
+ }
440
459
 
441
460
  for (const record of fixture.enquiries) {
442
461
  const stageId = stageIdByKindAndSlug.get(
@@ -552,7 +571,81 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
552
571
  })
553
572
  }
554
573
 
574
+ if (includeEvents) {
575
+ for (const record of fixture.events) {
576
+ const startsAt = new Date(now.getTime() + record.startOffsetHours * 60 * 60 * 1000)
577
+ const endsAt = new Date(startsAt.getTime() + record.durationHours * 60 * 60 * 1000)
578
+ const id = dependencies.createId('crmEvent')
579
+ eventCount += 1
580
+
581
+ await tx.insert(events).values({
582
+ id,
583
+ workspaceId,
584
+ name: record.name,
585
+ kind: record.kind,
586
+ startsAt,
587
+ endsAt,
588
+ timezone: record.timezone,
589
+ location: record.location,
590
+ meetingUrl: record.meetingUrl,
591
+ format: record.format,
592
+ details: record.details,
593
+ status: record.status,
594
+ summary: record.summary,
595
+ tags: [...record.tags],
596
+ createdAt: now,
597
+ updatedAt: now,
598
+ })
599
+
600
+ if (record.dealKey !== null && includeDeals) {
601
+ const dealId = dealIds.get(record.dealKey)
602
+
603
+ if (dealId === undefined) {
604
+ throw new Error(
605
+ `Sample event "${record.name}" names unknown deal "${record.dealKey}"`,
606
+ )
607
+ }
608
+
609
+ await tx.insert(eventAssociations).values({
610
+ id: dependencies.createId('eventAssociation'),
611
+ workspaceId,
612
+ eventId: id,
613
+ targetType: 'deal',
614
+ targetId: dealId,
615
+ createdAt: now,
616
+ })
617
+ }
618
+
619
+ for (const attendee of record.attendees) {
620
+ const personId = personIds.get(attendee.personKey)
621
+
622
+ if (personId === undefined) {
623
+ throw new Error(
624
+ `Sample event "${record.name}" names unknown person "${attendee.personKey}"`,
625
+ )
626
+ }
627
+
628
+ attendanceCount += 1
629
+
630
+ await tx.insert(attendances).values({
631
+ id: dependencies.createId('attendance'),
632
+ workspaceId,
633
+ eventId: id,
634
+ personId,
635
+ status: attendee.status,
636
+ source: attendee.source,
637
+ createdAt: now,
638
+ updatedAt: now,
639
+ })
640
+ }
641
+ }
642
+ }
643
+
555
644
  for (const record of fixture.plans) {
645
+ if (!includeDeals) {
646
+ continue
647
+ }
648
+
556
649
  const dealId = dealIds.get(record.targetDealKey)
557
650
 
558
651
  if (dealId === undefined) {
@@ -561,6 +654,8 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
561
654
  )
562
655
  }
563
656
 
657
+ planItemCount += 1
658
+
564
659
  await tx.insert(planItems).values({
565
660
  id: dependencies.createId('planItem'),
566
661
  workspaceId,
@@ -587,11 +682,17 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
587
682
  })
588
683
 
589
684
  if (targetId === undefined) {
685
+ if (isOptionalModuleNoteTarget(record.targetType)) {
686
+ continue
687
+ }
688
+
590
689
  throw new Error(
591
690
  `Sample note names unknown ${record.targetType} "${record.targetKey}"`,
592
691
  )
593
692
  }
594
693
 
694
+ notesWritten += 1
695
+
595
696
  await tx.insert(notes).values({
596
697
  id: dependencies.createId('note'),
597
698
  workspaceId,
@@ -604,10 +705,34 @@ export function createSampleDataService(dependencies: SampleDataDependencies): S
604
705
  })
605
706
  }
606
707
 
607
- return countsFor(fixture)
608
- },
609
- { workspaceId, actor: toEventActor(actor) },
610
- )
708
+ return {
709
+ companies: fixture.companies.length,
710
+ people: fixture.people.length,
711
+ positions: fixture.positions.length,
712
+ deals: dealIds.size,
713
+ planItems: planItemCount,
714
+ notes: notesWritten,
715
+ opportunities: opportunityIds.size,
716
+ raises: raiseIds.size,
717
+ partnerships: partnershipIds.size,
718
+ enquiries: enquiryIds.size,
719
+ roles: roleIds.size,
720
+ candidates: candidateIds.size,
721
+ events: eventCount,
722
+ attendances: attendanceCount,
723
+ }
724
+ },
725
+ { workspaceId, actor: toEventActor(actor) },
726
+ )
727
+ } catch (error: unknown) {
728
+ if (postgresErrorCode(error) === UNIQUE_VIOLATION) {
729
+ throw AppError.conflict(
730
+ 'A company domain or person email in the sample set already exists in this workspace',
731
+ )
732
+ }
733
+
734
+ throw error
735
+ }
611
736
  },
612
737
  }
613
738
  }
@@ -661,19 +786,11 @@ function stageLookupKey(kind: string, slug: string): string {
661
786
  return `${kind}::${slug}`
662
787
  }
663
788
 
664
- function countsFor(fixture: Fixture): SampleDataCounts {
665
- return {
666
- companies: fixture.companies.length,
667
- people: fixture.people.length,
668
- positions: fixture.positions.length,
669
- deals: fixture.deals.length,
670
- planItems: fixture.plans.length,
671
- notes: fixture.notes.length,
672
- opportunities: fixture.opportunities.length,
673
- raises: fixture.raises.length,
674
- partnerships: fixture.partnerships.length,
675
- enquiries: fixture.enquiries.length,
676
- roles: fixture.roles.length,
677
- candidates: fixture.candidates.length,
678
- }
789
+ function isOptionalModuleNoteTarget(targetType: string): boolean {
790
+ return (
791
+ targetType === 'deal' ||
792
+ targetType === 'opportunity' ||
793
+ targetType === 'raise' ||
794
+ targetType === 'partnership'
795
+ )
679
796
  }
@@ -8,8 +8,9 @@ import type { SampleDataService } from './service.ts'
8
8
  /**
9
9
  * `sample_data_install`: the MCP mirror of `POST /v1/workspaces/:id/sample-data`.
10
10
  *
11
- * Same admin check, same conflict on a workspace that already has data. The
12
- * tool takes no arguments: it installs into the actor's own workspace.
11
+ * Same admin check. Adds the fixture next to existing records. Fails when a
12
+ * sample email or domain is already in the workspace. The tool takes no
13
+ * arguments: it installs into the actor's own workspace.
13
14
  */
14
15
 
15
16
  const installArgs = z.strictObject({})
@@ -21,7 +22,7 @@ export function registerSampleDataTools(
21
22
  mcp.tool({
22
23
  name: 'sample_data_install',
23
24
  description:
24
- 'Populate this workspace with a small sample of companies, people, positions, deals, plans and notes. Refuses if the workspace already has data.',
25
+ 'Populate this workspace with a small sample of companies, people, and other CRM records. Adds them next to existing records. Skips objects for modules that are switched off. Fails if a sample email or domain already exists.',
25
26
  inputSchema: installArgs,
26
27
  async invoke(_args, actor) {
27
28
  const counts = await service.install(actor, requireWorkspaceId(actor))
@@ -4,7 +4,7 @@ import { createSearchService } from './service.ts'
4
4
  import { registerSearchTools } from './tools.ts'
5
5
 
6
6
  /**
7
- * One box across ten collections.
7
+ * One box across twelve collections.
8
8
  *
9
9
  * It contributes no tables and calls no `context.schema`. The `search_vector`
10
10
  * column each searchable table carries belongs to that table's own module, which
@@ -34,6 +34,7 @@ export function createSearchModule(): KelpieModule {
34
34
  'opportunities',
35
35
  'partnerships',
36
36
  'raises',
37
+ 'events',
37
38
  'hiring',
38
39
  'plans',
39
40
  'decisions',
@@ -18,6 +18,7 @@ import { pipelineStages } from '../pipelines/schema.ts'
18
18
  import { planItems } from '../plans/schema.ts'
19
19
  import { positions } from '../positions/schema.ts'
20
20
  import { raises } from '../raises/schema.ts'
21
+ import { events } from '../events/schema.ts'
21
22
 
22
23
  /**
23
24
  * The reads behind `GET /v1/search`: one query per collection, each against that
@@ -211,7 +212,7 @@ function byPlanItem(
211
212
  db: Queryable,
212
213
  workspaceId: string,
213
214
  query: SQL,
214
- targetType: 'deal' | 'opportunity' | 'raise' | 'enquiry',
215
+ targetType: 'deal' | 'opportunity' | 'raise' | 'enquiry' | 'event',
215
216
  ) {
216
217
  return db
217
218
  .select({ id: planItems.targetId, rank: relatedRank(planItems.searchVector, query).as('rank') })
@@ -370,6 +371,36 @@ export async function searchPartnerships(
370
371
  return collect('partnership', rows)
371
372
  }
372
373
 
374
+ export async function searchEvents(
375
+ db: Queryable,
376
+ workspaceId: string,
377
+ query: SQL,
378
+ limit: number,
379
+ ): Promise<CollectionHits> {
380
+ const own = db
381
+ .select({ id: events.id, rank: rank(events.searchVector, query).as('rank') })
382
+ .from(events)
383
+ .where(and(eq(events.workspaceId, workspaceId), matches(events.searchVector, query)))
384
+
385
+ const hits = unionAll(own, byPlanItem(db, workspaceId, query, 'event')).as('hits')
386
+
387
+ const rows = await db
388
+ .select({
389
+ id: events.id,
390
+ title: events.name,
391
+ subtitle: events.kind,
392
+ snippetSource: events.summary,
393
+ total,
394
+ })
395
+ .from(hits)
396
+ .innerJoin(events, eq(events.id, hits.id))
397
+ .groupBy(events.id)
398
+ .orderBy(desc(bestRank(hits.rank)), asc(events.id))
399
+ .limit(limit)
400
+
401
+ return collect('event', rows)
402
+ }
403
+
373
404
  export async function searchDecisions(
374
405
  db: Queryable,
375
406
  workspaceId: string,
@@ -11,6 +11,7 @@ import {
11
11
  searchDeals,
12
12
  searchDecisions,
13
13
  searchEnquiries,
14
+ searchEvents,
14
15
  searchHandbookPages,
15
16
  searchOpportunities,
16
17
  searchPartnerships,
@@ -21,7 +22,7 @@ import {
21
22
  import type { CollectionHits } from './repository.ts'
22
23
 
23
24
  /**
24
- * One box across ten collections, ranked per collection rather than merged.
25
+ * One box across twelve collections, ranked per collection rather than merged.
25
26
  *
26
27
  * A single merged ranking would need `ts_rank` values from different tables to
27
28
  * mean the same thing, and they do not: a handbook page is a thousand words and a
@@ -50,7 +51,7 @@ export interface SearchResults {
50
51
  readonly limit: number
51
52
  /** Across every group. The number a "12 results" heading is built from. */
52
53
  readonly total: number
53
- /** Always all ten, in `SEARCH_COLLECTIONS` order, empty ones included. */
54
+ /** Always every collection, in `SEARCH_COLLECTIONS` order, empty ones included. */
54
55
  readonly groups: readonly SearchGroup[]
55
56
  }
56
57
 
@@ -95,6 +96,7 @@ const SEARCHES: Readonly<Record<SearchCollection, CollectionSearch>> = {
95
96
  opportunity: searchOpportunities,
96
97
  raise: searchRaises,
97
98
  partnership: searchPartnerships,
99
+ event: searchEvents,
98
100
  decision: searchDecisions,
99
101
  }
100
102
 
@@ -136,8 +138,8 @@ export function createSearchService(dependencies: SearchServiceDependencies): Se
136
138
  const query = compileQuery(tsQuery)
137
139
  const words = searchWords(request.term)
138
140
 
139
- // Ten independent reads with nothing to share but the compiled query. Run
140
- // in sequence they would be ten round trips deep rather than wide.
141
+ // Independent reads with nothing to share but the compiled query. Run
142
+ // in sequence they would be round trips deep rather than wide.
141
143
  const groups = await Promise.all(
142
144
  wanted.map(async (collection) =>
143
145
  toGroup(await SEARCHES[collection](dependencies.db, workspaceId, query, limit), words),
@@ -15,6 +15,12 @@ import type { KelpieModule } from '../../runtime/module.ts'
15
15
  * or another provider module's name). Reads `EMAIL_FROM` and `SMTP_*` from the
16
16
  * environment; missing or malformed values fail boot.
17
17
  *
18
+ * `SMTP_USER` and `SMTP_PASSWORD` are optional and go together. Set both to
19
+ * authenticate against the relay. Omit both to connect without SMTP AUTH,
20
+ * which is how local catch-alls (maildev, MailHog, smtp4dev) work. Setting
21
+ * exactly one fails boot: a half-configured credential pair is a bug, not a
22
+ * mode.
23
+ *
18
24
  * A commercial email integration (Resend, Postmark) belongs in its own module
19
25
  * following the same shape, registered under a different name.
20
26
  */
@@ -27,14 +33,26 @@ export const SMTP_EMAIL_PROVIDER = 'smtp'
27
33
 
28
34
  export type { EmailMessage } from '../../lib/email.ts'
29
35
 
30
- const smtpEmailConfigSchema = z.object({
31
- EMAIL_FROM: z.string().min(1),
32
- SMTP_HOST: z.string().min(1),
33
- SMTP_PORT: z.coerce.number().int().positive().max(65535),
34
- SMTP_SECURE: z.enum(['true', 'false']).transform((value) => value === 'true'),
35
- SMTP_USER: z.string().min(1),
36
- SMTP_PASSWORD: z.string().min(1),
37
- })
36
+ export const smtpEmailConfigSchema = z
37
+ .object({
38
+ EMAIL_FROM: z.string().min(1),
39
+ SMTP_HOST: z.string().min(1),
40
+ SMTP_PORT: z.coerce.number().int().positive().max(65535),
41
+ SMTP_SECURE: z.enum(['true', 'false']).transform((value) => value === 'true'),
42
+ SMTP_USER: z.string().min(1).optional(),
43
+ SMTP_PASSWORD: z.string().min(1).optional(),
44
+ })
45
+ .superRefine((config, ctx) => {
46
+ // A half-set pair is a misconfiguration, not a mode. Report both keys so
47
+ // the operator sees which side of the pair is missing without having to
48
+ // guess.
49
+ if ((config.SMTP_USER === undefined) !== (config.SMTP_PASSWORD === undefined)) {
50
+ const message = 'SMTP_USER and SMTP_PASSWORD must be set together, or both omitted for an unauthenticated relay'
51
+
52
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message, path: ['SMTP_USER'] })
53
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message, path: ['SMTP_PASSWORD'] })
54
+ }
55
+ })
38
56
 
39
57
  export type SmtpEmailConfig = z.infer<typeof smtpEmailConfigSchema>
40
58
 
@@ -54,11 +72,13 @@ export interface SmtpTransport {
54
72
  }
55
73
 
56
74
  function createNodemailerTransport(config: SmtpEmailConfig): SmtpTransport {
75
+ const authenticated = config.SMTP_USER !== undefined && config.SMTP_PASSWORD !== undefined
76
+
57
77
  return nodemailer.createTransport({
58
78
  host: config.SMTP_HOST,
59
79
  port: config.SMTP_PORT,
60
80
  secure: config.SMTP_SECURE,
61
- auth: { user: config.SMTP_USER, pass: config.SMTP_PASSWORD },
81
+ ...(authenticated ? { auth: { user: config.SMTP_USER, pass: config.SMTP_PASSWORD } } : {}),
62
82
  })
63
83
  }
64
84
 
@@ -100,7 +120,9 @@ export interface SmtpEmailModuleOptions {
100
120
  * The built-in SMTP module. Registers a factory under `SMTP_EMAIL_PROVIDER`;
101
121
  * the factory reads the SMTP environment and builds the nodemailer sender, but
102
122
  * only if the assembly's `email.provider` picks this module. Boot fails loudly
103
- * if any SMTP variable is missing or malformed at that point.
123
+ * on a missing or malformed required variable, or on a half-set
124
+ * `SMTP_USER` / `SMTP_PASSWORD` pair. Omitting both credentials is valid and
125
+ * makes the module connect without SMTP AUTH.
104
126
  *
105
127
  * The module stays in `coreModules` unconditionally: on any other provider
106
128
  * (e.g. `EMAIL_PROVIDER=log`) the factory never runs, so the SMTP environment
@@ -6,6 +6,7 @@ import { users } from '../auth/schema.ts'
6
6
  import { consentPurposes } from '../consent-purposes/schema.ts'
7
7
  import { deals } from '../deals/schema.ts'
8
8
  import { decisions } from '../decisions/schema.ts'
9
+ import { events } from '../events/schema.ts'
9
10
  import { handbookPages } from '../handbook/schema.ts'
10
11
  import { notes } from '../notes/schema.ts'
11
12
  import { opportunities } from '../opportunities/schema.ts'
@@ -138,6 +139,7 @@ export async function countMemberReferences(
138
139
  ): Promise<readonly { readonly type: string; readonly count: number }[]> {
139
140
  const sources = [
140
141
  { type: 'deal', table: deals, column: deals.ownerId, workspaceId: deals.workspaceId },
142
+ { type: 'event', table: events, column: events.ownerId, workspaceId: events.workspaceId },
141
143
  { type: 'opportunity', table: opportunities, column: opportunities.ownerId, workspaceId: opportunities.workspaceId },
142
144
  { type: 'partnership', table: partnerships, column: partnerships.ownerId, workspaceId: partnerships.workspaceId },
143
145
  { type: 'raise', table: raises, column: raises.ownerId, workspaceId: raises.workspaceId },
@@ -366,6 +368,22 @@ export async function deleteInvite(db: Queryable, id: string): Promise<void> {
366
368
  await db.delete(invites).where(eq(invites.id, id))
367
369
  }
368
370
 
371
+ export async function deleteAllHandbookPages(db: Queryable, workspaceId: string): Promise<number> {
372
+ const deleted = await db
373
+ .delete(handbookPages)
374
+ .where(eq(handbookPages.workspaceId, workspaceId))
375
+ .returning({ id: handbookPages.id })
376
+
377
+ return deleted.length
378
+ }
379
+
380
+ export async function listHandbookPages(
381
+ db: Queryable,
382
+ workspaceId: string,
383
+ ): Promise<(typeof handbookPages.$inferSelect)[]> {
384
+ return db.select().from(handbookPages).where(eq(handbookPages.workspaceId, workspaceId))
385
+ }
386
+
369
387
  export async function insertHandbookPages(
370
388
  db: Queryable,
371
389
  values: readonly (typeof handbookPages.$inferInsert)[],
@@ -9,6 +9,7 @@ import { resolveActorFrom } from '../auth/credentials.ts'
9
9
  import type { CredentialDependencies } from '../auth/credentials.ts'
10
10
  import { INVITABLE_ROLES, MEMBER_ROLES } from './roles.ts'
11
11
  import type { InviteView, MemberView, ModuleSettingView, WorkspaceService, WorkspaceView } from './service.ts'
12
+ import { HANDBOOK_TEMPLATE_IDS } from './starters.ts'
12
13
 
13
14
  /** Wire shapes for `/v1/workspaces`, per `onboarding.md`'s API sketch. */
14
15
 
@@ -21,6 +22,11 @@ const createBody = z.object({
21
22
  timezone: timezoneSchema,
22
23
  })
23
24
 
25
+ const seedHandbookBodySchema = z.object({
26
+ handbook_template: z.enum(HANDBOOK_TEMPLATE_IDS),
27
+ replace: z.boolean().optional().default(false),
28
+ })
29
+
24
30
  export const updateBody = z
25
31
  .object({
26
32
  name: z.string().min(1),
@@ -106,6 +112,16 @@ export function mountWorkspaceRoutes(router: Hono, dependencies: WorkspaceRoutes
106
112
  return context.json(workspaceResponse(workspace), 201)
107
113
  })
108
114
 
115
+ router.post('/workspaces/:id/handbook/seed', async (context) => {
116
+ const body = await readBody(context, seedHandbookBodySchema)
117
+ const result = await dependencies.service.seedHandbook(await requireActor(context), context.req.param('id'), {
118
+ handbookTemplate: body.handbook_template,
119
+ replace: body.replace,
120
+ })
121
+
122
+ return context.json({ handbook_pages: result.handbookPages }, 201)
123
+ })
124
+
109
125
  router.get('/workspaces/:id', async (context) => {
110
126
  const workspace = await dependencies.service.get(await requireActor(context), context.req.param('id'))
111
127