@owlmeans/server-planning 0.1.18-rc.2 → 0.1.18-rc.21

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 (217) hide show
  1. package/README.md +40 -12
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-planning/SKILL.md +193 -18
  4. package/build/actions/cards.d.ts +3 -5
  5. package/build/actions/cards.d.ts.map +1 -1
  6. package/build/actions/cards.js +1 -1
  7. package/build/actions/cards.js.map +1 -1
  8. package/build/actions/execute.d.ts +24 -3
  9. package/build/actions/execute.d.ts.map +1 -1
  10. package/build/actions/execute.js +82 -8
  11. package/build/actions/execute.js.map +1 -1
  12. package/build/actions/schemas.d.ts +27 -5
  13. package/build/actions/schemas.d.ts.map +1 -1
  14. package/build/actions/schemas.js +67 -7
  15. package/build/actions/schemas.js.map +1 -1
  16. package/build/actions/specs.d.ts +2 -4
  17. package/build/actions/specs.d.ts.map +1 -1
  18. package/build/actions/specs.js.map +1 -1
  19. package/build/actions/types.d.ts +4 -0
  20. package/build/actions/types.d.ts.map +1 -0
  21. package/build/actions/types.js +2 -0
  22. package/build/actions/types.js.map +1 -0
  23. package/build/actions/watch.d.ts.map +1 -1
  24. package/build/actions/watch.js +4 -2
  25. package/build/actions/watch.js.map +1 -1
  26. package/build/conformance/assert.d.ts +20 -0
  27. package/build/conformance/assert.d.ts.map +1 -0
  28. package/build/conformance/assert.js +54 -0
  29. package/build/conformance/assert.js.map +1 -0
  30. package/build/conformance/cases.d.ts +7 -0
  31. package/build/conformance/cases.d.ts.map +1 -0
  32. package/build/conformance/cases.js +447 -0
  33. package/build/conformance/cases.js.map +1 -0
  34. package/build/conformance/consts.d.ts +19 -0
  35. package/build/conformance/consts.d.ts.map +1 -0
  36. package/build/conformance/consts.js +42 -0
  37. package/build/conformance/consts.js.map +1 -0
  38. package/build/conformance/fixtures.d.ts +14 -0
  39. package/build/conformance/fixtures.d.ts.map +1 -0
  40. package/build/conformance/fixtures.js +109 -0
  41. package/build/conformance/fixtures.js.map +1 -0
  42. package/build/conformance/index.d.ts +12 -0
  43. package/build/conformance/index.d.ts.map +1 -0
  44. package/build/conformance/index.js +11 -0
  45. package/build/conformance/index.js.map +1 -0
  46. package/build/conformance/types.d.ts +18 -0
  47. package/build/conformance/types.d.ts.map +1 -0
  48. package/build/conformance/types.js +2 -0
  49. package/build/conformance/types.js.map +1 -0
  50. package/build/consts.d.ts +5 -0
  51. package/build/consts.d.ts.map +1 -1
  52. package/build/consts.js +5 -0
  53. package/build/consts.js.map +1 -1
  54. package/build/definitions.d.ts +17 -0
  55. package/build/definitions.d.ts.map +1 -0
  56. package/build/definitions.js +211 -0
  57. package/build/definitions.js.map +1 -0
  58. package/build/executor/changes.d.ts +2 -2
  59. package/build/executor/changes.d.ts.map +1 -1
  60. package/build/executor/changes.js.map +1 -1
  61. package/build/executor/code.d.ts +3 -3
  62. package/build/executor/code.d.ts.map +1 -1
  63. package/build/executor/code.js +1 -1
  64. package/build/executor/code.js.map +1 -1
  65. package/build/executor/consts.local.d.ts +5 -0
  66. package/build/executor/consts.local.d.ts.map +1 -0
  67. package/build/executor/consts.local.js +6 -0
  68. package/build/executor/consts.local.js.map +1 -0
  69. package/build/executor/creator.d.ts +30 -0
  70. package/build/executor/creator.d.ts.map +1 -0
  71. package/build/executor/creator.js +46 -0
  72. package/build/executor/creator.js.map +1 -0
  73. package/build/executor/resolve.d.ts +31 -23
  74. package/build/executor/resolve.d.ts.map +1 -1
  75. package/build/executor/resolve.js +74 -15
  76. package/build/executor/resolve.js.map +1 -1
  77. package/build/executor/types.d.ts +24 -0
  78. package/build/executor/types.d.ts.map +1 -0
  79. package/build/executor/types.js +2 -0
  80. package/build/executor/types.js.map +1 -0
  81. package/build/executor/validate.d.ts +6 -5
  82. package/build/executor/validate.d.ts.map +1 -1
  83. package/build/executor/validate.js +18 -15
  84. package/build/executor/validate.js.map +1 -1
  85. package/build/executor.d.ts +10 -7
  86. package/build/executor.d.ts.map +1 -1
  87. package/build/executor.js +17 -14
  88. package/build/executor.js.map +1 -1
  89. package/build/facade.d.ts +20 -2
  90. package/build/facade.d.ts.map +1 -1
  91. package/build/facade.js +128 -18
  92. package/build/facade.js.map +1 -1
  93. package/build/helper.d.ts +3 -2
  94. package/build/helper.d.ts.map +1 -1
  95. package/build/helper.js +7 -3
  96. package/build/helper.js.map +1 -1
  97. package/build/index.d.ts +1 -0
  98. package/build/index.d.ts.map +1 -1
  99. package/build/index.js +1 -0
  100. package/build/index.js.map +1 -1
  101. package/build/projection.d.ts +2 -10
  102. package/build/projection.d.ts.map +1 -1
  103. package/build/projection.js +1 -1
  104. package/build/projection.js.map +1 -1
  105. package/build/queue/consts.d.ts +5 -0
  106. package/build/queue/consts.d.ts.map +1 -0
  107. package/build/queue/consts.js +5 -0
  108. package/build/queue/consts.js.map +1 -0
  109. package/build/queue/index.d.ts +4 -16
  110. package/build/queue/index.d.ts.map +1 -1
  111. package/build/queue/index.js +2 -4
  112. package/build/queue/index.js.map +1 -1
  113. package/build/queue/types.d.ts +13 -0
  114. package/build/queue/types.d.ts.map +1 -0
  115. package/build/queue/types.js +2 -0
  116. package/build/queue/types.js.map +1 -0
  117. package/build/registry.d.ts +2 -21
  118. package/build/registry.d.ts.map +1 -1
  119. package/build/registry.js +3 -1
  120. package/build/registry.js.map +1 -1
  121. package/build/service.d.ts +3 -16
  122. package/build/service.d.ts.map +1 -1
  123. package/build/service.js +9 -0
  124. package/build/service.js.map +1 -1
  125. package/build/store/commits.d.ts.map +1 -1
  126. package/build/store/commits.js +3 -1
  127. package/build/store/commits.js.map +1 -1
  128. package/build/store/composite.d.ts +1 -0
  129. package/build/store/composite.d.ts.map +1 -1
  130. package/build/store/composite.js +3 -0
  131. package/build/store/composite.js.map +1 -1
  132. package/build/store/consts.local.d.ts +2 -0
  133. package/build/store/consts.local.d.ts.map +1 -0
  134. package/build/store/consts.local.js +2 -0
  135. package/build/store/consts.local.js.map +1 -0
  136. package/build/store/fold.d.ts +4 -3
  137. package/build/store/fold.d.ts.map +1 -1
  138. package/build/store/fold.js +14 -6
  139. package/build/store/fold.js.map +1 -1
  140. package/build/store/memory.d.ts +12 -0
  141. package/build/store/memory.d.ts.map +1 -1
  142. package/build/store/memory.js +0 -0
  143. package/build/store/memory.js.map +1 -1
  144. package/build/store/types.d.ts +15 -1
  145. package/build/store/types.d.ts.map +1 -1
  146. package/build/store/types.local.d.ts +3 -0
  147. package/build/store/types.local.d.ts.map +1 -0
  148. package/build/store/types.local.js +2 -0
  149. package/build/store/types.local.js.map +1 -0
  150. package/build/types.d.ts +111 -1
  151. package/build/types.d.ts.map +1 -1
  152. package/build/types.local.d.ts +7 -0
  153. package/build/types.local.d.ts.map +1 -0
  154. package/build/types.local.js +2 -0
  155. package/build/types.local.js.map +1 -0
  156. package/build/utils/index.d.ts +1 -0
  157. package/build/utils/index.d.ts.map +1 -1
  158. package/build/utils/scope.d.ts +39 -2
  159. package/build/utils/scope.d.ts.map +1 -1
  160. package/build/utils/scope.js +79 -15
  161. package/build/utils/scope.js.map +1 -1
  162. package/build/utils/types.d.ts +8 -0
  163. package/build/utils/types.d.ts.map +1 -0
  164. package/build/utils/types.js +2 -0
  165. package/build/utils/types.js.map +1 -0
  166. package/package.json +28 -20
  167. package/src/actions/cards.ts +2 -6
  168. package/src/actions/execute.ts +95 -12
  169. package/src/actions/schemas.ts +81 -9
  170. package/src/actions/specs.ts +1 -2
  171. package/src/actions/types.ts +4 -0
  172. package/src/actions/watch.ts +5 -2
  173. package/src/conformance/assert.ts +59 -0
  174. package/src/conformance/cases.ts +522 -0
  175. package/src/conformance/consts.ts +44 -0
  176. package/src/conformance/fixtures.ts +121 -0
  177. package/src/conformance/index.ts +11 -0
  178. package/src/conformance/types.ts +20 -0
  179. package/src/consts.ts +6 -0
  180. package/src/definitions.ts +250 -0
  181. package/src/executor/changes.ts +2 -6
  182. package/src/executor/code.ts +3 -9
  183. package/src/executor/consts.local.ts +8 -0
  184. package/src/executor/creator.ts +51 -0
  185. package/src/executor/resolve.ts +84 -40
  186. package/src/executor/types.ts +25 -0
  187. package/src/executor/validate.ts +22 -26
  188. package/src/executor.ts +21 -21
  189. package/src/facade.ts +143 -25
  190. package/src/helper.ts +7 -3
  191. package/src/index.ts +1 -0
  192. package/src/projection.ts +4 -14
  193. package/src/queue/consts.ts +5 -0
  194. package/src/queue/index.ts +6 -21
  195. package/src/queue/types.ts +14 -0
  196. package/src/registry.ts +5 -29
  197. package/src/service.ts +13 -20
  198. package/src/store/commits.ts +5 -4
  199. package/src/store/composite.ts +3 -0
  200. package/src/store/consts.local.ts +1 -0
  201. package/src/store/fold.ts +15 -12
  202. package/src/store/memory.ts +0 -0
  203. package/src/store/types.local.ts +3 -0
  204. package/src/store/types.ts +16 -1
  205. package/src/types.local.ts +7 -0
  206. package/src/types.ts +126 -4
  207. package/src/utils/index.ts +1 -0
  208. package/src/utils/scope.ts +97 -17
  209. package/src/utils/types.ts +8 -0
  210. package/tests/access.spec.ts +160 -0
  211. package/tests/conformance.spec.ts +47 -0
  212. package/tests/create-example.spec.ts +202 -0
  213. package/tests/creator.spec.ts +257 -0
  214. package/tests/entrypoints.spec.ts +5 -0
  215. package/tests/narrowing.spec.ts +94 -0
  216. package/tests/plugins.spec.ts +4 -3
  217. package/tests/writes.spec.ts +140 -0
