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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +40 -12
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-planning/SKILL.md +193 -18
  4. package/build/actions/execute.d.ts +24 -3
  5. package/build/actions/execute.d.ts.map +1 -1
  6. package/build/actions/execute.js +82 -8
  7. package/build/actions/execute.js.map +1 -1
  8. package/build/actions/schemas.d.ts +27 -3
  9. package/build/actions/schemas.d.ts.map +1 -1
  10. package/build/actions/schemas.js +67 -7
  11. package/build/actions/schemas.js.map +1 -1
  12. package/build/conformance/assert.d.ts +20 -0
  13. package/build/conformance/assert.d.ts.map +1 -0
  14. package/build/conformance/assert.js +54 -0
  15. package/build/conformance/assert.js.map +1 -0
  16. package/build/conformance/cases.d.ts +22 -0
  17. package/build/conformance/cases.d.ts.map +1 -0
  18. package/build/conformance/cases.js +446 -0
  19. package/build/conformance/cases.js.map +1 -0
  20. package/build/conformance/fixtures.d.ts +31 -0
  21. package/build/conformance/fixtures.d.ts.map +1 -0
  22. package/build/conformance/fixtures.js +147 -0
  23. package/build/conformance/fixtures.js.map +1 -0
  24. package/build/conformance/index.d.ts +10 -0
  25. package/build/conformance/index.d.ts.map +1 -0
  26. package/build/conformance/index.js +10 -0
  27. package/build/conformance/index.js.map +1 -0
  28. package/build/consts.d.ts +5 -0
  29. package/build/consts.d.ts.map +1 -1
  30. package/build/consts.js +5 -0
  31. package/build/consts.js.map +1 -1
  32. package/build/definitions.d.ts +33 -0
  33. package/build/definitions.d.ts.map +1 -0
  34. package/build/definitions.js +211 -0
  35. package/build/definitions.js.map +1 -0
  36. package/build/executor/creator.d.ts +30 -0
  37. package/build/executor/creator.d.ts.map +1 -0
  38. package/build/executor/creator.js +47 -0
  39. package/build/executor/creator.js.map +1 -0
  40. package/build/executor/resolve.d.ts +16 -3
  41. package/build/executor/resolve.d.ts.map +1 -1
  42. package/build/executor/resolve.js +68 -11
  43. package/build/executor/resolve.js.map +1 -1
  44. package/build/executor/validate.d.ts +3 -2
  45. package/build/executor/validate.d.ts.map +1 -1
  46. package/build/executor/validate.js +16 -13
  47. package/build/executor/validate.js.map +1 -1
  48. package/build/executor.d.ts +8 -5
  49. package/build/executor.d.ts.map +1 -1
  50. package/build/executor.js +12 -9
  51. package/build/executor.js.map +1 -1
  52. package/build/facade.d.ts +19 -1
  53. package/build/facade.d.ts.map +1 -1
  54. package/build/facade.js +128 -18
  55. package/build/facade.js.map +1 -1
  56. package/build/helper.d.ts +3 -2
  57. package/build/helper.d.ts.map +1 -1
  58. package/build/helper.js +7 -3
  59. package/build/helper.js.map +1 -1
  60. package/build/index.d.ts +1 -0
  61. package/build/index.d.ts.map +1 -1
  62. package/build/index.js +1 -0
  63. package/build/index.js.map +1 -1
  64. package/build/service.d.ts +10 -1
  65. package/build/service.d.ts.map +1 -1
  66. package/build/service.js +9 -0
  67. package/build/service.js.map +1 -1
  68. package/build/store/composite.d.ts +1 -0
  69. package/build/store/composite.d.ts.map +1 -1
  70. package/build/store/composite.js +3 -0
  71. package/build/store/composite.js.map +1 -1
  72. package/build/store/fold.d.ts +3 -1
  73. package/build/store/fold.d.ts.map +1 -1
  74. package/build/store/fold.js +8 -2
  75. package/build/store/fold.js.map +1 -1
  76. package/build/store/memory.d.ts +12 -0
  77. package/build/store/memory.d.ts.map +1 -1
  78. package/build/store/memory.js +0 -0
  79. package/build/store/memory.js.map +1 -1
  80. package/build/store/types.d.ts +15 -1
  81. package/build/store/types.d.ts.map +1 -1
  82. package/build/types.d.ts +46 -0
  83. package/build/types.d.ts.map +1 -1
  84. package/build/utils/scope.d.ts +43 -2
  85. package/build/utils/scope.d.ts.map +1 -1
  86. package/build/utils/scope.js +79 -15
  87. package/build/utils/scope.js.map +1 -1
  88. package/package.json +24 -17
  89. package/src/actions/execute.ts +95 -12
  90. package/src/actions/schemas.ts +85 -9
  91. package/src/conformance/assert.ts +59 -0
  92. package/src/conformance/cases.ts +547 -0
  93. package/src/conformance/fixtures.ts +168 -0
  94. package/src/conformance/index.ts +9 -0
  95. package/src/consts.ts +6 -0
  96. package/src/definitions.ts +282 -0
  97. package/src/executor/creator.ts +54 -0
  98. package/src/executor/resolve.ts +84 -14
  99. package/src/executor/validate.ts +18 -13
  100. package/src/executor.ts +13 -9
  101. package/src/facade.ts +144 -19
  102. package/src/helper.ts +7 -3
  103. package/src/index.ts +1 -0
  104. package/src/service.ts +23 -1
  105. package/src/store/composite.ts +3 -0
  106. package/src/store/fold.ts +8 -2
  107. package/src/store/memory.ts +0 -0
  108. package/src/store/types.ts +16 -1
  109. package/src/types.ts +50 -0
  110. package/src/utils/scope.ts +103 -17
  111. package/tests/access.spec.ts +160 -0
  112. package/tests/conformance.spec.ts +47 -0
  113. package/tests/create-example.spec.ts +202 -0
  114. package/tests/creator.spec.ts +257 -0
  115. package/tests/entrypoints.spec.ts +5 -0
  116. package/tests/narrowing.spec.ts +94 -0
  117. package/tests/writes.spec.ts +140 -0
package/README.md CHANGED
@@ -10,13 +10,16 @@ the `@owlmeans/server-planning/store` subpath.
10
10
  ## Installation
11
11
 
12
12
  ```sh
13
- bun add @owlmeans/server-planning@^0.1.18-rc.2 @owlmeans/planning@^0.1.18-rc.2 ajv
13
+ bun add @owlmeans/server-planning@^0.1.18-rc.20 @owlmeans/planning@^0.1.18-rc.17 ajv
14
14
  ```
15
15
 
16
16
  ## Concepts
17
17
 
