@owlmeans/server-planning 0.1.18-rc.8 → 0.1.18

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 (438) hide show
  1. package/README.md +92 -19
  2. package/agent-meta/manifest.json +105 -4
  3. package/agent-meta/skills/server-planning/SKILL.md +89 -138
  4. package/agent-meta/skills/server-planning/parts/auxiliary.md +54 -0
  5. package/agent-meta/skills/server-planning/parts/executor.md +82 -0
  6. package/agent-meta/skills/server-planning/parts/schemas.md +26 -0
  7. package/agent-meta/skills/server-planning/parts/serving.md +88 -0
  8. package/agent-meta/skills/server-planning/parts/stores.md +91 -0
  9. package/build/actions/apply-schema-request.d.ts +8 -0
  10. package/build/actions/apply-schema-request.d.ts.map +1 -0
  11. package/build/actions/apply-schema-request.js +33 -0
  12. package/build/actions/apply-schema-request.js.map +1 -0
  13. package/build/actions/commit.d.ts +3 -3
  14. package/build/actions/commit.d.ts.map +1 -1
  15. package/build/actions/commit.js +5 -4
  16. package/build/actions/commit.js.map +1 -1
  17. package/build/actions/define-schemas.d.ts +13 -0
  18. package/build/actions/define-schemas.d.ts.map +1 -0
  19. package/build/actions/define-schemas.js +28 -0
  20. package/build/actions/define-schemas.js.map +1 -0
  21. package/build/actions/execute.d.ts +11 -10
  22. package/build/actions/execute.d.ts.map +1 -1
  23. package/build/actions/execute.js +17 -27
  24. package/build/actions/execute.js.map +1 -1
  25. package/build/actions/execution/types.d.ts +34 -0
  26. package/build/actions/execution/types.d.ts.map +1 -0
  27. package/build/actions/execution/types.js +2 -0
  28. package/build/actions/execution/types.js.map +1 -0
  29. package/build/actions/execution.d.ts +4 -0
  30. package/build/actions/execution.d.ts.map +1 -0
  31. package/build/actions/execution.js +88 -0
  32. package/build/actions/execution.js.map +1 -0
  33. package/build/actions/get-card.d.ts +6 -0
  34. package/build/actions/get-card.d.ts.map +1 -0
  35. package/build/actions/get-card.js +9 -0
  36. package/build/actions/get-card.js.map +1 -0
  37. package/build/actions/get-specification.d.ts +6 -0
  38. package/build/actions/get-specification.d.ts.map +1 -0
  39. package/build/actions/get-specification.js +9 -0
  40. package/build/actions/get-specification.js.map +1 -0
  41. package/build/actions/index.d.ts +12 -3
  42. package/build/actions/index.d.ts.map +1 -1
  43. package/build/actions/index.js +11 -3
  44. package/build/actions/index.js.map +1 -1
  45. package/build/actions/links.d.ts +3 -3
  46. package/build/actions/links.d.ts.map +1 -1
  47. package/build/actions/links.js +6 -5
  48. package/build/actions/links.js.map +1 -1
  49. package/build/actions/list-card-specifications.d.ts +6 -0
  50. package/build/actions/list-card-specifications.d.ts.map +1 -0
  51. package/build/actions/list-card-specifications.js +11 -0
  52. package/build/actions/list-card-specifications.js.map +1 -0
  53. package/build/actions/list-card-transitions.d.ts +6 -0
  54. package/build/actions/list-card-transitions.d.ts.map +1 -0
  55. package/build/actions/list-card-transitions.js +11 -0
  56. package/build/actions/list-card-transitions.js.map +1 -0
  57. package/build/actions/list-cards.d.ts +6 -0
  58. package/build/actions/list-cards.d.ts.map +1 -0
  59. package/build/actions/list-cards.js +10 -0
  60. package/build/actions/list-cards.js.map +1 -0
  61. package/build/actions/list-schemas.d.ts +13 -0
  62. package/build/actions/list-schemas.d.ts.map +1 -0
  63. package/build/actions/list-schemas.js +20 -0
  64. package/build/actions/list-schemas.js.map +1 -0
  65. package/build/actions/list-specification-revisions.d.ts +6 -0
  66. package/build/actions/list-specification-revisions.d.ts.map +1 -0
  67. package/build/actions/list-specification-revisions.js +10 -0
  68. package/build/actions/list-specification-revisions.js.map +1 -0
  69. package/build/actions/resources.d.ts +5 -0
  70. package/build/actions/resources.d.ts.map +1 -0
  71. package/build/actions/resources.js +111 -0
  72. package/build/actions/resources.js.map +1 -0
  73. package/build/actions/summarize-cards.d.ts +6 -0
  74. package/build/actions/summarize-cards.d.ts.map +1 -0
  75. package/build/actions/summarize-cards.js +11 -0
  76. package/build/actions/summarize-cards.js.map +1 -0
  77. package/build/actions/transitions.d.ts +3 -3
  78. package/build/actions/transitions.d.ts.map +1 -1
  79. package/build/actions/transitions.js +4 -3
  80. package/build/actions/transitions.js.map +1 -1
  81. package/build/actions/types.d.ts +6 -0
  82. package/build/actions/types.d.ts.map +1 -0
  83. package/build/actions/types.js +2 -0
  84. package/build/actions/types.js.map +1 -0
  85. package/build/actions/watch.d.ts +2 -2
  86. package/build/actions/watch.d.ts.map +1 -1
  87. package/build/actions/watch.js +8 -6
  88. package/build/actions/watch.js.map +1 -1
  89. package/build/conformance/assert/types.d.ts +19 -0
  90. package/build/conformance/assert/types.d.ts.map +1 -0
  91. package/build/conformance/assert/types.js +2 -0
  92. package/build/conformance/assert/types.js.map +1 -0
  93. package/build/conformance/assert.d.ts +15 -0
  94. package/build/conformance/assert.d.ts.map +1 -0
  95. package/build/conformance/assert.js +52 -0
  96. package/build/conformance/assert.js.map +1 -0
  97. package/build/conformance/cases.d.ts +7 -0
  98. package/build/conformance/cases.d.ts.map +1 -0
  99. package/build/conformance/cases.js +544 -0
  100. package/build/conformance/cases.js.map +1 -0
  101. package/build/conformance/clock.d.ts +6 -0
  102. package/build/conformance/clock.d.ts.map +1 -0
  103. package/build/conformance/clock.js +12 -0
  104. package/build/conformance/clock.js.map +1 -0
  105. package/build/conformance/consts.d.ts +30 -0
  106. package/build/conformance/consts.d.ts.map +1 -0
  107. package/build/conformance/consts.js +159 -0
  108. package/build/conformance/consts.js.map +1 -0
  109. package/build/conformance/errors.d.ts +5 -0
  110. package/build/conformance/errors.d.ts.map +1 -0
  111. package/build/conformance/errors.js +8 -0
  112. package/build/conformance/errors.js.map +1 -0
  113. package/build/conformance/fixtures/types.d.ts +13 -0
  114. package/build/conformance/fixtures/types.d.ts.map +1 -0
  115. package/build/conformance/fixtures/types.js +2 -0
  116. package/build/conformance/fixtures/types.js.map +1 -0
  117. package/build/conformance/fixtures.d.ts +14 -0
  118. package/build/conformance/fixtures.d.ts.map +1 -0
  119. package/build/conformance/fixtures.js +26 -0
  120. package/build/conformance/fixtures.js.map +1 -0
  121. package/build/conformance/index.d.ts +15 -0
  122. package/build/conformance/index.d.ts.map +1 -0
  123. package/build/conformance/index.js +12 -0
  124. package/build/conformance/index.js.map +1 -0
  125. package/build/conformance/resources.d.ts +4 -0
  126. package/build/conformance/resources.d.ts.map +1 -0
  127. package/build/conformance/resources.js +228 -0
  128. package/build/conformance/resources.js.map +1 -0
  129. package/build/conformance/types.d.ts +18 -0
  130. package/build/conformance/types.d.ts.map +1 -0
  131. package/build/conformance/types.js +2 -0
  132. package/build/conformance/types.js.map +1 -0
  133. package/build/consts.d.ts +5 -0
  134. package/build/consts.d.ts.map +1 -1
  135. package/build/consts.js +5 -0
  136. package/build/consts.js.map +1 -1
  137. package/build/definitions.d.ts +18 -0
  138. package/build/definitions.d.ts.map +1 -0
  139. package/build/definitions.js +225 -0
  140. package/build/definitions.js.map +1 -0
  141. package/build/executor/changes/types.d.ts +36 -0
  142. package/build/executor/changes/types.d.ts.map +1 -0
  143. package/build/executor/changes/types.js +2 -0
  144. package/build/executor/changes/types.js.map +1 -0
  145. package/build/executor/changes.d.ts +3 -30
  146. package/build/executor/changes.d.ts.map +1 -1
  147. package/build/executor/changes.js +42 -51
  148. package/build/executor/changes.js.map +1 -1
  149. package/build/executor/code/types.d.ts +17 -0
  150. package/build/executor/code/types.d.ts.map +1 -0
  151. package/build/executor/code/types.js +2 -0
  152. package/build/executor/code/types.js.map +1 -0
  153. package/build/executor/code.d.ts +3 -14
  154. package/build/executor/code.d.ts.map +1 -1
  155. package/build/executor/code.js +50 -54
  156. package/build/executor/code.js.map +1 -1
  157. package/build/executor/consts.local.d.ts +5 -0
  158. package/build/executor/consts.local.d.ts.map +1 -0
  159. package/build/executor/consts.local.js +6 -0
  160. package/build/executor/consts.local.js.map +1 -0
  161. package/build/executor/creator/types.d.ts +33 -0
  162. package/build/executor/creator/types.d.ts.map +1 -0
  163. package/build/executor/creator/types.js +2 -0
  164. package/build/executor/creator/types.js.map +1 -0
  165. package/build/executor/creator.d.ts +11 -0
  166. package/build/executor/creator.d.ts.map +1 -0
  167. package/build/executor/creator.js +32 -0
  168. package/build/executor/creator.js.map +1 -0
  169. package/build/executor/resolve/types.d.ts +30 -0
  170. package/build/executor/resolve/types.d.ts.map +1 -0
  171. package/build/executor/resolve/types.js +2 -0
  172. package/build/executor/resolve/types.js.map +1 -0
  173. package/build/executor/resolve.d.ts +5 -34
  174. package/build/executor/resolve.d.ts.map +1 -1
  175. package/build/executor/resolve.js +169 -110
  176. package/build/executor/resolve.js.map +1 -1
  177. package/build/executor/types.d.ts +23 -0
  178. package/build/executor/types.d.ts.map +1 -0
  179. package/build/executor/types.js +2 -0
  180. package/build/executor/types.js.map +1 -0
  181. package/build/executor/validate.d.ts +6 -5
  182. package/build/executor/validate.d.ts.map +1 -1
  183. package/build/executor/validate.js +38 -27
  184. package/build/executor/validate.js.map +1 -1
  185. package/build/executor.d.ts +11 -7
  186. package/build/executor.d.ts.map +1 -1
  187. package/build/executor.js +27 -21
  188. package/build/executor.js.map +1 -1
  189. package/build/facade.d.ts +20 -2
  190. package/build/facade.d.ts.map +1 -1
  191. package/build/facade.js +144 -22
  192. package/build/facade.js.map +1 -1
  193. package/build/helper.d.ts +5 -9
  194. package/build/helper.d.ts.map +1 -1
  195. package/build/helper.js +12 -11
  196. package/build/helper.js.map +1 -1
  197. package/build/hierarchy/types.d.ts +20 -0
  198. package/build/hierarchy/types.d.ts.map +1 -0
  199. package/build/hierarchy/types.js +2 -0
  200. package/build/hierarchy/types.js.map +1 -0
  201. package/build/hierarchy.d.ts +5 -0
  202. package/build/hierarchy.d.ts.map +1 -0
  203. package/build/hierarchy.js +133 -0
  204. package/build/hierarchy.js.map +1 -0
  205. package/build/index.d.ts +1 -0
  206. package/build/index.d.ts.map +1 -1
  207. package/build/index.js +1 -0
  208. package/build/index.js.map +1 -1
  209. package/build/invariants/types.d.ts +6 -0
  210. package/build/invariants/types.d.ts.map +1 -0
  211. package/build/invariants/types.js +2 -0
  212. package/build/invariants/types.js.map +1 -0
  213. package/build/invariants.d.ts +5 -0
  214. package/build/invariants.d.ts.map +1 -0
  215. package/build/invariants.js +126 -0
  216. package/build/invariants.js.map +1 -0
  217. package/build/projection.d.ts +4 -10
  218. package/build/projection.d.ts.map +1 -1
  219. package/build/projection.js +7 -5
  220. package/build/projection.js.map +1 -1
  221. package/build/queue/consts.d.ts +5 -0
  222. package/build/queue/consts.d.ts.map +1 -0
  223. package/build/queue/consts.js +5 -0
  224. package/build/queue/consts.js.map +1 -0
  225. package/build/queue/declare.d.ts +5 -0
  226. package/build/queue/declare.d.ts.map +1 -0
  227. package/build/queue/declare.js +5 -0
  228. package/build/queue/declare.js.map +1 -0
  229. package/build/queue/index.d.ts +3 -18
  230. package/build/queue/index.d.ts.map +1 -1
  231. package/build/queue/index.js +2 -7
  232. package/build/queue/index.js.map +1 -1
  233. package/build/queue/types.d.ts +13 -0
  234. package/build/queue/types.d.ts.map +1 -0
  235. package/build/queue/types.js +2 -0
  236. package/build/queue/types.js.map +1 -0
  237. package/build/registry.d.ts +5 -22
  238. package/build/registry.d.ts.map +1 -1
  239. package/build/registry.js +16 -10
  240. package/build/registry.js.map +1 -1
  241. package/build/resources.d.ts +5 -0
  242. package/build/resources.d.ts.map +1 -0
  243. package/build/resources.js +287 -0
  244. package/build/resources.js.map +1 -0
  245. package/build/service.d.ts +3 -16
  246. package/build/service.d.ts.map +1 -1
  247. package/build/service.js +12 -0
  248. package/build/service.js.map +1 -1
  249. package/build/store/commits.d.ts.map +1 -1
  250. package/build/store/commits.js +3 -1
  251. package/build/store/commits.js.map +1 -1
  252. package/build/store/composite.d.ts +1 -0
  253. package/build/store/composite.d.ts.map +1 -1
  254. package/build/store/composite.js +7 -2
  255. package/build/store/composite.js.map +1 -1
  256. package/build/store/consts.local.d.ts +2 -0
  257. package/build/store/consts.local.d.ts.map +1 -0
  258. package/build/store/consts.local.js +2 -0
  259. package/build/store/consts.local.js.map +1 -0
  260. package/build/store/fold/types.d.ts +43 -0
  261. package/build/store/fold/types.d.ts.map +1 -0
  262. package/build/store/fold/types.js +2 -0
  263. package/build/store/fold/types.js.map +1 -0
  264. package/build/store/fold.d.ts +8 -30
  265. package/build/store/fold.d.ts.map +1 -1
  266. package/build/store/fold.js +190 -198
  267. package/build/store/fold.js.map +1 -1
  268. package/build/store/index.d.ts +1 -0
  269. package/build/store/index.d.ts.map +1 -1
  270. package/build/store/index.js.map +1 -1
  271. package/build/store/memory.d.ts +12 -0
  272. package/build/store/memory.d.ts.map +1 -1
  273. package/build/store/memory.js +0 -0
  274. package/build/store/memory.js.map +1 -1
  275. package/build/store/types.d.ts +15 -1
  276. package/build/store/types.d.ts.map +1 -1
  277. package/build/store/types.local.d.ts +5 -0
  278. package/build/store/types.local.d.ts.map +1 -0
  279. package/build/store/types.local.js +2 -0
  280. package/build/store/types.local.js.map +1 -0
  281. package/build/types.d.ts +126 -2
  282. package/build/types.d.ts.map +1 -1
  283. package/build/types.local.d.ts +7 -0
  284. package/build/types.local.d.ts.map +1 -0
  285. package/build/types.local.js +2 -0
  286. package/build/types.local.js.map +1 -0
  287. package/build/utils/access/types.d.ts +30 -0
  288. package/build/utils/access/types.d.ts.map +1 -0
  289. package/build/utils/access/types.js +2 -0
  290. package/build/utils/access/types.js.map +1 -0
  291. package/build/utils/access.d.ts +4 -0
  292. package/build/utils/access.d.ts.map +1 -0
  293. package/build/utils/access.js +28 -0
  294. package/build/utils/access.js.map +1 -0
  295. package/build/utils/guard/types.d.ts +23 -0
  296. package/build/utils/guard/types.d.ts.map +1 -0
  297. package/build/utils/guard/types.js +2 -0
  298. package/build/utils/guard/types.js.map +1 -0
  299. package/build/utils/guard.d.ts +7 -10
  300. package/build/utils/guard.d.ts.map +1 -1
  301. package/build/utils/guard.js +36 -32
  302. package/build/utils/guard.js.map +1 -1
  303. package/build/utils/handler/types.d.ts +26 -0
  304. package/build/utils/handler/types.d.ts.map +1 -0
  305. package/build/utils/handler/types.js +2 -0
  306. package/build/utils/handler/types.js.map +1 -0
  307. package/build/utils/handler.d.ts +6 -0
  308. package/build/utils/handler.d.ts.map +1 -0
  309. package/build/utils/handler.js +21 -0
  310. package/build/utils/handler.js.map +1 -0
  311. package/build/utils/index.d.ts +8 -0
  312. package/build/utils/index.d.ts.map +1 -1
  313. package/build/utils/index.js +3 -0
  314. package/build/utils/index.js.map +1 -1
  315. package/build/utils/request/types.d.ts +23 -0
  316. package/build/utils/request/types.d.ts.map +1 -0
  317. package/build/utils/request/types.js +2 -0
  318. package/build/utils/request/types.js.map +1 -0
  319. package/build/utils/request.d.ts +4 -0
  320. package/build/utils/request.d.ts.map +1 -0
  321. package/build/utils/request.js +27 -0
  322. package/build/utils/request.js.map +1 -0
  323. package/build/utils/scope.d.ts +17 -13
  324. package/build/utils/scope.d.ts.map +1 -1
  325. package/build/utils/scope.js +21 -35
  326. package/build/utils/scope.js.map +1 -1
  327. package/build/utils/types.d.ts +8 -0
  328. package/build/utils/types.d.ts.map +1 -0
  329. package/build/utils/types.js +2 -0
  330. package/build/utils/types.js.map +1 -0
  331. package/package.json +30 -22
  332. package/src/actions/apply-schema-request.ts +36 -0
  333. package/src/actions/commit.ts +7 -5
  334. package/src/actions/define-schemas.ts +34 -0
  335. package/src/actions/execute.ts +32 -34
  336. package/src/actions/execution/types.ts +34 -0
  337. package/src/actions/execution.ts +100 -0
  338. package/src/actions/get-card.ts +14 -0
  339. package/src/actions/get-specification.ts +14 -0
  340. package/src/actions/index.ts +13 -3
  341. package/src/actions/links.ts +8 -6
  342. package/src/actions/list-card-specifications.ts +15 -0
  343. package/src/actions/list-card-transitions.ts +15 -0
  344. package/src/actions/list-cards.ts +14 -0
  345. package/src/actions/list-schemas.ts +29 -0
  346. package/src/actions/list-specification-revisions.ts +15 -0
  347. package/src/actions/resources.ts +79 -0
  348. package/src/actions/summarize-cards.ts +15 -0
  349. package/src/actions/transitions.ts +6 -4
  350. package/src/actions/types.ts +5 -0
  351. package/src/actions/watch.ts +11 -7
  352. package/src/conformance/assert/types.ts +16 -0
  353. package/src/conformance/assert.ts +67 -0
  354. package/src/conformance/cases.ts +631 -0
  355. package/src/conformance/clock.ts +11 -0
  356. package/src/conformance/consts.ts +165 -0
  357. package/src/conformance/errors.ts +7 -0
  358. package/src/conformance/fixtures/types.ts +11 -0
  359. package/src/conformance/fixtures.ts +46 -0
  360. package/src/conformance/index.ts +14 -0
  361. package/src/conformance/resources.ts +227 -0
  362. package/src/conformance/types.ts +20 -0
  363. package/src/consts.ts +6 -0
  364. package/src/definitions.ts +264 -0
  365. package/src/executor/changes/types.ts +38 -0
  366. package/src/executor/changes.ts +49 -68
  367. package/src/executor/code/types.ts +23 -0
  368. package/src/executor/code.ts +62 -69
  369. package/src/executor/consts.local.ts +8 -0
  370. package/src/executor/creator/types.ts +33 -0
  371. package/src/executor/creator.ts +43 -0
  372. package/src/executor/resolve/types.ts +30 -0
  373. package/src/executor/resolve.ts +181 -139
  374. package/src/executor/types.ts +23 -0
  375. package/src/executor/validate.ts +40 -38
  376. package/src/executor.ts +33 -27
  377. package/src/facade.ts +132 -29
  378. package/src/helper.ts +12 -11
  379. package/src/hierarchy/types.ts +20 -0
  380. package/src/hierarchy.ts +113 -0
  381. package/src/index.ts +1 -0
  382. package/src/invariants/types.ts +6 -0
  383. package/src/invariants.ts +106 -0
  384. package/src/projection.ts +12 -18
  385. package/src/queue/consts.ts +5 -0
  386. package/src/queue/declare.ts +7 -0
  387. package/src/queue/index.ts +3 -25
  388. package/src/queue/types.ts +14 -0
  389. package/src/registry.ts +18 -36
  390. package/src/resources.ts +256 -0
  391. package/src/service.ts +16 -20
  392. package/src/store/commits.ts +5 -4
  393. package/src/store/composite.ts +7 -2
  394. package/src/store/consts.local.ts +1 -0
  395. package/src/store/fold/types.ts +44 -0
  396. package/src/store/fold.ts +214 -212
  397. package/src/store/index.ts +1 -0
  398. package/src/store/memory.ts +0 -0
  399. package/src/store/types.local.ts +3 -0
  400. package/src/store/types.ts +16 -1
  401. package/src/types.local.ts +7 -0
  402. package/src/types.ts +139 -6
  403. package/src/utils/access/types.ts +30 -0
  404. package/src/utils/access.ts +33 -0
  405. package/src/utils/guard/types.ts +21 -0
  406. package/src/utils/guard.ts +44 -31
  407. package/src/utils/handler/types.ts +28 -0
  408. package/src/utils/handler.ts +35 -0
  409. package/src/utils/index.ts +9 -0
  410. package/src/utils/request/types.ts +23 -0
  411. package/src/utils/request.ts +40 -0
  412. package/src/utils/scope.ts +39 -42
  413. package/src/utils/types.ts +8 -0
  414. package/tests/access.spec.ts +160 -0
  415. package/tests/app.spec.ts +112 -0
  416. package/tests/conformance.spec.ts +47 -0
  417. package/tests/create-example.spec.ts +202 -0
  418. package/tests/creator.spec.ts +257 -0
  419. package/tests/entrypoints.spec.ts +5 -0
  420. package/tests/memory-store.spec.ts +52 -0
  421. package/tests/narrowing.spec.ts +94 -0
  422. package/tests/plugins.spec.ts +4 -3
  423. package/tests/writes.spec.ts +140 -0
  424. package/build/actions/cards.d.ts +0 -16
  425. package/build/actions/cards.d.ts.map +0 -1
  426. package/build/actions/cards.js +0 -32
  427. package/build/actions/cards.js.map +0 -1
  428. package/build/actions/schemas.d.ts +0 -6
  429. package/build/actions/schemas.d.ts.map +0 -1
  430. package/build/actions/schemas.js +0 -9
  431. package/build/actions/schemas.js.map +0 -1
  432. package/build/actions/specs.d.ts +0 -10
  433. package/build/actions/specs.d.ts.map +0 -1
  434. package/build/actions/specs.js +0 -14
  435. package/build/actions/specs.js.map +0 -1
  436. package/src/actions/cards.ts +0 -52
  437. package/src/actions/schemas.ts +0 -15
  438. package/src/actions/specs.ts +0 -23