package/src/facade.ts CHANGED
@@ -1,14 +1,25 @@
1
- import {
2
- criteriaOf, currentSpecification, isSpecification, linkWhereOf, listOptionsOf, modelOf,
3
- PlanningUnsupported, specCriteriaOf, transitionWhereOf, WorkcardKind, WorkcardNotFound,
4
- } from '@owlmeans/planning'
5
- import type {
6
- CommitSource, PlanningFacade, PlanningScope, Specification, Workcard, WorkcardModel,
7
- } from '@owlmeans/planning'
1
+ import { criteriaOf, currentSpecification, isProject, isSpecification, linkWhereOf, listOptionsOf, modelOf, projectOf, PlanningUnsupported, specCriteriaOf, transitionWhereOf, WorkcardKind, WorkcardNotFound, type CommitEvent, type CommitSource, type PlanningFacade, type PlanningScope, type Relationship, type Specification, type Transition, type Workcard, type WorkcardModel, type WorkcardQuery } from '@owlmeans/planning'
2
+ import type { Criteria } from '@owlmeans/resource'
3
+ import { makeDefinitions } from './definitions.js'
8
4
  import { executeTransition } from './executor.js'
9
- import type { PlanningRuntime } from './service.js'
5
+ import type { PlanningRuntime } from './types.js'
6
+ import { wantsSpecifications } from './store/memory.js'
10
7
  import type { CommitHub } from './store/types.js'
