@owlmeans/server-planning 0.1.18-rc.12 → 0.1.18-rc.14

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 (107) hide show
  1. package/README.md +33 -11
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-planning/SKILL.md +138 -16
  4. package/build/actions/execute.d.ts +10 -2
  5. package/build/actions/execute.d.ts.map +1 -1
  6. package/build/actions/execute.js +28 -4
  7. package/build/actions/execute.js.map +1 -1
  8. package/build/actions/schemas.d.ts +26 -3
  9. package/build/actions/schemas.d.ts.map +1 -1
  10. package/build/actions/schemas.js +63 -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 +392 -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/resolve.d.ts +16 -3
  37. package/build/executor/resolve.d.ts.map +1 -1
  38. package/build/executor/resolve.js +68 -11
  39. package/build/executor/resolve.js.map +1 -1
  40. package/build/executor/validate.d.ts.map +1 -1
  41. package/build/executor/validate.js +11 -11
  42. package/build/executor/validate.js.map +1 -1
  43. package/build/executor.d.ts.map +1 -1
  44. package/build/executor.js +2 -3
  45. package/build/executor.js.map +1 -1
  46. package/build/facade.d.ts +14 -1
  47. package/build/facade.d.ts.map +1 -1
  48. package/build/facade.js +92 -18
  49. package/build/facade.js.map +1 -1
  50. package/build/helper.d.ts +3 -2
  51. package/build/helper.d.ts.map +1 -1
  52. package/build/helper.js +7 -3
  53. package/build/helper.js.map +1 -1
  54. package/build/index.d.ts +1 -0
  55. package/build/index.d.ts.map +1 -1
  56. package/build/index.js +1 -0
  57. package/build/index.js.map +1 -1
  58. package/build/service.d.ts +10 -1
  59. package/build/service.d.ts.map +1 -1
  60. package/build/service.js +9 -0
  61. package/build/service.js.map +1 -1
  62. package/build/store/composite.d.ts +1 -0
  63. package/build/store/composite.d.ts.map +1 -1
  64. package/build/store/composite.js +3 -0
  65. package/build/store/composite.js.map +1 -1
  66. package/build/store/fold.d.ts +3 -1
  67. package/build/store/fold.d.ts.map +1 -1
  68. package/build/store/fold.js +8 -2
  69. package/build/store/fold.js.map +1 -1
  70. package/build/store/memory.d.ts +12 -0
  71. package/build/store/memory.d.ts.map +1 -1
  72. package/build/store/memory.js +0 -0
  73. package/build/store/memory.js.map +1 -1
  74. package/build/store/types.d.ts +15 -1
  75. package/build/store/types.d.ts.map +1 -1
  76. package/build/types.d.ts +37 -0
  77. package/build/types.d.ts.map +1 -1
  78. package/build/utils/scope.d.ts +28 -1
  79. package/build/utils/scope.d.ts.map +1 -1
  80. package/build/utils/scope.js +54 -15
  81. package/build/utils/scope.js.map +1 -1
  82. package/package.json +18 -11
  83. package/src/actions/execute.ts +36 -8
  84. package/src/actions/schemas.ts +81 -9
  85. package/src/conformance/assert.ts +59 -0
  86. package/src/conformance/cases.ts +483 -0
  87. package/src/conformance/fixtures.ts +168 -0
  88. package/src/conformance/index.ts +9 -0
  89. package/src/consts.ts +6 -0
  90. package/src/definitions.ts +282 -0
  91. package/src/executor/resolve.ts +84 -14
  92. package/src/executor/validate.ts +13 -11
  93. package/src/executor.ts +2 -3
  94. package/src/facade.ts +106 -19
  95. package/src/helper.ts +7 -3
  96. package/src/index.ts +1 -0
  97. package/src/service.ts +23 -1
  98. package/src/store/composite.ts +3 -0
  99. package/src/store/fold.ts +8 -2
  100. package/src/store/memory.ts +0 -0
  101. package/src/store/types.ts +16 -1
  102. package/src/types.ts +41 -0
  103. package/src/utils/scope.ts +75 -16
  104. package/tests/access.spec.ts +160 -0
  105. package/tests/conformance.spec.ts +47 -0
  106. package/tests/create-example.spec.ts +202 -0
  107. package/tests/entrypoints.spec.ts +5 -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.12 @owlmeans/planning@^0.1.18-rc.10 ajv
