@kelpie/server 0.5.2 → 0.7.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 (406) hide show
  1. package/README.md +5 -2
  2. package/dist/app.d.ts +13 -0
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +9 -0
  5. package/dist/app.js.map +1 -1
  6. package/dist/boot.d.ts +42 -0
  7. package/dist/boot.d.ts.map +1 -0
  8. package/dist/boot.js +57 -0
  9. package/dist/boot.js.map +1 -0
  10. package/dist/index.d.ts +20 -8
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +13 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/lib/actor.d.ts +9 -1
  15. package/dist/lib/actor.d.ts.map +1 -1
  16. package/dist/lib/actor.js +13 -0
  17. package/dist/lib/actor.js.map +1 -1
  18. package/dist/lib/config.d.ts +51 -6
  19. package/dist/lib/config.d.ts.map +1 -1
  20. package/dist/lib/config.js +11 -5
  21. package/dist/lib/config.js.map +1 -1
  22. package/dist/lib/email.d.ts +15 -63
  23. package/dist/lib/email.d.ts.map +1 -1
  24. package/dist/lib/email.js +13 -65
  25. package/dist/lib/email.js.map +1 -1
  26. package/dist/lib/fromEnv.d.ts +60 -0
  27. package/dist/lib/fromEnv.d.ts.map +1 -0
  28. package/dist/lib/fromEnv.js +94 -0
  29. package/dist/lib/fromEnv.js.map +1 -0
  30. package/dist/lib/ids.d.ts +6 -0
  31. package/dist/lib/ids.d.ts.map +1 -1
  32. package/dist/lib/ids.js +6 -0
  33. package/dist/lib/ids.js.map +1 -1
  34. package/dist/lib/kelpieConfigFile.d.ts +129 -0
  35. package/dist/lib/kelpieConfigFile.d.ts.map +1 -0
  36. package/dist/lib/kelpieConfigFile.js +112 -0
  37. package/dist/lib/kelpieConfigFile.js.map +1 -0
  38. package/dist/lib/logger.d.ts +38 -7
  39. package/dist/lib/logger.d.ts.map +1 -1
  40. package/dist/lib/logger.js +58 -20
  41. package/dist/lib/logger.js.map +1 -1
  42. package/dist/modules/agent-tasks/index.d.ts.map +1 -1
  43. package/dist/modules/agent-tasks/index.js +4 -1
  44. package/dist/modules/agent-tasks/index.js.map +1 -1
  45. package/dist/modules/agent-tasks/prompt.d.ts +36 -6
  46. package/dist/modules/agent-tasks/prompt.d.ts.map +1 -1
  47. package/dist/modules/agent-tasks/prompt.js +33 -4
  48. package/dist/modules/agent-tasks/prompt.js.map +1 -1
  49. package/dist/modules/agent-tasks/service.d.ts.map +1 -1
  50. package/dist/modules/agent-tasks/service.js +3 -2
  51. package/dist/modules/agent-tasks/service.js.map +1 -1
  52. package/dist/modules/agent-tasks/wire.d.ts +11 -0
  53. package/dist/modules/agent-tasks/wire.d.ts.map +1 -1
  54. package/dist/modules/agent-tasks/wire.js +1 -0
  55. package/dist/modules/agent-tasks/wire.js.map +1 -1
  56. package/dist/modules/auth/index.d.ts.map +1 -1
  57. package/dist/modules/auth/index.js +8 -7
  58. package/dist/modules/auth/index.js.map +1 -1
  59. package/dist/modules/companies/events.d.ts +22 -0
  60. package/dist/modules/companies/events.d.ts.map +1 -0
  61. package/dist/modules/companies/events.js +8 -0
  62. package/dist/modules/companies/events.js.map +1 -0
  63. package/dist/modules/companies/index.d.ts.map +1 -1
  64. package/dist/modules/companies/index.js +2 -0
  65. package/dist/modules/companies/index.js.map +1 -1
  66. package/dist/modules/companies/service.d.ts +1 -0
  67. package/dist/modules/companies/service.d.ts.map +1 -1
  68. package/dist/modules/companies/service.js +8 -11
  69. package/dist/modules/companies/service.js.map +1 -1
  70. package/dist/modules/core.d.ts.map +1 -1
  71. package/dist/modules/core.js +4 -0
  72. package/dist/modules/core.js.map +1 -1
  73. package/dist/modules/deals/events.d.ts +31 -0
  74. package/dist/modules/deals/events.d.ts.map +1 -0
  75. package/dist/modules/deals/events.js +12 -0
  76. package/dist/modules/deals/events.js.map +1 -0
  77. package/dist/modules/deals/index.d.ts.map +1 -1
  78. package/dist/modules/deals/index.js +2 -0
  79. package/dist/modules/deals/index.js.map +1 -1
  80. package/dist/modules/deals/service.d.ts +1 -0
  81. package/dist/modules/deals/service.d.ts.map +1 -1
  82. package/dist/modules/deals/service.js +9 -18
  83. package/dist/modules/deals/service.js.map +1 -1
  84. package/dist/modules/decisions/events.d.ts +21 -0
  85. package/dist/modules/decisions/events.d.ts.map +1 -0
  86. package/dist/modules/decisions/events.js +11 -0
  87. package/dist/modules/decisions/events.js.map +1 -0
  88. package/dist/modules/decisions/index.d.ts.map +1 -1
  89. package/dist/modules/decisions/index.js +2 -0
  90. package/dist/modules/decisions/index.js.map +1 -1
  91. package/dist/modules/decisions/service.d.ts +1 -0
  92. package/dist/modules/decisions/service.d.ts.map +1 -1
  93. package/dist/modules/decisions/service.js +4 -7
  94. package/dist/modules/decisions/service.js.map +1 -1
  95. package/dist/modules/eventCatalogs.d.ts +35 -0
  96. package/dist/modules/eventCatalogs.d.ts.map +1 -0
  97. package/dist/modules/eventCatalogs.js +35 -0
  98. package/dist/modules/eventCatalogs.js.map +1 -0
  99. package/dist/modules/forms/events.d.ts +37 -0
  100. package/dist/modules/forms/events.d.ts.map +1 -0
  101. package/dist/modules/forms/events.js +15 -0
  102. package/dist/modules/forms/events.js.map +1 -0
  103. package/dist/modules/forms/index.d.ts.map +1 -1
  104. package/dist/modules/forms/index.js +2 -0
  105. package/dist/modules/forms/index.js.map +1 -1
  106. package/dist/modules/forms/service.d.ts +1 -0
  107. package/dist/modules/forms/service.d.ts.map +1 -1
  108. package/dist/modules/forms/service.js +8 -11
  109. package/dist/modules/forms/service.js.map +1 -1
  110. package/dist/modules/forms/submission.d.ts +5 -0
  111. package/dist/modules/forms/submission.d.ts.map +1 -1
  112. package/dist/modules/forms/submission.js +54 -28
  113. package/dist/modules/forms/submission.js.map +1 -1
  114. package/dist/modules/handbook/events.d.ts +28 -0
  115. package/dist/modules/handbook/events.d.ts.map +1 -0
  116. package/dist/modules/handbook/events.js +14 -0
  117. package/dist/modules/handbook/events.js.map +1 -0
  118. package/dist/modules/handbook/index.d.ts.map +1 -1
  119. package/dist/modules/handbook/index.js +2 -0
  120. package/dist/modules/handbook/index.js.map +1 -1
  121. package/dist/modules/handbook/service.d.ts +1 -0
  122. package/dist/modules/handbook/service.d.ts.map +1 -1
  123. package/dist/modules/handbook/service.js +8 -15
  124. package/dist/modules/handbook/service.js.map +1 -1
  125. package/dist/modules/hiring/candidates.d.ts +1 -0
  126. package/dist/modules/hiring/candidates.d.ts.map +1 -1
  127. package/dist/modules/hiring/candidates.js +8 -11
  128. package/dist/modules/hiring/candidates.js.map +1 -1
  129. package/dist/modules/hiring/events.d.ts +35 -0
  130. package/dist/modules/hiring/events.d.ts.map +1 -0
  131. package/dist/modules/hiring/events.js +11 -0
  132. package/dist/modules/hiring/events.js.map +1 -0
  133. package/dist/modules/hiring/index.d.ts.map +1 -1
  134. package/dist/modules/hiring/index.js +2 -0
  135. package/dist/modules/hiring/index.js.map +1 -1
  136. package/dist/modules/hiring/roles.d.ts +1 -0
  137. package/dist/modules/hiring/roles.d.ts.map +1 -1
  138. package/dist/modules/hiring/roles.js +8 -11
  139. package/dist/modules/hiring/roles.js.map +1 -1
  140. package/dist/modules/import-export/drafts.d.ts +9 -0
  141. package/dist/modules/import-export/drafts.d.ts.map +1 -1
  142. package/dist/modules/import-export/drafts.js +25 -0
  143. package/dist/modules/import-export/drafts.js.map +1 -1
  144. package/dist/modules/import-export/events.d.ts +22 -0
  145. package/dist/modules/import-export/events.d.ts.map +1 -0
  146. package/dist/modules/import-export/events.js +12 -0
  147. package/dist/modules/import-export/events.js.map +1 -0
  148. package/dist/modules/import-export/exportRows.d.ts +9 -0
  149. package/dist/modules/import-export/exportRows.d.ts.map +1 -1
  150. package/dist/modules/import-export/exportRows.js +10 -1
  151. package/dist/modules/import-export/exportRows.js.map +1 -1
  152. package/dist/modules/import-export/index.d.ts.map +1 -1
  153. package/dist/modules/import-export/index.js +2 -0
  154. package/dist/modules/import-export/index.js.map +1 -1
  155. package/dist/modules/import-export/plan.d.ts +23 -1
  156. package/dist/modules/import-export/plan.d.ts.map +1 -1
  157. package/dist/modules/import-export/plan.js +62 -3
  158. package/dist/modules/import-export/plan.js.map +1 -1
  159. package/dist/modules/import-export/presets.d.ts.map +1 -1
  160. package/dist/modules/import-export/presets.js +25 -0
  161. package/dist/modules/import-export/presets.js.map +1 -1
  162. package/dist/modules/import-export/repository.d.ts +17 -0
  163. package/dist/modules/import-export/repository.d.ts.map +1 -1
  164. package/dist/modules/import-export/repository.js +21 -1
  165. package/dist/modules/import-export/repository.js.map +1 -1
  166. package/dist/modules/import-export/routes.d.ts.map +1 -1
  167. package/dist/modules/import-export/routes.js +5 -1
  168. package/dist/modules/import-export/routes.js.map +1 -1
  169. package/dist/modules/import-export/schema.d.ts +55 -0
  170. package/dist/modules/import-export/schema.d.ts.map +1 -1
  171. package/dist/modules/import-export/schema.js +13 -1
  172. package/dist/modules/import-export/schema.js.map +1 -1
  173. package/dist/modules/import-export/service.d.ts +9 -1
  174. package/dist/modules/import-export/service.d.ts.map +1 -1
  175. package/dist/modules/import-export/service.js +104 -17
  176. package/dist/modules/import-export/service.js.map +1 -1
  177. package/dist/modules/import-export/tools.d.ts.map +1 -1
  178. package/dist/modules/import-export/tools.js +7 -1
  179. package/dist/modules/import-export/tools.js.map +1 -1
  180. package/dist/modules/import-export/writes.d.ts +16 -0
  181. package/dist/modules/import-export/writes.d.ts.map +1 -1
  182. package/dist/modules/import-export/writes.js +77 -2
  183. package/dist/modules/import-export/writes.js.map +1 -1
  184. package/dist/modules/notes/events.d.ts +22 -0
  185. package/dist/modules/notes/events.d.ts.map +1 -0
  186. package/dist/modules/notes/events.js +12 -0
  187. package/dist/modules/notes/events.js.map +1 -0
  188. package/dist/modules/notes/index.d.ts.map +1 -1
  189. package/dist/modules/notes/index.js +2 -0
  190. package/dist/modules/notes/index.js.map +1 -1
  191. package/dist/modules/notes/service.d.ts +1 -0
  192. package/dist/modules/notes/service.d.ts.map +1 -1
  193. package/dist/modules/notes/service.js +4 -7
  194. package/dist/modules/notes/service.js.map +1 -1
  195. package/dist/modules/opportunities/events.d.ts +31 -0
  196. package/dist/modules/opportunities/events.d.ts.map +1 -0
  197. package/dist/modules/opportunities/events.js +12 -0
  198. package/dist/modules/opportunities/events.js.map +1 -0
  199. package/dist/modules/opportunities/index.d.ts.map +1 -1
  200. package/dist/modules/opportunities/index.js +2 -0
  201. package/dist/modules/opportunities/index.js.map +1 -1
  202. package/dist/modules/opportunities/service.d.ts +1 -0
  203. package/dist/modules/opportunities/service.d.ts.map +1 -1
  204. package/dist/modules/opportunities/service.js +9 -18
  205. package/dist/modules/opportunities/service.js.map +1 -1
  206. package/dist/modules/partnerships/events.d.ts +31 -0
  207. package/dist/modules/partnerships/events.d.ts.map +1 -0
  208. package/dist/modules/partnerships/events.js +12 -0
  209. package/dist/modules/partnerships/events.js.map +1 -0
  210. package/dist/modules/partnerships/index.d.ts.map +1 -1
  211. package/dist/modules/partnerships/index.js +2 -0
  212. package/dist/modules/partnerships/index.js.map +1 -1
  213. package/dist/modules/partnerships/service.d.ts +1 -0
  214. package/dist/modules/partnerships/service.d.ts.map +1 -1
  215. package/dist/modules/partnerships/service.js +9 -18
  216. package/dist/modules/partnerships/service.js.map +1 -1
  217. package/dist/modules/people/events.d.ts +28 -0
  218. package/dist/modules/people/events.d.ts.map +1 -0
  219. package/dist/modules/people/events.js +18 -0
  220. package/dist/modules/people/events.js.map +1 -0
  221. package/dist/modules/people/index.d.ts.map +1 -1
  222. package/dist/modules/people/index.js +2 -0
  223. package/dist/modules/people/index.js.map +1 -1
  224. package/dist/modules/people/service.d.ts +1 -0
  225. package/dist/modules/people/service.d.ts.map +1 -1
  226. package/dist/modules/people/service.js +8 -11
  227. package/dist/modules/people/service.js.map +1 -1
  228. package/dist/modules/pipelines/service.d.ts +4 -0
  229. package/dist/modules/pipelines/service.d.ts.map +1 -1
  230. package/dist/modules/pipelines/service.js +31 -8
  231. package/dist/modules/pipelines/service.js.map +1 -1
  232. package/dist/modules/plans/events.d.ts +21 -0
  233. package/dist/modules/plans/events.d.ts.map +1 -0
  234. package/dist/modules/plans/events.js +11 -0
  235. package/dist/modules/plans/events.js.map +1 -0
  236. package/dist/modules/plans/index.d.ts.map +1 -1
  237. package/dist/modules/plans/index.js +2 -0
  238. package/dist/modules/plans/index.js.map +1 -1
  239. package/dist/modules/plans/service.d.ts +1 -0
  240. package/dist/modules/plans/service.d.ts.map +1 -1
  241. package/dist/modules/plans/service.js +6 -14
  242. package/dist/modules/plans/service.js.map +1 -1
  243. package/dist/modules/positions/events.d.ts +22 -0
  244. package/dist/modules/positions/events.d.ts.map +1 -0
  245. package/dist/modules/positions/events.js +8 -0
  246. package/dist/modules/positions/events.js.map +1 -0
  247. package/dist/modules/positions/index.d.ts.map +1 -1
  248. package/dist/modules/positions/index.js +2 -0
  249. package/dist/modules/positions/index.js.map +1 -1
  250. package/dist/modules/positions/service.d.ts +1 -0
  251. package/dist/modules/positions/service.d.ts.map +1 -1
  252. package/dist/modules/positions/service.js +8 -11
  253. package/dist/modules/positions/service.js.map +1 -1
  254. package/dist/modules/raises/events.d.ts +31 -0
  255. package/dist/modules/raises/events.d.ts.map +1 -0
  256. package/dist/modules/raises/events.js +12 -0
  257. package/dist/modules/raises/events.js.map +1 -0
  258. package/dist/modules/raises/index.d.ts.map +1 -1
  259. package/dist/modules/raises/index.js +2 -0
  260. package/dist/modules/raises/index.js.map +1 -1
  261. package/dist/modules/raises/service.d.ts +1 -0
  262. package/dist/modules/raises/service.d.ts.map +1 -1
  263. package/dist/modules/raises/service.js +9 -18
  264. package/dist/modules/raises/service.js.map +1 -1
  265. package/dist/modules/reseal.d.ts +55 -0
  266. package/dist/modules/reseal.d.ts.map +1 -1
  267. package/dist/modules/reseal.js +90 -1
  268. package/dist/modules/reseal.js.map +1 -1
  269. package/dist/modules/smtp-email/index.d.ts +69 -0
  270. package/dist/modules/smtp-email/index.d.ts.map +1 -0
  271. package/dist/modules/smtp-email/index.js +82 -0
  272. package/dist/modules/smtp-email/index.js.map +1 -0
  273. package/dist/modules/webhooks/index.d.ts.map +1 -1
  274. package/dist/modules/webhooks/index.js +5 -1
  275. package/dist/modules/webhooks/index.js.map +1 -1
  276. package/dist/modules/webhooks/payloads.d.ts +22 -7
  277. package/dist/modules/webhooks/payloads.d.ts.map +1 -1
  278. package/dist/modules/webhooks/payloads.js +63 -44
  279. package/dist/modules/webhooks/payloads.js.map +1 -1
  280. package/dist/modules/workspace/events.d.ts +52 -0
  281. package/dist/modules/workspace/events.d.ts.map +1 -0
  282. package/dist/modules/workspace/events.js +16 -0
  283. package/dist/modules/workspace/events.js.map +1 -0
  284. package/dist/modules/workspace/index.d.ts.map +1 -1
  285. package/dist/modules/workspace/index.js +6 -2
  286. package/dist/modules/workspace/index.js.map +1 -1
  287. package/dist/modules/workspace/service.d.ts +1 -0
  288. package/dist/modules/workspace/service.d.ts.map +1 -1
  289. package/dist/modules/workspace/service.js +13 -15
  290. package/dist/modules/workspace/service.js.map +1 -1
  291. package/dist/runtime/events.d.ts +86 -108
  292. package/dist/runtime/events.d.ts.map +1 -1
  293. package/dist/runtime/events.js +123 -52
  294. package/dist/runtime/events.js.map +1 -1
  295. package/dist/runtime/module.d.ts +54 -2
  296. package/dist/runtime/module.d.ts.map +1 -1
  297. package/dist/runtime/registry.d.ts +23 -0
  298. package/dist/runtime/registry.d.ts.map +1 -1
  299. package/dist/runtime/registry.js +102 -2
  300. package/dist/runtime/registry.js.map +1 -1
  301. package/dist/runtime/transaction.d.ts +29 -4
  302. package/dist/runtime/transaction.d.ts.map +1 -1
  303. package/dist/runtime/transaction.js +48 -7
  304. package/dist/runtime/transaction.js.map +1 -1
  305. package/dist/testing/app.d.ts +5 -1
  306. package/dist/testing/app.d.ts.map +1 -1
  307. package/dist/testing/app.js +13 -3
  308. package/dist/testing/app.js.map +1 -1
  309. package/dist/testing/database.d.ts.map +1 -1
  310. package/dist/testing/database.js +6 -3
  311. package/dist/testing/database.js.map +1 -1
  312. package/dist/testing/services.d.ts +21 -1
  313. package/dist/testing/services.d.ts.map +1 -1
  314. package/dist/testing/services.js +20 -5
  315. package/dist/testing/services.js.map +1 -1
  316. package/migrations/0019_sharp_red_skull.sql +4 -0
  317. package/migrations/0020_early_nemesis.sql +2 -0
  318. package/migrations/meta/0019_snapshot.json +5138 -0
  319. package/migrations/meta/0020_snapshot.json +5138 -0
  320. package/migrations/meta/_journal.json +14 -0
  321. package/package.json +5 -2
  322. package/src/app.ts +26 -0
  323. package/src/boot.ts +89 -0
  324. package/src/index.ts +52 -9
  325. package/src/lib/actor.ts +15 -1
  326. package/src/lib/config.ts +65 -7
  327. package/src/lib/email.ts +22 -92
  328. package/src/lib/fromEnv.ts +143 -0
  329. package/src/lib/ids.ts +6 -0
  330. package/src/lib/kelpieConfigFile.ts +291 -0
  331. package/src/lib/logger.ts +92 -34
  332. package/src/modules/agent-tasks/index.ts +4 -1
  333. package/src/modules/agent-tasks/prompt.ts +57 -9
  334. package/src/modules/agent-tasks/service.ts +3 -2
  335. package/src/modules/agent-tasks/wire.ts +12 -0
  336. package/src/modules/auth/index.ts +11 -9
  337. package/src/modules/companies/events.ts +25 -0
  338. package/src/modules/companies/index.ts +2 -0
  339. package/src/modules/companies/service.ts +8 -11
  340. package/src/modules/core.ts +4 -0
  341. package/src/modules/deals/events.ts +34 -0
  342. package/src/modules/deals/index.ts +2 -0
  343. package/src/modules/deals/service.ts +17 -18
  344. package/src/modules/decisions/events.ts +24 -0
  345. package/src/modules/decisions/index.ts +2 -0
  346. package/src/modules/decisions/service.ts +8 -7
  347. package/src/modules/eventCatalogs.ts +35 -0
  348. package/src/modules/forms/events.ts +37 -0
  349. package/src/modules/forms/index.ts +2 -0
  350. package/src/modules/forms/service.ts +12 -11
  351. package/src/modules/forms/submission.ts +80 -29
  352. package/src/modules/handbook/events.ts +31 -0
  353. package/src/modules/handbook/index.ts +2 -0
  354. package/src/modules/handbook/service.ts +8 -15
  355. package/src/modules/hiring/candidates.ts +8 -11
  356. package/src/modules/hiring/events.ts +36 -0
  357. package/src/modules/hiring/index.ts +2 -0
  358. package/src/modules/hiring/roles.ts +8 -11
  359. package/src/modules/import-export/drafts.ts +27 -0
  360. package/src/modules/import-export/events.ts +25 -0
  361. package/src/modules/import-export/exportRows.ts +10 -1
  362. package/src/modules/import-export/index.ts +2 -0
  363. package/src/modules/import-export/plan.ts +107 -5
  364. package/src/modules/import-export/presets.ts +25 -0
  365. package/src/modules/import-export/repository.ts +35 -1
  366. package/src/modules/import-export/routes.ts +5 -0
  367. package/src/modules/import-export/schema.ts +13 -0
  368. package/src/modules/import-export/service.ts +153 -19
  369. package/src/modules/import-export/tools.ts +9 -0
  370. package/src/modules/import-export/writes.ts +116 -3
  371. package/src/modules/notes/events.ts +25 -0
  372. package/src/modules/notes/index.ts +2 -0
  373. package/src/modules/notes/service.ts +8 -7
  374. package/src/modules/opportunities/events.ts +34 -0
  375. package/src/modules/opportunities/index.ts +2 -0
  376. package/src/modules/opportunities/service.ts +17 -18
  377. package/src/modules/partnerships/events.ts +34 -0
  378. package/src/modules/partnerships/index.ts +2 -0
  379. package/src/modules/partnerships/service.ts +17 -18
  380. package/src/modules/people/events.ts +36 -0
  381. package/src/modules/people/index.ts +2 -0
  382. package/src/modules/people/service.ts +97 -87
  383. package/src/modules/pipelines/service.ts +47 -8
  384. package/src/modules/plans/events.ts +24 -0
  385. package/src/modules/plans/index.ts +2 -0
  386. package/src/modules/plans/service.ts +14 -14
  387. package/src/modules/positions/events.ts +25 -0
  388. package/src/modules/positions/index.ts +2 -0
  389. package/src/modules/positions/service.ts +8 -11
  390. package/src/modules/raises/events.ts +34 -0
  391. package/src/modules/raises/index.ts +2 -0
  392. package/src/modules/raises/service.ts +17 -18
  393. package/src/modules/reseal.ts +144 -1
  394. package/src/modules/smtp-email/index.ts +119 -0
  395. package/src/modules/webhooks/index.ts +5 -1
  396. package/src/modules/webhooks/payloads.ts +94 -48
  397. package/src/modules/workspace/events.ts +46 -0
  398. package/src/modules/workspace/index.ts +6 -2
  399. package/src/modules/workspace/service.ts +37 -15
  400. package/src/runtime/events.ts +268 -107
  401. package/src/runtime/module.ts +55 -2
  402. package/src/runtime/registry.ts +169 -1
  403. package/src/runtime/transaction.ts +94 -17
  404. package/src/testing/app.ts +18 -4
  405. package/src/testing/database.ts +6 -3
  406. package/src/testing/services.ts +34 -5