11
8
 
9
+ /**
10
+ * The criteria a scope narrowed to `projects` adds to every card list, count and summary: the
11
+ * projects themselves, every card whose `parents` name one of them, and the specifications whose
12
+ * parent card is one of `through` — the cards of those projects a specification is seen through,
13
+ * exactly as a single read sees it (a specification's `parents` hold only its own card).
14
+ */
15
+ export const projectCriteriaOf = (projects: readonly string[], through: readonly string[] = []): Criteria<Workcard> => ({
16
+ $or: [
17
+ { id: { $in: [...projects] } },
18
+ { parents: { $overlaps: [...projects] } },
19
+ ...(through.length > 0 ? [{ kind: WorkcardKind.Specification, parent: { $in: [...through] } }] : []),
20
+ ],
21
+ }) as Criteria<Workcard>
22
+
12
23
  /**
13
24
  * The scoped facade over the service's stores.
14
25
  *
@@ -16,13 +27,75 @@ import type { CommitHub } from './store/types.js'
16
27
  * a card of another entity is simply absent — `load` answers `null`, `get` throws
17
28
  * `WorkcardNotFound`, exactly as for an id that never existed. Reads go through the composite
18
29
  * store (the default store plus the stores plugins own); writes go through the executor.
30
+ *
31
+ * A scope naming `projects` sees those projects, the cards whose `parents` name one of them, and a
32
+ * specification whose parent card is one of those (a card's specification through its card);
33
+ * everything else reads as absent. Lists, counts and summaries are narrowed by an AND'd criteria
34
+ * that admits the same cards a single read does — the parent cards a specification is seen through
35
+ * are resolved once per call ({@link projectCriteriaOf}) — and commit events and transitions
36
+ * outside the set are dropped or refused.
37
+ *
38
+ * `definitions` is present when the default store holds data-defined schemas; a model is then
39
+ * built over the registry of the card's own project.
19
40
  */
