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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) 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/cards.d.ts +3 -5
  5. package/build/actions/cards.d.ts.map +1 -1
  6. package/build/actions/cards.js +1 -1
  7. package/build/actions/cards.js.map +1 -1
  8. package/build/actions/execute.d.ts +24 -3
  9. package/build/actions/execute.d.ts.map +1 -1
  10. package/build/actions/execute.js +82 -8
  11. package/build/actions/execute.js.map +1 -1
  12. package/build/actions/schemas.d.ts +27 -5
  13. package/build/actions/schemas.d.ts.map +1 -1
  14. package/build/actions/schemas.js +67 -7
  15. package/build/actions/schemas.js.map +1 -1
  16. package/build/actions/specs.d.ts +2 -4
  17. package/build/actions/specs.d.ts.map +1 -1
  18. package/build/actions/specs.js.map +1 -1
  19. package/build/actions/types.d.ts +4 -0
  20. package/build/actions/types.d.ts.map +1 -0
  21. package/build/actions/types.js +2 -0
  22. package/build/actions/types.js.map +1 -0
  23. package/build/actions/watch.d.ts.map +1 -1
  24. package/build/actions/watch.js +4 -2
  25. package/build/actions/watch.js.map +1 -1
  26. package/build/conformance/assert.d.ts +20 -0
  27. package/build/conformance/assert.d.ts.map +1 -0
  28. package/build/conformance/assert.js +54 -0
  29. package/build/conformance/assert.js.map +1 -0
  30. package/build/conformance/cases.d.ts +7 -0
  31. package/build/conformance/cases.d.ts.map +1 -0
  32. package/build/conformance/cases.js +447 -0
  33. package/build/conformance/cases.js.map +1 -0
  34. package/build/conformance/consts.d.ts +19 -0
  35. package/build/conformance/consts.d.ts.map +1 -0
  36. package/build/conformance/consts.js +42 -0
  37. package/build/conformance/consts.js.map +1 -0
  38. package/build/conformance/fixtures.d.ts +14 -0
  39. package/build/conformance/fixtures.d.ts.map +1 -0
  40. package/build/conformance/fixtures.js +109 -0
  41. package/build/conformance/fixtures.js.map +1 -0
  42. package/build/conformance/index.d.ts +12 -0
  43. package/build/conformance/index.d.ts.map +1 -0
  44. package/build/conformance/index.js +11 -0
  45. package/build/conformance/index.js.map +1 -0
  46. package/build/conformance/types.d.ts +18 -0
  47. package/build/conformance/types.d.ts.map +1 -0
  48. package/build/conformance/types.js +2 -0
  49. package/build/conformance/types.js.map +1 -0
  50. package/build/consts.d.ts +5 -0
  51. package/build/consts.d.ts.map +1 -1
  52. package/build/consts.js +5 -0
  53. package/build/consts.js.map +1 -1
  54. package/build/definitions.d.ts +17 -0
  55. package/build/definitions.d.ts.map +1 -0
  56. package/build/definitions.js +211 -0
  57. package/build/definitions.js.map +1 -0
  58. package/build/executor/changes.d.ts +2 -2
  59. package/build/executor/changes.d.ts.map +1 -1
  60. package/build/executor/changes.js.map +1 -1
  61. package/build/executor/code.d.ts +3 -3
  62. package/build/executor/code.d.ts.map +1 -1
  63. package/build/executor/code.js +1 -1
  64. package/build/executor/code.js.map +1 -1
  65. package/build/executor/consts.local.d.ts +5 -0
  66. package/build/executor/consts.local.d.ts.map +1 -0
  67. package/build/executor/consts.local.js +6 -0
  68. package/build/executor/consts.local.js.map +1 -0
  69. package/build/executor/creator.d.ts +30 -0
  70. package/build/executor/creator.d.ts.map +1 -0
  71. package/build/executor/creator.js +46 -0
  72. package/build/executor/creator.js.map +1 -0
  73. package/build/executor/resolve.d.ts +31 -23
  74. package/build/executor/resolve.d.ts.map +1 -1
  75. package/build/executor/resolve.js +74 -15
  76. package/build/executor/resolve.js.map +1 -1
  77. package/build/executor/types.d.ts +24 -0
  78. package/build/executor/types.d.ts.map +1 -0
  79. package/build/executor/types.js +2 -0
  80. package/build/executor/types.js.map +1 -0
  81. package/build/executor/validate.d.ts +6 -5
  82. package/build/executor/validate.d.ts.map +1 -1
  83. package/build/executor/validate.js +18 -15
  84. package/build/executor/validate.js.map +1 -1
  85. package/build/executor.d.ts +10 -7
  86. package/build/executor.d.ts.map +1 -1
  87. package/build/executor.js +17 -14
  88. package/build/executor.js.map +1 -1
  89. package/build/facade.d.ts +20 -2
  90. package/build/facade.d.ts.map +1 -1
  91. package/build/facade.js +128 -18
  92. package/build/facade.js.map +1 -1
  93. package/build/helper.d.ts +3 -2
  94. package/build/helper.d.ts.map +1 -1
  95. package/build/helper.js +7 -3
  96. package/build/helper.js.map +1 -1
  97. package/build/index.d.ts +1 -0
  98. package/build/index.d.ts.map +1 -1
  99. package/build/index.js +1 -0
  100. package/build/index.js.map +1 -1
  101. package/build/projection.d.ts +2 -10
  102. package/build/projection.d.ts.map +1 -1
  103. package/build/projection.js +1 -1
  104. package/build/projection.js.map +1 -1
  105. package/build/queue/consts.d.ts +5 -0
  106. package/build/queue/consts.d.ts.map +1 -0
  107. package/build/queue/consts.js +5 -0
  108. package/build/queue/consts.js.map +1 -0
  109. package/build/queue/index.d.ts +4 -16
  110. package/build/queue/index.d.ts.map +1 -1
  111. package/build/queue/index.js +2 -4
  112. package/build/queue/index.js.map +1 -1
  113. package/build/queue/types.d.ts +13 -0
  114. package/build/queue/types.d.ts.map +1 -0
  115. package/build/queue/types.js +2 -0
  116. package/build/queue/types.js.map +1 -0
  117. package/build/registry.d.ts +2 -21
  118. package/build/registry.d.ts.map +1 -1
  119. package/build/registry.js +3 -1
  120. package/build/registry.js.map +1 -1
  121. package/build/service.d.ts +3 -16
  122. package/build/service.d.ts.map +1 -1
  123. package/build/service.js +9 -0
  124. package/build/service.js.map +1 -1
  125. package/build/store/commits.d.ts.map +1 -1
  126. package/build/store/commits.js +3 -1
  127. package/build/store/commits.js.map +1 -1
  128. package/build/store/composite.d.ts +1 -0
  129. package/build/store/composite.d.ts.map +1 -1
  130. package/build/store/composite.js +3 -0
  131. package/build/store/composite.js.map +1 -1
  132. package/build/store/consts.local.d.ts +2 -0
  133. package/build/store/consts.local.d.ts.map +1 -0
  134. package/build/store/consts.local.js +2 -0
  135. package/build/store/consts.local.js.map +1 -0
  136. package/build/store/fold.d.ts +4 -3
  137. package/build/store/fold.d.ts.map +1 -1
  138. package/build/store/fold.js +14 -6
  139. package/build/store/fold.js.map +1 -1
  140. package/build/store/memory.d.ts +12 -0
  141. package/build/store/memory.d.ts.map +1 -1
  142. package/build/store/memory.js +0 -0
  143. package/build/store/memory.js.map +1 -1
  144. package/build/store/types.d.ts +15 -1
  145. package/build/store/types.d.ts.map +1 -1
  146. package/build/store/types.local.d.ts +3 -0
  147. package/build/store/types.local.d.ts.map +1 -0
  148. package/build/store/types.local.js +2 -0
  149. package/build/store/types.local.js.map +1 -0
  150. package/build/types.d.ts +111 -1
  151. package/build/types.d.ts.map +1 -1
  152. package/build/types.local.d.ts +7 -0
  153. package/build/types.local.d.ts.map +1 -0
  154. package/build/types.local.js +2 -0
  155. package/build/types.local.js.map +1 -0
  156. package/build/utils/index.d.ts +1 -0
  157. package/build/utils/index.d.ts.map +1 -1
  158. package/build/utils/scope.d.ts +39 -2
  159. package/build/utils/scope.d.ts.map +1 -1
  160. package/build/utils/scope.js +79 -15
  161. package/build/utils/scope.js.map +1 -1
  162. package/build/utils/types.d.ts +8 -0
  163. package/build/utils/types.d.ts.map +1 -0
  164. package/build/utils/types.js +2 -0
  165. package/build/utils/types.js.map +1 -0
  166. package/package.json +28 -20
  167. package/src/actions/cards.ts +2 -6
  168. package/src/actions/execute.ts +95 -12
  169. package/src/actions/schemas.ts +81 -9
  170. package/src/actions/specs.ts +1 -2
  171. package/src/actions/types.ts +4 -0
  172. package/src/actions/watch.ts +5 -2
  173. package/src/conformance/assert.ts +59 -0
  174. package/src/conformance/cases.ts +522 -0
  175. package/src/conformance/consts.ts +44 -0
  176. package/src/conformance/fixtures.ts +121 -0
  177. package/src/conformance/index.ts +11 -0
  178. package/src/conformance/types.ts +20 -0
  179. package/src/consts.ts +6 -0
  180. package/src/definitions.ts +250 -0
  181. package/src/executor/changes.ts +2 -6
  182. package/src/executor/code.ts +3 -9
  183. package/src/executor/consts.local.ts +8 -0
  184. package/src/executor/creator.ts +51 -0
  185. package/src/executor/resolve.ts +84 -40
  186. package/src/executor/types.ts +25 -0
  187. package/src/executor/validate.ts +22 -26
  188. package/src/executor.ts +21 -21
  189. package/src/facade.ts +143 -25
  190. package/src/helper.ts +7 -3
  191. package/src/index.ts +1 -0
  192. package/src/projection.ts +4 -14
  193. package/src/queue/consts.ts +5 -0
  194. package/src/queue/index.ts +6 -21
  195. package/src/queue/types.ts +14 -0
  196. package/src/registry.ts +5 -29
  197. package/src/service.ts +13 -20
  198. package/src/store/commits.ts +5 -4
  199. package/src/store/composite.ts +3 -0
  200. package/src/store/consts.local.ts +1 -0
  201. package/src/store/fold.ts +15 -12
  202. package/src/store/memory.ts +0 -0
  203. package/src/store/types.local.ts +3 -0
  204. package/src/store/types.ts +16 -1
  205. package/src/types.local.ts +7 -0
  206. package/src/types.ts +126 -4
  207. package/src/utils/index.ts +1 -0
  208. package/src/utils/scope.ts +97 -17
  209. package/src/utils/types.ts +8 -0
  210. package/tests/access.spec.ts +160 -0
  211. package/tests/conformance.spec.ts +47 -0
  212. package/tests/create-example.spec.ts +202 -0
  213. package/tests/creator.spec.ts +257 -0
  214. package/tests/entrypoints.spec.ts +5 -0
  215. package/tests/narrowing.spec.ts +94 -0
  216. package/tests/plugins.spec.ts +4 -3
  217. package/tests/writes.spec.ts +140 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/server-planning",
