@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-planning",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "The general planning implementation — the planning service and its plugin/middleware registry, the transition executor, the in-memory store, the protocol handlers and the commit feed.",
|
|
6
6
|
"type": "module",
|
|
@@ -34,24 +34,32 @@
|
|
|
34
34
|
"default": "./build/queue/index.js",
|
|
35
35
|
"module": "./build/queue/index.js",
|
|
36
36
|
"types": "./build/queue/index.d.ts"
|
|
37
|
+
},
|
|
38
|
+
"./conformance": {
|
|
39
|
+
"import": "./build/conformance/index.js",
|
|
40
|
+
"require": "./build/conformance/index.js",
|
|
41
|
+
"default": "./build/conformance/index.js",
|
|
42
|
+
"module": "./build/conformance/index.js",
|
|
43
|
+
"types": "./build/conformance/index.d.ts"
|
|
37
44
|
}
|
|
38
45
|
},
|
|
39
46
|
"dependencies": {
|
|
40
|
-
"@owlmeans/auth": "^0.1.18-rc.
|
|
41
|
-
"@owlmeans/auth-common": "^0.1.18-rc.
|
|
42
|
-
"@owlmeans/basic-ids": "^0.1.18-rc.
|
|
43
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
44
|
-
"@owlmeans/entrypoint": "^0.1.18-rc.
|
|
45
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
46
|
-
"@owlmeans/planning": "^0.1.18-rc.
|
|
47
|
-
"@owlmeans/queue": "^0.1.18-rc.
|
|
48
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
49
|
-
"@owlmeans/
|
|
50
|
-
"@owlmeans/
|
|
51
|
-
"@owlmeans/server-
|
|
52
|
-
"@owlmeans/server-
|
|
53
|
-
"@owlmeans/server-
|
|
54
|
-
"@owlmeans/socket": "^0.1.18-rc.
|
|
47
|
+
"@owlmeans/auth": "^0.1.18-rc.39",
|
|
48
|
+
"@owlmeans/auth-common": "^0.1.18-rc.46",
|
|
49
|
+
"@owlmeans/basic-ids": "^0.1.18-rc.36",
|
|
50
|
+
"@owlmeans/context": "^0.1.18-rc.33",
|
|
51
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.40",
|
|
52
|
+
"@owlmeans/error": "^0.1.18-rc.37",
|
|
53
|
+
"@owlmeans/planning": "^0.1.18-rc.18",
|
|
54
|
+
"@owlmeans/queue": "^0.1.18-rc.40",
|
|
55
|
+
"@owlmeans/resource": "^0.1.18-rc.39",
|
|
56
|
+
"@owlmeans/log": "^0.1.18-rc.1",
|
|
57
|
+
"@owlmeans/route": "^0.1.18-rc.34",
|
|
58
|
+
"@owlmeans/server-api": "^0.1.18-rc.50",
|
|
59
|
+
"@owlmeans/server-context": "^0.1.18-rc.42",
|
|
60
|
+
"@owlmeans/server-entrypoint": "^0.1.18-rc.39",
|
|
61
|
+
"@owlmeans/server-socket": "^0.1.18-rc.53",
|
|
62
|
+
"@owlmeans/socket": "^0.1.18-rc.39"
|
|
55
63
|
},
|
|
56
64
|
"peerDependencies": {
|
|
57
65
|
"ajv": "*",
|
|
@@ -59,10 +67,10 @@
|
|
|
59
67
|
},
|
|
60
68
|
"devDependencies": {
|
|
61
69
|
"@owlmeans/dep-config": "workspace:*",
|
|
62
|
-
"@owlmeans/test": "^0.1.18-rc.
|
|
63
|
-
"@types/bun": "^1.4.
|
|
64
|
-
"@types/node": "^26.
|
|
65
|
-
"ajv": "^8.
|
|
70
|
+
"@owlmeans/test": "^0.1.18-rc.34",
|
|
71
|
+
"@types/bun": "^1.4.2",
|
|
72
|
+
"@types/node": "^26.6.4",
|
|
73
|
+
"ajv": "^8.20.0",
|
|
66
74
|
"ajv-formats": "^3.0.1",
|
|
67
75
|
"nodemon": "^3.1.14",
|
|
68
76
|
"typescript": "^7.0.2"
|
package/src/actions/cards.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { handlers } from '@owlmeans/server-api'
|
|
2
|
-
import {
|
|
3
|
-
decodeSpecificationQuery, decodeSummaryQuery, decodeTransitionQuery, decodeWorkcardQuery,
|
|
4
|
-
} from '@owlmeans/planning'
|
|
5
|
-
import type { PlanningProtocols } from '@owlmeans/planning'
|
|
2
|
+
import { decodeSpecificationQuery, decodeSummaryQuery, decodeTransitionQuery, decodeWorkcardQuery, type PlanningProtocols } from '@owlmeans/planning'
|
|
6
3
|
import type { Context, PlanningHandlerOptions } from '../types.js'
|
|
7
4
|
import { concealed, handlerFacade } from '../utils/index.js'
|
|
8
|
-
|
|
9
|
-
type RequestHandler = ReturnType<ReturnType<typeof handlers<Context>>['request']>
|
|
5
|
+
import type { RequestHandler } from './types.js'
|
|
10
6
|
|
|
11
7
|
/** Cards of the caller's entity; the query arrives in its wire shape and is decoded here. */
|
|
12
8
|
export const listCards = (protocol: PlanningProtocols['card']['list'], opts?: PlanningHandlerOptions): RequestHandler =>
|
package/src/actions/execute.ts
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import { handlers } from '@owlmeans/server-api'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_COMMIT_TIMEOUT, isProject, isSpecification, MAX_COMMIT_POLL, normalizeParents, PlanningForbidden,
|
|
4
|
+
TransitionAction, WorkcardKind,
|
|
5
|
+
} from '@owlmeans/planning'
|
|
3
6
|
import type {
|
|
4
|
-
ExecuteOptions, ExecuteRequest, PlanningProtocols, PlanningScope, TransitionExecution,
|
|
7
|
+
ExecuteOptions, ExecuteRequest, PlanningFacade, PlanningProtocols, PlanningScope, TransitionExecution,
|
|
8
|
+
TransitionReceiptView, Workcard,
|
|
5
9
|
} from '@owlmeans/planning'
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
10
|
+
import { withCreator } from '../executor/creator.js'
|
|
11
|
+
import type { Context, PlanningAccess, PlanningHandlerOptions } from '../types.js'
|
|
12
|
+
import { assertGranted, concealed, handlerScopeOf, writableIn } from '../utils/index.js'
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
15
|
* What a wire execution may say. `actor` is dropped — the scope names the writer — and a create's
|
|
11
|
-
* `createdBy` is the
|
|
16
|
+
* `createdBy` claim is dropped too: the executor's {@link withCreator} stamps the authenticated
|
|
17
|
+
* subject, the same rule an in-process create follows. Applied here as well, so the returned
|
|
18
|
+
* execution already carries it. A `createdBy` in `changes` or `unset` is not dropped: the executor
|
|
19
|
+
* refuses it on every action (`assertCreatorFixed`), so no body moves an owner.
|
|
12
20
|
*/
|
|
13
21
|
export const wireExecution = (body: ExecuteRequest, scope: PlanningScope): TransitionExecution => {
|
|
14
22
|
const { wait: _wait, timeout: _timeout, actor: _actor, ...exec } = body
|
|
15
23
|
if (exec.action === TransitionAction.Create && exec.card != null && typeof exec.card === 'object') {
|
|
16
24
|
const { createdBy: _createdBy, ...draft } = exec.card
|
|
17
|
-
|
|
18
|
-
exec.card = subject != null ? { ...draft, createdBy: subject } : draft
|
|
25
|
+
exec.card = draft
|
|
19
26
|
}
|
|
20
|
-
return exec
|
|
27
|
+
return withCreator(exec, scope)
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/** `wait`/`timeout` as a server grants them: the timeout never holds a request past `maxPoll`. */
|
|
@@ -30,16 +37,92 @@ export const executeOptionsOf = (body: Pick<ExecuteRequest, 'wait' | 'timeout'>,
|
|
|
30
37
|
return { wait: true, timeout: Math.min(Number.isFinite(asked) && asked > 0 ? asked : DEFAULT_COMMIT_TIMEOUT, ceiling) }
|
|
31
38
|
}
|
|
32
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Refuse an execution outside the access's `writes`: the card it acts on (a specification through
|
|
42
|
+
* its parent card), or every parent a create names, must be one {@link writableIn} admits. A create
|
|
43
|
+
* of a project is `grants.createProjects`' alone. A card or parent this scope cannot see is left to
|
|
44
|
+
* the executor, which answers it as absent.
|
|
45
|
+
*
|
|
46
|
+
* @throws {PlanningForbidden}
|
|
47
|
+
*/
|
|
48
|
+
export const assertExecutionWrites = async (
|
|
49
|
+
facade: PlanningFacade, exec: TransitionExecution, access?: PlanningAccess
|
|
50
|
+
): Promise<void> => {
|
|
51
|
+
if (access?.writes == null) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
const admitted = async (card: Workcard): Promise<boolean> => {
|
|
55
|
+
if (writableIn(access, card)) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
if (!isSpecification(card) || card.parent == null) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
const parent = await facade.cards.load(card.parent)
|
|
62
|
+
return parent == null || writableIn(access, parent)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (exec.action === TransitionAction.Create && exec.card != null && typeof exec.card === 'object') {
|
|
66
|
+
if (exec.card.kind === WorkcardKind.Project) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const parents = normalizeParents(exec.card.parent, exec.card.parents)
|
|
70
|
+
if (parents.length === 0) {
|
|
71
|
+
throw new PlanningForbidden('writes:root')
|
|
72
|
+
}
|
|
73
|
+
for (const id of parents) {
|
|
74
|
+
const parent = await facade.cards.load(id)
|
|
75
|
+
if (parent != null && !await admitted(parent)) {
|
|
76
|
+
throw new PlanningForbidden(`writes:${id}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (typeof exec.card === 'string' && exec.card !== '') {
|
|
82
|
+
const card = await facade.cards.load(exec.card)
|
|
83
|
+
if (card != null && !await admitted(card)) {
|
|
84
|
+
throw new PlanningForbidden(`writes:${exec.card}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The access a wire execution needs. `writes` first ({@link assertExecutionWrites}); then the
|
|
91
|
+
* grants: `createProjects` for a project create (its primary parent, or the root),
|
|
92
|
+
* `deleteProjects` for the delete of a project card. A card this scope cannot see is left to the
|
|
93
|
+
* executor, which answers it as absent.
|
|
94
|
+
*
|
|
95
|
+
* @throws {PlanningForbidden}
|
|
96
|
+
*/
|
|
97
|
+
export const assertExecutionGranted = async (
|
|
98
|
+
facade: PlanningFacade, exec: TransitionExecution, access?: PlanningAccess
|
|
99
|
+
): Promise<void> => {
|
|
100
|
+
await assertExecutionWrites(facade, exec, access)
|
|
101
|
+
if (access?.grants == null) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
if (exec.action === TransitionAction.Create && typeof exec.card === 'object' && exec.card?.kind === WorkcardKind.Project) {
|
|
105
|
+
assertGranted(access, 'createProjects', normalizeParents(exec.card.parent, exec.card.parents)[0])
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (exec.action === TransitionAction.Delete && typeof exec.card === 'string') {
|
|
109
|
+
const card = await facade.cards.load(exec.card)
|
|
110
|
+
if (card != null && isProject(card)) {
|
|
111
|
+
assertGranted(access, 'deleteProjects', card.id)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
33
116
|
/** The one write route. */
|
|
34
117
|
export const executePlanning = (
|
|
35
118
|
protocol: PlanningProtocols['execute'], opts?: PlanningHandlerOptions
|
|
36
119
|
): ReturnType<ReturnType<typeof handlers<Context>>['request']> =>
|
|
37
120
|
handlers<Context>().request(protocol, async (req, ctx) => concealed(async (): Promise<TransitionReceiptView> => {
|
|
38
|
-
const facade = await
|
|
121
|
+
const { facade, access } = await handlerScopeOf(ctx, req, opts)
|
|
39
122
|
const body = (req.body ?? {}) as ExecuteRequest
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
)
|
|
123
|
+
const exec = wireExecution(body, facade.scope)
|
|
124
|
+
await assertExecutionGranted(facade, exec, access)
|
|
125
|
+
const receipt = await facade.execute(exec, executeOptionsOf(body, opts?.maxPoll ?? MAX_COMMIT_POLL))
|
|
43
126
|
|
|
44
127
|
return receipt.card !== undefined
|
|
45
128
|
? { transition: receipt.transition, card: receipt.card }
|
package/src/actions/schemas.ts
CHANGED
|
@@ -1,15 +1,87 @@
|
|
|
1
1
|
import { handlers } from '@owlmeans/server-api'
|
|
2
|
-
import type
|
|
2
|
+
import { PlanningUnsupported, SchemaWriteMode, type PlanningDefinitions, type PlanningProtocols, type SchemaDefineReply, type SchemaDefineRequest, type ScopedSchemaBundle, type ScopedSchemaRecord } from '@owlmeans/planning'
|
|
3
3
|
import type { Context, PlanningHandlerOptions } from '../types.js'
|
|
4
|
-
import {
|
|
4
|
+
import { assertGranted, assertWrites, concealed, handlerScopeOf } from '../utils/index.js'
|
|
5
|
+
import type { RequestHandler } from './types.js'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const text = (value: unknown): string | undefined =>
|
|
8
|
+
value == null || value === '' ? undefined : `${Array.isArray(value) ? value[0] : value}`
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The schema bundle — what a client loads to answer `can()` with no round trip. Scoped like every
|
|
12
|
+
* other leaf (no organization, no bundle). Where the store holds data-defined schemas it is the
|
|
13
|
+
* resolved layer of the organization, or of the project the `project` query names (a project this
|
|
14
|
+
* scope can see); otherwise the service's code bundle.
|
|
15
|
+
*
|
|
16
|
+
* @throws {WorkcardNotFound} for a project this scope cannot see
|
|
17
|
+
*/
|
|
7
18
|
export const listSchemas = (
|
|
8
19
|
protocol: PlanningProtocols['schema']['list'], opts?: PlanningHandlerOptions
|
|
9
|
-
):
|
|
10
|
-
handlers<Context>().request(protocol, async (req, ctx) => {
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
): RequestHandler =>
|
|
21
|
+
handlers<Context>().request(protocol, async (req, ctx) => concealed(async (): Promise<ScopedSchemaBundle> => {
|
|
22
|
+
const { facade } = await handlerScopeOf(ctx, req, opts)
|
|
23
|
+
if (facade.definitions == null) {
|
|
24
|
+
return facade.schemas.bundle()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return await facade.definitions.bundle(text((req.query as { project?: unknown } | undefined)?.project))
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Apply one `schema.define` request to a facade's definitions: the declarations in the request's
|
|
32
|
+
* mode (flows before types), then the retirements — answering what was written and the layer as it
|
|
33
|
+
* resolves afterwards.
|
|
34
|
+
*/
|
|
35
|
+
export const applySchemaRequest = async (
|
|
36
|
+
definitions: PlanningDefinitions, request: SchemaDefineRequest
|
|
37
|
+
): Promise<SchemaDefineReply> => {
|
|
38
|
+
const layer = request.project != null ? { project: request.project } : {}
|
|
39
|
+
const declarations = { types: request.types ?? [], flows: request.flows ?? [] }
|
|
40
|
+
const records: ScopedSchemaRecord[] = []
|
|
41
|
+
|
|
42
|
+
switch (request.mode ?? SchemaWriteMode.Define) {
|
|
43
|
+
case SchemaWriteMode.Put:
|
|
44
|
+
for (const flow of declarations.flows) {
|
|
45
|
+
records.push(await definitions.putFlow(flow, layer))
|
|
46
|
+
}
|
|
47
|
+
for (const type of declarations.types) {
|
|
48
|
+
records.push(await definitions.putType(type, layer))
|
|
49
|
+
}
|
|
50
|
+
break
|
|
51
|
+
case SchemaWriteMode.Seed:
|
|
52
|
+
records.push(...await definitions.seed(declarations, layer))
|
|
53
|
+
break
|
|
54
|
+
default:
|
|
55
|
+
records.push(...await definitions.define(declarations, layer))
|
|
56
|
+
}
|
|
57
|
+
for (const key of request.retire ?? []) {
|
|
58
|
+
records.push(await definitions.retire(key.kind, key.key, layer))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { records, bundle: await definitions.bundle(request.project) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The write of data-defined types and flows. A project layer needs the project in the access's
|
|
66
|
+
* `writes` when the resolver answers it; then `grants.defineSchemas` gates when the resolver answers
|
|
67
|
+
* grants (the organization-wide layer needs `true`, a project layer its id).
|
|
68
|
+
*
|
|
69
|
+
* @throws {PlanningUnsupported} where the store holds no data-defined schemas
|
|
70
|
+
* @throws {PlanningForbidden | SchemaConflict | SchemaSealed | SchemaInvalid | SchemaInUse | WorkcardNotFound}
|
|
71
|
+
*/
|
|
72
|
+
export const defineSchemas = (
|
|
73
|
+
protocol: NonNullable<PlanningProtocols['schema']['define']>, opts?: PlanningHandlerOptions
|
|
74
|
+
): RequestHandler =>
|
|
75
|
+
handlers<Context>().request(protocol, async (req, ctx) => concealed(async (): Promise<SchemaDefineReply> => {
|
|
76
|
+
const { facade, access } = await handlerScopeOf(ctx, req, opts)
|
|
77
|
+
const request = (req.body ?? {}) as SchemaDefineRequest
|
|
78
|
+
if (facade.definitions == null) {
|
|
79
|
+
throw new PlanningUnsupported('definitions')
|
|
80
|
+
}
|
|
81
|
+
if (request.project != null) {
|
|
82
|
+
assertWrites(access, request.project)
|
|
83
|
+
}
|
|
84
|
+
assertGranted(access, 'defineSchemas', request.project)
|
|
13
85
|
|
|
14
|
-
return
|
|
15
|
-
})
|
|
86
|
+
return await applySchemaRequest(facade.definitions, request)
|
|
87
|
+
}))
|
package/src/actions/specs.ts
CHANGED
|
@@ -2,8 +2,7 @@ import { handlers } from '@owlmeans/server-api'
|
|
|
2
2
|
import type { PlanningProtocols } from '@owlmeans/planning'
|
|
3
3
|
import type { Context, PlanningHandlerOptions } from '../types.js'
|
|
4
4
|
import { concealed, handlerFacade } from '../utils/index.js'
|
|
5
|
-
|
|
6
|
-
type RequestHandler = ReturnType<ReturnType<typeof handlers<Context>>['request']>
|
|
5
|
+
import type { RequestHandler } from './types.js'
|
|
7
6
|
|
|
8
7
|
/** @throws {WorkcardNotFound} */
|
|
9
8
|
export const getSpecification = (protocol: PlanningProtocols['spec']['get'], opts?: PlanningHandlerOptions): RequestHandler =>
|
package/src/actions/watch.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from '@owlmeans/log'
|
|
1
2
|
import { PLANNING_COMMIT_EVENT } from '@owlmeans/planning'
|
|
2
3
|
import type { CommitEvent, CommitFeedQuery, PlanningProtocols } from '@owlmeans/planning'
|
|
3
4
|
import { connection } from '@owlmeans/server-socket'
|
|
@@ -6,6 +7,8 @@ import { MessageType } from '@owlmeans/socket'
|
|
|
6
7
|
import type { Context, PlanningHandlerOptions } from '../types.js'
|
|
7
8
|
import { handlerFacade } from '../utils/index.js'
|
|
8
9
|
|
|
10
|
+
const log = logger('planning:watch')
|
|
11
|
+
|
|
9
12
|
const text = (value: unknown): string | undefined =>
|
|
10
13
|
value == null || value === '' ? undefined : `${value}`
|
|
11
14
|
|
|
@@ -32,7 +35,7 @@ export const watchCommits = (
|
|
|
32
35
|
const enriched = event.record !== undefined ? event : { ...event, record: await facade.cards.load(event.card) }
|
|
33
36
|
await conn.notify(frame, enriched)
|
|
34
37
|
} catch (e) {
|
|
35
|
-
|
|
38
|
+
log.warn('Planning commit notify failed', e)
|
|
36
39
|
}
|
|
37
40
|
}, filter)
|
|
38
41
|
|
|
@@ -45,7 +48,7 @@ export const watchCommits = (
|
|
|
45
48
|
try {
|
|
46
49
|
unsubscribe()
|
|
47
50
|
} catch (e) {
|
|
48
|
-
|
|
51
|
+
log.warn('Planning commit unsubscribe failed', e)
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { sameValue } from '@owlmeans/planning'
|
|
2
|
+
|
|
3
|
+
/** A conformance expectation that did not hold. Plain `Error`, so any test runner reports it. */
|
|
4
|
+
export class ConformanceFailure extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(`planning conformance: ${message}`)
|
|
7
|
+
this.name = 'ConformanceFailure'
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const show = (value: unknown): string => {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.stringify(value)
|
|
14
|
+
} catch {
|
|
15
|
+
return `${value}`
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @throws {ConformanceFailure} */
|
|
20
|
+
export const check = (condition: unknown, message: string): void => {
|
|
21
|
+
if (condition !== true) {
|
|
22
|
+
throw new ConformanceFailure(message)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Structural equality over JSON values. @throws {ConformanceFailure} */
|
|
27
|
+
export const same = (actual: unknown, expected: unknown, message: string): void => {
|
|
28
|
+
if (!sameValue(actual, expected)) {
|
|
29
|
+
throw new ConformanceFailure(`${message}: expected ${show(expected)}, got ${show(actual)}`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The same members in any order. @throws {ConformanceFailure} */
|
|
34
|
+
export const sameSet = (actual: readonly unknown[], expected: readonly unknown[], message: string): void => {
|
|
35
|
+
const sort = (values: readonly unknown[]) => [...values].map(show).sort()
|
|
36
|
+
same(sort(actual), sort(expected), message)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The promise rejects with an error of the class (matched by instance or by its registered type
|
|
41
|
+
* name, so a duplicate module copy counts). Answers the error.
|
|
42
|
+
*
|
|
43
|
+
* @throws {ConformanceFailure}
|
|
44
|
+
*/
|
|
45
|
+
export const rejects = async (
|
|
46
|
+
run: Promise<unknown> | (() => Promise<unknown>), expected: { typeName: string }, message: string
|
|
47
|
+
): Promise<Error> => {
|
|
48
|
+
try {
|
|
49
|
+
await (typeof run === 'function' ? run() : run)
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const type = (error as { type?: unknown } | null)?.type
|
|
52
|
+
if (error instanceof (expected as unknown as abstract new (...args: never[]) => unknown)
|
|
53
|
+
|| (typeof type === 'string' && type === expected.typeName)) {
|
|
54
|
+
return error as Error
|
|
55
|
+
}
|
|
56
|
+
throw new ConformanceFailure(`${message}: expected ${expected.typeName}, got ${show((error as Error)?.message ?? error)}`)
|
|
57
|
+
}
|
|
58
|
+
throw new ConformanceFailure(`${message}: expected ${expected.typeName}, nothing was thrown`)
|
|
59
|
+
}
|