@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.
- package/README.md +40 -12
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-planning/SKILL.md +193 -18
- package/build/actions/cards.d.ts +3 -5
- package/build/actions/cards.d.ts.map +1 -1
- package/build/actions/cards.js +1 -1
- package/build/actions/cards.js.map +1 -1
- package/build/actions/execute.d.ts +24 -3
- package/build/actions/execute.d.ts.map +1 -1
- package/build/actions/execute.js +82 -8
- package/build/actions/execute.js.map +1 -1
- package/build/actions/schemas.d.ts +27 -5
- package/build/actions/schemas.d.ts.map +1 -1
- package/build/actions/schemas.js +67 -7
- package/build/actions/schemas.js.map +1 -1
- package/build/actions/specs.d.ts +2 -4
- package/build/actions/specs.d.ts.map +1 -1
- package/build/actions/specs.js.map +1 -1
- package/build/actions/types.d.ts +4 -0
- package/build/actions/types.d.ts.map +1 -0
- package/build/actions/types.js +2 -0
- package/build/actions/types.js.map +1 -0
- package/build/actions/watch.d.ts.map +1 -1
- package/build/actions/watch.js +4 -2
- package/build/actions/watch.js.map +1 -1
- package/build/conformance/assert.d.ts +20 -0
- package/build/conformance/assert.d.ts.map +1 -0
- package/build/conformance/assert.js +54 -0
- package/build/conformance/assert.js.map +1 -0
- package/build/conformance/cases.d.ts +7 -0
- package/build/conformance/cases.d.ts.map +1 -0
- package/build/conformance/cases.js +447 -0
- package/build/conformance/cases.js.map +1 -0
- package/build/conformance/consts.d.ts +19 -0
- package/build/conformance/consts.d.ts.map +1 -0
- package/build/conformance/consts.js +42 -0
- package/build/conformance/consts.js.map +1 -0
- package/build/conformance/fixtures.d.ts +14 -0
- package/build/conformance/fixtures.d.ts.map +1 -0
- package/build/conformance/fixtures.js +109 -0
- package/build/conformance/fixtures.js.map +1 -0
- package/build/conformance/index.d.ts +12 -0
- package/build/conformance/index.d.ts.map +1 -0
- package/build/conformance/index.js +11 -0
- package/build/conformance/index.js.map +1 -0
- package/build/conformance/types.d.ts +18 -0
- package/build/conformance/types.d.ts.map +1 -0
- package/build/conformance/types.js +2 -0
- package/build/conformance/types.js.map +1 -0
- package/build/consts.d.ts +5 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +5 -0
- package/build/consts.js.map +1 -1
- package/build/definitions.d.ts +17 -0
- package/build/definitions.d.ts.map +1 -0
- package/build/definitions.js +211 -0
- package/build/definitions.js.map +1 -0
- package/build/executor/changes.d.ts +2 -2
- package/build/executor/changes.d.ts.map +1 -1
- package/build/executor/changes.js.map +1 -1
- package/build/executor/code.d.ts +3 -3
- package/build/executor/code.d.ts.map +1 -1
- package/build/executor/code.js +1 -1
- package/build/executor/code.js.map +1 -1
- package/build/executor/consts.local.d.ts +5 -0
- package/build/executor/consts.local.d.ts.map +1 -0
- package/build/executor/consts.local.js +6 -0
- package/build/executor/consts.local.js.map +1 -0
- package/build/executor/creator.d.ts +30 -0
- package/build/executor/creator.d.ts.map +1 -0
- package/build/executor/creator.js +46 -0
- package/build/executor/creator.js.map +1 -0
- package/build/executor/resolve.d.ts +31 -23
- package/build/executor/resolve.d.ts.map +1 -1
- package/build/executor/resolve.js +74 -15
- package/build/executor/resolve.js.map +1 -1
- package/build/executor/types.d.ts +24 -0
- package/build/executor/types.d.ts.map +1 -0
- package/build/executor/types.js +2 -0
- package/build/executor/types.js.map +1 -0
- package/build/executor/validate.d.ts +6 -5
- package/build/executor/validate.d.ts.map +1 -1
- package/build/executor/validate.js +18 -15
- package/build/executor/validate.js.map +1 -1
- package/build/executor.d.ts +10 -7
- package/build/executor.d.ts.map +1 -1
- package/build/executor.js +17 -14
- package/build/executor.js.map +1 -1
- package/build/facade.d.ts +20 -2
- package/build/facade.d.ts.map +1 -1
- package/build/facade.js +128 -18
- package/build/facade.js.map +1 -1
- package/build/helper.d.ts +3 -2
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +7 -3
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/projection.d.ts +2 -10
- package/build/projection.d.ts.map +1 -1
- package/build/projection.js +1 -1
- package/build/projection.js.map +1 -1
- package/build/queue/consts.d.ts +5 -0
- package/build/queue/consts.d.ts.map +1 -0
- package/build/queue/consts.js +5 -0
- package/build/queue/consts.js.map +1 -0
- package/build/queue/index.d.ts +4 -16
- package/build/queue/index.d.ts.map +1 -1
- package/build/queue/index.js +2 -4
- package/build/queue/index.js.map +1 -1
- package/build/queue/types.d.ts +13 -0
- package/build/queue/types.d.ts.map +1 -0
- package/build/queue/types.js +2 -0
- package/build/queue/types.js.map +1 -0
- package/build/registry.d.ts +2 -21
- package/build/registry.d.ts.map +1 -1
- package/build/registry.js +3 -1
- package/build/registry.js.map +1 -1
- package/build/service.d.ts +3 -16
- package/build/service.d.ts.map +1 -1
- package/build/service.js +9 -0
- package/build/service.js.map +1 -1
- package/build/store/commits.d.ts.map +1 -1
- package/build/store/commits.js +3 -1
- package/build/store/commits.js.map +1 -1
- package/build/store/composite.d.ts +1 -0
- package/build/store/composite.d.ts.map +1 -1
- package/build/store/composite.js +3 -0
- package/build/store/composite.js.map +1 -1
- package/build/store/consts.local.d.ts +2 -0
- package/build/store/consts.local.d.ts.map +1 -0
- package/build/store/consts.local.js +2 -0
- package/build/store/consts.local.js.map +1 -0
- package/build/store/fold.d.ts +4 -3
- package/build/store/fold.d.ts.map +1 -1
- package/build/store/fold.js +14 -6
- package/build/store/fold.js.map +1 -1
- package/build/store/memory.d.ts +12 -0
- package/build/store/memory.d.ts.map +1 -1
- package/build/store/memory.js +0 -0
- package/build/store/memory.js.map +1 -1
- package/build/store/types.d.ts +15 -1
- package/build/store/types.d.ts.map +1 -1
- package/build/store/types.local.d.ts +3 -0
- package/build/store/types.local.d.ts.map +1 -0
- package/build/store/types.local.js +2 -0
- package/build/store/types.local.js.map +1 -0
- package/build/types.d.ts +111 -1
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +7 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/scope.d.ts +39 -2
- package/build/utils/scope.d.ts.map +1 -1
- package/build/utils/scope.js +79 -15
- package/build/utils/scope.js.map +1 -1
- package/build/utils/types.d.ts +8 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/package.json +28 -20
- package/src/actions/cards.ts +2 -6
- package/src/actions/execute.ts +95 -12
- package/src/actions/schemas.ts +81 -9
- package/src/actions/specs.ts +1 -2
- package/src/actions/types.ts +4 -0
- package/src/actions/watch.ts +5 -2
- package/src/conformance/assert.ts +59 -0
- package/src/conformance/cases.ts +522 -0
- package/src/conformance/consts.ts +44 -0
- package/src/conformance/fixtures.ts +121 -0
- package/src/conformance/index.ts +11 -0
- package/src/conformance/types.ts +20 -0
- package/src/consts.ts +6 -0
- package/src/definitions.ts +250 -0
- package/src/executor/changes.ts +2 -6
- package/src/executor/code.ts +3 -9
- package/src/executor/consts.local.ts +8 -0
- package/src/executor/creator.ts +51 -0
- package/src/executor/resolve.ts +84 -40
- package/src/executor/types.ts +25 -0
- package/src/executor/validate.ts +22 -26
- package/src/executor.ts +21 -21
- package/src/facade.ts +143 -25
- package/src/helper.ts +7 -3
- package/src/index.ts +1 -0
- package/src/projection.ts +4 -14
- package/src/queue/consts.ts +5 -0
- package/src/queue/index.ts +6 -21
- package/src/queue/types.ts +14 -0
- package/src/registry.ts +5 -29
- package/src/service.ts +13 -20
- package/src/store/commits.ts +5 -4
- package/src/store/composite.ts +3 -0
- package/src/store/consts.local.ts +1 -0
- package/src/store/fold.ts +15 -12
- package/src/store/memory.ts +0 -0
- package/src/store/types.local.ts +3 -0
- package/src/store/types.ts +16 -1
- package/src/types.local.ts +7 -0
- package/src/types.ts +126 -4
- package/src/utils/index.ts +1 -0
- package/src/utils/scope.ts +97 -17
- package/src/utils/types.ts +8 -0
- package/tests/access.spec.ts +160 -0
- package/tests/conformance.spec.ts +47 -0
- package/tests/create-example.spec.ts +202 -0
- package/tests/creator.spec.ts +257 -0
- package/tests/entrypoints.spec.ts +5 -0
- package/tests/narrowing.spec.ts +94 -0
- package/tests/plugins.spec.ts +4 -3
- package/tests/writes.spec.ts +140 -0
package/src/facade.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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 './
|
|
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(
|
|
147
|
+
list: async query => await reader().cards.list(await narrowed(query), listOptionsOf(query)),
|
|
66
148
|
|
|
67
|
-
count: async query => await reader().cards.count(
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
16
|
-
* wants its own handler for one leaf binds
|
|
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
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
|
|
4
|
-
import {
|
|
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
|
|
package/src/queue/index.ts
CHANGED
|
@@ -1,25 +1,10 @@
|
|
|
1
|
-
import { declareQueue } from '@owlmeans/queue'
|
|
2
|
-
import
|
|
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 {
|
|
4
|
-
import type
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
package/src/store/commits.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
log.error('Planning commit listener failed', { transition: event.transition, error: result.reason })
|
|
62
63
|
}
|
|
63
64
|
})
|
|
64
65
|
},
|
package/src/store/composite.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/store/memory.ts
CHANGED
|
Binary file
|