@@ -0,0 +1,106 @@
1
+ import {
2
+ FieldsInvalid, PlanningResourceKind, PLANNING_ASSIGNEE, PLANNING_REPORTER, ProjectMode,
3
+ RelationshipRefused, TransitionAction, validateHelper, WorkcardKind, type RelationshipDraft, type Workcard,
4
+ } from '@owlmeans/planning'
5
+ import { makePlanningHierarchy } from './hierarchy.js'
6
+ import type { PlanningRuntime } from './types.js'
7
+ import type { PlanningInvariants } from './invariants/types.js'
8
+
9
+ /** Checks projected state again at persistence time, including queued writes whose parents moved. */
10
+ export const makePlanningInvariants = (runtime: PlanningRuntime): PlanningInvariants => {
11
+ const registryFor = async (card: Workcard) => await runtime.schemasFor(card.entityId, await makePlanningHierarchy(runtime).project(card))
12
+ const referencesOf = async (card: Workcard): Promise<RelationshipDraft[]> => {
13
+ const schemas = await registryFor(card)
14
+ const refs: RelationshipDraft[] = []
15
+ if (card.reporter != null) refs.push({ type: PLANNING_REPORTER, to: card.reporter, toKind: PlanningResourceKind.Assignee })
16
+ if (card.assignee != null) refs.push({ type: PLANNING_ASSIGNEE, to: card.assignee, toKind: PlanningResourceKind.Assignee })
17
+ for (const rule of schemas.type(card.type).relationships ?? []) {
18
+ if (rule.field == null) continue
19
+ const value = card.fields[rule.field]
20
+ if (value == null) continue
21
+ if (rule.multiple !== true && typeof value !== 'string') throw new FieldsInvalid(`relationship:field:${rule.field}:single`)
22
+ if (rule.multiple === true && (!Array.isArray(value) || value.some(id => typeof id !== 'string'))) throw new FieldsInvalid(`relationship:field:${rule.field}:multiple`)
23
+ for (const id of Array.isArray(value) ? value : [value]) refs.push({ type: rule.name, to: id as string, toKind: rule.toKind ?? PlanningResourceKind.Workcard })
24
+ }
25
+ return refs
26
+ }
27
+ const assertReference = async (card: Workcard, ref: RelationshipDraft, existing: boolean): Promise<void> => {
28
+ const reader = runtime.reader()
29
+ const kind = ref.toKind ?? PlanningResourceKind.Workcard
30
+ const target = kind === PlanningResourceKind.Assignee ? await reader.assignees?.get(ref.to, card.entityId)
31
+ : kind === PlanningResourceKind.Team ? await reader.teams?.get(ref.to, card.entityId)
32
+ : kind === PlanningResourceKind.Workcard ? await reader.cards.get(ref.to, card.entityId) : null
33
+ if (target == null || target.entityId !== card.entityId) throw new RelationshipRefused(`reference:${kind}:${ref.to}`)
34
+ const rule = (await registryFor(card)).type(card.type).relationships?.find(rule => rule.name === ref.type)
35
+ if (rule?.from != null && !rule.from.includes(card.type)) throw new RelationshipRefused(`from-type:${ref.type}:${card.type}`)
36
+ if (rule?.to != null && !rule.to.includes('type' in target ? String(target.type) : '')) throw new RelationshipRefused(`to-type:${ref.type}`)
37
+ if (kind === PlanningResourceKind.Assignee && !existing && 'retired' in target && target.retired) throw new RelationshipRefused(`assignee:retired:${ref.to}`)
38
+ }
39
+ const invariants: PlanningInvariants = {
40
+ validate: async (before, after, transition) => {
41
+ const hierarchy = makePlanningHierarchy(runtime)
42
+ if (after == null) {
43
+ if (before != null) {
44
+ await hierarchy.validate(before, true)
45
+ // What the delete's fold removes: a project's whole subtree, any other card's own documents.
46
+ const removed = before.kind === WorkcardKind.Project ? await hierarchy.descendants(before.id!, before.entityId) : await hierarchy.documents(before.id!, before.entityId)
47
+ const doomed = new Set([before.id!, ...removed.map(card => card.id!)])
48
+ const incoming = await runtime.reader().links?.list({ entityId: before.entityId, to: [...doomed], toKind: PlanningResourceKind.Workcard }, { size: 0 })
49
+ for (const edge of incoming?.items ?? []) {
50
+ if (doomed.has(edge.from) || (edge.fromKind ?? PlanningResourceKind.Workcard) !== PlanningResourceKind.Workcard) continue
51
+ const owner = await runtime.reader().cards.get(edge.from, before.entityId)
52
+ if (owner != null && (await registryFor(owner)).type(owner.type).relationships?.some(rule => rule.name === edge.type && rule.field != null)) {
53
+ throw new RelationshipRefused(`reference-in-use:${edge.to}:${edge.from}`)
54
+ }
55
+ }
56
+ }
57
+ return
58
+ }
59
+ await hierarchy.validate(after)
60
+ const schemas = await registryFor(after)
61
+ validateHelper.validateCard(after, schemas)
62
+ if (after.kind !== WorkcardKind.Project && after.mode != null) throw new FieldsInvalid('mode:project-only')
63
+ if (after.kind === WorkcardKind.Project && after.mode == null) after.mode = ProjectMode.Opened
64
+ const previous = before == null ? [] : await referencesOf(before)
65
+ for (const ref of await referencesOf(after)) await assertReference(after, ref, previous.some(entry => entry.type === ref.type && entry.to === ref.to && entry.toKind === ref.toKind))
66
+ const counted = new Map<string, Set<string>>()
67
+ for (const link of transition.links ?? (transition.link != null ? [transition.link] : [])) {
68
+ const rule = schemas.type(after.type).relationships?.find(rule => rule.name === link.type)
69
+ if (rule == null) throw new RelationshipRefused(`unknown:${link.type}`)
70
+ if ((link.toKind ?? PlanningResourceKind.Workcard) !== (rule.toKind ?? PlanningResourceKind.Workcard) || (link.fromKind ?? PlanningResourceKind.Workcard) !== PlanningResourceKind.Workcard) throw new RelationshipRefused(`kind:${link.type}`)
71
+ if (link.from != null && link.from !== after.id) throw new RelationshipRefused(`from:${link.from}`)
72
+ if (rule?.field != null) throw new RelationshipRefused(`field-authoritative:${rule.field}`)
73
+ if (transition.action !== TransitionAction.Unlink) {
74
+ await assertReference(after, link, false)
75
+ if (rule.single) {
76
+ const destinations = counted.get(rule.name) ?? new Set<string>()
77
+ destinations.add(link.to)
78
+ for (const edge of (await runtime.reader().links?.list({ entityId: after.entityId, from: after.id, type: rule.name }, { size: 0 }))?.items ?? []) destinations.add(edge.to)
79
+ counted.set(rule.name, destinations)
80
+ if (destinations.size > 1) throw new RelationshipRefused(`single:${rule.name}`)
81
+ }
82
+ }
83
+ }
84
+ if (before != null && (before.parent !== after.parent || JSON.stringify(before.parents) !== JSON.stringify(after.parents))) {
85
+ const reader = runtime.reader()
86
+ const alternate: PlanningRuntime = { ...runtime, reader: () => ({ ...reader, cards: { ...reader.cards, get: async (id, entityId) => id === after.id && entityId === after.entityId ? after : await reader.cards.get(id, entityId) } }) }
87
+ const hypothetical = makePlanningHierarchy(alternate)
88
+ for (const child of await hierarchy.descendants(after.id!, after.entityId)) {
89
+ await hypothetical.validate(child)
90
+ await makePlanningInvariants(alternate).validate(child, child, { ...transition, links: undefined, link: undefined, action: TransitionAction.Update })
91
+ }
92
+ }
93
+ },
94
+ references: async (after, transition) => {
95
+ const links = runtime.service().store(after.type).links
96
+ if (links == null) return
97
+ const schemas = await registryFor(after)
98
+ const types = [PLANNING_REPORTER, PLANNING_ASSIGNEE, ...(schemas.type(after.type).relationships ?? []).filter(rule => rule.field != null).map(rule => rule.name)]
99
+ const wanted = await referencesOf(after)
100
+ const stored = await links.list({ entityId: after.entityId, from: after.id, type: types }, { size: 0 })
101
+ for (const link of stored.items) if (!wanted.some(ref => ref.type === link.type && ref.to === link.to && (ref.toKind ?? PlanningResourceKind.Workcard) === (link.toKind ?? PlanningResourceKind.Workcard))) await links.drop({ entityId: after.entityId, id: link.id })
102
+ for (const ref of wanted) await links.put({ ...ref, from: after.id!, fromKind: PlanningResourceKind.Workcard, entityId: after.entityId, createdAt: transition.at, transition: transition.id, project: transition.project })
103
+ },
104
+ }
105
+ return invariants
106
+ }
package/src/projection.ts CHANGED
@@ -1,22 +1,12 @@
1
1
  import type { BasicConfig, BasicContext } from '@owlmeans/context'