3
- "version": "0.1.18-rc.2",
3
+ "version": "0.1.18-rc.21",
4
4
  "license": "MIT",
5
5
  "description": "The general planning implementation — the planning service and its plugin/middleware registry, the transition executor, the in-memory store, the protocol handlers and the commit feed.",
6
6
  "type": "module",
@@ -34,24 +34,32 @@
34
34
  "default": "./build/queue/index.js",
35
35
  "module": "./build/queue/index.js",
36
36
  "types": "./build/queue/index.d.ts"
37
+ },
38
+ "./conformance": {
39
+ "import": "./build/conformance/index.js",
40
+ "require": "./build/conformance/index.js",
41
+ "default": "./build/conformance/index.js",
42
+ "module": "./build/conformance/index.js",
43
+ "types": "./build/conformance/index.d.ts"
37
44
  }
38
45
  },
39
46
  "dependencies": {
40
- "@owlmeans/auth": "^0.1.18-rc.28",
41
- "@owlmeans/auth-common": "^0.1.18-rc.30",
42
- "@owlmeans/basic-ids": "^0.1.18-rc.27",
43
- "@owlmeans/context": "^0.1.18-rc.26",
44
- "@owlmeans/entrypoint": "^0.1.18-rc.29",
45
- "@owlmeans/error": "^0.1.18-rc.28",
46
- "@owlmeans/planning": "^0.1.18-rc.2",
47
- "@owlmeans/queue": "^0.1.18-rc.24",
48
- "@owlmeans/resource": "^0.1.18-rc.28",
49
- "@owlmeans/route": "^0.1.18-rc.27",
50
- "@owlmeans/server-api": "^0.1.18-rc.34",
51
- "@owlmeans/server-context": "^0.1.18-rc.29",
52
- "@owlmeans/server-entrypoint": "^0.1.18-rc.28",
53
- "@owlmeans/server-socket": "^0.1.18-rc.36",
54
- "@owlmeans/socket": "^0.1.18-rc.26"
47
+ "@owlmeans/auth": "^0.1.18-rc.39",
48
+ "@owlmeans/auth-common": "^0.1.18-rc.46",
49
+ "@owlmeans/basic-ids": "^0.1.18-rc.36",
50
+ "@owlmeans/context": "^0.1.18-rc.33",
51
+ "@owlmeans/entrypoint": "^0.1.18-rc.40",
52
+ "@owlmeans/error": "^0.1.18-rc.37",
53
+ "@owlmeans/planning": "^0.1.18-rc.18",
54
+ "@owlmeans/queue": "^0.1.18-rc.40",
55
+ "@owlmeans/resource": "^0.1.18-rc.39",
56
+ "@owlmeans/log": "^0.1.18-rc.1",
57
+ "@owlmeans/route": "^0.1.18-rc.34",
58
+ "@owlmeans/server-api": "^0.1.18-rc.50",
59
+ "@owlmeans/server-context": "^0.1.18-rc.42",
60
+ "@owlmeans/server-entrypoint": "^0.1.18-rc.39",
61
+ "@owlmeans/server-socket": "^0.1.18-rc.53",
62
+ "@owlmeans/socket": "^0.1.18-rc.39"
55
63
  },
