@owlmeans/server-planning 0.1.18-rc.21 → 0.1.18-rc.23
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 +24 -17
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-planning/SKILL.md +28 -28
- package/build/actions/apply-schema-request.d.ts +8 -0
- package/build/actions/apply-schema-request.d.ts.map +1 -0
- package/build/actions/apply-schema-request.js +31 -0
- package/build/actions/apply-schema-request.js.map +1 -0
- package/build/actions/cards.d.ts +5 -3
- 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/commit.d.ts +3 -3
- package/build/actions/commit.d.ts.map +1 -1
- package/build/actions/commit.js +5 -4
- package/build/actions/commit.js.map +1 -1
- package/build/actions/define-schemas.d.ts +13 -0
- package/build/actions/define-schemas.d.ts.map +1 -0
- package/build/actions/define-schemas.js +28 -0
- package/build/actions/define-schemas.js.map +1 -0
- package/build/actions/execute.d.ts +8 -28
- package/build/actions/execute.d.ts.map +1 -1
- package/build/actions/execute.js +17 -101
- package/build/actions/execute.js.map +1 -1
- package/build/actions/execution/types.d.ts +34 -0
- package/build/actions/execution/types.d.ts.map +1 -0
- package/build/actions/execution/types.js +2 -0
- package/build/actions/execution/types.js.map +1 -0
- package/build/actions/execution.d.ts +4 -0
- package/build/actions/execution.d.ts.map +1 -0
- package/build/actions/execution.js +79 -0
- package/build/actions/execution.js.map +1 -0
- package/build/actions/get-card.d.ts +6 -0
- package/build/actions/get-card.d.ts.map +1 -0
- package/build/actions/get-card.js +9 -0
- package/build/actions/get-card.js.map +1 -0
- package/build/actions/get-specification.d.ts +6 -0
- package/build/actions/get-specification.d.ts.map +1 -0
- package/build/actions/get-specification.js +9 -0
- package/build/actions/get-specification.js.map +1 -0
- package/build/actions/index.d.ts +12 -3
- package/build/actions/index.d.ts.map +1 -1
- package/build/actions/index.js +11 -3
- package/build/actions/index.js.map +1 -1
- package/build/actions/links.d.ts +3 -3
- package/build/actions/links.d.ts.map +1 -1
- package/build/actions/links.js +6 -5
- package/build/actions/links.js.map +1 -1
- package/build/actions/list-card-specifications.d.ts +6 -0
- package/build/actions/list-card-specifications.d.ts.map +1 -0
- package/build/actions/list-card-specifications.js +11 -0
- package/build/actions/list-card-specifications.js.map +1 -0
- package/build/actions/list-card-transitions.d.ts +6 -0
- package/build/actions/list-card-transitions.d.ts.map +1 -0
- package/build/actions/list-card-transitions.js +11 -0
- package/build/actions/list-card-transitions.js.map +1 -0
- package/build/actions/list-cards.d.ts +6 -0
- package/build/actions/list-cards.d.ts.map +1 -0
- package/build/actions/list-cards.js +10 -0
- package/build/actions/list-cards.js.map +1 -0
- package/build/actions/list-schemas.d.ts +13 -0
- package/build/actions/list-schemas.d.ts.map +1 -0
- package/build/actions/list-schemas.js +20 -0
- package/build/actions/list-schemas.js.map +1 -0
- package/build/actions/list-specification-revisions.d.ts +6 -0
- package/build/actions/list-specification-revisions.d.ts.map +1 -0
- package/build/actions/list-specification-revisions.js +10 -0
- package/build/actions/list-specification-revisions.js.map +1 -0
- package/build/actions/schemas.d.ts +5 -3
- package/build/actions/schemas.d.ts.map +1 -1
- package/build/actions/schemas.js.map +1 -1
- package/build/actions/specs.d.ts +4 -2
- package/build/actions/specs.d.ts.map +1 -1
- package/build/actions/specs.js.map +1 -1
- package/build/actions/summarize-cards.d.ts +6 -0
- package/build/actions/summarize-cards.d.ts.map +1 -0
- package/build/actions/summarize-cards.js +11 -0
- package/build/actions/summarize-cards.js.map +1 -0
- package/build/actions/transitions.d.ts +3 -3
- package/build/actions/transitions.d.ts.map +1 -1
- package/build/actions/transitions.js +4 -3
- package/build/actions/transitions.js.map +1 -1
- package/build/actions/types.d.ts +5 -3
- package/build/actions/types.d.ts.map +1 -1
- package/build/actions/watch.d.ts +2 -2
- package/build/actions/watch.d.ts.map +1 -1
- package/build/actions/watch.js +2 -2
- package/build/actions/watch.js.map +1 -1
- package/build/conformance/assert/types.d.ts +19 -0
- package/build/conformance/assert/types.d.ts.map +1 -0
- package/build/conformance/assert/types.js +2 -0
- package/build/conformance/assert/types.js.map +1 -0
- package/build/conformance/assert.d.ts +8 -13
- package/build/conformance/assert.d.ts.map +1 -1
- package/build/conformance/assert.js +41 -43
- package/build/conformance/assert.js.map +1 -1
- package/build/conformance/cases.d.ts.map +1 -1
- package/build/conformance/cases.js +86 -67
- package/build/conformance/cases.js.map +1 -1
- package/build/conformance/clock.d.ts +6 -0
- package/build/conformance/clock.d.ts.map +1 -0
- package/build/conformance/clock.js +12 -0
- package/build/conformance/clock.js.map +1 -0
- package/build/conformance/consts.d.ts +3 -1
- package/build/conformance/consts.d.ts.map +1 -1
- package/build/conformance/consts.js +84 -1
- package/build/conformance/consts.js.map +1 -1
- package/build/conformance/errors.d.ts +5 -0
- package/build/conformance/errors.d.ts.map +1 -0
- package/build/conformance/errors.js +8 -0
- package/build/conformance/errors.js.map +1 -0
- package/build/conformance/fixtures/types.d.ts +13 -0
- package/build/conformance/fixtures/types.d.ts.map +1 -0
- package/build/conformance/fixtures/types.js +2 -0
- package/build/conformance/fixtures/types.js.map +1 -0
- package/build/conformance/fixtures.d.ts +8 -8
- package/build/conformance/fixtures.d.ts.map +1 -1
- package/build/conformance/fixtures.js +24 -107
- package/build/conformance/fixtures.js.map +1 -1
- package/build/conformance/index.d.ts +3 -0
- package/build/conformance/index.d.ts.map +1 -1
- package/build/conformance/index.js +1 -0
- package/build/conformance/index.js.map +1 -1
- package/build/definitions.d.ts.map +1 -1
- package/build/definitions.js +9 -9
- package/build/definitions.js.map +1 -1
- package/build/executor/changes/types.d.ts +36 -0
- package/build/executor/changes/types.d.ts.map +1 -0
- package/build/executor/changes/types.js +2 -0
- package/build/executor/changes/types.js.map +1 -0
- package/build/executor/changes.d.ts +3 -30
- package/build/executor/changes.d.ts.map +1 -1
- package/build/executor/changes.js +42 -51
- package/build/executor/changes.js.map +1 -1
- package/build/executor/code/types.d.ts +17 -0
- package/build/executor/code/types.d.ts.map +1 -0
- package/build/executor/code/types.js +2 -0
- package/build/executor/code/types.js.map +1 -0
- package/build/executor/code.d.ts +3 -14
- package/build/executor/code.d.ts.map +1 -1
- package/build/executor/code.js +50 -54
- package/build/executor/code.js.map +1 -1
- package/build/executor/creator/types.d.ts +33 -0
- package/build/executor/creator/types.d.ts.map +1 -0
- package/build/executor/creator/types.js +2 -0
- package/build/executor/creator/types.js.map +1 -0
- package/build/executor/creator.d.ts +6 -25
- package/build/executor/creator.d.ts.map +1 -1
- package/build/executor/creator.js +26 -41
- package/build/executor/creator.js.map +1 -1
- package/build/executor/resolve/types.d.ts +30 -0
- package/build/executor/resolve/types.d.ts.map +1 -0
- package/build/executor/resolve/types.js +2 -0
- package/build/executor/resolve/types.js.map +1 -0
- package/build/executor/resolve.d.ts +4 -41
- package/build/executor/resolve.d.ts.map +1 -1
- package/build/executor/resolve.js +142 -157
- package/build/executor/resolve.js.map +1 -1
- package/build/executor/types.d.ts +0 -2
- package/build/executor/types.d.ts.map +1 -1
- package/build/executor/validate.d.ts +1 -1
- package/build/executor/validate.d.ts.map +1 -1
- package/build/executor/validate.js +16 -15
- package/build/executor/validate.js.map +1 -1
- package/build/executor.d.ts +4 -3
- package/build/executor.d.ts.map +1 -1
- package/build/executor.js +18 -17
- package/build/executor.js.map +1 -1
- package/build/facade.d.ts.map +1 -1
- package/build/facade.js +16 -16
- package/build/facade.js.map +1 -1
- package/build/helper.d.ts +1 -6
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +3 -8
- package/build/helper.js.map +1 -1
- package/build/projection.d.ts +2 -0
- package/build/projection.d.ts.map +1 -1
- package/build/projection.js +6 -4
- package/build/projection.js.map +1 -1
- package/build/queue/declare.d.ts +5 -0
- package/build/queue/declare.d.ts.map +1 -0
- package/build/queue/declare.js +5 -0
- package/build/queue/declare.js.map +1 -0
- package/build/queue/index.d.ts +1 -4
- package/build/queue/index.d.ts.map +1 -1
- package/build/queue/index.js +1 -4
- package/build/queue/index.js.map +1 -1
- package/build/store/composite.d.ts.map +1 -1
- package/build/store/composite.js +2 -2
- package/build/store/composite.js.map +1 -1
- package/build/store/fold/types.d.ts +43 -0
- package/build/store/fold/types.d.ts.map +1 -0
- package/build/store/fold/types.js +2 -0
- package/build/store/fold/types.js.map +1 -0
- package/build/store/fold.d.ts +7 -30
- package/build/store/fold.d.ts.map +1 -1
- package/build/store/fold.js +185 -203
- package/build/store/fold.js.map +1 -1
- package/build/store/index.d.ts +1 -0
- package/build/store/index.d.ts.map +1 -1
- package/build/store/index.js.map +1 -1
- 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.local.d.ts +3 -1
- package/build/store/types.local.d.ts.map +1 -1
- package/build/types.d.ts +2 -1
- package/build/types.d.ts.map +1 -1
- package/build/utils/access/types.d.ts +30 -0
- package/build/utils/access/types.d.ts.map +1 -0
- package/build/utils/access/types.js +2 -0
- package/build/utils/access/types.js.map +1 -0
- package/build/utils/access.d.ts +4 -0
- package/build/utils/access.d.ts.map +1 -0
- package/build/utils/access.js +28 -0
- package/build/utils/access.js.map +1 -0
- package/build/utils/guard/types.d.ts +21 -0
- package/build/utils/guard/types.d.ts.map +1 -0
- package/build/utils/guard/types.js +2 -0
- package/build/utils/guard/types.js.map +1 -0
- package/build/utils/guard.d.ts +7 -10
- package/build/utils/guard.d.ts.map +1 -1
- package/build/utils/guard.js +33 -31
- package/build/utils/guard.js.map +1 -1
- package/build/utils/handler/types.d.ts +26 -0
- package/build/utils/handler/types.d.ts.map +1 -0
- package/build/utils/handler/types.js +2 -0
- package/build/utils/handler/types.js.map +1 -0
- package/build/utils/handler.d.ts +6 -0
- package/build/utils/handler.d.ts.map +1 -0
- package/build/utils/handler.js +21 -0
- package/build/utils/handler.js.map +1 -0
- package/build/utils/index.d.ts +8 -1
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +3 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/request/types.d.ts +23 -0
- package/build/utils/request/types.d.ts.map +1 -0
- package/build/utils/request/types.js +2 -0
- package/build/utils/request/types.js.map +1 -0
- package/build/utils/request.d.ts +4 -0
- package/build/utils/request.d.ts.map +1 -0
- package/build/utils/request.js +27 -0
- package/build/utils/request.js.map +1 -0
- package/build/utils/scope.d.ts +10 -43
- package/build/utils/scope.d.ts.map +1 -1
- package/build/utils/scope.js +21 -99
- package/build/utils/scope.js.map +1 -1
- package/package.json +18 -18
- package/src/actions/apply-schema-request.ts +35 -0
- package/src/actions/commit.ts +7 -5
- package/src/actions/define-schemas.ts +34 -0
- package/src/actions/execute.ts +30 -115
- package/src/actions/execution/types.ts +34 -0
- package/src/actions/execution.ts +96 -0
- package/src/actions/get-card.ts +14 -0
- package/src/actions/get-specification.ts +14 -0
- package/src/actions/index.ts +13 -3
- package/src/actions/links.ts +8 -6
- package/src/actions/list-card-specifications.ts +15 -0
- package/src/actions/list-card-transitions.ts +15 -0
- package/src/actions/list-cards.ts +14 -0
- package/src/actions/list-schemas.ts +29 -0
- package/src/actions/{specs.ts → list-specification-revisions.ts} +4 -11
- package/src/actions/summarize-cards.ts +15 -0
- package/src/actions/transitions.ts +6 -4
- package/src/actions/types.ts +4 -3
- package/src/actions/watch.ts +4 -3
- package/src/conformance/assert/types.ts +16 -0
- package/src/conformance/assert.ts +49 -41
- package/src/conformance/cases.ts +88 -67
- package/src/conformance/clock.ts +11 -0
- package/src/conformance/consts.ts +87 -1
- package/src/conformance/errors.ts +7 -0
- package/src/conformance/fixtures/types.ts +11 -0
- package/src/conformance/fixtures.ts +32 -107
- package/src/conformance/index.ts +3 -0
- package/src/definitions.ts +9 -9
- package/src/executor/changes/types.ts +38 -0
- package/src/executor/changes.ts +48 -63
- package/src/executor/code/types.ts +23 -0
- package/src/executor/code.ts +60 -61
- package/src/executor/creator/types.ts +33 -0
- package/src/executor/creator.ts +33 -41
- package/src/executor/resolve/types.ts +30 -0
- package/src/executor/resolve.ts +153 -167
- package/src/executor/types.ts +0 -3
- package/src/executor/validate.ts +17 -16
- package/src/executor.ts +20 -17
- package/src/facade.ts +16 -16
- package/src/helper.ts +3 -8
- package/src/projection.ts +8 -4
- package/src/queue/declare.ts +7 -0
- package/src/queue/index.ts +1 -8
- package/src/store/composite.ts +2 -2
- package/src/store/fold/types.ts +44 -0
- package/src/store/fold.ts +209 -212
- package/src/store/index.ts +1 -0
- package/src/store/memory.ts +0 -0
- package/src/store/types.local.ts +1 -1
- package/src/types.ts +1 -1
- package/src/utils/access/types.ts +30 -0
- package/src/utils/access.ts +33 -0
- package/src/utils/guard/types.ts +19 -0
- package/src/utils/guard.ts +41 -30
- package/src/utils/handler/types.ts +28 -0
- package/src/utils/handler.ts +35 -0
- package/src/utils/index.ts +9 -1
- package/src/utils/request/types.ts +23 -0
- package/src/utils/request.ts +40 -0
- package/src/utils/scope.ts +25 -108
- package/tests/creator.spec.ts +18 -18
- package/src/actions/cards.ts +0 -48
- package/src/actions/schemas.ts +0 -87
package/src/utils/guard.ts
CHANGED
|
@@ -1,40 +1,51 @@
|
|
|
1
1
|
import { PlanningScopeMismatch, WorkcardNotFound } from '@owlmeans/planning'
|
|
2
2
|
import type { Workcard } from '@owlmeans/planning'
|
|
3
|
+
import type { GuardHelper } from './guard/types.js'
|
|
3
4
|
|
|
4
|
-
export const
|
|
5
|
+
export const createGuardHelper = (): GuardHelper => {
|
|
6
|
+
const notFoundOf = (id: string): WorkcardNotFound => new WorkcardNotFound(id)
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
): T => {
|
|
14
|
-
if (record == null || record.entityId !== scope.entityId) {
|
|
15
|
-
throw notFoundOf(id)
|
|
8
|
+
const assertScope = <T extends Pick<Workcard, 'entityId'>>(
|
|
9
|
+
record: T | null | undefined, scope: { entityId: string }, id: string
|
|
10
|
+
): T => {
|
|
11
|
+
if (record == null || record.entityId !== scope.entityId) {
|
|
12
|
+
throw notFoundOf(id)
|
|
13
|
+
}
|
|
14
|
+
return record
|
|
16
15
|
}
|
|
17
|
-
return record
|
|
18
|
-
}
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|| (error as { type?: string } | null)?.type === PlanningScopeMismatch.typeName) {
|
|
30
|
-
throw new WorkcardNotFound()
|
|
17
|
+
const concealed = async <T>(run: () => Promise<T>): Promise<T> => {
|
|
18
|
+
try {
|
|
19
|
+
return await run()
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error instanceof PlanningScopeMismatch
|
|
22
|
+
|| (error as { type?: string } | null)?.type === PlanningScopeMismatch.typeName) {
|
|
23
|
+
throw new WorkcardNotFound()
|
|
24
|
+
}
|
|
25
|
+
throw error
|
|
31
26
|
}
|
|
32
|
-
throw error
|
|
33
27
|
}
|
|
34
|
-
}
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
const clampSeconds = (value: unknown, max: number): number => {
|
|
30
|
+
const number = typeof value === 'number' ? value : Number(value ?? 0)
|
|
31
|
+
return Number.isFinite(number) ? Math.min(Math.max(number, 0), max) : 0
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { notFoundOf, assertScope, concealed, clampSeconds }
|
|
40
35
|
}
|
|
36
|
+
|
|
37
|
+
export const guardHelper = createGuardHelper()
|
|
38
|
+
|
|
39
|
+
/** @deprecated compat:factory-refactor — use `guardHelper.notFoundOf(…)` */
|
|
40
|
+
export const notFoundOf = (id: string): WorkcardNotFound => guardHelper.notFoundOf(id)
|
|
41
|
+
|
|
42
|
+
/** @deprecated compat:factory-refactor — use `guardHelper.assertScope(…)` */
|
|
43
|
+
export const assertScope = <T extends Pick<Workcard, 'entityId'>>(
|
|
44
|
+
record: T | null | undefined, scope: { entityId: string }, id: string
|
|
45
|
+
): T => guardHelper.assertScope<T>(record, scope, id)
|
|
46
|
+
|
|
47
|
+
/** @deprecated compat:factory-refactor — use `guardHelper.concealed(…)` */
|
|
48
|
+
export const concealed = async <T>(run: () => Promise<T>): Promise<T> => await guardHelper.concealed<T>(run)
|
|
49
|
+
|
|
50
|
+
/** @deprecated compat:factory-refactor — use `guardHelper.clampSeconds(…)` */
|
|
51
|
+
export const clampSeconds = (value: unknown, max: number): number => guardHelper.clampSeconds(value, max)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
2
|
+
import type { PlanningFacade, PlanningScope, PlanningService } from '@owlmeans/planning'
|
|
3
|
+
import type { PlanningHandlerOptions } from '../../types.js'
|
|
4
|
+
import type { HandlerScope } from '../types.js'
|
|
5
|
+
|
|
6
|
+
/** The planning service of one context, as a request handler reaches it. */
|
|
7
|
+
export interface PlanningHandlerHelper {
|
|
8
|
+
planningServiceOf: (opts?: Pick<PlanningHandlerOptions, 'service'>) => PlanningService
|
|
9
|
+
/**
|
|
10
|
+
* The facade a handler works through, and the access it was resolved under.
|
|
11
|
+
*
|
|
12
|
+
* Without `opts.access` it is exactly the request's scope plus what `opts.scope` derives. With it,
|
|
13
|
+
* the resolver names the organization and the visible projects; a throw from the resolver is the
|
|
14
|
+
* request's error.
|
|
15
|
+
*/
|
|
16
|
+
handlerScopeOf: (req: AbstractRequest, opts?: PlanningHandlerOptions) => Promise<HandlerScope>
|
|
17
|
+
/** The facade a handler works through: the request's scope plus what `opts.scope` derives. */
|
|
18
|
+
handlerFacade: (req: AbstractRequest, opts?: PlanningHandlerOptions) => Promise<PlanningFacade>
|
|
19
|
+
/**
|
|
20
|
+
* The facade a hand-written handler works through: the request's entity and subject, plus `extra`
|
|
21
|
+
* (a `channel`) — never an entity from the request body.
|
|
22
|
+
*
|
|
23
|
+
* @throws {AuthorizationError} when the request carries no organization
|
|
24
|
+
*/
|
|
25
|
+
planningFor: (
|
|
26
|
+
req: AbstractRequest, extra?: Partial<PlanningScope>, opts?: Pick<PlanningHandlerOptions, 'service'>
|
|
27
|
+
) => PlanningFacade
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { memoHelper, type BasicConfig, type BasicContext } from '@owlmeans/context'
|
|
2
|
+
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
3
|
+
import { PLANNING_SERVICE, type PlanningFacade, type PlanningScope, type PlanningService } from '@owlmeans/planning'
|
|
4
|
+
import type { PlanningHandlerOptions, PlanningHostService } from '../types.js'
|
|
5
|
+
import type { PlanningHandlerHelper } from './handler/types.js'
|
|
6
|
+
import { makeRequestScope } from './request.js'
|
|
7
|
+
import type { HandlerScope } from './types.js'
|
|
8
|
+
|
|
9
|
+
export const makePlanningHandlerHelper = (ctx: BasicContext<BasicConfig>): PlanningHandlerHelper => {
|
|
10
|
+
const planningServiceOf = (opts?: Pick<PlanningHandlerOptions, 'service'>): PlanningService =>
|
|
11
|
+
ctx.service<PlanningHostService>(opts?.service ?? PLANNING_SERVICE)
|
|
12
|
+
|
|
13
|
+
const handlerScopeOf = async (req: AbstractRequest, opts?: PlanningHandlerOptions): Promise<HandlerScope> => {
|
|
14
|
+
const planningService = planningServiceOf(opts)
|
|
15
|
+
const extra = await opts?.scope?.(req, ctx)
|
|
16
|
+
if (opts?.access == null) {
|
|
17
|
+
return { facade: planningService.for(makeRequestScope(req).scopeOf(extra ?? undefined)) }
|
|
18
|
+
}
|
|
19
|
+
const access = await opts.access(req, ctx)
|
|
20
|
+
|
|
21
|
+
return { facade: planningService.for(makeRequestScope(req).accessScopeOf(access, extra ?? undefined)), access }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const handlerFacade = async (req: AbstractRequest, opts?: PlanningHandlerOptions): Promise<PlanningFacade> =>
|
|
25
|
+
(await handlerScopeOf(req, opts)).facade
|
|
26
|
+
|
|
27
|
+
const planningFor = (
|
|
28
|
+
req: AbstractRequest, extra?: Partial<PlanningScope>, opts?: Pick<PlanningHandlerOptions, 'service'>
|
|
29
|
+
): PlanningFacade => planningServiceOf(opts).for(makeRequestScope(req).scopeOf(extra))
|
|
30
|
+
|
|
31
|
+
return { planningServiceOf, handlerScopeOf, handlerFacade, planningFor }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The handler helper of a context — one per context. */
|
|
35
|
+
export const planningHandlerOf = memoHelper.oncePer(makePlanningHandlerHelper)
|
package/src/utils/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
export type * from './types.js'
|
|
2
|
+
export type * from './access/types.js'
|
|
3
|
+
export type * from './guard/types.js'
|
|
4
|
+
export type * from './handler/types.js'
|
|
5
|
+
export type * from './request/types.js'
|
|
6
|
+
|
|
7
|
+
export * from './access.js'
|
|
1
8
|
export * from './guard.js'
|
|
9
|
+
export * from './handler.js'
|
|
10
|
+
export * from './request.js'
|
|
2
11
|
export * from './scope.js'
|
|
3
|
-
export type * from './types.js'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PlanningScope, TransitionActor } from '@owlmeans/planning'
|
|
2
|
+
import type { PlanningAccess } from '../../types.js'
|
|
3
|
+
|
|
4
|
+
/** One request's identity as planning reads it — built at the top of a handler, never stored. */
|
|
5
|
+
export interface RequestScope {
|
|
6
|
+
/** The authenticated subject of a request, as a transition names it. */
|
|
7
|
+
actorOf: () => TransitionActor
|
|
8
|
+
/**
|
|
9
|
+
* The scope a request reads and writes as.
|
|
10
|
+
*
|
|
11
|
+
* `entityId` is `requireEntityKey(req)` — the resolved organization id, never a value from the
|
|
12
|
+
* body or the query — and `extra` cannot replace it; `extra` adds what the deployment derives
|
|
13
|
+
* (a `channel`, a `service`). The actor is the authenticated subject plus that channel.
|
|
14
|
+
*
|
|
15
|
+
* @throws {AuthorizationError} when the request carries no organization
|
|
16
|
+
*/
|
|
17
|
+
scopeOf: (extra?: Partial<PlanningScope>) => PlanningScope
|
|
18
|
+
/**
|
|
19
|
+
* The scope of a request under a resolved {@link PlanningAccess}: the organization is the
|
|
20
|
+
* resolver's — never the token's, never `extra`'s — and `projects` narrows it.
|
|
21
|
+
*/
|
|
22
|
+
accessScopeOf: (access: PlanningAccess, extra?: Partial<PlanningScope>) => PlanningScope
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
|
|
2
|
+
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
3
|
+
import type { PlanningScope, TransitionActor } from '@owlmeans/planning'
|
|
4
|
+
import type { PlanningAccess } from '../types.js'
|
|
5
|
+
import type { RequestScope } from './request/types.js'
|
|
6
|
+
import { makeEntityScope } from '@owlmeans/auth-common'
|
|
7
|
+
|
|
8
|
+
const clean = <T extends object>(record: T): T =>
|
|
9
|
+
Object.fromEntries(Object.entries(record).filter(([, value]) => value != null && value !== '')) as T
|
|
10
|
+
|
|
11
|
+
export const makeRequestScope = (req: AbstractRequest): RequestScope => {
|
|
12
|
+
const actorOf = (): TransitionActor => clean({
|
|
13
|
+
profileId: req.auth?.profileId,
|
|
14
|
+
userId: req.auth?.userId,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const scopeWith = (entityId: string, extra?: Partial<PlanningScope>): PlanningScope => {
|
|
18
|
+
const subject = actorOf()
|
|
19
|
+
const channel = extra?.channel ?? extra?.actor?.channel
|
|
20
|
+
|
|
21
|
+
return clean({
|
|
22
|
+
...extra,
|
|
23
|
+
...subject,
|
|
24
|
+
entityId,
|
|
25
|
+
channel,
|
|
26
|
+
actor: clean({ ...extra?.actor, ...subject, channel }),
|
|
27
|
+
}) as PlanningScope
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const scopeOf = (extra?: Partial<PlanningScope>): PlanningScope => scopeWith(makeEntityScope(req).requireEntityKey(), extra)
|
|
31
|
+
|
|
32
|
+
const accessScopeOf = (access: PlanningAccess, extra?: Partial<PlanningScope>): PlanningScope => {
|
|
33
|
+
const { projects: _projects, entityId: _entityId, ...rest } = extra ?? {}
|
|
34
|
+
const scope = scopeWith(access.entityId, rest)
|
|
35
|
+
|
|
36
|
+
return access.projects != null ? { ...scope, projects: [...access.projects] } : scope
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { actorOf, scopeOf, accessScopeOf }
|
|
40
|
+
}
|
package/src/utils/scope.ts
CHANGED
|
@@ -1,131 +1,48 @@
|
|
|
1
|
-
import { requireEntityKey } from '@owlmeans/auth-common'
|
|
2
1
|
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
2
|
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
} from '
|
|
3
|
+
import type { PlanningFacade, PlanningScope, PlanningService, TransitionActor, Workcard } from '@owlmeans/planning'
|
|
4
|
+
import type { PlanningAccess, PlanningAccessGrants, PlanningHandlerOptions } from '../types.js'
|
|
5
|
+
import { makePlanningAccessModel } from './access.js'
|
|
6
|
+
import { planningHandlerOf } from './handler.js'
|
|
7
|
+
import { makeRequestScope } from './request.js'
|
|
8
8
|
import type { HandlerScope } from './types.js'
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/** @deprecated compat:factory-refactor — use `makeRequestScope(req).actorOf()` */
|
|
11
|
+
export const actorOf = (req: AbstractRequest): TransitionActor => makeRequestScope(req).actorOf()
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
export const actorOf = (req: AbstractRequest): TransitionActor => clean({
|
|
15
|
-
profileId: req.auth?.profileId,
|
|
16
|
-
userId: req.auth?.userId,
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
const scopeWith = (req: AbstractRequest, entityId: string, extra?: Partial<PlanningScope>): PlanningScope => {
|
|
20
|
-
const subject = actorOf(req)
|
|
21
|
-
const channel = extra?.channel ?? extra?.actor?.channel
|
|
22
|
-
|
|
23
|
-
return clean({
|
|
24
|
-
...extra,
|
|
25
|
-
...subject,
|
|
26
|
-
entityId,
|
|
27
|
-
channel,
|
|
28
|
-
actor: clean({ ...extra?.actor, ...subject, channel }),
|
|
29
|
-
}) as PlanningScope
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The scope a request reads and writes as.
|
|
34
|
-
*
|
|
35
|
-
* `entityId` is `requireEntityKey(req)` — the resolved organization id, never a value from the
|
|
36
|
-
* body or the query — and `extra` cannot replace it; `extra` adds what the deployment derives
|
|
37
|
-
* (a `channel`, a `service`). The actor is the authenticated subject plus that channel.
|
|
38
|
-
*
|
|
39
|
-
* @throws {AuthorizationError} when the request carries no organization
|
|
40
|
-
*/
|
|
13
|
+
/** @deprecated compat:factory-refactor — use `makeRequestScope(req).scopeOf(…)` */
|
|
41
14
|
export const scopeOf = (req: AbstractRequest, extra?: Partial<PlanningScope>): PlanningScope =>
|
|
42
|
-
|
|
15
|
+
makeRequestScope(req).scopeOf(extra)
|
|
43
16
|
|
|
44
|
-
/**
|
|
45
|
-
* The scope of a request under a resolved {@link PlanningAccess}: the organization is the
|
|
46
|
-
* resolver's — never the token's, never `extra`'s — and `projects` narrows it.
|
|
47
|
-
*/
|
|
17
|
+
/** @deprecated compat:factory-refactor — use `makeRequestScope(req).accessScopeOf(…)` */
|
|
48
18
|
export const accessScopeOf = (
|
|
49
19
|
req: AbstractRequest, access: PlanningAccess, extra?: Partial<PlanningScope>
|
|
50
|
-
): PlanningScope =>
|
|
51
|
-
const { projects: _projects, entityId: _entityId, ...rest } = extra ?? {}
|
|
52
|
-
const scope = scopeWith(req, access.entityId, rest)
|
|
53
|
-
|
|
54
|
-
return access.projects != null ? { ...scope, projects: [...access.projects] } : scope
|
|
55
|
-
}
|
|
20
|
+
): PlanningScope => makeRequestScope(req).accessScopeOf(access, extra)
|
|
56
21
|
|
|
22
|
+
/** @deprecated compat:factory-refactor — use `planningHandlerOf(ctx).planningServiceOf(…)` */
|
|
57
23
|
export const planningServiceOf = (
|
|
58
24
|
ctx: BasicContext<BasicConfig>, opts?: Pick<PlanningHandlerOptions, 'service'>
|
|
59
|
-
): PlanningService => ctx.
|
|
25
|
+
): PlanningService => planningHandlerOf(ctx).planningServiceOf(opts)
|
|
60
26
|
|
|
61
|
-
/**
|
|
62
|
-
* The facade a handler works through, and the access it was resolved under.
|
|
63
|
-
*
|
|
64
|
-
* Without `opts.access` it is exactly the request's scope plus what `opts.scope` derives. With it,
|
|
65
|
-
* the resolver names the organization and the visible projects; a throw from the resolver is the
|
|
66
|
-
* request's error.
|
|
67
|
-
*/
|
|
27
|
+
/** @deprecated compat:factory-refactor — use `planningHandlerOf(ctx).handlerScopeOf(…)` */
|
|
68
28
|
export const handlerScopeOf = async (
|
|
69
29
|
ctx: BasicContext<BasicConfig>, req: AbstractRequest, opts?: PlanningHandlerOptions
|
|
70
|
-
): Promise<HandlerScope> =>
|
|
71
|
-
const extra = await opts?.scope?.(req, ctx)
|
|
72
|
-
if (opts?.access == null) {
|
|
73
|
-
return { facade: planningServiceOf(ctx, opts).for(scopeOf(req, extra ?? undefined)) }
|
|
74
|
-
}
|
|
75
|
-
const access = await opts.access(req, ctx)
|
|
76
|
-
|
|
77
|
-
return { facade: planningServiceOf(ctx, opts).for(accessScopeOf(req, access, extra ?? undefined)), access }
|
|
78
|
-
}
|
|
30
|
+
): Promise<HandlerScope> => await planningHandlerOf(ctx).handlerScopeOf(req, opts)
|
|
79
31
|
|
|
80
|
-
/**
|
|
32
|
+
/** @deprecated compat:factory-refactor — use `planningHandlerOf(ctx).handlerFacade(…)` */
|
|
81
33
|
export const handlerFacade = async (
|
|
82
34
|
ctx: BasicContext<BasicConfig>, req: AbstractRequest, opts?: PlanningHandlerOptions
|
|
83
|
-
): Promise<PlanningFacade> =>
|
|
35
|
+
): Promise<PlanningFacade> => await planningHandlerOf(ctx).handlerFacade(req, opts)
|
|
84
36
|
|
|
85
|
-
/**
|
|
86
|
-
* Refuse a write the access does not grant. No access, or access without `grants`, gates nothing;
|
|
87
|
-
* a `grants` object refuses every flag it leaves out. `true` grants everything; a list grants the
|
|
88
|
-
* project ids it names — `target` is the project the write is about (`undefined` for the
|
|
89
|
-
* organization's root, which only `true` reaches).
|
|
90
|
-
*
|
|
91
|
-
* @throws {PlanningForbidden}
|
|
92
|
-
*/
|
|
37
|
+
/** @deprecated compat:factory-refactor — use `makePlanningAccessModel(access).assertGranted(…)` */
|
|
93
38
|
export const assertGranted = (
|
|
94
39
|
access: PlanningAccess | undefined, grant: keyof PlanningAccessGrants, target?: string
|
|
95
|
-
): void =>
|
|
96
|
-
if (access?.grants == null) {
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
const granted = access.grants[grant]
|
|
100
|
-
if (granted === true || (Array.isArray(granted) && target != null && granted.includes(target))) {
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
throw new PlanningForbidden(`${grant}:${target ?? 'root'}`)
|
|
104
|
-
}
|
|
40
|
+
): void => makePlanningAccessModel(access).assertGranted(grant, target)
|
|
105
41
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
* by the caller. No `writes` admits everything.
|
|
110
|
-
*/
|
|
111
|
-
export const writableIn = (access: PlanningAccess | undefined, card: Pick<Workcard, 'id' | 'parents'>): boolean => {
|
|
112
|
-
const writes = access?.writes
|
|
113
|
-
if (writes == null) {
|
|
114
|
-
return true
|
|
115
|
-
}
|
|
116
|
-
return (card.id != null && writes.includes(card.id)) || card.parents.some(parent => writes.includes(parent))
|
|
117
|
-
}
|
|
42
|
+
/** @deprecated compat:factory-refactor — use `makePlanningAccessModel(access).writableIn(…)` */
|
|
43
|
+
export const writableIn = (access: PlanningAccess | undefined, card: Pick<Workcard, 'id' | 'parents'>): boolean =>
|
|
44
|
+
makePlanningAccessModel(access).writableIn(card)
|
|
118
45
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* `writes` never admits).
|
|
123
|
-
*
|
|
124
|
-
* @throws {PlanningForbidden}
|
|
125
|
-
*/
|
|
126
|
-
export const assertWrites = (access: PlanningAccess | undefined, target?: string): void => {
|
|
127
|
-
if (access?.writes == null || (target != null && access.writes.includes(target))) {
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
throw new PlanningForbidden(`writes:${target ?? 'root'}`)
|
|
131
|
-
}
|
|
46
|
+
/** @deprecated compat:factory-refactor — use `makePlanningAccessModel(access).assertWrites(…)` */
|
|
47
|
+
export const assertWrites = (access: PlanningAccess | undefined, target?: string): void =>
|
|
48
|
+
makePlanningAccessModel(access).assertWrites(target)
|
package/tests/creator.spec.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import { CommitState, makePlanningProtocols, PlanningError, TransitionAction, WorkcardKind } from '@owlmeans/planning'
|
|
3
3
|
import type { PlanningFacade, Transition, TransitionExecution, WorkcardChanges, WorkcardDraft } from '@owlmeans/planning'
|
|
4
|
-
import { executePlanning,
|
|
5
|
-
import { assertCreatorFixed, creatorOf, withCreator } from '../src/executor.js'
|
|
4
|
+
import { executePlanning, executionHelper } from '../src/actions/index.js'
|
|
6
5
|
import { createProject, createTask, ENTITY, invoke, makeTestPlanning, PROFILE, PROJECT, session, TASK } from './context.js'
|
|
6
|
+
import { creatorHelper } from '../src/executor.js'
|
|
7
7
|
|
|
8
8
|
const tree = makePlanningProtocols({ base: { alias: 'test:planning' }, guards: 'test-guard' })
|
|
9
9
|
|
|
@@ -101,21 +101,21 @@ describe('@owlmeans/server-planning — a create\'s createdBy', () => {
|
|
|
101
101
|
test('wireExecution still answers the execution the executor will run', () => {
|
|
102
102
|
const body = { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'W', createdBy: 'intruder' }, action: TransitionAction.Create }
|
|
103
103
|
|
|
104
|
-
expect(draftOf(wireExecution(body, { entityId: ENTITY, profileId: PROFILE, userId: 'user-1' })).createdBy).toBe(PROFILE)
|
|
105
|
-
expect(draftOf(wireExecution(body, { entityId: ENTITY, userId: 'user-1' })).createdBy).toBe('user-1')
|
|
106
|
-
expect(draftOf(wireExecution(body, { entityId: ENTITY })).createdBy).toBeUndefined()
|
|
104
|
+
expect(draftOf(executionHelper.wireExecution(body, { entityId: ENTITY, profileId: PROFILE, userId: 'user-1' })).createdBy).toBe(PROFILE)
|
|
105
|
+
expect(draftOf(executionHelper.wireExecution(body, { entityId: ENTITY, userId: 'user-1' })).createdBy).toBe('user-1')
|
|
106
|
+
expect(draftOf(executionHelper.wireExecution(body, { entityId: ENTITY })).createdBy).toBeUndefined()
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
test('creatorOf and withCreator', () => {
|
|
110
|
-
expect(creatorOf({ entityId: ENTITY, profileId: 'p', userId: 'u', actor: { profileId: 'a' } })).toBe('p')
|
|
111
|
-
expect(creatorOf({ entityId: ENTITY, profileId: '', userId: 'u' })).toBe('u')
|
|
112
|
-
expect(creatorOf({ entityId: ENTITY, actor: { userId: 'au', service: 's' } })).toBe('au')
|
|
113
|
-
expect(creatorOf({ entityId: ENTITY, service: 's' })).toBeUndefined()
|
|
110
|
+
expect(creatorHelper.creatorOf({ entityId: ENTITY, profileId: 'p', userId: 'u', actor: { profileId: 'a' } })).toBe('p')
|
|
111
|
+
expect(creatorHelper.creatorOf({ entityId: ENTITY, profileId: '', userId: 'u' })).toBe('u')
|
|
112
|
+
expect(creatorHelper.creatorOf({ entityId: ENTITY, actor: { userId: 'au', service: 's' } })).toBe('au')
|
|
113
|
+
expect(creatorHelper.creatorOf({ entityId: ENTITY, service: 's' })).toBeUndefined()
|
|
114
114
|
|
|
115
115
|
const update: TransitionExecution = { card: 'card-1', action: TransitionAction.Update, changes: { title: 'T' } }
|
|
116
|
-
expect(withCreator(update, { entityId: ENTITY, profileId: 'p' })).toBe(update)
|
|
116
|
+
expect(creatorHelper.withCreator(update, { entityId: ENTITY, profileId: 'p' })).toBe(update)
|
|
117
117
|
const blank: TransitionExecution = { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'T', createdBy: '' }, action: TransitionAction.Create }
|
|
118
|
-
expect(draftOf(withCreator(blank, { entityId: ENTITY, profileId: 'p' })).createdBy).toBe('p')
|
|
118
|
+
expect(draftOf(creatorHelper.withCreator(blank, { entityId: ENTITY, profileId: 'p' })).createdBy).toBe('p')
|
|
119
119
|
})
|
|
120
120
|
})
|
|
121
121
|
|
|
@@ -246,12 +246,12 @@ describe('@owlmeans/server-planning — createdBy never moves after the create',
|
|
|
246
246
|
test('assertCreatorFixed', () => {
|
|
247
247
|
const draft = { kind: WorkcardKind.Project, type: PROJECT, title: 'T', createdBy: 'owner' }
|
|
248
248
|
|
|
249
|
-
expect(() => assertCreatorFixed({ card: draft, action: TransitionAction.Create })).not.toThrow()
|
|
250
|
-
expect(() => assertCreatorFixed({ card: 'c', action: TransitionAction.Update, changes: { title: 'T', createdBy: undefined } })).not.toThrow()
|
|
251
|
-
expect(() => assertCreatorFixed({ card: 'c', action: TransitionAction.Update, unset: ['fields.createdBy'] })).not.toThrow()
|
|
252
|
-
expect(() => assertCreatorFixed({ card: 'c', action: TransitionAction.Update, changes: { fields: { createdBy: 'x' } } })).not.toThrow()
|
|
253
|
-
expect(() => assertCreatorFixed({ card: 'c', action: TransitionAction.Update, changes: { createdBy: 'x' } })).toThrow('planning:immutable:createdBy')
|
|
254
|
-
expect(() => assertCreatorFixed({ card: 'c', action: TransitionAction.Delete, unset: ['createdBy'] })).toThrow('planning:immutable:createdBy')
|
|
255
|
-
expect(() => assertCreatorFixed({ card: draft, action: TransitionAction.Create, changes: { createdBy: 'x' } })).toThrow('planning:immutable:createdBy')
|
|
249
|
+
expect(() => creatorHelper.assertCreatorFixed({ card: draft, action: TransitionAction.Create })).not.toThrow()
|
|
250
|
+
expect(() => creatorHelper.assertCreatorFixed({ card: 'c', action: TransitionAction.Update, changes: { title: 'T', createdBy: undefined } })).not.toThrow()
|
|
251
|
+
expect(() => creatorHelper.assertCreatorFixed({ card: 'c', action: TransitionAction.Update, unset: ['fields.createdBy'] })).not.toThrow()
|
|
252
|
+
expect(() => creatorHelper.assertCreatorFixed({ card: 'c', action: TransitionAction.Update, changes: { fields: { createdBy: 'x' } } })).not.toThrow()
|
|
253
|
+
expect(() => creatorHelper.assertCreatorFixed({ card: 'c', action: TransitionAction.Update, changes: { createdBy: 'x' } })).toThrow('planning:immutable:createdBy')
|
|
254
|
+
expect(() => creatorHelper.assertCreatorFixed({ card: 'c', action: TransitionAction.Delete, unset: ['createdBy'] })).toThrow('planning:immutable:createdBy')
|
|
255
|
+
expect(() => creatorHelper.assertCreatorFixed({ card: draft, action: TransitionAction.Create, changes: { createdBy: 'x' } })).toThrow('planning:immutable:createdBy')
|
|
256
256
|
})
|
|
257
257
|
})
|
package/src/actions/cards.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { handlers } from '@owlmeans/server-api'
|
|
2
|
-
import { decodeSpecificationQuery, decodeSummaryQuery, decodeTransitionQuery, decodeWorkcardQuery, type PlanningProtocols } from '@owlmeans/planning'
|
|
3
|
-
import type { Context, PlanningHandlerOptions } from '../types.js'
|
|
4
|
-
import { concealed, handlerFacade } from '../utils/index.js'
|
|
5
|
-
import type { RequestHandler } from './types.js'
|
|
6
|
-
|
|
7
|
-
/** Cards of the caller's entity; the query arrives in its wire shape and is decoded here. */
|
|
8
|
-
export const listCards = (protocol: PlanningProtocols['card']['list'], opts?: PlanningHandlerOptions): RequestHandler =>
|
|
9
|
-
handlers<Context>().request(protocol, async (req, ctx) => concealed(async () => {
|
|
10
|
-
const facade = await handlerFacade(ctx, req, opts)
|
|
11
|
-
|
|
12
|
-
return await facade.cards.list(decodeWorkcardQuery(req.query))
|
|
13
|
-
}))
|
|
14
|
-
|
|
15
|
-
/** Intrinsic counts of DIRECT children per parent. */
|
|
16
|
-
export const summarizeCards = (protocol: PlanningProtocols['card']['summary'], opts?: PlanningHandlerOptions): RequestHandler =>
|
|
17
|
-
handlers<Context>().request(protocol, async (req, ctx) => concealed(async () => {
|
|
18
|
-
const facade = await handlerFacade(ctx, req, opts)
|
|
19
|
-
const { parents, ...query } = decodeSummaryQuery(req.query)
|
|
20
|
-
|
|
21
|
-
return await facade.cards.summary(parents, query)
|
|
22
|
-
}))
|
|
23
|
-
|
|
24
|
-
/** @throws {WorkcardNotFound} for an absent card and for another entity's alike */
|
|
25
|
-
export const getCard = (protocol: PlanningProtocols['card']['get'], opts?: PlanningHandlerOptions): RequestHandler =>
|
|
26
|
-
handlers<Context>().request(protocol, async (req, ctx) => concealed(async () => {
|
|
27
|
-
const facade = await handlerFacade(ctx, req, opts)
|
|
28
|
-
|
|
29
|
-
return await facade.cards.get(`${req.params.id}`)
|
|
30
|
-
}))
|
|
31
|
-
|
|
32
|
-
/** One card's log. */
|
|
33
|
-
export const listCardTransitions = (protocol: PlanningProtocols['card']['transitions'], opts?: PlanningHandlerOptions): RequestHandler =>
|
|
34
|
-
handlers<Context>().request(protocol, async (req, ctx) => concealed(async () => {
|
|
35
|
-
const facade = await handlerFacade(ctx, req, opts)
|
|
36
|
-
const card = await facade.cards.get(`${req.params.id}`)
|
|
37
|
-
|
|
38
|
-
return await facade.transitions.list({ ...decodeTransitionQuery(req.query), card: card.id })
|
|
39
|
-
}))
|
|
40
|
-
|
|
41
|
-
/** One card's specifications — the current document per category unless `all`. */
|
|
42
|
-
export const listCardSpecifications = (protocol: PlanningProtocols['card']['specifications'], opts?: PlanningHandlerOptions): RequestHandler =>
|
|
43
|
-
handlers<Context>().request(protocol, async (req, ctx) => concealed(async () => {
|
|
44
|
-
const facade = await handlerFacade(ctx, req, opts)
|
|
45
|
-
const card = await facade.cards.get(`${req.params.id}`)
|
|
46
|
-
|
|
47
|
-
return await facade.specifications.list(card.id!, decodeSpecificationQuery(req.query))
|
|
48
|
-
}))
|
package/src/actions/schemas.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { handlers } from '@owlmeans/server-api'
|
|
2
|
-
import { PlanningUnsupported, SchemaWriteMode, type PlanningDefinitions, type PlanningProtocols, type SchemaDefineReply, type SchemaDefineRequest, type ScopedSchemaBundle, type ScopedSchemaRecord } from '@owlmeans/planning'
|
|
3
|
-
import type { Context, PlanningHandlerOptions } from '../types.js'
|
|
4
|
-
import { assertGranted, assertWrites, concealed, handlerScopeOf } from '../utils/index.js'
|
|
5
|
-
import type { RequestHandler } from './types.js'
|
|
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
|
-
*/
|
|
18
|
-
export const listSchemas = (
|
|
19
|
-
protocol: PlanningProtocols['schema']['list'], opts?: PlanningHandlerOptions
|
|
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)
|
|
85
|
-
|
|
86
|
-
return await applySchemaRequest(facade.definitions, request)
|
|
87
|
-
}))
|