13
+ bun add @owlmeans/server-planning@^0.1.18-rc.14 @owlmeans/planning@^0.1.18-rc.12 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.
@@ -105,18 +108,33 @@ const cards = await planningFor(ctx, req, { channel: 'web' }).cards.list({ paren
105
108
  - Handlers: `servePlanningEntrypoints`, `planningFor`, `listSchemas`, `listCards`,
106
109
  `summarizeCards`, `getCard`, `listCardTransitions`, `listCardSpecifications`, `getSpecification`,
107
110
  `listSpecificationRevisions`, `listLinks`, `getTransition`, `executePlanning`, `wireExecution`,
108
- `executeOptionsOf`, `getCommit`, `watchCommits`, `PlanningHandlerOptions`, `PlanningScopeExtractor`
109
- - Scope: `scopeOf`, `actorOf`, `handlerFacade`, `planningServiceOf`, `concealed`, `assertScope`,
110
- `notFoundOf`, `clampSeconds`
111
+ `executeOptionsOf`, `assertExecutionGranted`, `getCommit`, `watchCommits`, `defineSchemas`,
112
+ `applySchemaRequest`, `PlanningHandlerOptions`, `PlanningScopeExtractor`, `PlanningAccessResolver`,
113
+ `PlanningAccess`, `PlanningAccessGrants`, `PlanningGrant`
114
+ - Scope: `scopeOf`, `accessScopeOf`, `actorOf`, `handlerFacade`, `handlerScopeOf`, `assertGranted`,
115
+ `planningServiceOf`, `concealed`, `assertScope`, `notFoundOf`, `clampSeconds`, `projectCriteriaOf`
116
+ - Data-defined schemas: `makeDefinitions`, `makeSchemaViews`, `SchemaViews`, `SchemaViewsOptions`
111
117
  - Projection: `makeProjectionProcessor`, `planningQueueHooks`, `ProjectionOptions`
112
- - Constants: `DEFAULT_ALIAS`, `MEMORY_STORE_ALIAS`, `DEFAULT_COMMIT_MEMORY`, `COMMIT_POLL_LADDER`
118
+ - Constants: `DEFAULT_ALIAS`, `MEMORY_STORE_ALIAS`, `DEFAULT_COMMIT_MEMORY`, `COMMIT_POLL_LADDER`,
119
+ `DEFAULT_SCHEMA_VIEWS`
113
120
  - `@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`
121
+ `revisionsFromLog`, `commitEventOf`, `makeCommitHub`, `makeCompositeStore`, `wantsSpecifications`,
122
+ and the types `BindablePlanningStore`, `CommitListener`, `CommitHub`, `CommitHubOptions`,
123
+ `FoldOptions`, `FoldResult`, `MemoryPlanningStore`, `MemoryPlanningStoreOptions`, `StoreRoute`
124
+ - `@owlmeans/server-planning/conformance`: `planningConformance`, `conformanceCasesFor`,
125
+ `planningConformancePlugin`, `conformanceClock`, `ConformanceFailure`, `check`, `same`,
126
+ `sameSet`, `rejects`, the library fixtures (`LIBRARY`, `createBranch`, `createBook`, `transit`)
127
+ and the types `ConformanceCase`, `ConformanceSubject`, `ConformanceCapability`
117
128
 
118
129
  ## Common pitfalls
119
130
 
131
+ - A create's draft goes under `card` — `draft` is refused as `planning:malformed:create-without-draft`.
132
+ - `{ wait: true }` is the SECOND argument of `execute`; inside the execution it is ignored and the
133
+ receipt returns before the commit.
134
+ - A type or flow declaration has no `name` or `key`: the display name is `label`, the key is `type`
135
+ (a type) or `id` (a flow). `definitions.define` refuses anything else with `SchemaInvalid`; a code
136
+ plugin's registry does not check, so keep the literals typed.
137
+
120
138
  - `after` hooks run where the transition is FOLDED, once per commit — register the same plugins in
121
139
  every folding process, and never run hooks from a commit-bus subscription.
122
140
  - The memory store is per-process heap: wrong for multiple processes or restarts.
@@ -126,11 +144,15 @@ const cards = await planningFor(ctx, req, { channel: 'web' }).cards.list({ paren
126
144
  - `expectSeq` compares against `head`; a stale one is `WorkcardConflict`, re-read and retry.
127
145
  - Call `appendPlanningService` before any `ensurePlanningService`, or the appended host replaces
128
146
  what was registered on the default one.
147
+ - `opts.access` makes the resolver the only source of the organization; a `grants` object refuses
148
+ every flag it leaves out.
149
+ - A durable store is not done until it passes `@owlmeans/server-planning/conformance`.
129
150
 
130
151
  ## Related packages
131
152
 
132
153
  - `@owlmeans/planning` — records, flows, fold, query language, protocol tree, models
133
154
  - `@owlmeans/client-planning` — the remote facade and state mirror
155
+ - `@owlmeans/planning-postgres` — the durable Postgres store
134
156
  - `@owlmeans/server-job`, `@owlmeans/queue` — job feeds and the projection queue
135
157
 
136
158
  <!-- owlmeans:agent-guidance:start -->
@@ -141,7 +163,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
141
163
  your project's skill store (`.agents/skills/`):
142
164
 
143
165
  ```sh
144
- npx @owlmeans/agent-skills@^0.1.18-rc.40
166
+ npx @owlmeans/agent-skills@^0.1.18-rc.42
145
167
  ```
146
168
 
147
169
  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.12",
5
- "generatedAt": "2026-09-25T21:19:37.942Z",
4
+ "version": "0.1.18-rc.14",
5
+ "generatedAt": "2026-09-29T08:01:58.111Z",
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.12"` in `dependencies` (`ajv` is a peer)
11
+ **Install:** `"@owlmeans/server-planning": "^0.1.18-rc.14"` 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,16 @@ 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
+ | `servePlanningEntrypoints(protocols, opts?)` | One binding per protocol of a `makePlanningProtocols` tree (`schema.define` too when declared) |
28
28
  | `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 |
29
+ | `PlanningHandlerOptions` | `{ service?, event?, maxPoll?, scope?(req, ctx), access?(req, ctx) }` |
30
+ | `PlanningAccessResolver`, `PlanningAccess`, `PlanningAccessGrants`, `PlanningGrant` | The optional access decision a hosting app supplies |
31
+ | `listCards` … `executePlanning`, `getCommit`, `watchCommits`, `listSchemas`, `defineSchemas`, `applySchemaRequest` | The handlers, to bind one by hand |
32
+ | `scopeOf(req, extra?)`, `accessScopeOf(req, access, extra?)`, `handlerScopeOf`, `assertGranted`, `actorOf(req)`, `concealed(run)`, `clampSeconds` | Scope and security helpers |
33
+ | `makeDefinitions(runtime, facade)`, `makeSchemaViews(opts)`, `projectCriteriaOf(projects)` | Data-defined schemas and the project narrowing |
32
34
  | `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 |
35
+ | `./store`: `makeMemoryPlanningStore`, `foldPending`, `failPending`, `revisionsFromLog`, `commitEventOf`, `makeCommitHub`, `makeCompositeStore`, `wantsSpecifications` | What stores are built from |
36
+ | `./conformance`: `planningConformance`, `conformanceCasesFor`, `planningConformancePlugin`, `conformanceClock`, `ConformanceFailure` | The store conformance suite, runner-agnostic |
34
37
 
35
38
  ## Wiring
36
39
 
@@ -58,8 +61,68 @@ context.registerEntrypoints(servePlanningEntrypoints(planningProtocols, {
58
61
  `ensurePlanningService(ctx).use(plugin)` works from `makeContext`, before `init()`.
59
62
  - Call `appendPlanningService` BEFORE any `ensurePlanningService`: `ensure` registers a default
60
63
  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.
64
+ - **The facade:** `ctx.service<PlanningHostService>(PLANNING_SERVICE).for({ entityId, profileId?,
65
+ channel?, actor? })` → a `PlanningFacade` with no scope argument on any method. That form works
66
+ however the service was registered. `ctx.planning()` is only a shortcut that `appendPlanningService`
67
+ / `appendPostgresPlanning` install (typed `WithPlanningService`); a context that registered
68
+ `makePlanningService()` / `makePostgresPlanningService()` as a plain service — a generated target's
69
+ `services/planning.ts` — has no `ctx.planning` at all. In a request handler use
70
+ `planningFor(ctx, req)`: the entity comes from `requireEntityKey(req)`, never the token.
71
+
72
+ ## Creating and reading a card
73
+
74
+ ```ts
75
+ import { IntrinsicStatus, PLANNING_SERVICE, TransitionAction, WorkcardKind } from '@owlmeans/planning'
76
+ import type { PlanningHostService } from '@owlmeans/server-planning'
77
+
78
+ const planning = ctx.service<PlanningHostService>(PLANNING_SERVICE).for({ entityId, profileId })
79
+
80
+ const shed = await planning.execute({
81
+ action: TransitionAction.Create,
82
+ card: { kind: WorkcardKind.Project, type: 'shed:shed', title: 'Maple Street shed' },
83
+ }, { wait: true })
84
+ const drill = await planning.execute({
85
+ action: TransitionAction.Create,
86
+ card: { kind: WorkcardKind.Card, type: 'shed:tool', parent: shed.card!.id!, title: 'Cordless drill',
87
+ fields: { brand: 'Acme' } },
88
+ }, { wait: true }) // drill.card — the committed record, status 'available'
89
+
90
+ await planning.cards.get(drill.card!.id!)
91
+ await planning.cards.list({ parent: shed.card!.id!, type: 'shed:tool' })
92
+ await planning.execute({ action: TransitionAction.Transit, card: drill.card!.id!, transition: 'lend' }, { wait: true })
93
+
94
+ // A card type and a flow as DATA, scoped to this shed — `definitions` exists only where the store
95
+ // keeps schemas (planning-postgres; the memory store with `schemas: true`), and the shed's cards may
96
+ // use them when its project type says `scopedCardTypes: true` (or lists the type in `cardTypes`).
97
+ await planning.definitions!.define({
98
+ flows: [{
99
+ id: 'shed:repair', version: 1, label: 'Repair',
100
+ statuses: [
101
+ { key: 'reported', intrinsic: IntrinsicStatus.Planned, initial: true, label: 'Reported' },
102
+ { key: 'fixed', intrinsic: IntrinsicStatus.Closed, terminal: true, label: 'Fixed' },
103
+ ],
104
+ transitions: [{ name: 'fix', from: ['reported'], to: 'fixed', label: 'Mark fixed', explicit: true }],
105
+ }],
106
+ types: [{
107
+ type: 'shed:repair', kind: WorkcardKind.Card, version: 1, label: 'Repair ticket',
108
+ fields: { type: 'object', properties: { tool: { type: 'string' } }, additionalProperties: false },
109
+ flows: ['shed:repair'], specifications: [],
110
+ }],
111
+ }, { project: shed.card!.id! })
112
+ ```
113
+
114
+ | Wrong | What happens | Right |
115
+ |---|---|---|
116
+ | `execute({ action, draft: { … } })` | refused: `planning:malformed:create-without-draft` | the draft goes under `card` |
117
+ | `execute({ …, wait: true })` | in process the key is ignored: the receipt returns before the commit, with no `card` | `execute({ … }, { wait: true })` — only the wire body (`ExecuteRequest`) carries `wait` inside |
118
+ | `ctx.planning().for(…)` in a target | `ctx.planning is not a function` | `ctx.service<PlanningHostService>(PLANNING_SERVICE).for(…)` |
119
+ | `{ 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 |
120
+ | `action: 'create'` | a type error — `TransitionAction` is an enum | `action: TransitionAction.Create` |
121
+
122
+ Every wrong form is a compile error against the typed facade (`PlanningFacade`,
123
+ `TransitionExecution`, `WorkcardTypeSchema`); it reaches run time only through a helper or a context
124
+ typed `any`, so never type one that way. `tests/create-example.spec.ts` runs this example and each
125
+ wrong form as written.
63
126
 
64
127
  ## The executor, step by step
65
128
 
@@ -69,9 +132,15 @@ Nothing is appended before step 11 — every refusal leaves the log untouched.
69
132
  2. **Scope** — `entityId` must be present.
70
133
  3. **Idempotency** — a `key` already in the entity's log answers ITS receipt, before any
71
134
  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.
135
+ 4. **Resolve** — the card (`WorkcardNotFound`; another entity's card is `PlanningScopeMismatch`;
136
+ one outside the scope's `projects` is `WorkcardNotFound`), its type and flow, every draft parent
137
+ (`ParentNotFound` — loaded through the facade, so an invisible parent is absent) and the parent
138
+ project's `cardTypes`/`projectTypes` (`CardTypeNotAllowed`), the specification slot. Where the
139
+ default store holds data-defined schemas, the card's project is found first and everything
140
+ after reads that project's resolved layer (`Resolved.schemas`): a data-defined type of a
141
+ project with `scopedCardTypes` is admitted, a retired type creates nothing
142
+ (`UnknownWorkcardType('retired:…')`). Without the port `Resolved.schemas` IS the code registry
143
+ and the order is unchanged.
75
144
  5. **`before` chain** — in plugin order; re-resolved once when it changed the card, type or parent.
76
145
  6. **Validate** — shape (`planning:malformed:*`), the flow rule (`IllegalTransition`), immutables
77
146
  (`planning:immutable:*`), merged `fields` (`FieldsInvalid`), labels (`LabelNotAllowed`), a moved
@@ -125,11 +194,15 @@ the store; `bind` replaces the listener, so a store has exactly one.
125
194
 
126
195
  ## The memory store
127
196
 
128
- `makeMemoryPlanningStore({ sync?, ids?, now?, seed?, onCommitted?, alias? })`
197
+ `makeMemoryPlanningStore({ sync?, ids?, now?, seed?, onCommitted?, alias?, schemas? })`
129
198
 
130
199
  - Cards and projects in one map, specifications in their own, relationships in a third; every read
131
200
  goes through the `@owlmeans/resource` query engine. A card list includes specifications only when
132
- the criteria asks for `kind: specification` or a `category`.
201
+ the criteria asks for `kind: specification` or a `category` (`wantsSpecifications`, the rule every
202
+ store routes by).
203
+ - `schemas: true` adds the data-defined schema port (records per layer, a revision per
204
+ organization, synchronous `watch`); off by default, so a plain memory store resolves everything
205
+ in code exactly as before.
133
206
  - `sync` (default) folds, publishes and runs `committed` inside `project()`, serialized per card.
134
207
  `sync: false` folds nothing until `flush(card?)` — a queued store's shape, for tests that need a
135
208
  commit to stay pending.
@@ -155,7 +228,20 @@ the store; `bind` replaces the listener, so a store has exactly one.
155
228
  no card to advance, and a store that refuses the advancing write stops the fold with `followUp`,
156
229
  leaving the rest pending for a retry. `revisionsFromLog` replays past failed transitions the same
157
230
  way.
231
+ - A transactional store passes `FoldOptions.unit` — it wraps each transition's writes (a
232
+ savepoint), so a write the database refuses is undone whole and the fold marks that transition
233
+ failed and goes past it. `foldPending` folds PAST a failure it produces; a row that is ALREADY
234
+ failed at the cursor, or a gap in the seqs, is the store's to step over before calling it
235
+ (`planning-postgres` does it in a prelude).
158
236
  - `failPending` is what a dead projection job must do, or waiters hang to their timeout.
237
+ - The data-defined schema port (`schemas`) is optional; `makeCompositeStore` passes the default
238
+ store's through. A store that has it must purge a project's layer with the project.
239
+ - **Run the conformance suite.** `@owlmeans/server-planning/conformance` exports
240
+ `planningConformance` — named cases with no test runner inside (each throws
241
+ `ConformanceFailure`) — and `conformanceCasesFor(store)`, which drops the data-defined cases for a
242
+ store without the port. Boot a service with `planningConformancePlugin` and `conformanceClock()`
243
+ and hand each case `{ service, store, facade }`; every case works in its own organization, so one
244
+ boot serves them all.
159
245
  - `makeCommitHub({ status, remember?, ladder? })` is the `CommitSource`: feed `publish` from the bus,
160
246
  answer `status` from transition rows. `wait` subscribes first, polls once, then climbs the ladder.
161
247
  - `revisionsFromLog(transitions, limit?)` answers `specs.revisions`.
@@ -180,24 +266,60 @@ mapping external records (`mappers`) belongs to that plugin's own store adapter.
180
266
 
181
267
  ## Scope and security
182
268
 
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.
269
+ - Without `opts.access`: `entityId` is `requireEntityKey(req)`; `opts.scope(req, ctx)` adds a
270
+ `channel` or `service` and can never replace the entity. `actor` and a create's `createdBy` come
271
+ from the request, never the body.
186
272
  - Another entity's card, specification or transition answers `WorkcardNotFound` — the same as an
187
273
  absent id — on reads and writes alike (`concealed`).
188
274
  - Queries arrive in their wire form and are decoded with `decode*Query` before `criteriaOf`, which
189
275
  always adds the scope's `entityId`.
190
276
 
277
+ ### The access resolver
278
+
279
+ `opts.access: (req, ctx) => Promise<{ entityId, projects?, grants? }>` is the hosting app's
280
+ decision per request; a throw is the request's answer (an `AuthForbidden` answers 403).
281
+
282
+ - `entityId` is the resolver's — never the token's, never `opts.scope`'s.
283
+ - `projects` becomes `PlanningScope.projects`, which the server facade enforces everywhere: the
284
+ projects themselves and every card whose `parents` name one are visible (a specification
285
+ through its parent card); anything else reads as absent (`WorkcardNotFound`, `null`, an empty
286
+ list), a create under an invisible parent is `ParentNotFound` (the executor's own parent load),
287
+ lists, counts and summaries are AND'd with `projectCriteriaOf`, links and transitions are
288
+ filtered by `project` (and post-filtered, for a store that ignores the field), commit
289
+ subscriptions drop events outside the set and a status or wait refuses them.
290
+ - `grants` gates the stock handlers' writes: `createProjects` (a project create — `true` for the
291
+ root, a list names the parent projects), `deleteProjects` (the delete of a project card — a list
292
+ names the projects), `defineSchemas` (`schema.define` — `true` for the organization's layer, a
293
+ list names project layers). A `grants` object refuses every flag it leaves out with
294
+ `PlanningForbidden` (403); no `grants` gates nothing. In-process facade writes are not gated.
295
+
296
+ ## Data-defined types and flows
297
+
298
+ Where the default store has the schema port, `runtime.schemasFor(entityId, project?)` answers the
299
+ resolved layer (`resolveScopedBundle` over the code bundle and the organization's and project's
300
+ records), cached per (organization, project) and keyed by the store's `revision()` and the plugin
301
+ registry's version — a write anywhere moves the revision the next lookup reads, so a cached layer is
302
+ never stale; a store's `watch` evicts early. Without the port it answers the code registry itself.
303
+ `makeDefinitions` gives the facade `definitions`: every write checks the project (a project card the
304
+ scope can see), the seal, the closed-form checks (a type's flows resolve in its layer, counting flows
305
+ written beside it), then the store's compare-and-set. `retire` of a flow refuses with `SchemaInUse`
306
+ while a live type of the layer — or of any project layer, for an organization record — still runs
307
+ it. `facade.model(card)` builds over the layer of the card's own project, and `schema.list` answers
308
+ the scoped bundle (`?project=`) where `definitions` exists, the code bundle otherwise.
309
+
191
310
  ## Testing
192
311
 
193
312
  Category A. Build one real context with `makeBasicContext` + `appendPlanningService({ store:
194
313
  makeMemoryPlanningStore({ now }), plugins: [fixtures] })`, and run a handler through
195
314
  `handler.bind({ ref: { ctx } })(req, res)` with a request carrying `auth` and `entity`. A monotonic
196
315
  `now` keeps log order deterministic; `sync: false` + `flush()` pins pending and hook-count cases.
316
+ `tests/conformance.spec.ts` runs the conformance suite against the memory store with and without
317
+ `schemas: true`; a durable store's own tests run the same cases against a real database.
197
318
 
198
319
  ## Related
199
320
 
200
321
  - `planning` — records, flows, the fold, the query language, the protocol tree, the models
201
322
  - `client-planning` — the remote facade and the state mirror that talk to these handlers
323
+ - `planning-postgres` — the durable Postgres store built from these pieces
202
324
  - `server-job`, `queue` — the declare/serve/feed pattern and the projection queue
203
325
  - `resource` — the criteria engine every memory read goes through
@@ -1,6 +1,6 @@
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
6
  * `createdBy` is the authenticated subject, whatever the body claimed.
@@ -8,6 +8,14 @@ import type { Context, PlanningHandlerOptions } from '../types.js';
8
8
  export declare const wireExecution: (body: ExecuteRequest, scope: PlanningScope) => TransitionExecution;
9
9
  /** `wait`/`timeout` as a server grants them: the timeout never holds a request past `maxPoll`. */
10
10
  export declare const executeOptionsOf: (body: Pick<ExecuteRequest, 'wait' | 'timeout'>, maxPoll: number) => ExecuteOptions;
11
+ /**
12
+ * The grants a wire execution needs: `createProjects` for a project create (its primary parent,
13
+ * or the root), `deleteProjects` for the delete of a project card. A card this scope cannot see is
14
+ * left to the executor, which answers it as absent.
15
+ *
16
+ * @throws {PlanningForbidden}
17
+ */
18
+ export declare const assertExecutionGranted: (facade: PlanningFacade, exec: TransitionExecution, access?: PlanningAccess) => Promise<void>;
11
19
  /** The one write route. */
12
20
  export declare const executePlanning: (protocol: PlanningProtocols['execute'], opts?: PlanningHandlerOptions) => ReturnType<ReturnType<typeof handlers<Context>>['request']>;
13
21
  //# 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;AAI/C,OAAO,KAAK,EACV,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAEtG,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAGlF;;;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;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,WACzB,cAAc,QAAQ,mBAAmB,WAAW,cAAc,KACzE,OAAO,CAAC,IAAI,CAcd,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,6 +1,6 @@
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, MAX_COMMIT_POLL, normalizeParents, TransitionAction, WorkcardKind, } from '@owlmeans/planning';
3
+ import { assertGranted, concealed, handlerScopeOf } from '../utils/index.js';
4
4
  /**
5
5
  * What a wire execution may say. `actor` is dropped — the scope names the writer — and a create's
6
6
  * `createdBy` is the authenticated subject, whatever the body claimed.
@@ -23,11 +23,35 @@ export const executeOptionsOf = (body, maxPoll) => {
23
23
  const asked = Number(body.timeout ?? DEFAULT_COMMIT_TIMEOUT);
24
24
  return { wait: true, timeout: Math.min(Number.isFinite(asked) && asked > 0 ? asked : DEFAULT_COMMIT_TIMEOUT, ceiling) };
25
25
  };
26
+ /**
27
+ * The grants a wire execution needs: `createProjects` for a project create (its primary parent,
28
+ * or the root), `deleteProjects` for the delete of a project card. A card this scope cannot see is
29
+ * left to the executor, which answers it as absent.
30
+ *
31
+ * @throws {PlanningForbidden}
32
+ */
33
+ export const assertExecutionGranted = async (facade, exec, access) => {
34
+ if (access?.grants == null) {
35
+ return;
36
+ }
37
+ if (exec.action === TransitionAction.Create && typeof exec.card === 'object' && exec.card?.kind === WorkcardKind.Project) {
38
+ assertGranted(access, 'createProjects', normalizeParents(exec.card.parent, exec.card.parents)[0]);
39
+ return;
40
+ }
41
+ if (exec.action === TransitionAction.Delete && typeof exec.card === 'string') {
42
+ const card = await facade.cards.load(exec.card);
43
+ if (card != null && isProject(card)) {
44
+ assertGranted(access, 'deleteProjects', card.id);
45
+ }
46
+ }
47
+ };
26
48
  /** The one write route. */
27
49
  export const executePlanning = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
28
- const facade = await handlerFacade(ctx, req, opts);
50
+ const { facade, access } = await handlerScopeOf(ctx, req, opts);
29
51
  const body = (req.body ?? {});
30
- const receipt = await facade.execute(wireExecution(body, facade.scope), executeOptionsOf(body, opts?.maxPoll ?? MAX_COMMIT_POLL));
52
+ const exec = wireExecution(body, facade.scope);
53
+ await assertExecutionGranted(facade, exec, access);
54
+ const receipt = await facade.execute(exec, executeOptionsOf(body, opts?.maxPoll ?? MAX_COMMIT_POLL));
31
55
  return receipt.card !== undefined
32
56
  ? { transition: receipt.transition, card: receipt.card }
33
57
  : { 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,29 @@
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. Gated by `grants.defineSchemas` when the access
22
+ * resolver answers grants (the organization-wide layer needs `true`, a project layer its id).
23
+ *
24
+ * @throws {PlanningUnsupported} where the store holds no data-defined schemas
25
+ * @throws {PlanningForbidden | SchemaConflict | SchemaSealed | SchemaInvalid | SchemaInUse | WorkcardNotFound}
26
+ */
27
+ export declare const defineSchemas: (protocol: NonNullable<PlanningProtocols['schema']['define']>, opts?: PlanningHandlerOptions) => RequestHandler;
28
+ export {};
6
29
  //# 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;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,aACd,WAAW,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,SAAS,sBAAsB,KAC1F,cAUE,CAAA"}
@@ -1,9 +1,65 @@
1
1
  import { handlers } from '@owlmeans/server-api';
2
- import { planningServiceOf, scopeOf } from '../utils/index.js';
3
- /** The schema bundle the service holds — what a client loads to answer `can()` with no round trip. */
4
- export const listSchemas = (protocol, opts) => handlers().request(protocol, async (req, ctx) => {
5
- // Scoped like every other leaf: no organization, no bundle.
6
- scopeOf(req);
7
- return planningServiceOf(ctx, opts).schemas.bundle();
8
- });
2
+ import { PlanningUnsupported, SchemaWriteMode } from '@owlmeans/planning';
3
+ import { assertGranted, concealed, handlerScopeOf } from '../utils/index.js';
4
+ const text = (value) => value == null || value === '' ? undefined : `${Array.isArray(value) ? value[0] : value}`;
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 const listSchemas = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
14
+ const { facade } = await handlerScopeOf(ctx, req, opts);
15
+ if (facade.definitions == null) {
16
+ return facade.schemas.bundle();
17
+ }
18
+ return await facade.definitions.bundle(text(req.query?.project));
19
+ }));
20
+ /**
21
+ * Apply one `schema.define` request to a facade's definitions: the declarations in the request's
22
+ * mode (flows before types), then the retirements — answering what was written and the layer as it
23
+ * resolves afterwards.
24
+ */
25
+ export const applySchemaRequest = async (definitions, request) => {
26
+ const layer = request.project != null ? { project: request.project } : {};
27
+ const declarations = { types: request.types ?? [], flows: request.flows ?? [] };
28
+ const records = [];
29
+ switch (request.mode ?? SchemaWriteMode.Define) {
30
+ case SchemaWriteMode.Put:
31
+ for (const flow of declarations.flows) {
32
+ records.push(await definitions.putFlow(flow, layer));
33
+ }
34
+ for (const type of declarations.types) {
35
+ records.push(await definitions.putType(type, layer));
36
+ }
37
+ break;
38
+ case SchemaWriteMode.Seed:
39
+ records.push(...await definitions.seed(declarations, layer));
40
+ break;
41
+ default:
42
+ records.push(...await definitions.define(declarations, layer));
43
+ }
44
+ for (const key of request.retire ?? []) {
45
+ records.push(await definitions.retire(key.kind, key.key, layer));
46
+ }
47
+ return { records, bundle: await definitions.bundle(request.project) };
48
+ };
49
+ /**
50
+ * The write of data-defined types and flows. Gated by `grants.defineSchemas` when the access
51
+ * resolver answers grants (the organization-wide layer needs `true`, a project layer its id).
52
+ *
53
+ * @throws {PlanningUnsupported} where the store holds no data-defined schemas
54
+ * @throws {PlanningForbidden | SchemaConflict | SchemaSealed | SchemaInvalid | SchemaInUse | WorkcardNotFound}
55
+ */
56
+ export const defineSchemas = (protocol, opts) => handlers().request(protocol, async (req, ctx) => concealed(async () => {
57
+ const { facade, access } = await handlerScopeOf(ctx, req, opts);
58
+ const request = (req.body ?? {});
59
+ if (facade.definitions == null) {
60
+ throw new PlanningUnsupported('definitions');
61
+ }
62
+ assertGranted(access, 'defineSchemas', request.project);
63
+ return await applySchemaRequest(facade.definitions, request);
64
+ }));
9
65
  //# sourceMappingURL=schemas.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/actions/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AAG/C,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAE9D,sGAAsG;AACtG,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,QAA6C,EAAE,IAA6B,EACf,EAAE,CAC/D,QAAQ,EAAW,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;IACvD,4DAA4D;IAC5D,OAAO,CAAC,GAAG,CAAC,CAAA;IAEZ,OAAO,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAA;AACtD,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../src/actions/schemas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /** A conformance expectation that did not hold. Plain `Error`, so any test runner reports it. */
2
+ export declare class ConformanceFailure extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /** @throws {ConformanceFailure} */
6
+ export declare const check: (condition: unknown, message: string) => void;
7
+ /** Structural equality over JSON values. @throws {ConformanceFailure} */
8
+ export declare const same: (actual: unknown, expected: unknown, message: string) => void;
9
+ /** The same members in any order. @throws {ConformanceFailure} */
10
+ export declare const sameSet: (actual: readonly unknown[], expected: readonly unknown[], message: string) => void;
11
+ /**
12
+ * The promise rejects with an error of the class (matched by instance or by its registered type
13
+ * name, so a duplicate module copy counts). Answers the error.
14
+ *
15
+ * @throws {ConformanceFailure}
16
+ */
17
+ export declare const rejects: (run: Promise<unknown> | (() => Promise<unknown>), expected: {
18
+ typeName: string;
19
+ }, message: string) => Promise<Error>;
20
+ //# sourceMappingURL=assert.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert.d.ts","sourceRoot":"","sources":["../../src/conformance/assert.ts"],"names":[],"mappings":"AAEA,iGAAiG;AACjG,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAUD,mCAAmC;AACnC,eAAO,MAAM,KAAK,cAAe,OAAO,WAAW,MAAM,KAAG,IAI3D,CAAA;AAED,yEAAyE;AACzE,eAAO,MAAM,IAAI,WAAY,OAAO,YAAY,OAAO,WAAW,MAAM,KAAG,IAI1E,CAAA;AAED,kEAAkE;AAClE,eAAO,MAAM,OAAO,WAAY,SAAS,OAAO,EAAE,YAAY,SAAS,OAAO,EAAE,WAAW,MAAM,KAAG,IAGnG,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,OAAO,QACb,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,YAAY;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,WAAW,MAAM,KAChG,OAAO,CAAC,KAAK,CAYf,CAAA"}
@@ -0,0 +1,54 @@
1
+ import { sameValue } from '@owlmeans/planning';
2
+ /** A conformance expectation that did not hold. Plain `Error`, so any test runner reports it. */
3
+ export class ConformanceFailure extends Error {
4
+ constructor(message) {
5
+ super(`planning conformance: ${message}`);
6
+ this.name = 'ConformanceFailure';
7
+ }
8
+ }
9
+ const show = (value) => {
10
+ try {
11
+ return JSON.stringify(value);
12
+ }
13
+ catch {
14
+ return `${value}`;
15
+ }
16
+ };
17
+ /** @throws {ConformanceFailure} */
18
+ export const check = (condition, message) => {
19
+ if (condition !== true) {
20
+ throw new ConformanceFailure(message);
21
+ }
22
+ };
23
+ /** Structural equality over JSON values. @throws {ConformanceFailure} */
24
+ export const same = (actual, expected, message) => {
25
+ if (!sameValue(actual, expected)) {
26
+ throw new ConformanceFailure(`${message}: expected ${show(expected)}, got ${show(actual)}`);
27
+ }
28
+ };
29
+ /** The same members in any order. @throws {ConformanceFailure} */
30
+ export const sameSet = (actual, expected, message) => {
31
+ const sort = (values) => [...values].map(show).sort();
32
+ same(sort(actual), sort(expected), message);
33
+ };
34
+ /**
35
+ * The promise rejects with an error of the class (matched by instance or by its registered type
36
+ * name, so a duplicate module copy counts). Answers the error.
37
+ *
38
+ * @throws {ConformanceFailure}
39
+ */
40
+ export const rejects = async (run, expected, message) => {
41
+ try {
42
+ await (typeof run === 'function' ? run() : run);
43
+ }
44
+ catch (error) {
45
+ const type = error?.type;
46
+ if (error instanceof expected
47
+ || (typeof type === 'string' && type === expected.typeName)) {
48
+ return error;
49
+ }
50
+ throw new ConformanceFailure(`${message}: expected ${expected.typeName}, got ${show(error?.message ?? error)}`);
51
+ }
52
+ throw new ConformanceFailure(`${message}: expected ${expected.typeName}, nothing was thrown`);
53
+ };
54
+ //# sourceMappingURL=assert.js.map