2
- import { PLANNING_SERVICE } from '@owlmeans/planning'
3
- import type { CommitEvent, PlanningService, PlanningStore } from '@owlmeans/planning'
4
- import { PLANNING_PROJECT_JOB, PLANNING_PROJECTION_QUEUE } from './queue/index.js'
5
- import type { ProjectionRequest } from './queue/index.js'
2
+ import { PLANNING_SERVICE, type PlanningService, type PlanningStore } from '@owlmeans/planning'
3
+ import { PLANNING_PROJECT_JOB, PLANNING_PROJECTION_QUEUE } from './queue/consts.js'
4
+ import type { ProjectionRequest } from './queue/types.js'
6
5
  import type { JobProcessor, QueueHooks } from '@owlmeans/queue'
7
- import { failPending, foldPending } from './store/fold.js'
8
- import type { PlanningHostService } from './types.js'
6
+ import { foldHelper } from './store/fold.js'
7
+ import type { PlanningHostService, ProjectionOptions } from './types.js'
9
8
  import type { CommitHub, FoldResult } from './store/types.js'
10
9
 
11
- export interface ProjectionOptions {
12
- /** The planning service alias. */
13
- service?: string
14
- /** The store the queue folds — the service's default store when omitted. */
15
- store?: (service: PlanningService) => PlanningStore
16
- /** Where events go — the store's commit hub `publish` when it has one. */
17
- publish?: (event: CommitEvent) => Promise<void>
18
- }
19
-
20
10
  const serviceOf = (ctx: BasicContext<BasicConfig>, opts?: ProjectionOptions): PlanningService =>