20
41
  export const makeStoreFacade = (runtime: PlanningRuntime, scope: PlanningScope): PlanningFacade => {
21
42
  const entityId = scope.entityId
43
+ const projects = scope.projects != null ? [...new Set(scope.projects)] : undefined
22
44
  const reader = () => runtime.reader()
23
45
  const mine = <T extends { entityId: string }>(record: T | null | undefined): T | null =>
24
46
  record != null && record.entityId === entityId ? record : null
25
47
 
48
+ const inProjects = (card: Workcard): boolean => projects == null
49
+ || (card.id != null && projects.includes(card.id))
50
+ || card.parents.some(parent => projects.includes(parent))
51
+
52
+ /** A card this scope may see — a specification through its parent card. */
53
+ const visible = async (card: Workcard | null): Promise<Workcard | null> => {
54
+ if (card == null || inProjects(card)) {
55
+ return card
56
+ }
57
+ if (isSpecification(card) && card.parent != null) {
58
+ const parent = mine(await reader().cards.get(card.parent, entityId))
59
+ return parent != null && inProjects(parent) ? card : null
60
+ }
61
+ return null
62
+ }
63
+
64
+ /**
65
+ * The cards a narrowed list or count may see specifications THROUGH — what {@link visible} admits
66
+ * one by: its parent card in the scope's projects. One read per call, and only for criteria that
67
+ * ask for specifications at all (`wantsSpecifications`, the rule every store routes a list by): the
68
+ * parent the query names, or else the projects' own cards. A summary counts no specification.
69
+ */
70
+ const throughOf = async (where: Criteria<Workcard>, parent?: string): Promise<string[]> => {
71
+ if (projects == null || projects.length === 0 || !wantsSpecifications(where)) {
72
+ return []
73
+ }
74
+ if (parent != null) {
75
+ const card = mine(await reader().cards.get(parent, entityId))
76
+ return card != null && inProjects(card) ? [parent] : []
77
+ }
78
+ const listed = await reader().cards.list({ entityId, parents: { $overlaps: projects } } as unknown as Criteria<Workcard>, { size: 0 })
79
+ // A project's own specifications are matched by their `parents` already.
80
+ return listed.items.filter(card => card.id != null && !projects.includes(card.id) && inProjects(card)).map(card => card.id!)
81
+ }
82
+
83
+ /** A narrowed list's or count's criteria — `through` as {@link throughOf} resolved it. */
84
+ const narrowed = async (query?: WorkcardQuery | null): Promise<Criteria<Workcard>> => {
85
+ const where = criteriaOf(query, scope)
86
+ return narrow(where, await throughOf(where, query?.parent))
87
+ }
88
+
89
+ const narrow = (where: Criteria<Workcard>, through: readonly string[] = []): Criteria<Workcard> => projects == null
90
+ ? where
91
+ : {
92
+ ...where,
93
+ $and: [...((where as { $and?: Criteria<Workcard>[] }).$and ?? []), projectCriteriaOf(projects, through)],
94
+ } as Criteria<Workcard>
95
+
96
+ const projectVisible = (project?: string | null): boolean =>
97
+ projects == null || (project != null && projects.includes(project))
98
+
26
99
  const commitSource = (): CommitSource => {
27
100
  const commits = reader().commits
28
101
  if (commits == null) {
@@ -31,28 +104,37 @@ export const makeStoreFacade = (runtime: PlanningRuntime, scope: PlanningScope):
31
104
  return commits
32
105
  }
33
106
 
34
- /** The transition is this entity's — its row, or the remembered commit of a purged one. */
107
+ /** The transition is this entity's (and this scope's) — its row, or the remembered commit of a purged one. */
35
108
  const assertTransition = async (transition: string): Promise<void> => {
36
109
  const row = await reader().transitions?.get(transition)
37
110
  if (row != null) {
38
- if (row.entityId !== entityId) {
111
+ if (row.entityId !== entityId || !projectVisible(row.project)) {
39
112
  throw new WorkcardNotFound(`transition:${transition}`)
40
113
  }
41
114
  return
42
115
  }
43
116
  const recalled = (reader().commits as Partial<CommitHub> | undefined)?.recall?.(transition)
44
- if (recalled == null || recalled.entityId !== entityId) {
117
+ if (recalled == null || recalled.entityId !== entityId || !projectVisible(recalled.project)) {
45
118
  throw new WorkcardNotFound(`transition:${transition}`)
46
119
  }
47
120
  }
48
121
 
122
+ /** The project a card resolves its data-defined types in. */
123
+ const projectFor = async (card: Workcard): Promise<string | undefined> => {
124
+ if (isProject(card) || card.parent == null) {
125
+ return projectOf(card)
126
+ }
127
+ const parent = mine(await reader().cards.get(card.parent, entityId))
128
+ return projectOf(card, parent)
129
+ }
130
+
49
131
  const facade: PlanningFacade = {
50
- scope: Object.freeze({ ...scope }),
132
+ scope: Object.freeze({ ...scope, ...(projects != null ? { projects } : {}) }),
51
133
 
52
134
  schemas: runtime.service().schemas,
53
135
 
54
136
  cards: {
55
- load: async id => mine(await reader().cards.get(id, entityId)),
137
+ load: async id => await visible(mine(await reader().cards.get(id, entityId))),
56
138
 
57
139
  get: async id => {
58
140
  const card = await facade.cards.load(id)
@@ -62,17 +144,20 @@ export const makeStoreFacade = (runtime: PlanningRuntime, scope: PlanningScope):
62
144
  return card
63
145
  },
64
146
 
65
- list: async query => await reader().cards.list(criteriaOf(query, scope), listOptionsOf(query)),
147
+ list: async query => await reader().cards.list(await narrowed(query), listOptionsOf(query)),
66
148
 
67
- count: async query => await reader().cards.count(criteriaOf(query, scope)),
149
+ count: async query => await reader().cards.count(await narrowed(query)),
68
150
 
69
151
  summary: async (parents, query) => parents.length === 0
70
152
  ? {}
71
- : await reader().cards.summary(parents, criteriaOf({ kind: query?.kind, type: query?.type }, scope)),
153
+ : await reader().cards.summary(parents, narrow(criteriaOf({ kind: query?.kind, type: query?.type }, scope))),
72
154
  },
73
155
 
74
156
  specifications: {
75
157
  current: async (parent, category) => {
158
+ if (projects != null && await facade.cards.load(parent) == null) {
159
+ return null
160
+ }
76
161
  const specs = reader().specs
77
162
  if (specs != null) {
78
163
  return mine(await specs.current(parent, category, entityId))
@@ -82,6 +167,9 @@ export const makeStoreFacade = (runtime: PlanningRuntime, scope: PlanningScope):
82
167
  },
83
168
 
84
169
  list: async (parent, query) => {
170
+ if (projects != null && await facade.cards.load(parent) == null) {
171
+ return { items: [], total: 0 }
172
+ }
85
173
  const specs = reader().specs
86
174
  if (specs != null) {
87
175
  return await specs.list(parent, entityId, query)
@@ -110,16 +198,23 @@ export const makeStoreFacade = (runtime: PlanningRuntime, scope: PlanningScope):
110
198
  relationships: {
111
199
  list: async query => {
112
200
  const links = reader().links
113
- return links == null
114
- ? { items: [], total: 0 }
115
- : await links.list(linkWhereOf(query, scope), listOptionsOf(query))
201
+ if (links == null) {
202
+ return { items: [], total: 0 }
203
+ }
204
+ if (projects == null) {
205
+ return await links.list(linkWhereOf(query, scope), listOptionsOf(query))
206
+ }
207
+ const listed = await links.list({ ...linkWhereOf(query, scope), project: projects }, listOptionsOf(query))
208
+ // A store that ignores `project` must still not leak an edge filed under another project.
209
+ const items = listed.items.filter((link: Relationship) => projectVisible(link.project))
210
+ return items.length === listed.items.length ? listed : { ...listed, items, total: listed.total - (listed.items.length - items.length) }
116
211
  },
117
212
  },
118
213
 
119
214
  transitions: {
120
215
  get: async id => {
121
216
  const transition = mine(await reader().transitions?.get(id))
122
- if (transition == null) {
217
+ if (transition == null || !projectVisible(transition.project)) {
123
218
  throw new WorkcardNotFound(`transition:${id}`)
124
219
  }
125
220
  return transition
@@ -127,9 +222,20 @@ export const makeStoreFacade = (runtime: PlanningRuntime, scope: PlanningScope):
127
222
 
128
223
  list: async query => {
129
224
  const transitions = reader().transitions
130
- return transitions == null
131
- ? { items: [], total: 0 }
132
- : await transitions.list(transitionWhereOf(query, scope), listOptionsOf(query))
225
+ if (transitions == null) {
226
+ return { items: [], total: 0 }
227
+ }
228
+ if (projects == null) {
229
+ return await transitions.list(transitionWhereOf(query, scope), listOptionsOf(query))
230
+ }
231
+ if (query.project != null && !projects.includes(query.project)) {
232
+ return { items: [], total: 0 }
233
+ }
234
+ const listed = await transitions.list(
235
+ { ...transitionWhereOf(query, scope), project: query.project ?? projects }, listOptionsOf(query)
236
+ )
237
+ const items = listed.items.filter((transition: Transition) => projectVisible(transition.project))
238
+ return items.length === listed.items.length ? listed : { ...listed, items, total: listed.total - (listed.items.length - items.length) }
133
239
  },
134
240
  },
135
241
 
@@ -139,7 +245,10 @@ export const makeStoreFacade = (runtime: PlanningRuntime, scope: PlanningScope):
139
245
  return await commitSource().status(transition)
140
246
  },
141
247
 
142
- subscribe: (listener, filter) => commitSource().subscribe(listener, { ...filter, entityId }),
248
+ subscribe: (listener, filter) => commitSource().subscribe(projects == null
249
+ ? listener
250
+ : (event: CommitEvent) => projectVisible(event.project) ? listener(event) : undefined,
251
+ { ...filter, entityId }),
143
252
 
144
253
  wait: async (transition, opts) => {
145
254
  await assertTransition(transition)
@@ -154,9 +263,18 @@ export const makeStoreFacade = (runtime: PlanningRuntime, scope: PlanningScope):
154
263
  if (record.entityId !== entityId) {
155
264
  throw new WorkcardNotFound(record.id ?? WorkcardKind.Card)
156
265
  }
157
- return modelOf<T>(record, facade)
266
+ if (runtime.schemaStore() == null) {
267
+ return modelOf<T>(record, facade)
268
+ }
269
+ // A data-defined type answers from the registry of the card's own project.
270
+ const schemas = await runtime.schemasFor(entityId, await projectFor(record))
271
+ return modelOf<T>(record, { ...facade, schemas })
158
272
  },
159
273
  }
160
274
 
275
+ if (runtime.schemaStore() != null) {
276
+ facade.definitions = makeDefinitions(runtime, facade)
277
+ }
278
+
161
279
  return facade
162
280
  }
package/src/helper.ts CHANGED
@@ -3,7 +3,7 @@ import type { AbstractRequest } from '@owlmeans/entrypoint'
3
3
  import type { PlanningFacade, PlanningProtocols, PlanningScope } from '@owlmeans/planning'
4
4
  import { bind } from '@owlmeans/server-entrypoint'
5
5
  import {
6
- executePlanning, getCard, getCommit, getSpecification, getTransition, listCardSpecifications,
6
+ defineSchemas, executePlanning, getCard, getCommit, getSpecification, getTransition, listCardSpecifications,
7
7
  listCardTransitions, listCards, listLinks, listSchemas, listSpecificationRevisions, summarizeCards,
8
8
  watchCommits,
9
9
  } from './actions/index.js'
@@ -12,12 +12,16 @@ import { planningServiceOf, scopeOf } from './utils/index.js'
12
12
 
13
13
  /**
14
14
  * Bind this package's handlers to a tree declared by `makePlanningProtocols` — one binding per
15
- * protocol, the base included. Pass the tree itself, never a flattened list; an application that
16
- * wants its own handler for one leaf binds that protocol afterwards.
15
+ * protocol, the base included (and `schema.define` when the tree declares it). Pass the tree
16
+ * itself, never a flattened list; an application that wants its own handler for one leaf binds
17
+ * that protocol afterwards.
17
18
  */
18
19
  export const servePlanningEntrypoints = (protocols: PlanningProtocols, opts?: PlanningHandlerOptions) => [
19
20
  bind(protocols.base),
20
21
  bind(protocols.schema.list, listSchemas(protocols.schema.list, opts)),
22
+ ...(protocols.schema.define != null
23
+ ? [bind(protocols.schema.define, defineSchemas(protocols.schema.define, opts))]
24
+ : []),
21
25
  bind(protocols.card.list, listCards(protocols.card.list, opts)),
22
26
  bind(protocols.card.summary, summarizeCards(protocols.card.summary, opts)),
23
27
  bind(protocols.card.get, getCard(protocols.card.get, opts)),
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from './consts.js'
5
5
  export * from './service.js'
6
6
  export * from './registry.js'
7
7
  export * from './facade.js'
8
+ export * from './definitions.js'
8
9
  export * from './executor.js'
9
10
  export * from './helper.js'
10
11
  export * from './projection.js'
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
6
  import { failPending, foldPending } from './store/fold.js'
8
- import type { PlanningHostService } from './types.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
 
@@ -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'
@@ -1,25 +1,10 @@
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
- }
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'
22
4
 
23
5
  /** Declare the shared projection address; each process separately chooses whether to listen. */
24
6
  export const declarePlanningQueue = <C extends Config>(cfg: C, opts?: PlanningQueueOptions): C =>
25
7
  declareQueue(cfg, PLANNING_PROJECTION_QUEUE, [PLANNING_PROJECT_JOB], opts)
8
+
9
+ export { PLANNING_PROJECTION_QUEUE, PLANNING_PROJECT_JOB } from './consts.js'
10
+ 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 { 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
 
@@ -131,7 +107,7 @@ export const makePluginRegistry = (
131
107
  try {
132
108
  await plugin.after(event, contextOf(plugin))
133
109
  } catch (error) {
134
- console.error(`planning: after hook of ${plugin.name} failed on ${event.transition}:`, error)
110
+ log.error('Planning after hook failed', { plugin: plugin.name, transition: event.transition, error })
135
111
  }
136
112
  }
137
113
  },
package/src/service.ts CHANGED
@@ -1,29 +1,13 @@
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 { createLazyService, type BasicConfig, type BasicContext } from '@owlmeans/context'
2
+ import { makeSchemaRegistry, PlanningError, type PlanningService, type PlanningStore, type Transition, type WithPlanningService } from '@owlmeans/planning'
5
3
  import { DEFAULT_ALIAS } from './consts.js'
4
+ import { makeSchemaViews } from './definitions.js'
6
5
  import { makeStoreFacade } from './facade.js'
7
6
  import { makePluginRegistry } from './registry.js'
8
- import type { PluginRegistry } from './registry.js'
9
7
  import { makeCompositeStore } from './store/composite.js'
10
8
  import { makeMemoryPlanningStore } from './store/memory.js'
11
9
  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
- }
10
+ import type { PlanningHostService, PlanningServiceOptions, PlanningRuntime, PlanningServiceApi } from './types.js'
27
11
 
28
12
  const contextOf = (service: PlanningService): BasicContext<BasicConfig> | undefined =>
29
13
  (service as Partial<PlanningHostService>).ctx as BasicContext<BasicConfig> | undefined
@@ -54,6 +38,12 @@ export const planningServiceApi = (
54
38
 
55
39
  let reader: { version: number, store: PlanningStore } | undefined
56
40
 
41
+ const views = makeSchemaViews({
42
+ code: () => schemas,
43
+ version: () => registry.version(),
44
+ port: () => fallback.schemas,
45
+ })
46
+
57
47
  const runtime: PlanningRuntime = {
58
48
  service: self,
59
49
  registry,
@@ -66,6 +56,9 @@ export const planningServiceApi = (
66
56
  return reader.store
67
57
  },
68
58
  stores: () => [...new Set([fallback, ...registry.routes(runtime.context()).map(route => route.store)])],
59
+ schemaStore: () => fallback.schemas,
60
+ schemasFor: views.of,
61
+ scopedSchemas: views.scoped,
69
62
  }
70
63
 
71
64
  options.plugins?.forEach(registry.use)
@@ -1,9 +1,10 @@
1
- import { CommitFailed, CommitState, CommitTimeout, DEFAULT_COMMIT_TIMEOUT, WorkcardNotFound } from '@owlmeans/planning'
2
- import type { CommitEvent, CommitFilter, CommitStatus, Unsubscribe, Workcard } from '@owlmeans/planning'
1
+ import { logger } from '@owlmeans/log'
2
+ import { CommitFailed, CommitState, CommitTimeout, DEFAULT_COMMIT_TIMEOUT, WorkcardNotFound, type CommitEvent, type CommitFilter, type CommitStatus, type Unsubscribe, type Workcard } from '@owlmeans/planning'
3
3
  import { COMMIT_POLL_LADDER, DEFAULT_COMMIT_MEMORY } from '../consts.js'
4
4
  import type { CommitHub, CommitHubOptions } from './types.js'
5
+ import type { Listener } from './types.local.js'
5
6
 
6
- type Listener = (event: CommitEvent) => void | Promise<void>
7
+ const log = logger('planning:store')
7
8
 
8
9
  const matches = (event: CommitEvent, filter?: CommitFilter): boolean =>
9
10
  filter == null || (
@@ -58,7 +59,7 @@ export const makeCommitHub = (options: CommitHubOptions): CommitHub => {
58
59
  const results = await Promise.allSettled(targets.map(async entry => await entry.listener(event)))
59
60
  results.forEach(result => {
60
61
  if (result.status === 'rejected') {
61
- console.error('planning: commit listener failed:', result.reason)
62
+ log.error('Planning commit listener failed', { transition: event.transition, error: result.reason })
62
63
  }
63
64
  })
64
65
  },
@@ -29,6 +29,7 @@ const sum = async <S>(stores: S[], run: (store: S) => Promise<number>): Promise<
29
29
  * then each owner; a list whose criteria names one owned type (or several owned by one store) goes
30
30
  * there, and every other list is the default store's — a cross-type query is answered by the
31
31
  * default store alone, never merged. Relationship lists and commit subscriptions span every store.
32
+ * The data-defined schema port is the default store's.
32
33
  */
33
34
  export const makeCompositeStore = (routes: StoreRoute[], fallback: PlanningStore): PlanningStore => {
34
35
  const all = [...new Set([fallback, ...routes.map(route => route.store)])]
@@ -167,5 +168,7 @@ export const makeCompositeStore = (routes: StoreRoute[], fallback: PlanningStore
167
168
  ...(specs != null ? { specs } : {}),
168
169
  ...(links != null ? { links } : {}),
169
170
  ...(commits != null ? { commits } : {}),
171
+ // Data-defined schemas are the default store's alone: a layer describes the whole organization.
172
+ ...(fallback.schemas != null ? { schemas: fallback.schemas } : {}),
170
173
  }
171
174
  }
@@ -0,0 +1 @@
1
+ export const CONTENT_KEYS = ['body', 'ref', 'format', 'version'] as const
package/src/store/fold.ts CHANGED
@@ -1,10 +1,9 @@
1
- import {
2
- applyRelationship, applyTransition, CommitState, isProject, PlanningUnsupported, TransitionAction,
3
- } from '@owlmeans/planning'
4
- import type {
5
- CommitEvent, PlanningStore, Relationship, SpecificationRevision, Transition, Workcard,
6
- } from '@owlmeans/planning'
1
+ import { logger } from '@owlmeans/log'
2
+ import { applyRelationship, applyTransition, CommitState, isProject, PlanningUnsupported, TransitionAction, type CommitEvent, type PlanningStore, type Relationship, type SpecificationRevision, type Transition, type Workcard } from '@owlmeans/planning'
7
3
  import type { FoldOptions, FoldResult } from './types.js'
4
+ import { CONTENT_KEYS } from './consts.local.js'
5
+
6
+ const log = logger('planning:store')
8
7
 
9
8
  const isoNow = (): string => new Date().toISOString()
10
9
 
@@ -18,7 +17,7 @@ const safely = async (label: string, run: () => Promise<void> | void | undefined
18
17
  try {
19
18
  await run()
20
19
  } catch (error) {
21
- console.error(`planning: ${label} failed:`, error)
20
+ log.error('Planning store step failed', { step: label, error })
22
21
  }
23
22
  }
24
23
 
@@ -105,7 +104,9 @@ const writeFold = async (
105
104
  * to advance, so what follows it fails too; a store that refuses the advancing write itself stops
106
105
  * the fold there and reports `followUp`, leaving the rest pending for a retry.
107
106
  *
108
- * The caller owns single flight: two folds of one card running at once would both publish.
107
+ * The caller owns single flight: two folds of one card running at once would both publish. A
108
+ * transactional store passes `unit`, which wraps each transition's writes (a savepoint), so a write
109
+ * the database refuses is undone whole before the fold marks the transition failed.
109
110
  *
110
111
  * @throws {PlanningUnsupported} for a store without a transition log
111
112
  */
@@ -140,7 +141,11 @@ export const foldPending = async (
140
141
  // Already applied by another folder, which owns its commit and its event.
141
142
  continue
142
143
  }
143
- await writeFold(store, card, next, transition)
144
+ const before = card
145
+ const after = next
146
+ await (opts.unit != null
147
+ ? opts.unit(async () => { await writeFold(store, before, after, transition) })
148
+ : writeFold(store, before, after, transition))
144
149
  } catch (error) {
145
150
  failed++
146
151
  const message = errorText(error)
@@ -155,7 +160,7 @@ export const foldPending = async (
155
160
  } catch (advance) {
156
161
  // The store itself refuses writes: leave the rest pending for a retry rather than
157
162
  // failing every later transition as out of order.
158
- console.error(`planning: cannot advance ${cardId} past failed seq ${transition.seq}:`, advance)
163
+ log.error('Planning cannot advance a card past a failed transition', { cardId, seq: transition.seq, error: advance })
159
164
  await safely('commit failure', () => transitions.commit(transition.id!, { state: CommitState.Failed, at, error: message }))
160
165
  await safely('publish', () => opts.publish?.(commitEventOf(transition, CommitState.Failed, at, { error: message })))
161
166
  return { card, folded, failed, followUp: true }
@@ -204,8 +209,6 @@ export const failPending = async (
204
209
  return pending.items.length
205
210
  }
206
211
 
207
- const CONTENT_KEYS = ['body', 'ref', 'format', 'version'] as const
208
-
209
212
  /**
210
213
  * A specification's history, replayed from its log: one entry per create and per committed
211
214
  * content change, newest first. The fold is `applyTransition` itself, so an entry's `body` is the
Binary file
@@ -0,0 +1,3 @@
1
+ import type { CommitEvent } from '@owlmeans/planning'
2
+
3
+ export type Listener = (event: CommitEvent) => void | Promise<void>