18
- - **Facade** — `context.planning().for(scope)`: every read and write of one organization entity,
19
- with no scope argument on any method.
18
+ - **Facade** — `ctx.service<PlanningHostService>(PLANNING_SERVICE).for(scope)`: every read and
19
+ write of one organization entity, with no scope argument on any method. `context.planning()` is
20
+ the same service, but only on a context that went through `appendPlanningService`; a service
21
+ registered as a plain one (`makePlanningService()`, a generated target's `services/planning.ts`)
22
+ has no `context.planning`.
20
23
  - **Executor** — the only write path: normalize → idempotency → resolve → plugins' `before` →
21
24
  validate → id → code → changes → seq CAS → append → project → receipt. Nothing is appended by a
22
25
  refusal.
@@ -101,36 +104,61 @@ const cards = await planningFor(ctx, req, { channel: 'web' }).cards.list({ paren
101
104
 
102
105
  - Service: `appendPlanningService`, `ensurePlanningService`, `makePlanningService`,
103
106
  `planningServiceApi`, `makePluginRegistry`, `makeStoreFacade`, `executeTransition`,
104
- `PlanningServiceOptions`, `PlanningHostService`, `PlanningRuntime`, `PluginRegistry`
107
+ `creatorOf`, `withCreator`, `assertCreatorFixed`, `PlanningServiceOptions`, `PlanningHostService`,
108
+ `PlanningRuntime`, `PluginRegistry`
105
109
  - Handlers: `servePlanningEntrypoints`, `planningFor`, `listSchemas`, `listCards`,
106
110
  `summarizeCards`, `getCard`, `listCardTransitions`, `listCardSpecifications`, `getSpecification`,
107
111
  `listSpecificationRevisions`, `listLinks`, `getTransition`, `executePlanning`, `wireExecution`,
108
- `executeOptionsOf`, `getCommit`, `watchCommits`, `PlanningHandlerOptions`, `PlanningScopeExtractor`
109
- - Scope: `scopeOf`, `actorOf`, `handlerFacade`, `planningServiceOf`, `concealed`, `assertScope`,
110
- `notFoundOf`, `clampSeconds`
112
+ `executeOptionsOf`, `assertExecutionGranted`, `getCommit`, `watchCommits`, `defineSchemas`,
113
+ `applySchemaRequest`, `PlanningHandlerOptions`, `PlanningScopeExtractor`, `PlanningAccessResolver`,
114
+ `PlanningAccess`, `PlanningAccessGrants`, `PlanningGrant`
115
+ - Scope: `scopeOf`, `accessScopeOf`, `actorOf`, `handlerFacade`, `handlerScopeOf`, `assertGranted`,
116
+ `planningServiceOf`, `concealed`, `assertScope`, `notFoundOf`, `clampSeconds`, `projectCriteriaOf`
117
+ - Data-defined schemas: `makeDefinitions`, `makeSchemaViews`, `SchemaViews`, `SchemaViewsOptions`
111
118
  - Projection: `makeProjectionProcessor`, `planningQueueHooks`, `ProjectionOptions`
112
- - Constants: `DEFAULT_ALIAS`, `MEMORY_STORE_ALIAS`, `DEFAULT_COMMIT_MEMORY`, `COMMIT_POLL_LADDER`
119
+ - Constants: `DEFAULT_ALIAS`, `MEMORY_STORE_ALIAS`, `DEFAULT_COMMIT_MEMORY`, `COMMIT_POLL_LADDER`,
120
+ `DEFAULT_SCHEMA_VIEWS`
113
121
  - `@owlmeans/server-planning/store`: `makeMemoryPlanningStore`, `foldPending`, `failPending`,
114
- `revisionsFromLog`, `commitEventOf`, `makeCommitHub`, `makeCompositeStore`, and the types
115
- `BindablePlanningStore`, `CommitListener`, `CommitHub`, `CommitHubOptions`, `FoldOptions`,
116
- `FoldResult`, `MemoryPlanningStore`, `MemoryPlanningStoreOptions`, `StoreRoute`
122
+ `revisionsFromLog`, `commitEventOf`, `makeCommitHub`, `makeCompositeStore`, `wantsSpecifications`,
123
+ and the types `BindablePlanningStore`, `CommitListener`, `CommitHub`, `CommitHubOptions`,
124
+ `FoldOptions`, `FoldResult`, `MemoryPlanningStore`, `MemoryPlanningStoreOptions`, `StoreRoute`
125
+ - `@owlmeans/server-planning/conformance`: `planningConformance`, `conformanceCasesFor`,
126
+ `planningConformancePlugin`, `conformanceClock`, `ConformanceFailure`, `check`, `same`,
127
+ `sameSet`, `rejects`, the library fixtures (`LIBRARY`, `createBranch`, `createBook`, `transit`)
128
+ and the types `ConformanceCase`, `ConformanceSubject`, `ConformanceCapability`
117
129
 
118
130
  ## Common pitfalls
119
131
 
132
+ - A create's draft goes under `card` — `draft` is refused as `planning:malformed:create-without-draft`.
133
+ - `{ wait: true }` is the SECOND argument of `execute`; inside the execution it is ignored and the
134
+ receipt returns before the commit.
135
+ - A type or flow declaration has no `name` or `key`: the display name is `label`, the key is `type`
136
+ (a type) or `id` (a flow). `definitions.define` refuses anything else with `SchemaInvalid`; a code
137
+ plugin's registry does not check, so keep the literals typed.
138
+
120
139
  - `after` hooks run where the transition is FOLDED, once per commit — register the same plugins in
121
140
  every folding process, and never run hooks from a commit-bus subscription.
122
141
  - The memory store is per-process heap: wrong for multiple processes or restarts.
123
142
  - `actor` comes from the scope; a wire `actor` or `createdBy` is ignored.
143
+ - A create's `createdBy` defaults to the scope's subject (`profileId`, else `userId`, else the
144
+ scope actor's) on every path — in-process facade and HTTP alike; an in-process draft that names
145
+ one keeps it, a scope naming nobody leaves it unset.
146
+ - After the create `createdBy` never moves: an execution naming it in `changes` or `unset` is
147
+ refused with `planning:immutable:createdBy` — over HTTP, in process and from a `before` plugin.
124
148
  - Another entity's record is `WorkcardNotFound`, never a permission error.
125
149
  - Give retried writes a `key` — it is checked before validation, so a retry answers the first receipt.
126
150
  - `expectSeq` compares against `head`; a stale one is `WorkcardConflict`, re-read and retry.
127
151
  - Call `appendPlanningService` before any `ensurePlanningService`, or the appended host replaces
128
152
  what was registered on the default one.
153
+ - `opts.access` makes the resolver the only source of the organization; a `grants` object refuses
154
+ every flag it leaves out.
155
+ - A durable store is not done until it passes `@owlmeans/server-planning/conformance`.
129
156
 
130
157
  ## Related packages
131
158
 
132
159
  - `@owlmeans/planning` — records, flows, fold, query language, protocol tree, models
133
160
  - `@owlmeans/client-planning` — the remote facade and state mirror
161
+ - `@owlmeans/planning-postgres` — the durable Postgres store
134
162
  - `@owlmeans/server-job`, `@owlmeans/queue` — job feeds and the projection queue
135
163
 
136
164
  <!-- owlmeans:agent-guidance:start -->
@@ -141,7 +169,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
141
169
  your project's skill store (`.agents/skills/`):
142
170
 
143
171
  ```sh
144
- npx @owlmeans/agent-skills@^0.1.18-rc.30
172
+ npx @owlmeans/agent-skills@^0.1.18-rc.46
145
173
  ```
146
174
 
147
175
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/server-planning",
4
- "version": "0.1.18-rc.2",
5
- "generatedAt": "2026-09-19T13:42:56.215Z",
4
+ "version": "0.1.18-rc.20",
5
+ "generatedAt": "2026-10-04T01:01:54.399Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/server-planning
9
9
 
10
10
  **Layer:** Server
11
- **Install:** `"@owlmeans/server-planning": "^0.1.18-rc.2"` in `dependencies` (`ajv` is a peer)
11
+ **Install:** `"@owlmeans/server-planning": "^0.1.18-rc.20"` in `dependencies` (`ajv` is a peer)
12
12
 
13
13
  The general implementation of `@owlmeans/planning`: the planning service (a plugin host), the
14
14
  scoped facade, the executor every write goes through, the in-memory reference store, the commit
@@ -24,13 +24,18 @@ reuses the `./store` subpath (`foldPending`, `makeCommitHub`) without pulling fa
24
24
  | `makePlanningService(opts?, alias?)`, `planningServiceApi(opts, self)` | The service, and its body for a specialised service |
25
25
  | `PlanningServiceOptions` | `{ store?, plugins?, schemas?, hooks?, ids?, now? }` |
26
26
  | `makePluginRegistry`, `makeStoreFacade`, `executeTransition` | The pieces the service is made of |
27
- | `servePlanningEntrypoints(protocols, opts?)` | One binding per protocol of a `makePlanningProtocols` tree |
27
+ | `creatorOf(scope)`, `withCreator(exec, scope)`, `assertCreatorFixed(exec)` | The subject a create is stamped with as `createdBy`, the defaulting the executor applies, and its refusal of any later move |
28
+ | `servePlanningEntrypoints(protocols, opts?)` | One binding per protocol of a `makePlanningProtocols` tree (`schema.define` too when declared) |
28
29
  | `planningFor(ctx, req, extra?)` | The request-scoped facade for a hand-written handler |
29
- | `PlanningHandlerOptions` | `{ service?, event?, maxPoll?, scope?(req, ctx) }` |
30
- | `listCards` … `executePlanning`, `getCommit`, `watchCommits` | The handlers, to bind one by hand |
31
- | `scopeOf(req, extra?)`, `actorOf(req)`, `concealed(run)`, `clampSeconds` | Scope and security helpers |
30
+ | `PlanningHandlerOptions` | `{ service?, event?, maxPoll?, scope?(req, ctx), access?(req, ctx) }` |
31
+ | `PlanningAccessResolver`, `PlanningAccess`, `PlanningAccessGrants`, `PlanningGrant` | The optional access decision a hosting app supplies |
32
+ | `listCards` … `executePlanning`, `getCommit`, `watchCommits`, `listSchemas`, `defineSchemas`, `applySchemaRequest` | The handlers, to bind one by hand |
33
+ | `scopeOf(req, extra?)`, `accessScopeOf(req, access, extra?)`, `handlerScopeOf`, `assertGranted`, `assertWrites`, `writableIn`, `assertExecutionGranted`, `assertExecutionWrites`, `actorOf(req)`, `concealed(run)`, `clampSeconds` | Scope and security helpers |
34
+ | `makeDefinitions(runtime, facade)`, `makeSchemaViews(opts)`, `projectCriteriaOf(projects, through?)` | Data-defined schemas and the project narrowing |
32
35
  | `makeProjectionProcessor(ctx, opts?)`, `planningQueueHooks(ctx, opts?)` | A generic projection job body and its `onJobDead` |
33
- | `./store`: `makeMemoryPlanningStore`, `foldPending`, `failPending`, `revisionsFromLog`, `commitEventOf`, `makeCommitHub`, `makeCompositeStore` | What stores are built from |
36
+ | `./queue`: `declarePlanningQueue(cfg, opts?)`, `PLANNING_PROJECTION_QUEUE` (`planning-projection`), `PLANNING_PROJECT_JOB` (`planning:project`), `ProjectionRequest` | The projection queue's shared address; each process chooses separately whether to listen |
37
+ | `./store`: `makeMemoryPlanningStore`, `foldPending`, `failPending`, `revisionsFromLog`, `commitEventOf`, `makeCommitHub`, `makeCompositeStore`, `wantsSpecifications` | What stores are built from |
38
+ | `./conformance`: `planningConformance`, `conformanceCasesFor`, `planningConformancePlugin`, `conformanceClock`, `ConformanceFailure` | The store conformance suite, runner-agnostic |
34
39
 
35
40
  ## Wiring
36
41
 
@@ -58,23 +63,89 @@ context.registerEntrypoints(servePlanningEntrypoints(planningProtocols, {
58
63
  `ensurePlanningService(ctx).use(plugin)` works from `makeContext`, before `init()`.
59
64
  - Call `appendPlanningService` BEFORE any `ensurePlanningService`: `ensure` registers a default
60
65
  host when none exists, and a later `append` replaces it together with whatever was `use`d on it.
61
- - In code: `ctx.planning().for({ entityId, profileId?, channel?, actor? })` → a `PlanningFacade`
62
- with no scope argument on any method.
66
+ - **The facade:** `ctx.service<PlanningHostService>(PLANNING_SERVICE).for({ entityId, profileId?,
67
+ channel?, actor? })` → a `PlanningFacade` with no scope argument on any method. That form works
68
+ however the service was registered. `ctx.planning()` is only a shortcut that `appendPlanningService`
69
+ / `appendPostgresPlanning` install (typed `WithPlanningService`); a context that registered
70
+ `makePlanningService()` / `makePostgresPlanningService()` as a plain service — a generated target's
71
+ `services/planning.ts` — has no `ctx.planning` at all. In a request handler use
72
+ `planningFor(ctx, req)`: the entity comes from `requireEntityKey(req)`, never the token.
73
+
74
+ ## Creating and reading a card
75
+
76
+ ```ts
77
+ import { IntrinsicStatus, PLANNING_SERVICE, TransitionAction, WorkcardKind } from '@owlmeans/planning'
78
+ import type { PlanningHostService } from '@owlmeans/server-planning'
79
+
80
+ const planning = ctx.service<PlanningHostService>(PLANNING_SERVICE).for({ entityId, profileId })
81
+
82
+ const shed = await planning.execute({
83
+ action: TransitionAction.Create,
84
+ card: { kind: WorkcardKind.Project, type: 'shed:shed', title: 'Maple Street shed' },
85
+ }, { wait: true })
86
+ const drill = await planning.execute({
87
+ action: TransitionAction.Create,
88
+ card: { kind: WorkcardKind.Card, type: 'shed:tool', parent: shed.card!.id!, title: 'Cordless drill',
89
+ fields: { brand: 'Acme' } },
90
+ }, { wait: true }) // drill.card — the committed record, status 'available'
91
+
92
+ await planning.cards.get(drill.card!.id!)
93
+ await planning.cards.list({ parent: shed.card!.id!, type: 'shed:tool' })
94
+ await planning.execute({ action: TransitionAction.Transit, card: drill.card!.id!, transition: 'lend' }, { wait: true })
95
+
96
+ // A card type and a flow as DATA, scoped to this shed — `definitions` exists only where the store
97
+ // keeps schemas (planning-postgres; the memory store with `schemas: true`), and the shed's cards may
98
+ // use them when its project type says `scopedCardTypes: true` (or lists the type in `cardTypes`).
99
+ await planning.definitions!.define({
100
+ flows: [{
101
+ id: 'shed:repair', version: 1, label: 'Repair',
102
+ statuses: [
103
+ { key: 'reported', intrinsic: IntrinsicStatus.Planned, initial: true, label: 'Reported' },
104
+ { key: 'fixed', intrinsic: IntrinsicStatus.Closed, terminal: true, label: 'Fixed' },
105
+ ],
106
+ transitions: [{ name: 'fix', from: ['reported'], to: 'fixed', label: 'Mark fixed', explicit: true }],
107
+ }],
108
+ types: [{
109
+ type: 'shed:repair', kind: WorkcardKind.Card, version: 1, label: 'Repair ticket',
110
+ fields: { type: 'object', properties: { tool: { type: 'string' } }, additionalProperties: false },
111
+ flows: ['shed:repair'], specifications: [],
112
+ }],
113
+ }, { project: shed.card!.id! })
114
+ ```
115
+
116
+ | Wrong | What happens | Right |
117
+ |---|---|---|
118
+ | `execute({ action, draft: { … } })` | refused: `planning:malformed:create-without-draft` | the draft goes under `card` |
119
+ | `execute({ …, wait: true })` | in process the key is ignored: the receipt returns before the commit, with no `card` — hidden while an inline-folding store (`planning-postgres`) lands at once, exposed the first time another process holds the card's fold | `execute({ … }, { wait: true })` — only the wire body (`ExecuteRequest`) carries `wait` inside |
120
+ | `ctx.planning().for(…)` in a target | `ctx.planning is not a function` | `ctx.service<PlanningHostService>(PLANNING_SERVICE).for(…)` |
121
+ | `{ id, name: 'Repair', … }` on a flow, `{ key, name, … }` on a type | `define` refuses with `SchemaInvalid` (closed schemas); a code plugin's registry does not validate, so the key is silently dropped | a flow's key is `id`, a type's is `type`, the display name is `label` on both |
122
+ | `action: 'create'` | a type error — `TransitionAction` is an enum | `action: TransitionAction.Create` |
123
+
124
+ Every wrong form is a compile error against the typed facade (`PlanningFacade`,
125
+ `TransitionExecution`, `WorkcardTypeSchema`); it reaches run time only through a helper or a context
126
+ typed `any`, so never type one that way. `tests/create-example.spec.ts` runs this example and each
127
+ wrong form as written.
63
128
 
64
129
  ## The executor, step by step
65
130
 
66
131
  Nothing is appended before step 11 — every refusal leaves the log untouched.
67
132
 
68
- 1. **Normalize** — deep copy, trimmed text, `parents ∋ parent` (first).
133
+ 1. **Normalize** — deep copy, trimmed text, `parents ∋ parent` (first); a create's draft without
134
+ `createdBy` gets `creatorOf(scope)` (below).
69
135
  2. **Scope** — `entityId` must be present.
70
136
  3. **Idempotency** — a `key` already in the entity's log answers ITS receipt, before any
71
137
  validation or middleware.
72
- 4. **Resolve** — the card (`WorkcardNotFound`; another entity's card is `PlanningScopeMismatch`),
73
- its type and flow, every draft parent (`ParentNotFound`) and the parent project's
74
- `cardTypes`/`projectTypes` (`CardTypeNotAllowed`), the specification slot.
138
+ 4. **Resolve** — the card (`WorkcardNotFound`; another entity's card is `PlanningScopeMismatch`;
139
+ one outside the scope's `projects` is `WorkcardNotFound`), its type and flow, every draft parent
140
+ (`ParentNotFound` — loaded through the facade, so an invisible parent is absent) and the parent
141
+ project's `cardTypes`/`projectTypes` (`CardTypeNotAllowed`), the specification slot. Where the
142
+ default store holds data-defined schemas, the card's project is found first and everything
143
+ after reads that project's resolved layer (`Resolved.schemas`): a data-defined type of a
144
+ project with `scopedCardTypes` is admitted, a retired type creates nothing
145
+ (`UnknownWorkcardType('retired:…')`). Without the port `Resolved.schemas` IS the code registry.
75
146
  5. **`before` chain** — in plugin order; re-resolved once when it changed the card, type or parent.
76
147
  6. **Validate** — shape (`planning:malformed:*`), the flow rule (`IllegalTransition`), immutables
77
- (`planning:immutable:*`), merged `fields` (`FieldsInvalid`), labels (`LabelNotAllowed`), a moved
148
+ (`planning:immutable:*`, `createdBy` among them — `assertCreatorFixed`), merged `fields` (`FieldsInvalid`), labels (`LabelNotAllowed`), a moved
78
149
  parent, the slot (`SpecificationSlotUnknown`, `SpecificationRevisionConflict`, JSON body schema
79
150
  → `FieldsInvalid`), relationships (`RelationshipRefused`: undeclared type, `from` not the card,
80
151
  missing target, type constraints, `single`, unlinking an absent edge).
@@ -92,6 +163,17 @@ Nothing is appended before step 11 — every refusal leaves the log untouched.
92
163
  `actor` is the SCOPE's: `profileId`/`userId`/`service`/`channel` come from the scope only; an
93
164
  in-process caller may add `agent`/`runId` on the execution. A handler never passes a wire `actor`.
94
165
 
166
+ A create's `createdBy` is the scope's subject too — `creatorOf(scope)`: `profileId`, else `userId`,
167
+ else the same two of `scope.actor`. The executor stamps it on every path, so an in-process facade
168
+ create and the HTTP `execute` produce the same owner. An in-process draft that names `createdBy`
169
+ keeps it (explicit wins — a manager creating on someone's behalf); a scope naming nobody (a service
170
+ or system scope) leaves it unset; the wire drops a draft's `createdBy` before the stamp.
171
+
172
+ `createdBy` never moves after the create (the rule is `planning` → The record model). The executor
173
+ enforces it itself with `assertCreatorFixed` at step 6, after the `before` chain, so a wire body, an
174
+ in-process caller and a plugin are refused alike, whatever `@owlmeans/planning` build the host
175
+ resolves. The conformance suite checks a store folds a stored row naming it as the pure fold does.
176
+
95
177
  ## Idempotency and `expectSeq`
96
178
 
97
179
  - Give every retried or replayed write a `key` (`import:<source>:<n>`). The key is unique per entity
@@ -125,11 +207,15 @@ the store; `bind` replaces the listener, so a store has exactly one.
125
207
 
126
208
  ## The memory store
127
209
 
128
- `makeMemoryPlanningStore({ sync?, ids?, now?, seed?, onCommitted?, alias? })`
210
+ `makeMemoryPlanningStore({ sync?, ids?, now?, seed?, onCommitted?, alias?, schemas? })`
129
211
 
130
212
  - Cards and projects in one map, specifications in their own, relationships in a third; every read
131
213
  goes through the `@owlmeans/resource` query engine. A card list includes specifications only when
132
- the criteria asks for `kind: specification` or a `category`.
214
+ the criteria asks for `kind: specification` or a `category` (`wantsSpecifications`, the rule every
215
+ store routes by).
216
+ - `schemas: true` adds the data-defined schema port (records per layer, a revision per
217
+ organization, synchronous `watch`); off by default — without it every type and flow resolves
218
+ from code.
133
219
  - `sync` (default) folds, publishes and runs `committed` inside `project()`, serialized per card.
134
220
  `sync: false` folds nothing until `flush(card?)` — a queued store's shape, for tests that need a
135
221
  commit to stay pending.
@@ -155,7 +241,20 @@ the store; `bind` replaces the listener, so a store has exactly one.
155
241
  no card to advance, and a store that refuses the advancing write stops the fold with `followUp`,
156
242
  leaving the rest pending for a retry. `revisionsFromLog` replays past failed transitions the same
157
243
  way.
244
+ - A transactional store passes `FoldOptions.unit` — it wraps each transition's writes (a
245
+ savepoint), so a write the database refuses is undone whole and the fold marks that transition
246
+ failed and goes past it. `foldPending` folds PAST a failure it produces; a row that is ALREADY
247
+ failed at the cursor, or a gap in the seqs, is the store's to step over before calling it
248
+ (`planning-postgres` does it in a prelude).
158
249
  - `failPending` is what a dead projection job must do, or waiters hang to their timeout.
250
+ - The data-defined schema port (`schemas`) is optional; `makeCompositeStore` passes the default
251
+ store's through. A store that has it must purge a project's layer with the project.
252
+ - **Run the conformance suite.** `@owlmeans/server-planning/conformance` exports
253
+ `planningConformance` — named cases with no test runner inside (each throws
254
+ `ConformanceFailure`) — and `conformanceCasesFor(store)`, which drops the data-defined cases for a
255
+ store without the port. Boot a service with `planningConformancePlugin` and `conformanceClock()`
256
+ and hand each case `{ service, store, facade }`; every case works in its own organization, so one
257
+ boot serves them all.
159
258
  - `makeCommitHub({ status, remember?, ladder? })` is the `CommitSource`: feed `publish` from the bus,
160
259
  answer `status` from transition rows. `wait` subscribes first, polls once, then climbs the ladder.
161
260
  - `revisionsFromLog(transitions, limit?)` answers `specs.revisions`.
@@ -180,24 +279,100 @@ mapping external records (`mappers`) belongs to that plugin's own store adapter.
180
279
 
181
280
  ## Scope and security
182
281
 
183
- - `entityId` is `requireEntityKey(req)`; `opts.scope(req, ctx)` adds a `channel` or `service` and
184
- can never replace the entity. `actor` and a create's `createdBy` come from the request, never the
185
- body.
282
+ - Without `opts.access`: `entityId` is `requireEntityKey(req)`; `opts.scope(req, ctx)` adds a
283
+ `channel` or `service` and can never replace the entity. `actor` and a create's `createdBy` come
284
+ from the request, never the body (`wireExecution` drops a draft's claim, `withCreator` stamps the
285
+ subject, `assertCreatorFixed` refuses a body naming `createdBy` — an update never re-owns a card).
286
+ - The organization of a scope is the one the request ACTS IN. `requireEntityKey(req)` answers
287
+ `req.entity.id`, the stable record id (the token's `entitySlug` only where no entity resolver is
288
+ registered). Behind an OIDC relying party (`@owlmeans/server-oidc-rp`) the acting organization is
289
+ session state: the guard attaches `req.entity` with `id` = the organization claim's frozen
290
+ `entityKey`, so the tenant key survives a rename and follows a switch. Take it in the handler and
291
+ pass it down; never read it from the context or from a body.
186
292
  - Another entity's card, specification or transition answers `WorkcardNotFound` — the same as an
187
293
  absent id — on reads and writes alike (`concealed`).
188
294
  - Queries arrive in their wire form and are decoded with `decode*Query` before `criteriaOf`, which
189
295
  always adds the scope's `entityId`.
190
296
 
297
+ ### The access resolver
298
+
299
+ `opts.access: (req, ctx) => Promise<{ entityId, projects?, writes?, grants? }>` is the hosting app's
300
+ decision per request; a throw is the request's answer (an `AuthForbidden` answers 403).
301
+
302
+ - `entityId` is the resolver's — never the token's, never `opts.scope`'s.
303
+ - `projects` becomes `PlanningScope.projects`, which the server facade enforces on every read and
304
+ write. Visible are the projects, every card whose `parents` name one, and every specification
305
+ whose parent card is visible (its `parents` hold only that card). Anything else reads as absent
306
+ (`WorkcardNotFound`, `null`, an empty list); a create under an invisible parent is
307
+ `ParentNotFound`; links and transitions are filtered by `project` (and post-filtered, for a store
308
+ that ignores the field); commit subscriptions drop events outside the set and a status or wait
309
+ refuses them.
310
+ - **A narrowed list or count sees a specification whenever its parent card is visible** — exactly
311
+ what a single read admits. It is AND'd with `projectCriteriaOf(projects, through)`; when its
312
+ criteria ask for specifications (`wantsSpecifications`) the facade resolves `through` (the visible
313
+ parent cards) with ONE read per call: the query's `parent`, else the projects' own cards. A
314
+ summary counts no specification and pays nothing extra. `tests/narrowing.spec.ts` pins it.
315
+ - A person who may see only SOME projects of their organization (a tool-shed volunteer given one
316
+ shed) is expressed here: the resolver answers the project ids the person's grants list as
317
+ `projects`, and omits the key when a grant covers every project — never a hand-written filter in
318
+ each handler.
319
+ - `writes` is the subset of projects the person may WRITE in (`projects` are the ones they VIEW).
320
+ `execute` on a card, specification or link needs it to be writable — the rule a narrowed read
321
+ admits, over `writes`: the project itself, a card whose `parents` name one, a specification
322
+ through its parent card; a create needs every named parent writable, and a create at the root
323
+ (no parent, not a project) is refused. `schema.define` of a project layer needs the project in
324
+ `writes`. A refusal is `PlanningForbidden` (403, `forbidden:writes:<id>`), checked before the grants
325
+ and before the executor. NOT governed by it: a project create (`grants.createProjects` alone) and
326
+ the organization layer (`grants.defineSchemas`). A card the scope cannot see is still answered
327
+ as absent by the executor. Omitted — every visible project is writable.
328
+ `tests/writes.spec.ts` pins viewer, writer and omitted.
329
+ - `grants` gates the stock handlers' writes: `createProjects` (a project create — `true` for the
330
+ root, a list names the parent projects), `deleteProjects` (the delete of a project card — a list
331
+ names the projects), `defineSchemas` (`schema.define` — `true` for the organization's layer, a
332
+ list names project layers). A `grants` object refuses every flag it leaves out with
333
+ `PlanningForbidden` (403); no `grants` gates nothing. In-process facade writes are not gated.
334
+
335
+ ## Mounting in a target
336
+
337
+ The api half of the target mount (the common tree and the kit literals: `planning` → Mounting in a
338
+ target).
339
+
340
+ - Bind `servePlanningEntrypoints(tree, { access })` WITHOUT the commit socket —
341
+ `.filter(entrypoint => entrypoint.protocol !== tree.commit.events)`: nothing authenticates a
342
+ target's socket after it opens, so the client long-polls (`commit.get` with `wait`, `execute`
343
+ with `wait: true`). Never write card, transition or schema routes beside it.
344
+ - The resolver maps container permissions onto the access: `projects` = the containers the person
345
+ may view, `writes` = those they may modify, `grants` = `createProjects` / `deleteProjects` /
346
+ `defineSchemas` from their create and admin rights. A creator sees what they created — stamp
347
+ ownership in a `before` hook and add the card to both lists.
348
+
349
+ ## Data-defined types and flows
350
+
351
+ Where the default store has the schema port, `runtime.schemasFor(entityId, project?)` answers the
352
+ resolved layer (`resolveScopedBundle` over the code bundle and the organization's and project's
353
+ records), cached per (organization, project) and keyed by the store's `revision()` and the plugin
354
+ registry's version — a write anywhere moves the revision the next lookup reads, so a cached layer is
355
+ never stale; a store's `watch` evicts early. Without the port it answers the code registry itself.
356
+ `makeDefinitions` gives the facade `definitions`: every write checks the project (a project card the
357
+ scope can see), the seal, the closed-form checks (a type's flows resolve in its layer, counting flows
358
+ written beside it), then the store's compare-and-set. `retire` of a flow refuses with `SchemaInUse`
359
+ while a live type of the layer — or of any project layer, for an organization record — still runs
360
+ it. `facade.model(card)` builds over the layer of the card's own project, and `schema.list` answers
361
+ the scoped bundle (`?project=`) where `definitions` exists, the code bundle otherwise.
362
+
191
363
  ## Testing
192
364
 
193
365
  Category A. Build one real context with `makeBasicContext` + `appendPlanningService({ store:
194
366
  makeMemoryPlanningStore({ now }), plugins: [fixtures] })`, and run a handler through
195
367
  `handler.bind({ ref: { ctx } })(req, res)` with a request carrying `auth` and `entity`. A monotonic
196
368
  `now` keeps log order deterministic; `sync: false` + `flush()` pins pending and hook-count cases.
369
+ `tests/conformance.spec.ts` runs the conformance suite against the memory store with and without
370
+ `schemas: true`; a durable store's own tests run the same cases against a real database.
197
371
 
198
372
  ## Related
199
373
 
200
374
  - `planning` — records, flows, the fold, the query language, the protocol tree, the models
201
375
  - `client-planning` — the remote facade and the state mirror that talk to these handlers
376
+ - `planning-postgres` — the durable Postgres store built from these pieces
202
377
  - `server-job`, `queue` — the declare/serve/feed pattern and the projection queue
203
378
  - `resource` — the criteria engine every memory read goes through
@@ -1,13 +1,34 @@
1
1
  import { handlers } from '@owlmeans/server-api';
2
- import type { ExecuteOptions, ExecuteRequest, PlanningProtocols, PlanningScope, TransitionExecution } from '@owlmeans/planning';
3
- import type { Context, PlanningHandlerOptions } from '../types.js';
2
+ import type { ExecuteOptions, ExecuteRequest, PlanningFacade, PlanningProtocols, PlanningScope, TransitionExecution } from '@owlmeans/planning';
3
+ import type { Context, PlanningAccess, PlanningHandlerOptions } from '../types.js';
4
4
  /**
5
5
  * What a wire execution may say. `actor` is dropped — the scope names the writer — and a create's
6
- * `createdBy` is the authenticated subject, whatever the body claimed.
6
+ * `createdBy` claim is dropped too: the executor's {@link withCreator} stamps the authenticated
7
+ * subject, the same rule an in-process create follows. Applied here as well, so the returned
8
+ * execution already carries it. A `createdBy` in `changes` or `unset` is not dropped: the executor
9
+ * refuses it on every action (`assertCreatorFixed`), so no body moves an owner.
7
10
  */
8
11
  export declare const wireExecution: (body: ExecuteRequest, scope: PlanningScope) => TransitionExecution;
9
12
  /** `wait`/`timeout` as a server grants them: the timeout never holds a request past `maxPoll`. */
10
13
  export declare const executeOptionsOf: (body: Pick<ExecuteRequest, 'wait' | 'timeout'>, maxPoll: number) => ExecuteOptions;
14
+ /**
15
+ * Refuse an execution outside the access's `writes`: the card it acts on (a specification through
16
+ * its parent card), or every parent a create names, must be one {@link writableIn} admits. A create
17
+ * of a project is `grants.createProjects`' alone. A card or parent this scope cannot see is left to
18
+ * the executor, which answers it as absent.
19
+ *
20
+ * @throws {PlanningForbidden}
21
+ */
22
+ export declare const assertExecutionWrites: (facade: PlanningFacade, exec: TransitionExecution, access?: PlanningAccess) => Promise<void>;
23
+ /**
24
+ * The access a wire execution needs. `writes` first ({@link assertExecutionWrites}); then the
25
+ * grants: `createProjects` for a project create (its primary parent, or the root),
26
+ * `deleteProjects` for the delete of a project card. A card this scope cannot see is left to the
27
+ * executor, which answers it as absent.
28
+ *
29
+ * @throws {PlanningForbidden}
30
+ */
31
+ export declare const assertExecutionGranted: (facade: PlanningFacade, exec: TransitionExecution, access?: PlanningAccess) => Promise<void>;
11
32
  /** The one write route. */
12
33
  export declare const executePlanning: (protocol: PlanningProtocols['execute'], opts?: PlanningHandlerOptions) => ReturnType<ReturnType<typeof handlers<Context>>['request']>;
13
34
  //# sourceMappingURL=execute.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/actions/execute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C,OAAO,KAAK,EACV,cAAc,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EACtF,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE;;;GAGG;AACH,eAAO,MAAM,aAAa,SAAU,cAAc,SAAS,aAAa,KAAG,mBAQ1E,CAAA;AAED,kGAAkG;AAClG,eAAO,MAAM,gBAAgB,SAAU,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC,WAAW,MAAM,KAAG,cAOlG,CAAA;AAED,2BAA2B;AAC3B,eAAO,MAAM,eAAe,aAChB,iBAAiB,CAAC,SAAS,CAAC,SAAS,sBAAsB,KACpE,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAWxD,CAAA"}
1
+ {"version":3,"file":"execute.d.ts","sourceRoot":"","sources":["../../src/actions/execute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAK/C,OAAO,KAAK,EACV,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAEtG,MAAM,oBAAoB,CAAA;AAE3B,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,SAAU,cAAc,SAAS,aAAa,KAAG,mBAO1E,CAAA;AAED,kGAAkG;AAClG,eAAO,MAAM,gBAAgB,SAAU,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,CAAC,WAAW,MAAM,KAAG,cAOlG,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,WACxB,cAAc,QAAQ,mBAAmB,WAAW,cAAc,KACzE,OAAO,CAAC,IAAI,CAqCd,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,WACzB,cAAc,QAAQ,mBAAmB,WAAW,cAAc,KACzE,OAAO,CAAC,IAAI,CAed,CAAA;AAED,2BAA2B;AAC3B,eAAO,MAAM,eAAe,aAChB,iBAAiB,CAAC,SAAS,CAAC,SAAS,sBAAsB,KACpE,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAWxD,CAAA"}
@@ -1,18 +1,21 @@
1
1
  import { handlers } from '@owlmeans/server-api';
2
- import { DEFAULT_COMMIT_TIMEOUT, MAX_COMMIT_POLL, TransitionAction } from '@owlmeans/planning';
3
- import { concealed, handlerFacade } from '../utils/index.js';
2
+ import { DEFAULT_COMMIT_TIMEOUT, isProject, isSpecification, MAX_COMMIT_POLL, normalizeParents, PlanningForbidden, TransitionAction, WorkcardKind, } from '@owlmeans/planning';
3
+ import { withCreator } from '../executor/creator.js';
4
+ import { assertGranted, concealed, handlerScopeOf, writableIn } from '../utils/index.js';
4
5
  /**
5
6
  * What a wire execution may say. `actor` is dropped — the scope names the writer — and a create's
6
- * `createdBy` is the authenticated subject, whatever the body claimed.
7
+ * `createdBy` claim is dropped too: the executor's {@link withCreator} stamps the authenticated
8
+ * subject, the same rule an in-process create follows. Applied here as well, so the returned
9
+ * execution already carries it. A `createdBy` in `changes` or `unset` is not dropped: the executor
10
+ * refuses it on every action (`assertCreatorFixed`), so no body moves an owner.
7
11
  */
8
12
  export const wireExecution = (body, scope) => {
9
13
  const { wait: _wait, timeout: _timeout, actor: _actor, ...exec } = body;
10
14
  if (exec.action === TransitionAction.Create && exec.card != null && typeof exec.card === 'object') {
11
15
  const { createdBy: _createdBy, ...draft } = exec.card;
12
- const subject = scope.profileId ?? scope.userId;
13
- exec.card = subject != null ? { ...draft, createdBy: subject } : draft;
16
+ exec.card = draft;
14
17
  }
15
- return exec;
18
+ return withCreator(exec, scope);
16
19
  };
17
20
  /** `wait`/`timeout` as a server grants them: the timeout never holds a request past `maxPoll`. */
18
21
  export const executeOptionsOf = (body, maxPoll) => {
@@ -23,11 +26,82 @@ export const executeOptionsOf = (body, maxPoll) => {
23
26
  const asked = Number(body.timeout ?? DEFAULT_COMMIT_TIMEOUT);
24
27
  return { wait: true, timeout: Math.min(Number.isFinite(asked) && asked > 0 ? asked : DEFAULT_COMMIT_TIMEOUT, ceiling) };
25
28
  };
29
+ /**
30
+ * Refuse an execution outside the access's `writes`: the card it acts on (a specification through
31
+ * its parent card), or every parent a create names, must be one {@link writableIn} admits. A create
32
+ * of a project is `grants.createProjects`' alone. A card or parent this scope cannot see is left to
33
+ * the executor, which answers it as absent.
34
+ *
35
+ * @throws {PlanningForbidden}
36
+ */
37
+ export const assertExecutionWrites = async (facade, exec, access) => {
38
+ if (access?.writes == null) {
39
+ return;
40
+ }
41
+ const admitted = async (card) => {
42
+ if (writableIn(access, card)) {
43
+ return true;
44
+ }
45
+ if (!isSpecification(card) || card.parent == null) {
46
+ return false;
47
+ }
48
+ const parent = await facade.cards.load(card.parent);
49
+ return parent == null || writableIn(access, parent);
50
+ };
51
+ if (exec.action === TransitionAction.Create && exec.card != null && typeof exec.card === 'object') {
52
+ if (exec.card.kind === WorkcardKind.Project) {
53
+ return;
54
+ }
55
+ const parents = normalizeParents(exec.card.parent, exec.card.parents);
56
+ if (parents.length === 0) {
57
+ throw new PlanningForbidden('writes:root');
58
+ }
59
+ for (const id of parents) {
60
+ const parent = await facade.cards.load(id);
61
+ if (parent != null && !await admitted(parent)) {
62
+ throw new PlanningForbidden(`writes:${id}`);
63
+ }
64
+ }
65
+ return;
66
+ }
67
+ if (typeof exec.card === 'string' && exec.card !== '') {
68
+ const card = await facade.cards.load(exec.card);
69
+ if (card != null && !await admitted(card)) {
70
+ throw new PlanningForbidden(`writes:${exec.card}`);
71
+ }
72
+ }
73
+ };
74
+ /**
75
+ * The access a wire execution needs. `writes` first ({@link assertExecutionWrites}); then the
76
+ * grants: `createProjects` for a project create (its primary parent, or the root),
77
+ * `deleteProjects` for the delete of a project card. A card this scope cannot see is left to the
78
+ * executor, which answers it as absent.
79
+ *
80
+ * @throws {PlanningForbidden}
81
+ */
82
+ export const assertExecutionGranted = async (facade, exec, access) => {
83
+ await assertExecutionWrites(facade, exec, access);
84
+ if (access?.grants == null) {
85
+ return;
86
+ }
87
+ if (exec.action === TransitionAction.Create && typeof exec.card === 'object' && exec.card?.kind === WorkcardKind.Project) {
88
+ assertGranted(access, 'createProjects', normalizeParents(exec.card.parent, exec.card.parents)[0]);
89
+ return;
90
+ }
91
+ if (exec.action === TransitionAction.Delete && typeof exec.card === 'string') {
92
+ const card = await facade.cards.load(exec.card);
93
+ if (card != null && isProject(card)) {
94
+ assertGranted(access, 'deleteProjects', card.id);
95
+ }
96
+ }
97
+ };
26
98
  /** The one write route. */
27
99
  export const executePlanning = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
28
- const facade = await handlerFacade(ctx, req, opts);
100
+ const { facade, access } = await handlerScopeOf(ctx, req, opts);
29
101
  const body = (req.body ?? {});
30
- const receipt = await facade.execute(wireExecution(body, facade.scope), executeOptionsOf(body, opts?.maxPoll ?? MAX_COMMIT_POLL));
102
+ const exec = wireExecution(body, facade.scope);
103
+ await assertExecutionGranted(facade, exec, access);
104
+ const receipt = await facade.execute(exec, executeOptionsOf(body, opts?.maxPoll ?? MAX_COMMIT_POLL));
31
105
  return receipt.card !== undefined
32
106
  ? { transition: receipt.transition, card: receipt.card }
33
107
  : { transition: receipt.transition };
@@ -1 +1 @@
1
- {"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/actions/execute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAK9F,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAE5D;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAoB,EAAE,KAAoB,EAAuB,EAAE;IAC/F,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAA;IACvE,IAAI,IAAI,CAAC,MAAM,KAAK,gBAAgB,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAClG,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,IAAI,CAAA;QACrD,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,MAAM,CAAA;QAC/C,IAAI,CAAC,IAAI,GAAG,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;IACxE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED,kGAAkG;AAClG,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,IAA8C,EAAE,OAAe,EAAkB,EAAE;IAClH,MAAM,OAAO,GAAG,OAAO,GAAG,IAAI,CAAA;IAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QACvB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,sBAAsB,CAAC,CAAA;IAC5D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,sBAAsB,EAAE,OAAO,CAAC,EAAE,CAAA;AACzH,CAAC,CAAA;AAED,2BAA2B;AAC3B,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,QAAsC,EAAE,IAA6B,EACR,EAAE,CAC/D,QAAQ,EAAW,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,IAAoC,EAAE;IAC7G,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;IAClD,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAmB,CAAA;IAC/C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,OAAO,CAClC,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,IAAI,eAAe,CAAC,CAC5F,CAAA;IAED,OAAO,OAAO,CAAC,IAAI,KAAK,SAAS;QAC/B,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE;QACxD,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAA;AACxC,CAAC,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/actions/execute.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,30 @@
1
1
  import { handlers } from '@owlmeans/server-api';
2
- import type { PlanningProtocols } from '@owlmeans/planning';
2
+ import type { PlanningDefinitions, PlanningProtocols, SchemaDefineReply, SchemaDefineRequest } from '@owlmeans/planning';
3
3
  import type { Context, PlanningHandlerOptions } from '../types.js';
4
- /** The schema bundle the service holds — what a client loads to answer `can()` with no round trip. */
5
- export declare const listSchemas: (protocol: PlanningProtocols['schema']['list'], opts?: PlanningHandlerOptions) => ReturnType<ReturnType<typeof handlers<Context>>['request']>;
4
+ type RequestHandler = ReturnType<ReturnType<typeof handlers<Context>>['request']>;
5
+ /**
6
+ * The schema bundle — what a client loads to answer `can()` with no round trip. Scoped like every
7
+ * other leaf (no organization, no bundle). Where the store holds data-defined schemas it is the
8
+ * resolved layer of the organization, or of the project the `project` query names (a project this
9
+ * scope can see); otherwise the service's code bundle.
10
+ *
11
+ * @throws {WorkcardNotFound} for a project this scope cannot see
12
+ */
13
+ export declare const listSchemas: (protocol: PlanningProtocols['schema']['list'], opts?: PlanningHandlerOptions) => RequestHandler;
14
+ /**
15
+ * Apply one `schema.define` request to a facade's definitions: the declarations in the request's
16
+ * mode (flows before types), then the retirements — answering what was written and the layer as it
17
+ * resolves afterwards.
18
+ */
19
+ export declare const applySchemaRequest: (definitions: PlanningDefinitions, request: SchemaDefineRequest) => Promise<SchemaDefineReply>;
20
+ /**
21
+ * The write of data-defined types and flows. A project layer needs the project in the access's
22
+ * `writes` when the resolver answers it; then `grants.defineSchemas` gates when the resolver answers
23
+ * grants (the organization-wide layer needs `true`, a project layer its id).
24
+ *
25
+ * @throws {PlanningUnsupported} where the store holds no data-defined schemas
26
+ * @throws {PlanningForbidden | SchemaConflict | SchemaSealed | SchemaInvalid | SchemaInUse | WorkcardNotFound}
27
+ */
28
+ export declare const defineSchemas: (protocol: NonNullable<PlanningProtocols['schema']['define']>, opts?: PlanningHandlerOptions) => RequestHandler;
29
+ export {};
6
30
  //# sourceMappingURL=schemas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/actions/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAC/C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE,sGAAsG;AACtG,eAAO,MAAM,WAAW,aACZ,iBAAiB,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,sBAAsB,KAC3E,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAMzD,CAAA"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/actions/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAE/C,OAAO,KAAK,EACV,mBAAmB,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAC/E,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlE,KAAK,cAAc,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;AAKjF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,aACZ,iBAAiB,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,sBAAsB,KAC3E,cAQE,CAAA;AAEL;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,gBAChB,mBAAmB,WAAW,mBAAmB,KAC7D,OAAO,CAAC,iBAAiB,CAyB3B,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,aACd,WAAW,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,SAAS,sBAAsB,KAC1F,cAaE,CAAA"}