21
11
  ctx.service<PlanningHostService>(opts?.service ?? PLANNING_SERVICE)
22
12
 
@@ -40,7 +30,7 @@ export const makeProjectionProcessor = (
40
30
  const store = storeOf(service, opts)
41
31
  const publish = publisherOf(store, opts)
42
32
 
43
- return await foldPending(store, job.data.cardId, job.data.entityId, {
33
+ return await foldHelper.foldPending(store, job.data.cardId, job.data.entityId, {
44
34
  touch: job.touch,
45
35
  onCommitted: async event => { await service.committed(event) },
46
36
  ...(publish != null ? { publish } : {}),
@@ -52,7 +42,7 @@ export const makeProjectionProcessor = (
52
42
  * failed event is published — otherwise `execute({ wait: true })` and `commits.wait` hang to their
53
43
  * timeout. Merge it into the queue driver's hooks.
54
44
  */
55
- export const planningQueueHooks = (ctx: BasicContext<BasicConfig>, opts?: ProjectionOptions): QueueHooks => ({
45
+ export const makePlanningQueueHooks = (ctx: BasicContext<BasicConfig>, opts?: ProjectionOptions): QueueHooks => ({
56
46
  onJobDead: async (job, reason) => {
57
47
  if (job.queue !== PLANNING_PROJECTION_QUEUE || job.name !== PLANNING_PROJECT_JOB) {
58
48
  return
@@ -60,6 +50,10 @@ export const planningQueueHooks = (ctx: BasicContext<BasicConfig>, opts?: Projec
60
50
  const request = job.data as ProjectionRequest
61
51
  const store = storeOf(serviceOf(ctx, opts), opts)
62
52
  const publish = publisherOf(store, opts)
63
- await failPending(store, request.cardId, request.entityId, reason, publish != null ? { publish } : {})
53
+ await foldHelper.failPending(store, request.cardId, request.entityId, reason, publish != null ? { publish } : {})
64
54
  },
65
55
  })
56
+
57
+ /** @deprecated compat:factory-refactor — use `makePlanningQueueHooks(…)` */
58
+ export const planningQueueHooks = (ctx: BasicContext<BasicConfig>, opts?: ProjectionOptions): QueueHooks =>
59
+ makePlanningQueueHooks(ctx, opts)
@@ -0,0 +1,5 @@
1
+ /** The server-side queue used to project planning transitions. */
2
+ export const PLANNING_PROJECTION_QUEUE = 'planning-projection'
3
+
4
+ /** The processor name accepted by the planning projection queue. */
5
+ export const PLANNING_PROJECT_JOB = 'planning:project'
@@ -0,0 +1,7 @@
1
+ import { declareQueue, type Config } from '@owlmeans/queue'
2
+ import { PLANNING_PROJECT_JOB, PLANNING_PROJECTION_QUEUE } from './consts.js'
3
+ import type { PlanningQueueOptions } from './types.js'
4
+
5
+ /** Declare the shared projection address; each process separately chooses whether to listen. */
6
+ export const declarePlanningQueue = <C extends Config>(cfg: C, opts?: PlanningQueueOptions): C =>
7
+ declareQueue(cfg, PLANNING_PROJECTION_QUEUE, [PLANNING_PROJECT_JOB], opts)
@@ -1,25 +1,3 @@
1
- import { declareQueue } from '@owlmeans/queue'
2
- import type { Config, JobOptions, QueueWorkerOptions } from '@owlmeans/queue'
3
-
4
- /** The server-side queue used to project planning transitions. */
5
- export const PLANNING_PROJECTION_QUEUE = 'planning-projection'
6
-
7
- /** The processor name accepted by the planning projection queue. */
8
- export const PLANNING_PROJECT_JOB = 'planning:project'
9
-
10
- /** What one projection job folds. */
11
- export interface ProjectionRequest {
12
- cardId: string
13
- entityId: string
14
- /** The transition that requested projection; it is a hint rather than a fold bound. */
15
- transition?: string
16
- }
17
-
18
- export interface PlanningQueueOptions {
19
- worker?: QueueWorkerOptions
20
- defaults?: JobOptions
21
- }
22
-
23
- /** Declare the shared projection address; each process separately chooses whether to listen. */
24
- export const declarePlanningQueue = <C extends Config>(cfg: C, opts?: PlanningQueueOptions): C =>
25
- declareQueue(cfg, PLANNING_PROJECTION_QUEUE, [PLANNING_PROJECT_JOB], opts)
1
+ export { declarePlanningQueue } from './declare.js'
2
+ export { PLANNING_PROJECTION_QUEUE, PLANNING_PROJECT_JOB } from './consts.js'
3
+ export type { PlanningQueueOptions, ProjectionRequest } from './types.js'
@@ -0,0 +1,14 @@
1
+ import type { JobOptions, QueueWorkerOptions } from '@owlmeans/queue'
2
+
3
+ /** What one projection job folds. */
4
+ export interface ProjectionRequest {
5
+ cardId: string
6
+ entityId: string
7
+ /** The transition that requested projection; it is a hint rather than a fold bound. */
8
+ transition?: string
9
+ }
10
+
11
+ export interface PlanningQueueOptions {
12
+ worker?: QueueWorkerOptions
13
+ defaults?: JobOptions
14
+ }
package/src/registry.ts CHANGED
@@ -1,34 +1,10 @@
1
1
  import type { BasicConfig, BasicContext } from '@owlmeans/context'
2
2
  import { ResilientError } from '@owlmeans/error'
3
- import { DEFAULT_PLUGIN_ORDER, PlanningRefused } from '@owlmeans/planning'
4
- import type {
5
- CommitEvent, PlanningExecContext, PlanningHookContext, PlanningPlugin, PlanningSchemaRegistry,
6
- PlanningStore, TransitionExecution, WorkcardDraft,
7
- } from '@owlmeans/planning'
8
- import type { StoreRoute } from './store/types.js'
3
+ import { logger } from '@owlmeans/log'
4
+ import { appSchemaHelper, DEFAULT_PLUGIN_ORDER, PlanningRefused, type PlanningPlugin, type PlanningSchemaRegistry, type PlanningStore } from '@owlmeans/planning'
5
+ import type { PluginRegistry } from './types.js'
9
6
 
10
- export interface PluginRegistry {
11
- /** Register, or replace the plugin of the same `name`. Its schemas are contributed now. */
12
- use: (plugin: PlanningPlugin) => void
13
- /** Every plugin, `order` ascending (registration order among equals). */
14
- plugins: () => PlanningPlugin[]
15
- /** The owning plugins that supply a store, in plugin order, each store resolved once. */
16
- routes: (ctx?: BasicContext<BasicConfig>) => StoreRoute[]
17
- /** The first owning plugin's store for a type, else `fallback`. */
18
- storeFor: (type: string | undefined, fallback: PlanningStore, ctx?: BasicContext<BasicConfig>) => PlanningStore
19
- /** The first plugin answering a code wins. */
20
- mintCode: (
21
- draft: WorkcardDraft, taken: (code: string) => Promise<boolean>, contextOf: (plugin: PlanningPlugin) => PlanningExecContext
22
- ) => Promise<string | undefined>
23
- /** The `before` chain; answers the execution the chain left. */
24
- before: (
25
- exec: TransitionExecution, contextOf: (plugin: PlanningPlugin) => PlanningExecContext
26
- ) => Promise<TransitionExecution>
27
- /** The `after` chain; a failing hook is logged and the chain goes on. */
28
- after: (event: CommitEvent, contextOf: (plugin: PlanningPlugin) => PlanningHookContext) => Promise<void>
29
- /** Bumps on every `use` — what a cached composite store is keyed by. */
30
- version: () => number
31
- }
7
+ const log = logger('planning')
32
8
 
33
9
  const messageOf = (error: unknown): string => error instanceof Error ? error.message : `${error}`
34
10
 
@@ -37,21 +13,26 @@ const messageOf = (error: unknown): string => error instanceof Error ? error.mes
37
13
  *
38
14
  * Ordering is `order ?? DEFAULT_PLUGIN_ORDER` ascending, lower first; a plugin registered under a
39
15
  * name already present replaces it in place. A store factory is resolved once per plugin object and
40
- * handed to `onStore` (the service binds its `committed` there).
16
+ * handed to `onStore` (the service binds its `committed` there). The application's declarations the
17
+ * registry started with (the service's `schemas.app`) are the first source every `use` merges the
18
+ * plugins' `schemas.app` over, in plugin order.
41
19
  */
42
20
  export const makePluginRegistry = (
43
21
  schemas: PlanningSchemaRegistry, onStore?: (store: PlanningStore) => void
44
22
  ): PluginRegistry => {
45
23
  const registered: PlanningPlugin[] = []
46
24
  const stores = new WeakMap<PlanningPlugin, PlanningStore>()
25
+ const base = schemas.app()
47
26
  let version = 0
48
27
 
49
- const ordered = (): PlanningPlugin[] => registered
28
+ const sorted = (plugins: PlanningPlugin[]): PlanningPlugin[] => plugins
50
29
  .map((plugin, index) => ({ plugin, index }))
51
30
  .sort((left, right) => (left.plugin.order ?? DEFAULT_PLUGIN_ORDER) - (right.plugin.order ?? DEFAULT_PLUGIN_ORDER)
52
31
  || left.index - right.index)
53
32
  .map(entry => entry.plugin)
54
33
 
34
+ const ordered = (): PlanningPlugin[] => sorted(registered)
35
+
55
36
  const storeOf = (plugin: PlanningPlugin, ctx?: BasicContext<BasicConfig>): PlanningStore | undefined => {
56
37
  if (plugin.store == null) {
57
38
  return undefined
@@ -68,11 +49,12 @@ export const makePluginRegistry = (
68
49
  const registry: PluginRegistry = {
69
50
  use: plugin => {
70
51
  const at = registered.findIndex(entry => entry.name === plugin.name)
71
- if (at < 0) {
72
- registered.push(plugin)
73
- } else {
74
- registered[at] = plugin
75
- }
52
+ const next = at < 0 ? [...registered, plugin] : registered.map((entry, index) => index === at ? plugin : entry)
53
+ // Merged before anything changes: a malformed declaration leaves the registry as it was.
54
+ const app = appSchemaHelper.merge([base, ...sorted(next).map(entry => entry.schemas?.app)])
55
+ registered.splice(0, registered.length, ...next)
56
+ schemas.registerApp(app)
57
+ plugin.schemas?.assigneeTypes?.forEach(schemas.registerAssigneeType)
76
58
  plugin.schemas?.flows?.forEach(schemas.registerFlow)
77
59
  plugin.schemas?.types?.forEach(schemas.registerType)
78
60
  version++
@@ -131,7 +113,7 @@ export const makePluginRegistry = (
131
113
  try {
132
114
  await plugin.after(event, contextOf(plugin))
133
115
  } catch (error) {
134
- console.error(`planning: after hook of ${plugin.name} failed on ${event.transition}:`, error)
116
+ log.error('Planning after hook failed', { plugin: plugin.name, transition: event.transition, error })
135
117
  }
136
118
  }
137
119
  },
@@ -0,0 +1,256 @@
1
+ import {
2
+ AssigneeKind, AssigneeSchema, CommentSchema, CommentMentionSchema, FieldsInvalid, mentionHelper,
3
+ PlanningForbidden, PlanningResourceKind, PlanningSchemaKind, PlanningUnsupported, PLANNING_PROJECT_TEAM,
4
+ PLANNING_TEAM_MEMBER, TeamSchema, WorkcardConflict, WorkcardKind, WorkcardNotFound, validateHelper,
5
+ } from '@owlmeans/planning'
6
+ import type {
7
+ Assignee, Comment, CommentMention, PlanningFacade, PlanningRecord, PlanningRecordStore,
8
+ PlanningResourceFacade, RelationshipStore, Team,
9
+ } from '@owlmeans/planning'
10
+ import { recordQueryHelper, type Criteria } from '@owlmeans/resource'
11
+ import type { PlanningRuntime } from './types.js'
12
+ import { makePlanningHierarchy } from './hierarchy.js'
13
+
14
+ /** Versioned auxiliary resources, using the same scoped schema and relationship ports as cards. */
15
+ export const makeResourceFacade = (runtime: PlanningRuntime, facade: () => PlanningFacade): PlanningResourceFacade => {
16
+ const scope = () => facade().scope
17
+ const entityId = () => scope().entityId
18
+ const now = runtime.options.now ?? (() => new Date().toISOString())
19
+ const store = () => runtime.service().store()
20
+ const unit = async <R>(run: () => Promise<R>): Promise<R> => store().unit == null ? await run() : await store().unit!(entityId(), run)
21
+ const port = <R extends PlanningRecord>(name: 'assignees' | 'teams' | 'comments' | 'mentions'): PlanningRecordStore<R> => {
22
+ const value = store()[name]
23
+ if (value == null) throw new PlanningUnsupported(name)
24
+ return value as unknown as PlanningRecordStore<R>
25
+ }
26
+ const links = (): RelationshipStore => {
27
+ if (store().links == null) throw new PlanningUnsupported('links')
28
+ return store().links!
29
+ }
30
+ const base = (): PlanningRecord => ({ entityId: entityId(), id: store().newId?.() ?? runtime.options.ids?.(), version: 1, createdAt: now() })
31
+ const get = async <R extends PlanningRecord>(name: 'assignees' | 'teams' | 'comments' | 'mentions', id: string): Promise<R> => {
32
+ const found = await port<R>(name).get(id, entityId())
33
+ if (found == null || found.entityId !== entityId()) throw new WorkcardNotFound(`${name}:${id}`)
34
+ return found
35
+ }
36
+ const check = (record: object, schema: object): void => {
37
+ // The same record validator is used for both built-in durable providers.
38
+ validateHelper.assertSchema(schema, record)
39
+ if ('fields' in record) {
40
+ const invalid = validateHelper.invalidFieldKeys(record.fields)
41
+ if (invalid.length > 0) throw new FieldsInvalid(`keys:${invalid.join(',')}`)
42
+ }
43
+ }
44
+ const assertVersion = (record: PlanningRecord, version: number): void => {
45
+ if (record.version !== version) throw new WorkcardConflict(`${record.id}:version:${version}:stored:${record.version}`)
46
+ }
47
+ const list = async <R extends PlanningRecord>(name: 'assignees' | 'teams' | 'comments' | 'mentions', query?: object) => {
48
+ const { page, size, sort, ids, authentication, nickname, ...where } = (query ?? {}) as Record<string, any>
49
+ return await port<R>(name).list({ ...where, entityId: entityId(),
50
+ ...(name === 'assignees' && where.retired === false ? { retired: { $ne: true } } : {}),
51
+ ...(ids != null ? { id: { $in: ids } } : {}),
52
+ ...(nickname != null ? { nicknameKey: mentionHelper.nicknameKey(nickname) } : {}),
53
+ ...(authentication != null ? { 'authentication.provider': authentication.provider, 'authentication.externalId': authentication.externalId } : {}),
54
+ } as Criteria<R>, { page, size, sort })
55
+ }
56
+ const validateAssignee = async (record: Assignee, creating: boolean): Promise<void> => {
57
+ check(record, AssigneeSchema)
58
+ const schemas = await runtime.schemasFor(entityId())
59
+ const schema = schemas.assigneeType(record.type)
60
+ if (creating && 'isRetired' in schemas && (schemas as any).isRetired(PlanningSchemaKind.AssigneeType, record.type)) {
61
+ throw new FieldsInvalid(`assignee:retired-type:${record.type}`)
62
+ }
63
+ if (schema.kind !== record.kind) throw new FieldsInvalid(`assignee:kind:${record.kind}`)
64
+ if ((schema.authentication === 'required' || (schema.authentication == null && record.kind === AssigneeKind.Human)) && record.authentication == null) {
65
+ throw new FieldsInvalid('assignee:authentication-required')
66
+ }
67
+ const validate = schemas.assigneeValidator(record.type)
68
+ if (!validate(record.fields)) throw new FieldsInvalid(validateHelper.ajvErrorText(validate.errors))
69
+ const duplicate = await list<Assignee>('assignees', { nickname: record.nickname, size: 0 })
70
+ if (duplicate.items.some(row => row.id !== record.id)) throw new WorkcardConflict(`assignee:nickname:${record.nickname}`)
71
+ if (record.authentication != null) {
72
+ const identity = await list<Assignee>('assignees', { authentication: record.authentication, size: 0 })
73
+ if (identity.items.some(row => row.id !== record.id)) throw new WorkcardConflict('assignee:authentication-taken')
74
+ }
75
+ }
76
+ const assertComment = async (record: Comment): Promise<void> => {
77
+ await facade().cards.get(record.card)
78
+ await get<Assignee>('assignees', record.author)
79
+ check(record, CommentSchema)
80
+ for (const id of mentionHelper.parse(record.body)) await get<Assignee>('assignees', id)
81
+ }
82
+ const assertCommentAuthor = (record: Comment): void => {
83
+ if (scope().assigneeId == null || scope().assigneeId !== record.author) throw new PlanningForbidden('comment:author')
84
+ }
85
+ const teamLink = async (team: string, target: string, member: boolean, add: boolean): Promise<void> => {
86
+ await get<Team>('teams', team)
87
+ if (member) {
88
+ const assignee = await get<Assignee>('assignees', target)
89
+ if (add && assignee.retired) throw new FieldsInvalid(`assignee:retired:${target}`)
90
+ } else {
91
+ const project = await facade().cards.get(target)
92
+ if (project.kind !== WorkcardKind.Project) throw new FieldsInvalid(`team:project:${target}`)
93
+ }
94
+ const edge = member
95
+ ? { from: team, to: target, fromKind: PlanningResourceKind.Team, toKind: PlanningResourceKind.Assignee, type: PLANNING_TEAM_MEMBER }
96
+ : { from: target, to: team, fromKind: PlanningResourceKind.Workcard, toKind: PlanningResourceKind.Team, type: PLANNING_PROJECT_TEAM, project: target }
97
+ if (add) await links().put({ ...edge, entityId: entityId(), createdAt: now() })
98
+ else await links().drop({ ...edge, entityId: entityId() })
99
+ }
100
+ const rebuild = async (id: string): Promise<CommentMention[]> => {
101
+ const comment = await get<Comment>('comments', id)
102
+ await facade().cards.get(comment.card)
103
+ const existing = await list<CommentMention>('mentions', { comment: id, size: 0 })
104
+ const wanted = mentionHelper.parse(comment.body)
105
+ const rows: CommentMention[] = []
106
+ for (const assignee of wanted) {
107
+ await get<Assignee>('assignees', assignee)
108
+ const previous = existing.items.find(row => row.assignee === assignee)
109
+ if (previous?.revision === comment.version) { rows.push(previous); continue }
110
+ const row: CommentMention = { ...(previous ?? base()), comment: id, card: comment.card, assignee,
111
+ version: (previous?.version ?? 0) + 1, revision: comment.version }
112
+ check(row, CommentMentionSchema)
113
+ rows.push(await port<CommentMention>('mentions').put(row))
114
+ }
115
+ for (const row of existing.items) if (!wanted.includes(row.assignee)) await port<CommentMention>('mentions').drop(row.id!, entityId(), row.version)
116
+ return rows
117
+ }
118
+ const resources: PlanningResourceFacade = {
119
+ assignees: {
120
+ get: async id => await get<Assignee>('assignees', id),
121
+ load: async id => await port<Assignee>('assignees').get(id, entityId()),
122
+ list: async query => await list<Assignee>('assignees', query),
123
+ create: async draft => await unit(async () => {
124
+ const record: Assignee = { ...base(), ...draft, nickname: draft.nickname.trim(), nicknameKey: mentionHelper.nicknameKey(draft.nickname), fields: draft.fields ?? {} }
125
+ await validateAssignee(record, true)
126
+ return await port<Assignee>('assignees').put(record)
127
+ }),
128
+ update: async (id, changes, opts) => await unit(async () => {
129
+ const previous = await get<Assignee>('assignees', id)
130
+ assertVersion(previous, opts.version)
131
+ const record = { ...previous, ...changes, nicknameKey: mentionHelper.nicknameKey(changes.nickname ?? previous.nickname), version: previous.version + 1, updatedAt: now() }
132
+ await validateAssignee(record, record.type !== previous.type)
133
+ if (record.type !== previous.type) {
134
+ const incoming = await links().list({ entityId: entityId(), to: id, toKind: PlanningResourceKind.Assignee }, { size: 0 })
135
+ for (const edge of incoming.items) {
136
+ if ((edge.fromKind ?? PlanningResourceKind.Workcard) !== PlanningResourceKind.Workcard) continue
137
+ const card = await runtime.reader().cards.get(edge.from, entityId())
138
+ if (card == null) continue
139
+ const schemas = await runtime.schemasFor(entityId(), await makePlanningHierarchy(runtime).project(card))
140
+ const rule = schemas.type(card.type).relationships?.find(rule => rule.name === edge.type)
141
+ if (rule?.to != null && !rule.to.includes(record.type)) throw new FieldsInvalid(`assignee:type-in-use:${record.type}`)
142
+ }
143
+ }
144
+ return await port<Assignee>('assignees').put(record)
145
+ }),
146
+ retire: async (id, opts) => await unit(async () => {
147
+ const previous = await get<Assignee>('assignees', id)
148
+ assertVersion(previous, opts.version)
149
+ return await port<Assignee>('assignees').put({ ...previous, retired: true, version: previous.version + 1, updatedAt: now() })
150
+ }),
151
+ },
152
+ teams: {
153
+ get: async id => await get<Team>('teams', id),
154
+ load: async id => await port<Team>('teams').get(id, entityId()),
155
+ list: async query => await list<Team>('teams', query),
156
+ create: async draft => await unit(async () => {
157
+ const record: Team = { ...base(), ...draft, fields: draft.fields ?? {} }
158
+ check(record, TeamSchema)
159
+ return await port<Team>('teams').put(record)
160
+ }),
161
+ update: async (id, changes, opts) => await unit(async () => {
162
+ const previous = await get<Team>('teams', id)
163
+ assertVersion(previous, opts.version)
164
+ const record = { ...previous, ...changes, version: previous.version + 1, updatedAt: now() }
165
+ check(record, TeamSchema)
166
+ return await port<Team>('teams').put(record)
167
+ }),
168
+ remove: async (id, opts) => await unit(async () => {
169
+ const previous = await get<Team>('teams', id)
170
+ assertVersion(previous, opts.version)
171
+ if ((await links().list({ entityId: entityId(), to: id, toKind: PlanningResourceKind.Team }, { size: 1 })).total > 0) throw new WorkcardConflict(`team:in-use:${id}`)
172
+ await links().drop({ entityId: entityId(), from: id, fromKind: PlanningResourceKind.Team })
173
+ await port<Team>('teams').drop(id, entityId(), opts.version)
174
+ }),
175
+ members: async team => {
176
+ await get<Team>('teams', team)
177
+ const edges = await links().list({ entityId: entityId(), from: team, type: PLANNING_TEAM_MEMBER, fromKind: PlanningResourceKind.Team }, { size: 0 })
178
+ return await Promise.all(edges.items.map(edge => get<Assignee>('assignees', edge.to)))
179
+ },
180
+ addMember: async (team, assignee) => await unit(async () => await teamLink(team, assignee, true, true)),
181
+ removeMember: async (team, assignee) => await unit(async () => await teamLink(team, assignee, true, false)),
182
+ attach: async (team, project) => await unit(async () => await teamLink(team, project, false, true)),
183
+ detach: async (team, project) => await unit(async () => await teamLink(team, project, false, false)),
184
+ projects: async team => {
185
+ await get<Team>('teams', team)
186
+ const edges = await links().list({ entityId: entityId(), to: team, type: PLANNING_PROJECT_TEAM, toKind: PlanningResourceKind.Team }, { size: 0 })
187
+ const visible = await Promise.all(edges.items.map(async edge => await facade().cards.load(edge.from) != null ? edge.from : undefined))
188
+ return visible.filter((id): id is string => id != null)
189
+ },
190
+ assignees: async project => {
191
+ await facade().cards.get(project)
192
+ const edges = await links().list({ entityId: entityId(), from: project, type: PLANNING_PROJECT_TEAM, toKind: PlanningResourceKind.Team }, { size: 0 })
193
+ const members = (await Promise.all(edges.items.map(edge => resources.teams.members(edge.to)))).flat()
194
+ return [...new Map(members.map(member => [member.id, member])).values()]
195
+ },
196
+ },
197
+ comments: {
198
+ get: async id => { const row = await get<Comment>('comments', id); await facade().cards.get(row.card); return row },
199
+ load: async id => { const row = await port<Comment>('comments').get(id, entityId()); return row != null && await facade().cards.load(row.card) != null ? row : null },
200
+ list: async query => {
201
+ if (query?.card != null) await facade().cards.get(query.card)
202
+ const result = await list<Comment>('comments', { ...query, size: 0 })
203
+ const visible: Comment[] = []
204
+ for (const row of result.items) if (await facade().cards.load(row.card) != null) visible.push(row)
205
+ return recordQueryHelper.applyQuery(visible, undefined, query)
206
+ },
207
+ create: async draft => await unit(async () => {
208
+ if (scope().assigneeId == null) throw new PlanningForbidden('comment:author-required')
209
+ const record: Comment = { ...base(), card: draft.card, body: draft.body, author: scope().assigneeId! }
210
+ await assertComment(record)
211
+ const written = await port<Comment>('comments').put(record)
212
+ await rebuild(written.id!)
213
+ return written
214
+ }),
215
+ update: async (id, changes, opts) => await unit(async () => {
216
+ const previous = await get<Comment>('comments', id)
217
+ assertCommentAuthor(previous)
218
+ assertVersion(previous, opts.version)
219
+ const record = { ...previous, body: changes.body, version: previous.version + 1, updatedAt: now() }
220
+ await assertComment(record)
221
+ const written = await port<Comment>('comments').put(record)
222
+ await rebuild(id)
223
+ return written
224
+ }),
225
+ remove: async (id, opts) => await unit(async () => {
226
+ const previous = await get<Comment>('comments', id)
227
+ await facade().cards.get(previous.card)
228
+ assertCommentAuthor(previous)
229
+ assertVersion(previous, opts.version)
230
+ const mentioned = await list<CommentMention>('mentions', { comment: id, size: 0 })
231
+ for (const row of mentioned.items) await port<CommentMention>('mentions').drop(row.id!, entityId(), row.version)
232
+ await port<Comment>('comments').drop(id, entityId(), opts.version)
233
+ }),
234
+ },
235
+ mentions: {
236
+ get: async id => {
237
+ const row = await get<CommentMention>('mentions', id)
238
+ const refreshed = await unit(async () => await rebuild(row.comment))
239
+ const found = refreshed.find(mention => mention.id === id)
240
+ if (found == null) throw new WorkcardNotFound(`mentions:${id}`)
241
+ return found
242
+ },
243
+ load: async id => {
244
+ try { return await resources.mentions.get(id) } catch (error) { if (error instanceof WorkcardNotFound) return null; throw error }
245
+ },
246
+ rebuild: async id => await unit(async () => await rebuild(id)),
247
+ list: async query => {
248
+ const comments = await resources.comments.list({ ...(query?.card != null ? { card: query.card } : {}), size: 0 })
249
+ const rows: CommentMention[] = []
250
+ for (const comment of comments.items) if (query?.comment == null || comment.id === query.comment) rows.push(...await unit(async () => await rebuild(comment.id!)))
251
+ return recordQueryHelper.applyQuery(rows, { ...(query?.assignee != null ? { assignee: query.assignee } : {}) }, query)
252
+ },
253
+ },
254
+ }
255
+ return resources
256
+ }
package/src/service.ts CHANGED
@@ -1,29 +1,14 @@
1
- import { createLazyService } from '@owlmeans/context'
2
- import type { BasicConfig, BasicContext } from '@owlmeans/context'
3
- import { makeSchemaRegistry, PlanningError } from '@owlmeans/planning'
4
- import type { PlanningService, PlanningStore, Transition, WithPlanningService } from '@owlmeans/planning'
1
+ import { makePlanningInvariants } from './invariants.js'
2
+ import { createLazyService, type BasicConfig, type BasicContext } from '@owlmeans/context'
3
+ import { makeSchemaRegistry, PlanningError, type PlanningService, type PlanningStore, type Transition, type WithPlanningService } from '@owlmeans/planning'
5
4
  import { DEFAULT_ALIAS } from './consts.js'
5
+ import { makeSchemaViews } from './definitions.js'
6
6
  import { makeStoreFacade } from './facade.js'
7
7
  import { makePluginRegistry } from './registry.js'
8
- import type { PluginRegistry } from './registry.js'
9
8
  import { makeCompositeStore } from './store/composite.js'
10
9
  import { makeMemoryPlanningStore } from './store/memory.js'
11
10
  import type { BindablePlanningStore } from './store/types.js'
12
- import type { PlanningHostService, PlanningServiceOptions } from './types.js'
13
-
14
- export type PlanningServiceApi = Pick<PlanningService, 'use' | 'plugins' | 'schemas' | 'store' | 'for' | 'committed'>
15
-
16
- /** What the facade and the executor reach the service through. */
17
- export interface PlanningRuntime {
18
- service: () => PlanningService
19
- registry: PluginRegistry
20
- options: PlanningServiceOptions
21
- context: () => BasicContext<BasicConfig> | undefined
22
- /** The composite store reads and commits go through. */
23
- reader: () => PlanningStore
24
- /** Every distinct store: the default one first, then each plugin's. */
25
- stores: () => PlanningStore[]
26
- }
11
+ import type { PlanningHostService, PlanningServiceOptions, PlanningRuntime, PlanningServiceApi } from './types.js'
27
12
 
28
13
  const contextOf = (service: PlanningService): BasicContext<BasicConfig> | undefined =>
29
14
  (service as Partial<PlanningHostService>).ctx as BasicContext<BasicConfig> | undefined
@@ -45,6 +30,8 @@ export const planningServiceApi = (
45
30
  return
46
31
  }
47
32
  bound.add(store)
33
+ store.validateProjection = async (before, after, transition) => await makePlanningInvariants(runtime).validate(before, after, transition)
34
+ store.projectReferences = async (after, transition) => await makePlanningInvariants(runtime).references(after, transition)
48
35
  ;(store as BindablePlanningStore).bind?.(async event => { await self().committed(event) })
49
36
  }
50
37
 
@@ -54,6 +41,12 @@ export const planningServiceApi = (
54
41
 
55
42
  let reader: { version: number, store: PlanningStore } | undefined
56
43
 
44
+ const views = makeSchemaViews({
45
+ code: () => schemas,
46
+ version: () => registry.version(),
47
+ port: () => fallback.schemas,
48
+ })
49
+
57
50
  const runtime: PlanningRuntime = {
58
51
  service: self,
59
52
  registry,
@@ -66,6 +59,9 @@ export const planningServiceApi = (
66
59
  return reader.store
67
60
  },
68
61
  stores: () => [...new Set([fallback, ...registry.routes(runtime.context()).map(route => route.store)])],
62
+ schemaStore: () => fallback.schemas,
63
+ schemasFor: views.of,
64
+ scopedSchemas: views.scoped,
69
65
  }
70
66
 
71
67
  options.plugins?.forEach(registry.use)