56
64
  "peerDependencies": {
57
65
  "ajv": "*",
@@ -59,10 +67,10 @@
59
67
  },
60
68
  "devDependencies": {
61
69
  "@owlmeans/dep-config": "workspace:*",
62
- "@owlmeans/test": "^0.1.18-rc.26",
63
- "@types/bun": "^1.4.0",
64
- "@types/node": "^26.1.0",
65
- "ajv": "^8.17.1",
70
+ "@owlmeans/test": "^0.1.18-rc.34",
71
+ "@types/bun": "^1.4.2",
72
+ "@types/node": "^26.6.4",
73
+ "ajv": "^8.20.0",
66
74
  "ajv-formats": "^3.0.1",
67
75
  "nodemon": "^3.1.14",
68
76
  "typescript": "^7.0.2"
@@ -1,12 +1,8 @@
1
1
  import { handlers } from '@owlmeans/server-api'
2
- import {
3
- decodeSpecificationQuery, decodeSummaryQuery, decodeTransitionQuery, decodeWorkcardQuery,
4
- } from '@owlmeans/planning'
5
- import type { PlanningProtocols } from '@owlmeans/planning'
2
+ import { decodeSpecificationQuery, decodeSummaryQuery, decodeTransitionQuery, decodeWorkcardQuery, type PlanningProtocols } from '@owlmeans/planning'
6
3
  import type { Context, PlanningHandlerOptions } from '../types.js'
7
4
  import { concealed, handlerFacade } from '../utils/index.js'
8
-
9
- type RequestHandler = ReturnType<ReturnType<typeof handlers<Context>>['request']>
5
+ import type { RequestHandler } from './types.js'
10
6
 
11
7
  /** Cards of the caller's entity; the query arrives in its wire shape and is decoded here. */
12
8
  export const listCards = (protocol: PlanningProtocols['card']['list'], opts?: PlanningHandlerOptions): RequestHandler =>
@@ -1,23 +1,30 @@
1
1
  import { handlers } from '@owlmeans/server-api'
2
- import { DEFAULT_COMMIT_TIMEOUT, MAX_COMMIT_POLL, TransitionAction } from '@owlmeans/planning'
2
+ import {
3
+ DEFAULT_COMMIT_TIMEOUT, isProject, isSpecification, MAX_COMMIT_POLL, normalizeParents, PlanningForbidden,
4
+ TransitionAction, WorkcardKind,
5
+ } from '@owlmeans/planning'
3
6
  import type {
4
- ExecuteOptions, ExecuteRequest, PlanningProtocols, PlanningScope, TransitionExecution, TransitionReceiptView,
7
+ ExecuteOptions, ExecuteRequest, PlanningFacade, PlanningProtocols, PlanningScope, TransitionExecution,
8
+ TransitionReceiptView, Workcard,
5
9
  } from '@owlmeans/planning'
6
- import type { Context, PlanningHandlerOptions } from '../types.js'
7
- import { concealed, handlerFacade } from '../utils/index.js'
10
+ import { withCreator } from '../executor/creator.js'
11
+ import type { Context, PlanningAccess, PlanningHandlerOptions } from '../types.js'
12
+ import { assertGranted, concealed, handlerScopeOf, writableIn } from '../utils/index.js'
8
13
 
9
14
  /**
10
15
  * What a wire execution may say. `actor` is dropped — the scope names the writer — and a create's
11
- * `createdBy` is the authenticated subject, whatever the body claimed.
16
+ * `createdBy` claim is dropped too: the executor's {@link withCreator} stamps the authenticated
17
+ * subject, the same rule an in-process create follows. Applied here as well, so the returned
18
+ * execution already carries it. A `createdBy` in `changes` or `unset` is not dropped: the executor
19
+ * refuses it on every action (`assertCreatorFixed`), so no body moves an owner.
12
20
  */
13
21
  export const wireExecution = (body: ExecuteRequest, scope: PlanningScope): TransitionExecution => {
14
22
  const { wait: _wait, timeout: _timeout, actor: _actor, ...exec } = body
15
23
  if (exec.action === TransitionAction.Create && exec.card != null && typeof exec.card === 'object') {
16
24
  const { createdBy: _createdBy, ...draft } = exec.card
17
- const subject = scope.profileId ?? scope.userId
18
- exec.card = subject != null ? { ...draft, createdBy: subject } : draft
25
+ exec.card = draft
19
26
  }
20
- return exec
27
+ return withCreator(exec, scope)
21
28
  }
22
29
 
23
30
  /** `wait`/`timeout` as a server grants them: the timeout never holds a request past `maxPoll`. */
@@ -30,16 +37,92 @@ export const executeOptionsOf = (body: Pick<ExecuteRequest, 'wait' | 'timeout'>,
30
37
  return { wait: true, timeout: Math.min(Number.isFinite(asked) && asked > 0 ? asked : DEFAULT_COMMIT_TIMEOUT, ceiling) }
31
38
  }
32
39
 
40
+ /**
41
+ * Refuse an execution outside the access's `writes`: the card it acts on (a specification through
42
+ * its parent card), or every parent a create names, must be one {@link writableIn} admits. A create
43
+ * of a project is `grants.createProjects`' alone. A card or parent this scope cannot see is left to
44
+ * the executor, which answers it as absent.
45
+ *
46
+ * @throws {PlanningForbidden}
47
+ */
48
+ export const assertExecutionWrites = async (
49
+ facade: PlanningFacade, exec: TransitionExecution, access?: PlanningAccess
50
+ ): Promise<void> => {
51
+ if (access?.writes == null) {
52
+ return
53
+ }
54
+ const admitted = async (card: Workcard): Promise<boolean> => {
55
+ if (writableIn(access, card)) {
56
+ return true
57
+ }
58
+ if (!isSpecification(card) || card.parent == null) {
59
+ return false
60
+ }
61
+ const parent = await facade.cards.load(card.parent)
62
+ return parent == null || writableIn(access, parent)
63
+ }
64
+
65
+ if (exec.action === TransitionAction.Create && exec.card != null && typeof exec.card === 'object') {
66
+ if (exec.card.kind === WorkcardKind.Project) {
67
+ return
68
+ }
69
+ const parents = normalizeParents(exec.card.parent, exec.card.parents)
70
+ if (parents.length === 0) {
71
+ throw new PlanningForbidden('writes:root')
72
+ }
73
+ for (const id of parents) {
74
+ const parent = await facade.cards.load(id)
75
+ if (parent != null && !await admitted(parent)) {
76
+ throw new PlanningForbidden(`writes:${id}`)
77
+ }
78
+ }
79
+ return
80
+ }
81
+ if (typeof exec.card === 'string' && exec.card !== '') {
82
+ const card = await facade.cards.load(exec.card)
83
+ if (card != null && !await admitted(card)) {
84
+ throw new PlanningForbidden(`writes:${exec.card}`)
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * The access a wire execution needs. `writes` first ({@link assertExecutionWrites}); then the
91
+ * grants: `createProjects` for a project create (its primary parent, or the root),
92
+ * `deleteProjects` for the delete of a project card. A card this scope cannot see is left to the
93
+ * executor, which answers it as absent.
94
+ *
95
+ * @throws {PlanningForbidden}
96
+ */
97
+ export const assertExecutionGranted = async (
98
+ facade: PlanningFacade, exec: TransitionExecution, access?: PlanningAccess
99
+ ): Promise<void> => {
100
+ await assertExecutionWrites(facade, exec, access)
101
+ if (access?.grants == null) {
102
+ return
103
+ }
104
+ if (exec.action === TransitionAction.Create && typeof exec.card === 'object' && exec.card?.kind === WorkcardKind.Project) {
105
+ assertGranted(access, 'createProjects', normalizeParents(exec.card.parent, exec.card.parents)[0])
106
+ return
107
+ }
108
+ if (exec.action === TransitionAction.Delete && typeof exec.card === 'string') {
109
+ const card = await facade.cards.load(exec.card)
110
+ if (card != null && isProject(card)) {
111
+ assertGranted(access, 'deleteProjects', card.id)
112
+ }
113
+ }
114
+ }
115
+
33
116
  /** The one write route. */
34
117
  export const executePlanning = (
35
118
  protocol: PlanningProtocols['execute'], opts?: PlanningHandlerOptions
36
119
  ): ReturnType<ReturnType<typeof handlers<Context>>['request']> =>
37
120
  handlers<Context>().request(protocol, async (req, ctx) => concealed(async (): Promise<TransitionReceiptView> => {
38
- const facade = await handlerFacade(ctx, req, opts)
121
+ const { facade, access } = await handlerScopeOf(ctx, req, opts)
39
122
  const body = (req.body ?? {}) as ExecuteRequest
40
- const receipt = await facade.execute(
41
- wireExecution(body, facade.scope), executeOptionsOf(body, opts?.maxPoll ?? MAX_COMMIT_POLL)
42
- )
123
+ const exec = wireExecution(body, facade.scope)
124
+ await assertExecutionGranted(facade, exec, access)
125
+ const receipt = await facade.execute(exec, executeOptionsOf(body, opts?.maxPoll ?? MAX_COMMIT_POLL))
43
126
 
44
127
  return receipt.card !== undefined
45
128
  ? { transition: receipt.transition, card: receipt.card }
@@ -1,15 +1,87 @@
1
1
  import { handlers } from '@owlmeans/server-api'
2
- import type { PlanningProtocols } from '@owlmeans/planning'
2
+ import { PlanningUnsupported, SchemaWriteMode, type PlanningDefinitions, type PlanningProtocols, type SchemaDefineReply, type SchemaDefineRequest, type ScopedSchemaBundle, type ScopedSchemaRecord } from '@owlmeans/planning'
3
3
  import type { Context, PlanningHandlerOptions } from '../types.js'
4
- import { planningServiceOf, scopeOf } from '../utils/index.js'
4
+ import { assertGranted, assertWrites, concealed, handlerScopeOf } from '../utils/index.js'
5
+ import type { RequestHandler } from './types.js'
5
6
 
6
- /** The schema bundle the service holds — what a client loads to answer `can()` with no round trip. */
7
+ const text = (value: unknown): string | undefined =>
8
+ value == null || value === '' ? undefined : `${Array.isArray(value) ? value[0] : value}`
9
+
10
+ /**
11
+ * The schema bundle — what a client loads to answer `can()` with no round trip. Scoped like every
12
+ * other leaf (no organization, no bundle). Where the store holds data-defined schemas it is the
13
+ * resolved layer of the organization, or of the project the `project` query names (a project this
14
+ * scope can see); otherwise the service's code bundle.
15
+ *
16
+ * @throws {WorkcardNotFound} for a project this scope cannot see
17
+ */
7
18
  export const listSchemas = (
8
19
  protocol: PlanningProtocols['schema']['list'], opts?: PlanningHandlerOptions
9
- ): ReturnType<ReturnType<typeof handlers<Context>>['request']> =>
10
- handlers<Context>().request(protocol, async (req, ctx) => {
11
- // Scoped like every other leaf: no organization, no bundle.
12
- scopeOf(req)
20
+ ): RequestHandler =>
21
+ handlers<Context>().request(protocol, async (req, ctx) => concealed(async (): Promise<ScopedSchemaBundle> => {
22
+ const { facade } = await handlerScopeOf(ctx, req, opts)
23
+ if (facade.definitions == null) {
24
+ return facade.schemas.bundle()
25
+ }
26
+
27
+ return await facade.definitions.bundle(text((req.query as { project?: unknown } | undefined)?.project))
28
+ }))
29
+
30
+ /**
31
+ * Apply one `schema.define` request to a facade's definitions: the declarations in the request's
32
+ * mode (flows before types), then the retirements — answering what was written and the layer as it
33
+ * resolves afterwards.
34
+ */
35
+ export const applySchemaRequest = async (
36
+ definitions: PlanningDefinitions, request: SchemaDefineRequest
37
+ ): Promise<SchemaDefineReply> => {
38
+ const layer = request.project != null ? { project: request.project } : {}
39
+ const declarations = { types: request.types ?? [], flows: request.flows ?? [] }
40
+ const records: ScopedSchemaRecord[] = []
41
+
42
+ switch (request.mode ?? SchemaWriteMode.Define) {
43
+ case SchemaWriteMode.Put:
44
+ for (const flow of declarations.flows) {
45
+ records.push(await definitions.putFlow(flow, layer))
46
+ }
47
+ for (const type of declarations.types) {
48
+ records.push(await definitions.putType(type, layer))
49
+ }
50
+ break
51
+ case SchemaWriteMode.Seed:
52
+ records.push(...await definitions.seed(declarations, layer))
53
+ break
54
+ default:
55
+ records.push(...await definitions.define(declarations, layer))
56
+ }
57
+ for (const key of request.retire ?? []) {
58
+ records.push(await definitions.retire(key.kind, key.key, layer))
59
+ }
60
+
61
+ return { records, bundle: await definitions.bundle(request.project) }
62
+ }
63
+
64
+ /**
65
+ * The write of data-defined types and flows. A project layer needs the project in the access's
66
+ * `writes` when the resolver answers it; then `grants.defineSchemas` gates when the resolver answers
67
+ * grants (the organization-wide layer needs `true`, a project layer its id).
68
+ *
69
+ * @throws {PlanningUnsupported} where the store holds no data-defined schemas
70
+ * @throws {PlanningForbidden | SchemaConflict | SchemaSealed | SchemaInvalid | SchemaInUse | WorkcardNotFound}
71
+ */
72
+ export const defineSchemas = (
73
+ protocol: NonNullable<PlanningProtocols['schema']['define']>, opts?: PlanningHandlerOptions
74
+ ): RequestHandler =>
75
+ handlers<Context>().request(protocol, async (req, ctx) => concealed(async (): Promise<SchemaDefineReply> => {
76
+ const { facade, access } = await handlerScopeOf(ctx, req, opts)
77
+ const request = (req.body ?? {}) as SchemaDefineRequest
78
+ if (facade.definitions == null) {
79
+ throw new PlanningUnsupported('definitions')
80
+ }
81
+ if (request.project != null) {
82
+ assertWrites(access, request.project)
83
+ }
84
+ assertGranted(access, 'defineSchemas', request.project)
13
85
 
14
- return planningServiceOf(ctx, opts).schemas.bundle()
15
- })
86
+ return await applySchemaRequest(facade.definitions, request)
87
+ }))
@@ -2,8 +2,7 @@ import { handlers } from '@owlmeans/server-api'
2
2
  import type { PlanningProtocols } from '@owlmeans/planning'
3
3
  import type { Context, PlanningHandlerOptions } from '../types.js'
4
4
  import { concealed, handlerFacade } from '../utils/index.js'
5
-
6
- type RequestHandler = ReturnType<ReturnType<typeof handlers<Context>>['request']>
5
+ import type { RequestHandler } from './types.js'
7
6
 
8
7
  /** @throws {WorkcardNotFound} */
9
8
  export const getSpecification = (protocol: PlanningProtocols['spec']['get'], opts?: PlanningHandlerOptions): RequestHandler =>
@@ -0,0 +1,4 @@
1
+ import { handlers } from '@owlmeans/server-api'
2
+ import type { Context } from '../types.js'
3
+
4
+ export type RequestHandler = ReturnType<ReturnType<typeof handlers<Context>>['request']>
@@ -1,3 +1,4 @@
1
+ import { logger } from '@owlmeans/log'
1
2
  import { PLANNING_COMMIT_EVENT } from '@owlmeans/planning'
2
3
  import type { CommitEvent, CommitFeedQuery, PlanningProtocols } from '@owlmeans/planning'
3
4
  import { connection } from '@owlmeans/server-socket'
@@ -6,6 +7,8 @@ import { MessageType } from '@owlmeans/socket'
6
7
  import type { Context, PlanningHandlerOptions } from '../types.js'
7
8
  import { handlerFacade } from '../utils/index.js'
8
9
 
10
+ const log = logger('planning:watch')
11
+
9
12
  const text = (value: unknown): string | undefined =>
10
13
  value == null || value === '' ? undefined : `${value}`
11
14
 
@@ -32,7 +35,7 @@ export const watchCommits = (
32
35
  const enriched = event.record !== undefined ? event : { ...event, record: await facade.cards.load(event.card) }
33
36
  await conn.notify(frame, enriched)
34
37
  } catch (e) {
35
- console.error('Planning commit notify error:', e)
38
+ log.warn('Planning commit notify failed', e)
36
39
  }
37
40
  }, filter)
38
41
 
@@ -45,7 +48,7 @@ export const watchCommits = (
45
48
  try {
46
49
  unsubscribe()
47
50
  } catch (e) {
48
- console.error('Planning commit unsubscribe error:', e)
51
+ log.warn('Planning commit unsubscribe failed', e)
49
52
  }
50
53
  }
51
54
  })
@@ -0,0 +1,59 @@
1
+ import { sameValue } from '@owlmeans/planning'
2
+
3
+ /** A conformance expectation that did not hold. Plain `Error`, so any test runner reports it. */
4
+ export class ConformanceFailure extends Error {
5
+ constructor(message: string) {
6
+ super(`planning conformance: ${message}`)
7
+ this.name = 'ConformanceFailure'
8
+ }
9
+ }
10
+
11
+ const show = (value: unknown): string => {
12
+ try {
13
+ return JSON.stringify(value)
14
+ } catch {
15
+ return `${value}`
16
+ }
17
+ }
18
+
19
+ /** @throws {ConformanceFailure} */
20
+ export const check = (condition: unknown, message: string): void => {
21
+ if (condition !== true) {
22
+ throw new ConformanceFailure(message)
23
+ }
24
+ }
25
+
26
+ /** Structural equality over JSON values. @throws {ConformanceFailure} */
27
+ export const same = (actual: unknown, expected: unknown, message: string): void => {
28
+ if (!sameValue(actual, expected)) {
29
+ throw new ConformanceFailure(`${message}: expected ${show(expected)}, got ${show(actual)}`)
30
+ }
31
+ }
32
+
33
+ /** The same members in any order. @throws {ConformanceFailure} */
34
+ export const sameSet = (actual: readonly unknown[], expected: readonly unknown[], message: string): void => {
35
+ const sort = (values: readonly unknown[]) => [...values].map(show).sort()
36
+ same(sort(actual), sort(expected), message)
37
+ }
38
+
39
+ /**
40
+ * The promise rejects with an error of the class (matched by instance or by its registered type
41
+ * name, so a duplicate module copy counts). Answers the error.
42
+ *
43
+ * @throws {ConformanceFailure}
44
+ */
45
+ export const rejects = async (
46
+ run: Promise<unknown> | (() => Promise<unknown>), expected: { typeName: string }, message: string
47
+ ): Promise<Error> => {
48
+ try {
49
+ await (typeof run === 'function' ? run() : run)
50
+ } catch (error) {
51
+ const type = (error as { type?: unknown } | null)?.type
52
+ if (error instanceof (expected as unknown as abstract new (...args: never[]) => unknown)
53
+ || (typeof type === 'string' && type === expected.typeName)) {
54
+ return error as Error
55
+ }
56
+ throw new ConformanceFailure(`${message}: expected ${expected.typeName}, got ${show((error as Error)?.message ?? error)}`)
57
+ }
58
+ throw new ConformanceFailure(`${message}: expected ${expected.typeName}, nothing was thrown`)
59
+ }