@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/store/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CommitEvent, CommitSource, CommitStatus, PlanningStore, ProjectionStore, Relationship,
|
|
3
|
-
RelationshipStore, SpecificationStore, Transition, TransitionStore,
|
|
3
|
+
RelationshipStore, SchemaStore, ScopedSchemaRecord, SpecificationStore, Transition, TransitionStore,
|
|
4
|
+
Workcard,
|
|
4
5
|
} from '@owlmeans/planning'
|
|
5
6
|
|
|
6
7
|
/** What a folding store calls for every committed transition — the service's `committed`. */
|
|
@@ -49,6 +50,11 @@ export interface FoldOptions {
|
|
|
49
50
|
limit?: number
|
|
50
51
|
/** Called between transitions — a queue processor renews its lock here. */
|
|
51
52
|
touch?: () => Promise<void>
|
|
53
|
+
/**
|
|
54
|
+
* Runs one transition's store writes as a unit — a transactional store opens a savepoint here, so
|
|
55
|
+
* a write the database refuses leaves nothing of that transition behind and the fold goes past it.
|
|
56
|
+
*/
|
|
57
|
+
unit?: <R>(run: () => Promise<R>) => Promise<R>
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
export interface FoldResult {
|
|
@@ -63,6 +69,8 @@ export interface MemoryPlanningStoreSeed {
|
|
|
63
69
|
cards?: Workcard[]
|
|
64
70
|
links?: Relationship[]
|
|
65
71
|
transitions?: Transition[]
|
|
72
|
+
/** Data-defined type and flow records — kept only by a store built with `schemas: true`. */
|
|
73
|
+
schemas?: ScopedSchemaRecord[]
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
export interface MemoryPlanningStoreOptions {
|
|
@@ -74,6 +82,11 @@ export interface MemoryPlanningStoreOptions {
|
|
|
74
82
|
/** The commit listener used until a service binds its own. */
|
|
75
83
|
onCommitted?: CommitListener
|
|
76
84
|
alias?: string
|
|
85
|
+
/**
|
|
86
|
+
* Hold data-defined types and flows (the `schemas` port). Off by default: a store without it
|
|
87
|
+
* resolves every type and flow from the code registry, exactly as before.
|
|
88
|
+
*/
|
|
89
|
+
schemas?: boolean
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
export interface MemoryPlanningStore extends BindablePlanningStore {
|
|
@@ -82,6 +95,8 @@ export interface MemoryPlanningStore extends BindablePlanningStore {
|
|
|
82
95
|
specs: SpecificationStore
|
|
83
96
|
links: RelationshipStore
|
|
84
97
|
commits: CommitHub
|
|
98
|
+
/** Present when the store was built with `schemas: true`. */
|
|
99
|
+
schemas?: SchemaStore
|
|
85
100
|
bind: (committed: CommitListener) => void
|
|
86
101
|
/** Fold what is pending — for one card, or for every card with a pending transition. */
|
|
87
102
|
flush: (card?: string) => Promise<void>
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { BasicConfig, BasicContext, LazyService } from '@owlmeans/context'
|
|
2
2
|
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
3
|
-
import type {
|
|
4
|
-
AnyTypeSchema, PlanningPlugin, PlanningScope, PlanningService, PlanningStore, StatusFlowSchema,
|
|
5
|
-
WithPlanningService,
|
|
6
|
-
} from '@owlmeans/planning'
|
|
3
|
+
import type { AnyTypeSchema, PlanningPlugin, PlanningScope, PlanningService, PlanningStore, StatusFlowSchema, WithPlanningService, PlanningSchemaRegistry, SchemaStore, ScopedSchemaRegistry, CommitEvent, PlanningExecContext, PlanningHookContext, TransitionExecution, WorkcardDraft } from '@owlmeans/planning'
|
|
7
4
|
import type { ApiServerAppend } from '@owlmeans/server-api'
|
|
8
5
|
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
6
|
+
import type { StoreRoute } from './store/types.js'
|
|
7
|
+
|
|
9
8
|
|
|
10
9
|
export interface Config extends ServerConfig { }
|
|
11
10
|
|
|
@@ -37,6 +36,51 @@ export interface PlanningScopeExtractor {
|
|
|
37
36
|
(req: AbstractRequest, ctx: BasicContext<BasicConfig>): Partial<PlanningScope> | Promise<Partial<PlanningScope>>
|
|
38
37
|
}
|
|
39
38
|
|
|
39
|
+
/**
|
|
40
|
+
* A grant: `true` for everything the request can see, a list for those project cards only (the
|
|
41
|
+
* parent of a created project, the deleted project, the schema layer).
|
|
42
|
+
*/
|
|
43
|
+
export type PlanningGrant = boolean | string[]
|
|
44
|
+
|
|
45
|
+
export interface PlanningAccessGrants {
|
|
46
|
+
/** Create a project — at the root only with `true`; a list names the parent projects. */
|
|
47
|
+
createProjects?: PlanningGrant
|
|
48
|
+
/** Delete a project — a list names the projects. */
|
|
49
|
+
deleteProjects?: PlanningGrant
|
|
50
|
+
/** Write data-defined types and flows — the organization-wide layer only with `true`. */
|
|
51
|
+
defineSchemas?: PlanningGrant
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What a hosting application decides about one request. */
|
|
55
|
+
export interface PlanningAccess {
|
|
56
|
+
/** The organization the request acts in — the only source of it once a resolver is given. */
|
|
57
|
+
entityId: string
|
|
58
|
+
/** Narrows every read and write to these project cards. Every project when omitted. */
|
|
59
|
+
projects?: string[]
|
|
60
|
+
/**
|
|
61
|
+
* The project cards the request may WRITE in: `execute` on a card, specification or link inside
|
|
62
|
+
* one of them (the same rule a narrowed read admits — the project, the cards whose `parents` name
|
|
63
|
+
* it, a specification through its parent card), and `schema.define` of a project layer. A project
|
|
64
|
+
* outside `projects` is still unreachable. Omitted — writes reach every project `projects` admits;
|
|
65
|
+
* present — anything else is refused with `PlanningForbidden` (403). Creating a project is
|
|
66
|
+
* `grants.createProjects`' alone; the organization-wide schema layer is `grants.defineSchemas`'.
|
|
67
|
+
*/
|
|
68
|
+
writes?: string[]
|
|
69
|
+
/**
|
|
70
|
+
* Gates the writes above. Omitted — nothing is gated; present — a flag it leaves out is refused
|
|
71
|
+
* with `PlanningForbidden`.
|
|
72
|
+
*/
|
|
73
|
+
grants?: PlanningAccessGrants
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Resolves a request's access. A throw is the request's answer (an `AuthForbidden` answers 403
|
|
78
|
+
* like any other guard refusal).
|
|
79
|
+
*/
|
|
80
|
+
export interface PlanningAccessResolver {
|
|
81
|
+
(req: AbstractRequest, ctx: BasicContext<BasicConfig>): Promise<PlanningAccess>
|
|
82
|
+
}
|
|
83
|
+
|
|
40
84
|
export interface PlanningHandlerOptions {
|
|
41
85
|
/** The planning service alias. `PLANNING_SERVICE` when omitted. */
|
|
42
86
|
service?: string
|
|
@@ -45,4 +89,82 @@ export interface PlanningHandlerOptions {
|
|
|
45
89
|
/** The longest single long poll, in seconds — clamps `commit.get`'s `wait` and `execute`'s `timeout`. */
|
|
46
90
|
maxPoll?: number
|
|
47
91
|
scope?: PlanningScopeExtractor
|
|
92
|
+
/**
|
|
93
|
+
* The hosting application's access decision per request. Without it the organization is the
|
|
94
|
+
* request's own (`requireEntityKey`) and nothing is narrowed or gated.
|
|
95
|
+
*/
|
|
96
|
+
access?: PlanningAccessResolver
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface SchemaViewsOptions {
|
|
100
|
+
/** The code registry every layer starts from. */
|
|
101
|
+
code: () => PlanningSchemaRegistry
|
|
102
|
+
/** Bumps whenever the code registry changes (a plugin's `use`). */
|
|
103
|
+
version: () => number
|
|
104
|
+
/** The schema port, when the store has one. */
|
|
105
|
+
port: () => SchemaStore | undefined
|
|
106
|
+
/** How many resolved layers are kept. */
|
|
107
|
+
limit?: number
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface SchemaViews {
|
|
111
|
+
/** The code registry itself without a port; the resolved layer with one. */
|
|
112
|
+
of: (entityId: string, project?: string) => Promise<PlanningSchemaRegistry>
|
|
113
|
+
/** @throws {PlanningUnsupported} without a port */
|
|
114
|
+
scoped: (entityId: string, project?: string) => Promise<ScopedSchemaRegistry>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ProjectionOptions {
|
|
118
|
+
/** The planning service alias. */
|
|
119
|
+
service?: string
|
|
120
|
+
/** The store the queue folds — the service's default store when omitted. */
|
|
121
|
+
store?: (service: PlanningService) => PlanningStore
|
|
122
|
+
/** Where events go — the store's commit hub `publish` when it has one. */
|
|
123
|
+
publish?: (event: CommitEvent) => Promise<void>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface PluginRegistry {
|
|
127
|
+
/** Register, or replace the plugin of the same `name`. Its schemas are contributed now. */
|
|
128
|
+
use: (plugin: PlanningPlugin) => void
|
|
129
|
+
/** Every plugin, `order` ascending (registration order among equals). */
|
|
130
|
+
plugins: () => PlanningPlugin[]
|
|
131
|
+
/** The owning plugins that supply a store, in plugin order, each store resolved once. */
|
|
132
|
+
routes: (ctx?: BasicContext<BasicConfig>) => StoreRoute[]
|
|
133
|
+
/** The first owning plugin's store for a type, else `fallback`. */
|
|
134
|
+
storeFor: (type: string | undefined, fallback: PlanningStore, ctx?: BasicContext<BasicConfig>) => PlanningStore
|
|
135
|
+
/** The first plugin answering a code wins. */
|
|
136
|
+
mintCode: (
|
|
137
|
+
draft: WorkcardDraft, taken: (code: string) => Promise<boolean>, contextOf: (plugin: PlanningPlugin) => PlanningExecContext
|
|
138
|
+
) => Promise<string | undefined>
|
|
139
|
+
/** The `before` chain; answers the execution the chain left. */
|
|
140
|
+
before: (
|
|
141
|
+
exec: TransitionExecution, contextOf: (plugin: PlanningPlugin) => PlanningExecContext
|
|
142
|
+
) => Promise<TransitionExecution>
|
|
143
|
+
/** The `after` chain; a failing hook is logged and the chain goes on. */
|
|
144
|
+
after: (event: CommitEvent, contextOf: (plugin: PlanningPlugin) => PlanningHookContext) => Promise<void>
|
|
145
|
+
/** Bumps on every `use` — what a cached composite store is keyed by. */
|
|
146
|
+
version: () => number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type PlanningServiceApi = Pick<PlanningService, 'use' | 'plugins' | 'schemas' | 'store' | 'for' | 'committed'>
|
|
150
|
+
|
|
151
|
+
/** What the facade and the executor reach the service through. */
|
|
152
|
+
export interface PlanningRuntime {
|
|
153
|
+
service: () => PlanningService
|
|
154
|
+
registry: PluginRegistry
|
|
155
|
+
options: PlanningServiceOptions
|
|
156
|
+
context: () => BasicContext<BasicConfig> | undefined
|
|
157
|
+
/** The composite store reads and commits go through. */
|
|
158
|
+
reader: () => PlanningStore
|
|
159
|
+
/** Every distinct store: the default one first, then each plugin's. */
|
|
160
|
+
stores: () => PlanningStore[]
|
|
161
|
+
/** The default store's data-defined schema port, when it has one. */
|
|
162
|
+
schemaStore: () => SchemaStore | undefined
|
|
163
|
+
/**
|
|
164
|
+
* The registry a card of this organization (and project) resolves through. Without a schema
|
|
165
|
+
* port it is the service's code registry itself — the very same object.
|
|
166
|
+
*/
|
|
167
|
+
schemasFor: (entityId: string, project?: string) => Promise<PlanningSchemaRegistry>
|
|
168
|
+
/** The resolved layer. @throws {PlanningUnsupported} without a schema port */
|
|
169
|
+
scopedSchemas: (entityId: string, project?: string) => Promise<ScopedSchemaRegistry>
|
|
48
170
|
}
|
package/src/utils/index.ts
CHANGED
package/src/utils/scope.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { requireEntityKey } from '@owlmeans/auth-common'
|
|
2
2
|
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
3
|
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
4
|
-
import { PLANNING_SERVICE } from '@owlmeans/planning'
|
|
5
|
-
import type {
|
|
6
|
-
|
|
4
|
+
import { PLANNING_SERVICE, PlanningForbidden, type PlanningFacade, type PlanningScope, type PlanningService, type TransitionActor, type Workcard } from '@owlmeans/planning'
|
|
5
|
+
import type {
|
|
6
|
+
PlanningAccess, PlanningAccessGrants, PlanningHandlerOptions, PlanningHostService,
|
|
7
|
+
} from '../types.js'
|
|
8
|
+
import type { HandlerScope } from './types.js'
|
|
7
9
|
|
|
8
10
|
const clean = <T extends object>(record: T): T =>
|
|
9
11
|
Object.fromEntries(Object.entries(record).filter(([, value]) => value != null && value !== '')) as T
|
|
@@ -14,17 +16,7 @@ export const actorOf = (req: AbstractRequest): TransitionActor => clean({
|
|
|
14
16
|
userId: req.auth?.userId,
|
|
15
17
|
})
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
* The scope a request reads and writes as.
|
|
19
|
-
*
|
|
20
|
-
* `entityId` is `requireEntityKey(req)` — the resolved organization id, never a value from the
|
|
21
|
-
* body or the query — and `extra` cannot replace it; `extra` adds what the deployment derives
|
|
22
|
-
* (a `channel`, a `service`). The actor is the authenticated subject plus that channel.
|
|
23
|
-
*
|
|
24
|
-
* @throws {AuthorizationError} when the request carries no organization
|
|
25
|
-
*/
|
|
26
|
-
export const scopeOf = (req: AbstractRequest, extra?: Partial<PlanningScope>): PlanningScope => {
|
|
27
|
-
const entityId = requireEntityKey(req)
|
|
19
|
+
const scopeWith = (req: AbstractRequest, entityId: string, extra?: Partial<PlanningScope>): PlanningScope => {
|
|
28
20
|
const subject = actorOf(req)
|
|
29
21
|
const channel = extra?.channel ?? extra?.actor?.channel
|
|
30
22
|
|
|
@@ -37,15 +29,103 @@ export const scopeOf = (req: AbstractRequest, extra?: Partial<PlanningScope>): P
|
|
|
37
29
|
}) as PlanningScope
|
|
38
30
|
}
|
|
39
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
|
+
*/
|
|
41
|
+
export const scopeOf = (req: AbstractRequest, extra?: Partial<PlanningScope>): PlanningScope =>
|
|
42
|
+
scopeWith(req, requireEntityKey(req), extra)
|
|
43
|
+
|
|
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
|
+
*/
|
|
48
|
+
export const accessScopeOf = (
|
|
49
|
+
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
|
+
}
|
|
56
|
+
|
|
40
57
|
export const planningServiceOf = (
|
|
41
58
|
ctx: BasicContext<BasicConfig>, opts?: Pick<PlanningHandlerOptions, 'service'>
|
|
42
59
|
): PlanningService => ctx.service<PlanningHostService>(opts?.service ?? PLANNING_SERVICE)
|
|
43
60
|
|
|
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
|
+
*/
|
|
68
|
+
export const handlerScopeOf = async (
|
|
69
|
+
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
|
+
}
|
|
79
|
+
|
|
44
80
|
/** The facade a handler works through: the request's scope plus what `opts.scope` derives. */
|
|
45
81
|
export const handlerFacade = async (
|
|
46
82
|
ctx: BasicContext<BasicConfig>, req: AbstractRequest, opts?: PlanningHandlerOptions
|
|
47
|
-
): Promise<PlanningFacade> =>
|
|
48
|
-
|
|
83
|
+
): Promise<PlanningFacade> => (await handlerScopeOf(ctx, req, opts)).facade
|
|
84
|
+
|
|
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
|
+
*/
|
|
93
|
+
export const assertGranted = (
|
|
94
|
+
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
|
+
}
|
|
49
105
|
|
|
50
|
-
|
|
106
|
+
/**
|
|
107
|
+
* A card the access's `writes` admits — the rule a narrowed read admits, over `writes`: a project
|
|
108
|
+
* it names, or a card whose `parents` name one. A specification is admitted through its parent card
|
|
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
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Refuse a write into a project the access's `writes` leaves out. No `writes` refuses nothing;
|
|
121
|
+
* `target` is the project the write is in (`undefined` for the organization's root, which a set
|
|
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'}`)
|
|
51
131
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PlanningFacade } from '@owlmeans/planning'
|
|
2
|
+
import type { PlanningAccess } from '../types.js'
|
|
3
|
+
|
|
4
|
+
export interface HandlerScope {
|
|
5
|
+
facade: PlanningFacade
|
|
6
|
+
/** The resolver's answer, when the handler options carry one. */
|
|
7
|
+
access?: PlanningAccess
|
|
8
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { AuthForbidden } from '@owlmeans/auth'
|
|
3
|
+
import {
|
|
4
|
+
CommitState, makePlanningProtocols, ParentNotFound, PlanningForbidden, PlanningSchemaKind, SchemaOrigin,
|
|
5
|
+
TransitionAction, WorkcardKind, WorkcardNotFound,
|
|
6
|
+
} from '@owlmeans/planning'
|
|
7
|
+
import type { CommitEvent, SchemaDefineReply, ScopedSchemaBundle } from '@owlmeans/planning'
|
|
8
|
+
import {
|
|
9
|
+
defineSchemas, executePlanning, getCard, getCommit, listCards, listLinks, listSchemas, summarizeCards,
|
|
10
|
+
} from '../src/actions/index.js'
|
|
11
|
+
import type { PlanningAccess, PlanningHandlerOptions } from '../src/types.js'
|
|
12
|
+
import {
|
|
13
|
+
createProject, createTask, ENTITY, invoke, makeTestPlanning, OTHER_ENTITY, PROJECT, session, TASK,
|
|
14
|
+
} from './context.js'
|
|
15
|
+
|
|
16
|
+
const tree = makePlanningProtocols({ base: { alias: 'test:planning' }, guards: 'test-guard', definitions: true })
|
|
17
|
+
|
|
18
|
+
const withAccess = (access: PlanningAccess | (() => PlanningAccess)): PlanningHandlerOptions => ({
|
|
19
|
+
access: async () => typeof access === 'function' ? access() : access,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('@owlmeans/server-planning — access resolver', () => {
|
|
23
|
+
test('the organization is the resolver\'s, never the token\'s, and a resolver throw is the answer', async () => {
|
|
24
|
+
const { context, facade } = await makeTestPlanning()
|
|
25
|
+
const theirs = await createProject(facade(OTHER_ENTITY), 'Theirs')
|
|
26
|
+
await createProject(facade(), 'Mine')
|
|
27
|
+
|
|
28
|
+
const listed = await invoke(listCards(tree.card.list, withAccess({ entityId: OTHER_ENTITY })), context, { ...session(ENTITY), query: {} })
|
|
29
|
+
expect(listed.items.map((card: { id: string }) => card.id)).toEqual([theirs.id])
|
|
30
|
+
|
|
31
|
+
const refusing: PlanningHandlerOptions = { access: async () => { throw new AuthForbidden('no-seat') } }
|
|
32
|
+
await expect(invoke(listCards(tree.card.list, refusing), context, { ...session(), query: {} })).rejects.toBeInstanceOf(AuthForbidden)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('projects narrow reads: an invisible project and its cards are absent, lists and summaries shrink', async () => {
|
|
36
|
+
const { context, facade } = await makeTestPlanning()
|
|
37
|
+
const visible = await createProject(facade(), 'Visible')
|
|
38
|
+
const hidden = await createProject(facade(), 'Hidden')
|
|
39
|
+
const seen = (await createTask(facade(), visible.id!, 'Seen')).card!
|
|
40
|
+
const unseen = (await createTask(facade(), hidden.id!, 'Unseen')).card!
|
|
41
|
+
await facade().execute({ card: unseen.id!, action: TransitionAction.Link, link: { type: 'blocks', to: seen.id! } }, { wait: true })
|
|
42
|
+
const opts = withAccess({ entityId: ENTITY, projects: [visible.id!] })
|
|
43
|
+
|
|
44
|
+
await expect(invoke(getCard(tree.card.get, opts), context, { ...session(), params: { id: unseen.id } })).rejects.toBeInstanceOf(WorkcardNotFound)
|
|
45
|
+
await expect(invoke(getCard(tree.card.get, opts), context, { ...session(), params: { id: hidden.id } })).rejects.toBeInstanceOf(WorkcardNotFound)
|
|
46
|
+
expect((await invoke(getCard(tree.card.get, opts), context, { ...session(), params: { id: seen.id } })).id).toBe(seen.id)
|
|
47
|
+
|
|
48
|
+
const listed = await invoke(listCards(tree.card.list, opts), context, { ...session(), query: { type: TASK } })
|
|
49
|
+
expect(listed.items.map((card: { id: string }) => card.id)).toEqual([seen.id])
|
|
50
|
+
const summary = await invoke(summarizeCards(tree.card.summary, opts), context, { ...session(), query: { parents: `${visible.id},${hidden.id}` } })
|
|
51
|
+
expect(Object.keys(summary)).toEqual([visible.id!])
|
|
52
|
+
expect((await invoke(listLinks(tree.link.list, opts), context, { ...session(), query: { to: seen.id } })).total).toBe(0)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('projects narrow writes: an invisible parent is ParentNotFound, an invisible card absent', async () => {
|
|
56
|
+
const { context, facade } = await makeTestPlanning()
|
|
57
|
+
const visible = await createProject(facade(), 'Visible')
|
|
58
|
+
const hidden = await createProject(facade(), 'Hidden')
|
|
59
|
+
const unseen = (await createTask(facade(), hidden.id!, 'Unseen')).card!
|
|
60
|
+
const execute = executePlanning(tree.execute, withAccess({ entityId: ENTITY, projects: [visible.id!] }))
|
|
61
|
+
|
|
62
|
+
await expect(invoke(execute, context, {
|
|
63
|
+
...session(), body: { card: { kind: WorkcardKind.Card, type: TASK, parent: hidden.id, title: 'Sneaky' }, action: TransitionAction.Create },
|
|
64
|
+
})).rejects.toBeInstanceOf(ParentNotFound)
|
|
65
|
+
await expect(invoke(execute, context, {
|
|
66
|
+
...session(), body: { card: unseen.id, action: TransitionAction.Update, changes: { title: 'Renamed' } },
|
|
67
|
+
})).rejects.toBeInstanceOf(WorkcardNotFound)
|
|
68
|
+
|
|
69
|
+
const made = await invoke(execute, context, {
|
|
70
|
+
...session(), body: { card: { kind: WorkcardKind.Card, type: TASK, parent: visible.id, title: 'Allowed' }, action: TransitionAction.Create, wait: true },
|
|
71
|
+
})
|
|
72
|
+
expect(made.card.title).toBe('Allowed')
|
|
73
|
+
expect((await facade().cards.get(unseen.id!)).title).toBe('Unseen')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('commit status and subscriptions stay inside the visible projects', async () => {
|
|
77
|
+
const { context, facade, service } = await makeTestPlanning()
|
|
78
|
+
const visible = await createProject(facade(), 'Visible')
|
|
79
|
+
const hidden = await createProject(facade(), 'Hidden')
|
|
80
|
+
const narrowed = service.for({ entityId: ENTITY, projects: [visible.id!] })
|
|
81
|
+
const heard: CommitEvent[] = []
|
|
82
|
+
const unsubscribe = await narrowed.commits.subscribe(event => { heard.push(event) })
|
|
83
|
+
|
|
84
|
+
const seen = await createTask(facade(), visible.id!, 'Seen')
|
|
85
|
+
const unseen = await createTask(facade(), hidden.id!, 'Unseen')
|
|
86
|
+
unsubscribe()
|
|
87
|
+
|
|
88
|
+
expect(heard.map(event => event.card)).toEqual([seen.card!.id!])
|
|
89
|
+
await expect(invoke(getCommit(tree.commit.get, withAccess({ entityId: ENTITY, projects: [visible.id!] })), context, {
|
|
90
|
+
...session(), params: { transition: unseen.transition.id }, query: {},
|
|
91
|
+
})).rejects.toBeInstanceOf(WorkcardNotFound)
|
|
92
|
+
expect((await narrowed.commits.status(seen.transition.id!)).state).toBe(CommitState.Committed)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('grants gate project creation, project deletion and schema definitions with a 403 refusal', async () => {
|
|
96
|
+
const { context, facade } = await makeTestPlanning({}, { schemas: true })
|
|
97
|
+
const parent = await createProject(facade(), 'Parent')
|
|
98
|
+
const other = await createProject(facade(), 'Other')
|
|
99
|
+
const nested = { kind: WorkcardKind.Project, type: PROJECT, parent: parent.id, title: 'Nested' }
|
|
100
|
+
const scoped = (grants: PlanningAccess['grants']) => withAccess({ entityId: ENTITY, grants })
|
|
101
|
+
|
|
102
|
+
await expect(invoke(executePlanning(tree.execute, scoped({})), context, {
|
|
103
|
+
...session(), body: { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Root' }, action: TransitionAction.Create },
|
|
104
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
105
|
+
expect(PlanningForbidden.httpStatus).toBe(403)
|
|
106
|
+
await expect(invoke(executePlanning(tree.execute, scoped({ createProjects: [parent.id!] })), context, {
|
|
107
|
+
...session(), body: { card: { kind: WorkcardKind.Project, type: PROJECT, title: 'Root' }, action: TransitionAction.Create },
|
|
108
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
109
|
+
const made = await invoke(executePlanning(tree.execute, scoped({ createProjects: [parent.id!] })), context, {
|
|
110
|
+
...session(), body: { card: nested, action: TransitionAction.Create, wait: true },
|
|
111
|
+
})
|
|
112
|
+
expect(made.card.parent).toBe(parent.id)
|
|
113
|
+
// A card is not a project: creating one needs no grant.
|
|
114
|
+
await invoke(executePlanning(tree.execute, scoped({})), context, {
|
|
115
|
+
...session(), body: { card: { kind: WorkcardKind.Card, type: TASK, parent: parent.id, title: 'Task' }, action: TransitionAction.Create },
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await expect(invoke(executePlanning(tree.execute, scoped({ deleteProjects: [other.id!] })), context, {
|
|
119
|
+
...session(), body: { card: parent.id, action: TransitionAction.Delete },
|
|
120
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
121
|
+
await invoke(executePlanning(tree.execute, scoped({ deleteProjects: [other.id!] })), context, {
|
|
122
|
+
...session(), body: { card: other.id, action: TransitionAction.Delete, wait: true },
|
|
123
|
+
})
|
|
124
|
+
expect(await facade().cards.load(other.id!)).toBeNull()
|
|
125
|
+
|
|
126
|
+
const flow = { id: 'test:rota', version: 1, statuses: [{ key: 'due', intrinsic: 'planned' }], transitions: [] }
|
|
127
|
+
await expect(invoke(defineSchemas(tree.schema.define!, scoped({ defineSchemas: [parent.id!] })), context, {
|
|
128
|
+
...session(), body: { flows: [flow] },
|
|
129
|
+
})).rejects.toBeInstanceOf(PlanningForbidden)
|
|
130
|
+
const layer: SchemaDefineReply = await invoke(defineSchemas(tree.schema.define!, scoped({ defineSchemas: [parent.id!] })), context, {
|
|
131
|
+
...session(), body: { project: parent.id, flows: [flow] },
|
|
132
|
+
})
|
|
133
|
+
expect([layer.bundle.origins?.flows['test:rota'], layer.records.map(record => record.key)]).toEqual([SchemaOrigin.Project, ['test:rota']])
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe('@owlmeans/server-planning — schema handlers', () => {
|
|
138
|
+
test('schema.list answers the code bundle without a schema port, and the scoped layer with one', async () => {
|
|
139
|
+
const plain = await makeTestPlanning()
|
|
140
|
+
const bundle = await invoke(listSchemas(tree.schema.list), plain.context, { ...session(), query: {} })
|
|
141
|
+
expect(bundle).toEqual(plain.service.schemas.bundle())
|
|
142
|
+
|
|
143
|
+
const scoped = await makeTestPlanning({}, { schemas: true })
|
|
144
|
+
const project = await createProject(scoped.facade(), 'Layered')
|
|
145
|
+
const note = { type: 'test:memo', kind: WorkcardKind.Card, version: 1, fields: { type: 'object' }, flows: ['test:task'], specifications: [] }
|
|
146
|
+
await invoke(defineSchemas(tree.schema.define!), scoped.context, { ...session(), body: { project: project.id, types: [note] } })
|
|
147
|
+
|
|
148
|
+
const entityLayer: ScopedSchemaBundle = await invoke(listSchemas(tree.schema.list), scoped.context, { ...session(), query: {} })
|
|
149
|
+
const projectLayer: ScopedSchemaBundle = await invoke(listSchemas(tree.schema.list), scoped.context, { ...session(), query: { project: project.id } })
|
|
150
|
+
expect(entityLayer.types.some(type => type.type === 'test:memo')).toBe(false)
|
|
151
|
+
expect(projectLayer.origins?.types['test:memo']).toBe(SchemaOrigin.Project)
|
|
152
|
+
await expect(invoke(listSchemas(tree.schema.list), scoped.context, { ...session(OTHER_ENTITY), query: { project: project.id } }))
|
|
153
|
+
.rejects.toBeInstanceOf(WorkcardNotFound)
|
|
154
|
+
|
|
155
|
+
const retired: SchemaDefineReply = await invoke(defineSchemas(tree.schema.define!), scoped.context, {
|
|
156
|
+
...session(), body: { project: project.id, retire: [{ kind: PlanningSchemaKind.Type, key: 'test:memo' }] },
|
|
157
|
+
})
|
|
158
|
+
expect([retired.bundle.retired?.types, retired.records[0]?.retired]).toEqual([['test:memo'], true])
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, test } from 'bun:test'
|
|
2
|
+
import { AppType, makeBasicContext } from '@owlmeans/context'
|
|
3
|
+
import type { BasicConfig } from '@owlmeans/context'
|
|
4
|
+
import { appendPlanningService } from '../src/service.js'
|
|
5
|
+
import { makeMemoryPlanningStore } from '../src/store/memory.js'
|
|
6
|
+
import {
|
|
7
|
+
conformanceCasesFor, conformanceClock, planningConformance, planningConformancePlugin,
|
|
8
|
+
} from '../src/conformance/index.js'
|
|
9
|
+
import type { ConformanceSubject } from '../src/conformance/index.js'
|
|
10
|
+
|
|
11
|
+
/** The memory store, booted the way a backend boots it, answering every conformance case. */
|
|
12
|
+
const boot = async (schemas: boolean): Promise<ConformanceSubject> => {
|
|
13
|
+
const now = conformanceClock()
|
|
14
|
+
const store = makeMemoryPlanningStore({ now, schemas })
|
|
15
|
+
const cfg: BasicConfig = { ready: false, service: 'planning-conformance', type: AppType.Backend, services: {} }
|
|
16
|
+
const context = appendPlanningService(makeBasicContext(cfg), { store, now, plugins: [planningConformancePlugin] })
|
|
17
|
+
context.configure()
|
|
18
|
+
await context.init()
|
|
19
|
+
const service = context.planning()
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
service,
|
|
23
|
+
store,
|
|
24
|
+
facade: entityId => service.for({ entityId, profileId: 'conformance-librarian', channel: 'conformance' }),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('@owlmeans/server-planning — store conformance (memory, with data-defined schemas)', () => {
|
|
29
|
+
const probe = makeMemoryPlanningStore({ schemas: true })
|
|
30
|
+
for (const entry of conformanceCasesFor(probe)) {
|
|
31
|
+
test(entry.name, async () => { await entry.run(await boot(true)) })
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('@owlmeans/server-planning — store conformance (memory, code schemas only)', () => {
|
|
36
|
+
const probe = makeMemoryPlanningStore()
|
|
37
|
+
const cases = conformanceCasesFor(probe)
|
|
38
|
+
test('a store without the schema port qualifies for every case but the data-defined ones', () => {
|
|
39
|
+
const skipped = planningConformance.filter(entry => !cases.includes(entry))
|
|
40
|
+
if (skipped.some(entry => !(entry.needs ?? []).includes('schemas')) || skipped.length === 0) {
|
|
41
|
+
throw new Error('only the data-defined cases are skipped')
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
for (const entry of cases) {
|
|
45
|
+
test(entry.name, async () => { await entry.run(await boot(false)) })
|
|
46
|
+
}
|
|
47
|
+
})
|