@@ -3,10 +3,11 @@ import type {
3
3
  ImportCounts,
4
4
  ImportObject,
5
5
  MatchKeyOption,
6
+ OnMissingCompany,
6
7
  } from '@kelpie/schemas'
7
8
 
8
9
  import { normaliseDomain, normaliseEmail } from '../../lib/normalisation.ts'
9
- import { companyDraft, dealFieldsDraft, personDraft } from './drafts.ts'
10
+ import { affiliationCompanyDraft, companyDraft, dealFieldsDraft, personDraft } from './drafts.ts'
10
11
  import type { CompanyDraft, DealFieldsDraft, PersonDraft } from './drafts.ts'
11
12
  import { buildMatchKey, splitList } from './mapping.ts'
12
13
  import { aliasedStageSlug } from './presets.ts'
@@ -29,6 +30,8 @@ export interface ImportLookups {
29
30
  readonly existing: ReadonlyMap<string, string>
30
31
  readonly personIdByEmail: ReadonlyMap<string, string>
31
32
  readonly companyIdByDomain: ReadonlyMap<string, string>
33
+ /** Company id by its folded name, for a People affiliation matched by name. */
34
+ readonly companyIdByName: ReadonlyMap<string, string>
32
35
  /** Workspace member id by the address of the user behind it. */
33
36
  readonly memberIdByEmail: ReadonlyMap<string, string>
34
37
  /** Deal stage id, keyed by both its slug and its folded label. */
@@ -39,13 +42,28 @@ export interface PlanContext {
39
42
  readonly object: ImportObject
40
43
  readonly matchKey: MatchKeyOption
41
44
  readonly conflictMode: ImportConflictMode
45
+ /** What a People row does with an absent company. Ignored by the other objects. */
46
+ readonly onMissingCompany: OnMissingCompany
42
47
  readonly lookups: ImportLookups
43
48
  }
44
49
 
50
+ /**
51
+ * A company affiliation a People row asks for: the position to write, and
52
+ * whether the company must be created before it can be linked.
53
+ */
54
+ export type PlannedAffiliation =
55
+ | { readonly kind: 'link'; readonly companyId: string; readonly title: string }
56
+ | { readonly kind: 'create'; readonly company: CompanyDraft; readonly title: string }
57
+
45
58
  /** The values a write applies, once every reference in the row has been resolved. */
46
59
  export type ImportWrite =
47
60
  | { readonly object: 'companies'; readonly draft: CompanyDraft }
48
- | { readonly object: 'people'; readonly draft: PersonDraft }
61
+ | {
62
+ readonly object: 'people'
63
+ readonly draft: PersonDraft
64
+ /** A position to upsert alongside the person, when the row named a company and a title. */
65
+ readonly affiliation?: PlannedAffiliation
66
+ }
49
67
  | {
50
68
  readonly object: 'positions'
51
69
  readonly personId: string
@@ -78,12 +96,19 @@ export type RowPlan =
78
96
  * reaches the row the earlier one has been written, so it resolves to an id.
79
97
  */
80
98
  | { readonly action: 'skip'; readonly key: string; readonly targetId: string | null }
81
- | { readonly action: 'create'; readonly key: string; readonly write: ImportWrite }
99
+ | {
100
+ readonly action: 'create'
101
+ readonly key: string
102
+ readonly write: ImportWrite
103
+ /** Non-fatal notes about the applied row, e.g. a People affiliation left unlinked. */
104
+ readonly warnings?: readonly StoredRowError[]
105
+ }
82
106
  | {
83
107
  readonly action: 'update'
84
108
  readonly key: string
85
109
  readonly targetId: string | null
86
110
  readonly write: ImportWrite
111
+ readonly warnings?: readonly StoredRowError[]
87
112
  }
88
113
 
89
114
  export interface PlannedRow {
@@ -233,6 +258,80 @@ function resolveWrite(context: PlanContext, mapped: Readonly<Record<string, stri
233
258
  }
234
259
  }
235
260
 
261
+ /**
262
+ * The company affiliation a People row asks for, if any, and any note about it.
263
+ *
264
+ * A row states an affiliation only when it carries both a title and a company
265
+ * identity, so a blank optional cell still says nothing, the additive rule every
266
+ * column follows. The company is matched by domain when the row has one and by
267
+ * name otherwise. A named company that is not here follows `on_missing_company`:
268
+ * `create` invents it from the row, `skip` imports the person alone and returns
269
+ * a warning.
270
+ */
271
+ function planAffiliation(
272
+ context: PlanContext,
273
+ mapped: Readonly<Record<string, string>>,
274
+ ): { readonly affiliation?: PlannedAffiliation; readonly warnings: readonly StoredRowError[] } {
275
+ const title = (mapped.title ?? '').trim()
276
+ const domainRaw = (mapped.company_domain ?? '').trim()
277
+ const nameRaw = (mapped.company_name ?? '').trim()
278
+
279
+ if (title.length === 0 || (domainRaw.length === 0 && nameRaw.length === 0)) {
280
+ return { warnings: [] }
281
+ }
282
+
283
+ const domain = normaliseDomain(domainRaw)
284
+ const companyId =
285
+ domainRaw.length > 0
286
+ ? domain === null
287
+ ? undefined
288
+ : context.lookups.companyIdByDomain.get(domain)
289
+ : context.lookups.companyIdByName.get(nameRaw.toLowerCase())
290
+
291
+ if (companyId !== undefined) {
292
+ return { affiliation: { kind: 'link', companyId, title }, warnings: [] }
293
+ }
294
+
295
+ if (context.onMissingCompany === 'create') {
296
+ return {
297
+ affiliation: { kind: 'create', company: affiliationCompanyDraft(mapped), title },
298
+ warnings: [],
299
+ }
300
+ }
301
+
302
+ const named = domainRaw.length > 0 ? domainRaw : nameRaw
303
+
304
+ return {
305
+ warnings: [
306
+ {
307
+ field: domainRaw.length > 0 ? 'company_domain' : 'company_name',
308
+ message: `No company here matches "${named}", so the person imported without a position`,
309
+ },
310
+ ],
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Folds a People row's affiliation into its create or update plan.
316
+ *
317
+ * A no-op for every other object, and for a person the row said nothing about a
318
+ * company for. The affiliation rides on the write; any note rides on the plan.
319
+ */
320
+ function withAffiliation(
321
+ context: PlanContext,
322
+ mapped: Readonly<Record<string, string>>,
323
+ plan: Extract<RowPlan, { action: 'create' } | { action: 'update' }>,
324
+ ): RowPlan {
325
+ if (context.object !== 'people' || plan.write.object !== 'people') {
326
+ return plan
327
+ }
328
+
329
+ const { affiliation, warnings } = planAffiliation(context, mapped)
330
+ const write = affiliation === undefined ? plan.write : { ...plan.write, affiliation }
331
+
332
+ return { ...plan, write, ...(warnings.length > 0 ? { warnings } : {}) }
333
+ }
334
+
236
335
  /**
237
336
  * @param mapped The row's cells by Kelpie column, from `mapRow`.
238
337
  * @returns What this row would do. `create` when its key matches nothing;
@@ -265,15 +364,18 @@ export function planRow(context: PlanContext, mapped: Readonly<Record<string, st
265
364
  const targetId = context.lookups.existing.get(key)
266
365
 
267
366
  if (targetId === undefined) {
268
- return { action: 'create', key, write }
367
+ return withAffiliation(context, mapped, { action: 'create', key, write })
269
368
  }
270
369
 
271
370
  // An empty string is the placeholder an in-file match carries: the record it
272
371
  // matched has not been written yet, so there is no id to report.
273
372
  const resolved = targetId.length === 0 ? null : targetId
274
373
 
374
+ // A skipped row is left entirely alone, affiliation included. A caller who
375
+ // wants an existing person's position updated runs the job in `update` mode,
376
+ // which is where the rename-in-place lives.
275
377
  return context.conflictMode === 'update'
276
- ? { action: 'update', key, targetId: resolved, write }
378
+ ? withAffiliation(context, mapped, { action: 'update', key, targetId: resolved, write })
277
379
  : { action: 'skip', key, targetId: resolved }
278
380
  }
279
381
 
@@ -31,6 +31,8 @@ export const SOURCE_PRESETS: Readonly<Record<ImportSource, SourcePreset>> = {
31
31
  email: 'Email',
32
32
  phones: 'Phone Number',
33
33
  location: 'City',
34
+ company_domain: 'Company Domain Name',
35
+ title: 'Job Title',
34
36
  },
35
37
  positions: {
36
38
  person_email: 'Email',
@@ -60,6 +62,8 @@ export const SOURCE_PRESETS: Readonly<Record<ImportSource, SourcePreset>> = {
60
62
  email: 'Email',
61
63
  phones: 'Phone',
62
64
  location: 'Mailing City',
65
+ company_name: 'Account Name',
66
+ title: 'Title',
63
67
  },
64
68
  positions: {
65
69
  person_email: 'Email',
@@ -76,6 +80,27 @@ export const SOURCE_PRESETS: Readonly<Record<ImportSource, SourcePreset>> = {
76
80
  external_id: 'Opportunity ID',
77
81
  },
78
82
  },
83
+ attio: {
84
+ // Attio's export writes one column per attribute, and a linked or nested
85
+ // attribute as `Parent > Child`. Only Companies and People are mapped: the
86
+ // sample export carries no Deals or Positions file, and an Attio People row
87
+ // names its company but not the company's domain, so a Position could not be
88
+ // keyed from it. Those objects fall back to `custom`.
89
+ companies: {
90
+ name: 'Record',
91
+ domain: 'Domains',
92
+ industry: 'Categories',
93
+ description: 'Description',
94
+ hq: 'Primary location > Country',
95
+ },
96
+ people: {
97
+ name: 'Record',
98
+ email: 'Email addresses',
99
+ location: 'Primary location > Country',
100
+ company_name: 'Company > Name',
101
+ title: 'Job title',
102
+ },
103
+ },
79
104
  }
80
105
 
81
106
  /**
@@ -175,6 +175,8 @@ export interface RowOutcome {
175
175
  readonly rowNumber: number
176
176
  readonly action: SettledRowAction
177
177
  readonly errors: readonly { readonly field: string; readonly message: string }[]
178
+ /** Non-fatal notes about a row that was applied anyway, e.g. an unlinked position. */
179
+ readonly warnings: readonly { readonly field: string; readonly message: string }[]
178
180
  }
179
181
 
180
182
  /**
@@ -202,12 +204,18 @@ export async function recordRowOutcome(
202
204
  values,
203
205
  action: outcome.action,
204
206
  errors: outcome.errors,
207
+ warnings: outcome.warnings,
205
208
  createdAt: now,
206
209
  updatedAt: now,
207
210
  })
208
211
  .onConflictDoUpdate({
209
212
  target: [importJobRows.jobId, importJobRows.rowNumber],
210
- set: { action: outcome.action, errors: outcome.errors, updatedAt: now },
213
+ set: {
214
+ action: outcome.action,
215
+ errors: outcome.errors,
216
+ warnings: outcome.warnings,
217
+ updatedAt: now,
218
+ },
211
219
  })
212
220
  }
213
221
 
@@ -381,6 +389,32 @@ export function findCompanyIdsByDomain(
381
389
  )
382
390
  }
383
391
 
392
+ /**
393
+ * Company ids by folded name, for a People affiliation matched on a company
394
+ * name rather than a domain.
395
+ *
396
+ * `lower(name) in (…)` rather than `ilike`, so a `%` inside a caller-supplied
397
+ * name is a literal, not a wildcard. The same rule `findCompanyKeys` follows.
398
+ * The caller passes names already lowercased.
399
+ */
400
+ export function findCompanyIdsByName(
401
+ db: Queryable,
402
+ workspaceId: string,
403
+ names: readonly string[],
404
+ ): Promise<{ name: string; id: string }[]> {
405
+ return overChunks(names, (chunk) =>
406
+ db
407
+ .select({ id: companies.id, name: companies.name })
408
+ .from(companies)
409
+ .where(
410
+ and(
411
+ eq(companies.workspaceId, workspaceId),
412
+ inArray(sql<string>`lower(${companies.name})`, [...chunk]),
413
+ ),
414
+ ),
415
+ )
416
+ }
417
+
384
418
  /** Every member of the workspace, by the address of the user behind them. */
385
419
  export function listMemberEmails(
386
420
  db: Queryable,
@@ -3,6 +3,7 @@ import {
3
3
  IMPORT_OBJECTS,
4
4
  IMPORT_SOURCES,
5
5
  MAX_IMPORT_FILE_BYTES,
6
+ ON_MISSING_COMPANY,
6
7
  defaultMatchKeyId,
7
8
  } from '@kelpie/schemas'
8
9
  import type { ImportColumnMap, ImportObject } from '@kelpie/schemas'
@@ -42,6 +43,7 @@ const createJobForm = z.object({
42
43
  source: z.enum(IMPORT_SOURCES),
43
44
  object: z.enum(IMPORT_OBJECTS),
44
45
  conflict_mode: z.enum(IMPORT_CONFLICT_MODES).default('skip'),
46
+ on_missing_company: z.enum(ON_MISSING_COMPANY).default('skip'),
45
47
  match_key: z.string().min(1).optional(),
46
48
  column_map: z.string().optional(),
47
49
  dry_run: z.enum(['true', 'false']).default('true'),
@@ -58,12 +60,14 @@ export function importJobResponse(job: ImportJobView): Record<string, unknown> {
58
60
  object: job.object,
59
61
  status: job.status,
60
62
  conflict_mode: job.conflictMode,
63
+ on_missing_company: job.onMissingCompany,
61
64
  match_key: job.matchKey,
62
65
  column_map: job.columnMap,
63
66
  source_headers: job.sourceHeaders,
64
67
  file_name: job.fileName,
65
68
  counts: job.counts,
66
69
  errors: job.errors,
70
+ warnings: job.warnings,
67
71
  preview: job.preview,
68
72
  created_at: job.createdAt.toISOString(),
69
73
  updated_at: job.updatedAt.toISOString(),
@@ -230,6 +234,7 @@ export function mountImportExportRoutes(
230
234
  source: parsed.data.source,
231
235
  object: parsed.data.object,
232
236
  conflictMode: parsed.data.conflict_mode,
237
+ onMissingCompany: parsed.data.on_missing_company,
233
238
  matchKeyId: parsed.data.match_key ?? defaultMatchKeyId(parsed.data.object),
234
239
  columnMap: readColumnMap(parsed.data.column_map),
235
240
  fileName: file.name.length === 0 ? null : file.name,
@@ -4,6 +4,7 @@ import {
4
4
  IMPORT_OBJECTS,
5
5
  IMPORT_ROW_ACTIONS,
6
6
  IMPORT_SOURCES,
7
+ ON_MISSING_COMPANY,
7
8
  } from '@kelpie/schemas'
8
9
  import type {
9
10
  ImportColumnMap,
@@ -68,12 +69,21 @@ export const importJobs = pgTable(
68
69
  object: text('object').notNull(),
69
70
  status: text('status').notNull(),
70
71
  conflictMode: text('conflict_mode').notNull(),
72
+ /**
73
+ * What a People import does with a row naming a company that is not here
74
+ * yet: `skip` leaves the affiliation unlinked and warns, `create` invents
75
+ * the company. Defaulted so a job predating this reads as `skip`, which is
76
+ * the behaviour every other object already has.
77
+ */
78
+ onMissingCompany: text('on_missing_company').notNull().default('skip'),
71
79
  matchKey: text('match_key').notNull(),
72
80
  columnMap: jsonb('column_map').$type<ImportColumnMap>().notNull().default({}),
73
81
  sourceHeaders: jsonb('source_headers').$type<readonly string[]>().notNull().default([]),
74
82
  counts: jsonb('counts').$type<ImportCounts>().notNull(),
75
83
  /** The first `IMPORT_REPORTED_ERRORS` failing rows. `counts.error` is the true number. */
76
84
  errors: jsonb('errors').$type<readonly ImportRowError[]>().notNull().default([]),
85
+ /** The first `IMPORT_REPORTED_ERRORS` non-fatal notes, e.g. a person imported with the position skipped. */
86
+ warnings: jsonb('warnings').$type<readonly ImportRowError[]>().notNull().default([]),
77
87
  /** The first `IMPORT_PREVIEW_ROWS` rows, mapped as Kelpie read them. */
78
88
  preview: jsonb('preview').$type<readonly ImportPreviewRow[]>().notNull().default([]),
79
89
  /**
@@ -97,6 +107,7 @@ export const importJobs = pgTable(
97
107
  checkOneOf('import_jobs_source_check', table.source, IMPORT_SOURCES),
98
108
  checkOneOf('import_jobs_object_check', table.object, IMPORT_OBJECTS),
99
109
  checkOneOf('import_jobs_conflict_mode_check', table.conflictMode, IMPORT_CONFLICT_MODES),
110
+ checkOneOf('import_jobs_on_missing_company_check', table.onMissingCompany, ON_MISSING_COMPANY),
100
111
  checkOneOf('import_jobs_status_check', table.status, IMPORT_JOB_STATUSES),
101
112
  ],
102
113
  )
@@ -128,6 +139,8 @@ export const importJobRows = pgTable(
128
139
  values: jsonb('values').$type<Readonly<Record<string, string>>>().notNull().default({}),
129
140
  action: text('action').notNull(),
130
141
  errors: jsonb('errors').$type<readonly StoredRowError[]>().notNull().default([]),
142
+ /** Non-fatal notes about a row that was applied anyway, e.g. an unlinked position. */
143
+ warnings: jsonb('warnings').$type<readonly StoredRowError[]>().notNull().default([]),
131
144
  createdAt: createdAt(),
132
145
  updatedAt: updatedAt(),
133
146
  },
@@ -17,6 +17,7 @@ import type {
17
17
  ImportRowError,
18
18
  ImportSource,
19
19
  MatchKeyOption,
20
+ OnMissingCompany,
20
21
  } from '@kelpie/schemas'
21
22
 
22
23
  import type { Database } from '../../lib/database.ts'
@@ -24,10 +25,78 @@ import { AppError, describeThrown } from '../../lib/errors.ts'
24
25
  import type { IdFactory } from '../../lib/ids.ts'
25
26
  import type { Logger } from '../../lib/logger.ts'
26
27
  import { normaliseDomain, normaliseEmail } from '../../lib/normalisation.ts'
27
- import type { Queryable, TransactionScope } from '../../runtime/transaction.ts'
28
+ import type { BufferedEvents, Queryable, TransactionScope } from '../../runtime/transaction.ts'
28
29
  import type { ActivityRecorder } from '../activities/recorder.ts'
30
+ import { toEventActor } from '../../lib/actor.ts'
31
+ import type { RecordObjectType } from '../../runtime/events.ts'
29
32
  import type { Actor } from '../auth/actor.ts'
30
33
  import { requireWorkspaceId } from '../auth/actor.ts'
34
+ import '../companies/events.ts'
35
+ import '../deals/events.ts'
36
+ import '../people/events.ts'
37
+ import '../positions/events.ts'
38
+ import './events.ts'
39
+
40
+ /**
41
+ * Fires the created event owned by the record's module for one imported row.
42
+ * Split from the loop so TypeScript can correlate the object type with its
43
+ * typed event name at each call site.
44
+ */
45
+ function emitImportedRecordCreated(
46
+ events: BufferedEvents,
47
+ objectType: RecordObjectType,
48
+ recordId: string,
49
+ ): void {
50
+ switch (objectType) {
51
+ case 'person':
52
+ events.emit('people.person.created', { type: 'person', id: recordId }, {})
53
+ return
54
+ case 'company':
55
+ events.emit('companies.company.created', { type: 'company', id: recordId }, {})
56
+ return
57
+ case 'position':
58
+ events.emit('positions.position.created', { type: 'position', id: recordId }, {})
59
+ return
60
+ case 'deal':
61
+ events.emit('deals.deal.created', { type: 'deal', id: recordId }, {})
62
+ return
63
+ // The remaining `RecordObjectType` values are not produced by an import.
64
+ default:
65
+ throw new Error(`import produced an unexpected object type: ${objectType}`)
66
+ }
67
+ }
68
+
69
+ function emitImportedRecordUpdated(
70
+ events: BufferedEvents,
71
+ objectType: RecordObjectType,
72
+ recordId: string,
73
+ changed: readonly string[],
74
+ ): void {
75
+ switch (objectType) {
76
+ case 'person':
77
+ events.emit('people.person.updated', { type: 'person', id: recordId }, { changed })
78
+ return
79
+ case 'company':
80
+ events.emit(
81
+ 'companies.company.updated',
82
+ { type: 'company', id: recordId },
83
+ { changed },
84
+ )
85
+ return
86
+ case 'position':
87
+ events.emit(
88
+ 'positions.position.updated',
89
+ { type: 'position', id: recordId },
90
+ { changed },
91
+ )
92
+ return
93
+ case 'deal':
94
+ events.emit('deals.deal.updated', { type: 'deal', id: recordId }, { changed })
95
+ return
96
+ default:
97
+ throw new Error(`import produced an unexpected object type: ${objectType}`)
98
+ }
99
+ }
31
100
  import { CsvFormatError, csvLine, fileDigest, parseCsv } from './csv.ts'
32
101
  import type { CsvRow, ParsedCsv } from './csv.ts'
33
102
  import { headersFor } from './exportRows.ts'
@@ -74,12 +143,14 @@ export interface ImportJobView {
74
143
  readonly object: ImportObject
75
144
  readonly status: string
76
145
  readonly conflictMode: ImportConflictMode
146
+ readonly onMissingCompany: OnMissingCompany
77
147
  readonly matchKey: string
78
148
  readonly columnMap: ImportColumnMap
79
149
  readonly sourceHeaders: readonly string[]
80
150
  readonly fileName: string | null
81
151
  readonly counts: ImportCounts
82
152
  readonly errors: readonly ImportRowError[]
153
+ readonly warnings: readonly ImportRowError[]
83
154
  readonly preview: readonly ImportPreviewRow[]
84
155
  readonly createdAt: Date
85
156
  readonly updatedAt: Date
@@ -89,6 +160,7 @@ export interface CreateImportJobInput {
89
160
  readonly source: ImportSource
90
161
  readonly object: ImportObject
91
162
  readonly conflictMode: ImportConflictMode
163
+ readonly onMissingCompany: OnMissingCompany
92
164
  readonly matchKeyId: string
93
165
  /** Absent means "derive one from the source preset and the file's own headers". */
94
166
  readonly columnMap: ImportColumnMap | undefined
@@ -145,6 +217,7 @@ function toView(job: ImportJobRecord): ImportJobView {
145
217
  source: rest.source as ImportSource,
146
218
  object: rest.object as ImportObject,
147
219
  conflictMode: rest.conflictMode as ImportConflictMode,
220
+ onMissingCompany: rest.onMissingCompany as OnMissingCompany,
148
221
  }
149
222
  }
150
223
 
@@ -290,11 +363,37 @@ export function createImportExportService(
290
363
  ): Promise<ImportLookups> {
291
364
  const existing = await findExistingKeys(db, workspaceId, object, matchKey, rows)
292
365
 
293
- if (object === 'companies' || object === 'people') {
366
+ if (object === 'companies') {
294
367
  return {
295
368
  existing,
296
369
  personIdByEmail: new Map(),
297
370
  companyIdByDomain: new Map(),
371
+ companyIdByName: new Map(),
372
+ memberIdByEmail: new Map(),
373
+ dealStageIdByName: new Map(),
374
+ }
375
+ }
376
+
377
+ // People only need companies, and only when a row carries an affiliation.
378
+ // Both a domain map and a name map, because a row may identify its company
379
+ // either way.
380
+ if (object === 'people') {
381
+ const domains = distinct(rows.map((row) => normaliseDomain(row.mapped.company_domain ?? '')))
382
+ const names = distinct(
383
+ rows.map((row) => (row.mapped.company_name ?? '').trim().toLowerCase() || null),
384
+ )
385
+ const [byDomain, byName] = await Promise.all([
386
+ repository.findCompanyIdsByDomain(db, workspaceId, domains),
387
+ repository.findCompanyIdsByName(db, workspaceId, names),
388
+ ])
389
+
390
+ return {
391
+ existing,
392
+ personIdByEmail: new Map(),
393
+ companyIdByDomain: new Map(
394
+ byDomain.flatMap((row) => (row.domain === null ? [] : [[row.domain, row.id] as const])),
395
+ ),
396
+ companyIdByName: new Map(byName.map((row) => [row.name.toLowerCase(), row.id] as const)),
298
397
  memberIdByEmail: new Map(),
299
398
  dealStageIdByName: new Map(),
300
399
  }
@@ -321,6 +420,7 @@ export function createImportExportService(
321
420
  companyIdByDomain: new Map(
322
421
  companyRows.flatMap((row) => (row.domain === null ? [] : [[row.domain, row.id] as const])),
323
422
  ),
423
+ companyIdByName: new Map(),
324
424
  memberIdByEmail: new Map(),
325
425
  dealStageIdByName: new Map(),
326
426
  }
@@ -348,6 +448,7 @@ export function createImportExportService(
348
448
  companyIdByDomain: new Map(
349
449
  companyRows.flatMap((row) => (row.domain === null ? [] : [[row.domain, row.id] as const])),
350
450
  ),
451
+ companyIdByName: new Map(),
351
452
  memberIdByEmail: new Map(members.map((member) => [member.email.toLowerCase(), member.id])),
352
453
  dealStageIdByName: stagesByName,
353
454
  }
@@ -358,6 +459,7 @@ export function createImportExportService(
358
459
  object: job.object as ImportObject,
359
460
  matchKey: requireMatchKey(job.object as ImportObject, job.matchKey),
360
461
  conflictMode: job.conflictMode as ImportConflictMode,
462
+ onMissingCompany: job.onMissingCompany as OnMissingCompany,
361
463
  lookups,
362
464
  }
363
465
  }
@@ -401,10 +503,13 @@ export function createImportExportService(
401
503
  }
402
504
 
403
505
  function toOutcome(planned: PlannedRow): repository.RowOutcome {
506
+ const { plan } = planned
507
+
404
508
  return {
405
509
  rowNumber: planned.row,
406
- action: planned.plan.action,
407
- errors: planned.plan.action === 'error' ? planned.plan.errors : [],
510
+ action: plan.action,
511
+ errors: plan.action === 'error' ? plan.errors : [],
512
+ warnings: plan.action === 'create' || plan.action === 'update' ? (plan.warnings ?? []) : [],
408
513
  }
409
514
  }
410
515
 
@@ -418,7 +523,11 @@ export function createImportExportService(
418
523
  function reportOf(
419
524
  outcomes: readonly repository.RowOutcome[],
420
525
  mapped: readonly MappedRow[],
421
- ): { errors: readonly ImportRowError[]; preview: readonly ImportPreviewRow[] } {
526
+ ): {
527
+ errors: readonly ImportRowError[]
528
+ warnings: readonly ImportRowError[]
529
+ preview: readonly ImportPreviewRow[]
530
+ } {
422
531
  const byRow = new Map(mapped.map((row) => [row.row, row.mapped]))
423
532
 
424
533
  return {
@@ -432,6 +541,16 @@ export function createImportExportService(
432
541
  message: problem.message,
433
542
  })),
434
543
  ),
544
+ warnings: outcomes
545
+ .filter((outcome) => outcome.warnings.length > 0)
546
+ .slice(0, IMPORT_REPORTED_ERRORS)
547
+ .flatMap((outcome) =>
548
+ outcome.warnings.map((problem) => ({
549
+ row: outcome.rowNumber,
550
+ field: problem.field,
551
+ message: problem.message,
552
+ })),
553
+ ),
435
554
  preview: outcomes.slice(0, IMPORT_PREVIEW_ROWS).map((outcome) => ({
436
555
  row: outcome.rowNumber,
437
556
  action: outcome.action,
@@ -533,6 +652,8 @@ export function createImportExportService(
533
652
  rowNumber: line.number,
534
653
  action: plan.action,
535
654
  errors: plan.action === 'error' ? plan.errors : [],
655
+ warnings:
656
+ plan.action === 'create' || plan.action === 'update' ? (plan.warnings ?? []) : [],
536
657
  }
537
658
 
538
659
  if (plan.action !== 'error' && plan.action !== 'skip') {
@@ -550,20 +671,26 @@ export function createImportExportService(
550
671
  )
551
672
 
552
673
  if (plan.action === 'create') {
553
- events.emit('record.created', {
554
- workspaceId,
555
- objectType: written.objectType,
556
- recordId: written.recordId,
557
- })
674
+ emitImportedRecordCreated(events, written.objectType, written.recordId)
558
675
  } else if (written.changedFields.length > 0) {
559
676
  // An update that moved nothing publishes nothing. Re-running the same
560
677
  // file must not wake every consumer watching for a change.
561
- events.emit('record.updated', {
562
- workspaceId,
563
- objectType: written.objectType,
564
- recordId: written.recordId,
565
- changedFields: written.changedFields,
566
- })
678
+ emitImportedRecordUpdated(
679
+ events,
680
+ written.objectType,
681
+ written.recordId,
682
+ written.changedFields,
683
+ )
684
+ }
685
+
686
+ // A People affiliation makes a Position, and maybe a Company. Each is its
687
+ // own record with its own module event, whatever the person row did.
688
+ for (const side of written.sideRecords ?? []) {
689
+ if (side.created) {
690
+ emitImportedRecordCreated(events, side.objectType, side.recordId)
691
+ } else if (side.changedFields.length > 0) {
692
+ emitImportedRecordUpdated(events, side.objectType, side.recordId, side.changedFields)
693
+ }
567
694
  }
568
695
  }
569
696
 
@@ -579,7 +706,7 @@ export function createImportExportService(
579
706
  )
580
707
 
581
708
  return outcome
582
- })
709
+ }, { workspaceId, actor: toEventActor(actor) })
583
710
  }
584
711
 
585
712
  /**
@@ -618,6 +745,7 @@ export function createImportExportService(
618
745
  rowNumber: row.row,
619
746
  action: 'error' as const,
620
747
  errors: [{ field: '', message: describeThrown(error) }],
748
+ warnings: [],
621
749
  }
622
750
 
623
751
  // Its own statement: the transaction that would have carried this one
@@ -658,10 +786,14 @@ export function createImportExportService(
658
786
  })
659
787
 
660
788
  await dependencies.transaction(({ events }) => {
661
- events.emit('import.completed', { workspaceId, importJobId: jobId, object: job.object })
789
+ events.emit(
790
+ 'imports.job.completed',
791
+ { type: 'import_job', id: jobId },
792
+ { object: job.object },
793
+ )
662
794
 
663
795
  return Promise.resolve()
664
- })
796
+ }, { workspaceId, actor: toEventActor(actor) })
665
797
  }
666
798
 
667
799
  /**
@@ -718,11 +850,13 @@ export function createImportExportService(
718
850
  object: input.object,
719
851
  status: 'pending',
720
852
  conflictMode: input.conflictMode,
853
+ onMissingCompany: input.onMissingCompany,
721
854
  matchKey: matchKey.id,
722
855
  columnMap,
723
856
  sourceHeaders: parsed.headers,
724
857
  counts: { ...EMPTY_COUNTS, total: parsed.rows.length },
725
858
  errors: [],
859
+ warnings: [],
726
860
  preview: [],
727
861
  fileSha256: fileDigest(input.csv),
728
862
  fileName: input.fileName,