@toa.io/extensions.exposition 0.24.0-alpha.9 → 1.0.0-alpha.2

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 (291) hide show
  1. package/components/context.toa.yaml +12 -0
  2. package/components/identity.bans/manifest.toa.yaml +1 -1
  3. package/components/identity.basic/manifest.toa.yaml +1 -1
  4. package/components/identity.basic/operations/authenticate.js +1 -2
  5. package/components/identity.basic/operations/authenticate.js.map +1 -1
  6. package/components/identity.basic/operations/transit.js.map +1 -1
  7. package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
  8. package/components/identity.basic/source/authenticate.ts +0 -1
  9. package/components/identity.federation/events/principal.js +22 -0
  10. package/components/identity.federation/manifest.toa.yaml +88 -0
  11. package/components/identity.federation/operations/assertions-as-values.cjs +45 -0
  12. package/components/identity.federation/operations/assertions-as-values.cjs.map +1 -0
  13. package/components/identity.federation/operations/assertions-as-values.d.cts +4 -0
  14. package/components/identity.federation/operations/authenticate.d.ts +3 -0
  15. package/components/identity.federation/operations/authenticate.js +20 -0
  16. package/components/identity.federation/operations/authenticate.js.map +1 -0
  17. package/components/identity.federation/operations/create.d.ts +10 -0
  18. package/components/identity.federation/operations/create.js +15 -0
  19. package/components/identity.federation/operations/create.js.map +1 -0
  20. package/components/identity.federation/operations/jwt.cjs +112 -0
  21. package/components/identity.federation/operations/jwt.cjs.map +1 -0
  22. package/components/identity.federation/operations/jwt.d.cts +19 -0
  23. package/components/identity.federation/operations/schemas.d.ts +43 -0
  24. package/components/identity.federation/operations/schemas.js +9 -0
  25. package/components/identity.federation/operations/schemas.js.map +1 -0
  26. package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -0
  27. package/components/identity.federation/operations/types.d.ts +51 -0
  28. package/components/identity.federation/operations/types.js +3 -0
  29. package/components/identity.federation/operations/types.js.map +1 -0
  30. package/components/identity.federation/source/assertions-as-values.cts +20 -0
  31. package/components/identity.federation/source/authenticate.ts +28 -0
  32. package/components/identity.federation/source/create.ts +26 -0
  33. package/components/identity.federation/source/jwt.cts +143 -0
  34. package/components/identity.federation/source/schemas.ts +45 -0
  35. package/components/identity.federation/source/types.ts +56 -0
  36. package/components/identity.federation/tsconfig.json +9 -0
  37. package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
  38. package/components/identity.tokens/manifest.toa.yaml +1 -1
  39. package/components/identity.tokens/operations/authenticate.js.map +1 -1
  40. package/components/identity.tokens/operations/decrypt.js.map +1 -1
  41. package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
  42. package/components/octets.storage/manifest.toa.yaml +1 -0
  43. package/components/octets.storage/operations/store.js +2 -2
  44. package/cucumber.js +0 -1
  45. package/documentation/components.md +24 -1
  46. package/documentation/identity.md +7 -7
  47. package/documentation/octets.md +90 -37
  48. package/documentation/protocol.md +21 -1
  49. package/documentation/query.md +1 -1
  50. package/documentation/vary.md +69 -0
  51. package/features/cors.feature +72 -0
  52. package/features/identity.feature +19 -3
  53. package/features/identity.federation.feature +125 -0
  54. package/features/octets.entries.feature +121 -0
  55. package/features/octets.feature +2 -28
  56. package/features/octets.meta.feature +65 -0
  57. package/features/octets.workflows.feature +138 -4
  58. package/features/response.feature +65 -0
  59. package/features/routes.feature +37 -0
  60. package/features/steps/Captures.ts +6 -0
  61. package/features/steps/Components.ts +18 -6
  62. package/features/steps/Gateway.ts +1 -2
  63. package/features/steps/HTTP.ts +34 -6
  64. package/features/steps/IdP.ts +120 -0
  65. package/features/steps/Parameters.ts +8 -2
  66. package/features/steps/Workspace.ts +5 -7
  67. package/features/steps/components/echo/operations/error.js +11 -0
  68. package/features/steps/components/octets.tester/manifest.toa.yaml +3 -0
  69. package/features/steps/components/octets.tester/operations/concat.js +7 -0
  70. package/features/steps/components/octets.tester/operations/echo.js +7 -0
  71. package/features/steps/components/users/manifest.toa.yaml +3 -0
  72. package/features/steps/components/users.properties/manifest.toa.yaml +13 -0
  73. package/features/steps/tsconfig.json +1 -1
  74. package/features/vary.feature +150 -0
  75. package/package.json +17 -18
  76. package/schemas/octets/delete.cos.yaml +2 -1
  77. package/schemas/octets/list.cos.yaml +2 -1
  78. package/source/Directive.test.ts +8 -2
  79. package/source/Directive.ts +19 -16
  80. package/source/Factory.ts +8 -7
  81. package/source/Gateway.ts +22 -8
  82. package/source/HTTP/Server.fixtures.ts +0 -1
  83. package/source/HTTP/Server.test.ts +61 -134
  84. package/source/HTTP/Server.ts +68 -37
  85. package/source/HTTP/formats/msgpack.ts +9 -6
  86. package/source/HTTP/formats/text.ts +1 -1
  87. package/source/HTTP/formats/yaml.ts +1 -1
  88. package/source/HTTP/messages.ts +15 -3
  89. package/source/Interception.ts +24 -0
  90. package/source/RTD/Directives.ts +2 -2
  91. package/source/RTD/Tree.ts +3 -0
  92. package/source/RTD/syntax/parse.ts +6 -6
  93. package/source/RTD/syntax/types.ts +1 -1
  94. package/source/deployment.ts +1 -2
  95. package/source/directives/auth/{Family.ts → Authorization.ts} +29 -33
  96. package/source/directives/auth/Incept.ts +1 -1
  97. package/source/directives/auth/Rule.ts +2 -2
  98. package/source/directives/auth/index.ts +2 -2
  99. package/source/directives/auth/schemes.ts +2 -1
  100. package/source/directives/auth/types.ts +9 -6
  101. package/source/directives/cache/{Family.ts → Cache.ts} +4 -5
  102. package/source/directives/cache/index.ts +2 -2
  103. package/source/directives/cache/types.ts +1 -1
  104. package/source/directives/cors/CORS.ts +54 -0
  105. package/source/directives/cors/index.ts +3 -0
  106. package/source/directives/dev/{Family.ts → Development.ts} +3 -4
  107. package/source/directives/dev/Stub.ts +4 -4
  108. package/source/directives/dev/Throw.ts +4 -4
  109. package/source/directives/dev/index.ts +2 -2
  110. package/source/directives/dev/types.ts +1 -1
  111. package/source/directives/index.ts +10 -6
  112. package/source/directives/octets/Context.ts +1 -1
  113. package/source/directives/octets/Delete.ts +50 -9
  114. package/source/directives/octets/Fetch.ts +18 -19
  115. package/source/directives/octets/List.ts +37 -7
  116. package/source/directives/octets/{Family.ts → Octets.ts} +7 -6
  117. package/source/directives/octets/Permute.ts +3 -3
  118. package/source/directives/octets/Store.ts +43 -99
  119. package/source/directives/octets/index.ts +2 -2
  120. package/source/directives/octets/schemas.ts +11 -6
  121. package/source/directives/octets/types.ts +4 -3
  122. package/source/directives/octets/workflow/Execution.ts +78 -0
  123. package/source/directives/octets/workflow/Workflow.ts +37 -0
  124. package/source/directives/octets/workflow/index.ts +1 -0
  125. package/source/directives/vary/Directive.ts +6 -0
  126. package/source/directives/vary/Embed.ts +62 -0
  127. package/source/directives/vary/Properties.ts +17 -0
  128. package/source/directives/vary/Vary.ts +48 -0
  129. package/source/directives/vary/embeddings/Embedding.ts +6 -0
  130. package/source/directives/vary/embeddings/Header.ts +30 -0
  131. package/source/directives/vary/embeddings/Language.ts +31 -0
  132. package/source/directives/vary/embeddings/index.ts +11 -0
  133. package/source/directives/vary/index.ts +3 -0
  134. package/source/io.ts +4 -0
  135. package/source/manifest.test.ts +6 -14
  136. package/source/manifest.ts +9 -6
  137. package/source/schemas.ts +7 -3
  138. package/transpiled/Composition.js.map +1 -1
  139. package/transpiled/Directive.d.ts +6 -7
  140. package/transpiled/Directive.js +12 -10
  141. package/transpiled/Directive.js.map +1 -1
  142. package/transpiled/Factory.d.ts +0 -1
  143. package/transpiled/Factory.js +7 -6
  144. package/transpiled/Factory.js.map +1 -1
  145. package/transpiled/Gateway.d.ts +8 -5
  146. package/transpiled/Gateway.js +12 -2
  147. package/transpiled/Gateway.js.map +1 -1
  148. package/transpiled/HTTP/Server.d.ts +5 -3
  149. package/transpiled/HTTP/Server.fixtures.d.ts +0 -1
  150. package/transpiled/HTTP/Server.fixtures.js +1 -2
  151. package/transpiled/HTTP/Server.fixtures.js.map +1 -1
  152. package/transpiled/HTTP/Server.js +50 -32
  153. package/transpiled/HTTP/Server.js.map +1 -1
  154. package/transpiled/HTTP/formats/msgpack.d.ts +2 -2
  155. package/transpiled/HTTP/formats/msgpack.js +8 -29
  156. package/transpiled/HTTP/formats/msgpack.js.map +1 -1
  157. package/transpiled/HTTP/formats/text.d.ts +3 -1
  158. package/transpiled/HTTP/formats/text.js.map +1 -1
  159. package/transpiled/HTTP/formats/yaml.js +1 -1
  160. package/transpiled/HTTP/formats/yaml.js.map +1 -1
  161. package/transpiled/HTTP/messages.d.ts +5 -0
  162. package/transpiled/HTTP/messages.js +8 -3
  163. package/transpiled/HTTP/messages.js.map +1 -1
  164. package/transpiled/Interception.d.ts +9 -0
  165. package/transpiled/Interception.js +19 -0
  166. package/transpiled/Interception.js.map +1 -0
  167. package/transpiled/Query.js.map +1 -1
  168. package/transpiled/RTD/Directives.d.ts +2 -2
  169. package/transpiled/RTD/Node.js.map +1 -1
  170. package/transpiled/RTD/Route.js.map +1 -1
  171. package/transpiled/RTD/Tree.js +2 -0
  172. package/transpiled/RTD/Tree.js.map +1 -1
  173. package/transpiled/RTD/syntax/parse.js +1 -1
  174. package/transpiled/RTD/syntax/parse.js.map +1 -1
  175. package/transpiled/RTD/syntax/types.js +1 -1
  176. package/transpiled/RTD/syntax/types.js.map +1 -1
  177. package/transpiled/deployment.js +1 -2
  178. package/transpiled/deployment.js.map +1 -1
  179. package/transpiled/directives/auth/{Family.d.ts → Authorization.d.ts} +4 -4
  180. package/transpiled/directives/auth/{Family.js → Authorization.js} +15 -8
  181. package/transpiled/directives/auth/Authorization.js.map +1 -0
  182. package/transpiled/directives/auth/Incept.js.map +1 -1
  183. package/transpiled/directives/auth/Role.js.map +1 -1
  184. package/transpiled/directives/auth/Rule.d.ts +2 -2
  185. package/transpiled/directives/auth/Rule.js.map +1 -1
  186. package/transpiled/directives/auth/index.d.ts +2 -2
  187. package/transpiled/directives/auth/index.js +4 -5
  188. package/transpiled/directives/auth/index.js.map +1 -1
  189. package/transpiled/directives/auth/schemes.js +2 -1
  190. package/transpiled/directives/auth/schemes.js.map +1 -1
  191. package/transpiled/directives/auth/types.d.ts +4 -4
  192. package/transpiled/directives/cache/{Family.d.ts → Cache.d.ts} +4 -5
  193. package/transpiled/directives/cache/{Family.js → Cache.js} +4 -2
  194. package/transpiled/directives/cache/{Family.js.map → Cache.js.map} +1 -1
  195. package/transpiled/directives/cache/index.d.ts +2 -2
  196. package/transpiled/directives/cache/index.js +4 -5
  197. package/transpiled/directives/cache/index.js.map +1 -1
  198. package/transpiled/directives/cache/types.d.ts +1 -1
  199. package/transpiled/directives/cors/CORS.d.ts +11 -0
  200. package/transpiled/directives/cors/CORS.js +44 -0
  201. package/transpiled/directives/cors/CORS.js.map +1 -0
  202. package/transpiled/directives/cors/index.d.ts +2 -0
  203. package/transpiled/directives/cors/index.js +6 -0
  204. package/transpiled/directives/cors/index.js.map +1 -0
  205. package/transpiled/directives/dev/{Family.d.ts → Development.d.ts} +3 -4
  206. package/transpiled/directives/dev/{Family.js → Development.js} +4 -2
  207. package/transpiled/directives/dev/Development.js.map +1 -0
  208. package/transpiled/directives/dev/Stub.d.ts +3 -3
  209. package/transpiled/directives/dev/Stub.js.map +1 -1
  210. package/transpiled/directives/dev/Throw.d.ts +3 -3
  211. package/transpiled/directives/dev/Throw.js.map +1 -1
  212. package/transpiled/directives/dev/index.d.ts +2 -2
  213. package/transpiled/directives/dev/index.js +4 -5
  214. package/transpiled/directives/dev/index.js.map +1 -1
  215. package/transpiled/directives/dev/types.d.ts +1 -1
  216. package/transpiled/directives/index.d.ts +3 -1
  217. package/transpiled/directives/index.js +9 -9
  218. package/transpiled/directives/index.js.map +1 -1
  219. package/transpiled/directives/octets/Context.d.ts +1 -1
  220. package/transpiled/directives/octets/Delete.d.ts +12 -3
  221. package/transpiled/directives/octets/Delete.js +32 -7
  222. package/transpiled/directives/octets/Delete.js.map +1 -1
  223. package/transpiled/directives/octets/Fetch.d.ts +5 -6
  224. package/transpiled/directives/octets/Fetch.js +11 -12
  225. package/transpiled/directives/octets/Fetch.js.map +1 -1
  226. package/transpiled/directives/octets/List.d.ts +7 -2
  227. package/transpiled/directives/octets/List.js +22 -4
  228. package/transpiled/directives/octets/List.js.map +1 -1
  229. package/transpiled/directives/octets/Octets.d.ts +12 -0
  230. package/transpiled/directives/octets/{Family.js → Octets.js} +6 -4
  231. package/transpiled/directives/octets/Octets.js.map +1 -0
  232. package/transpiled/directives/octets/Permute.d.ts +1 -1
  233. package/transpiled/directives/octets/Permute.js +2 -2
  234. package/transpiled/directives/octets/Permute.js.map +1 -1
  235. package/transpiled/directives/octets/Store.d.ts +10 -21
  236. package/transpiled/directives/octets/Store.js +25 -69
  237. package/transpiled/directives/octets/Store.js.map +1 -1
  238. package/transpiled/directives/octets/index.d.ts +2 -2
  239. package/transpiled/directives/octets/index.js +4 -5
  240. package/transpiled/directives/octets/index.js.map +1 -1
  241. package/transpiled/directives/octets/schemas.d.ts +11 -6
  242. package/transpiled/directives/octets/schemas.js.map +1 -1
  243. package/transpiled/directives/octets/types.d.ts +4 -3
  244. package/transpiled/directives/octets/workflow/Execution.d.ts +25 -0
  245. package/transpiled/directives/octets/workflow/Execution.js +55 -0
  246. package/transpiled/directives/octets/workflow/Execution.js.map +1 -0
  247. package/transpiled/directives/octets/workflow/Workflow.d.ts +12 -0
  248. package/transpiled/directives/octets/workflow/Workflow.js +25 -0
  249. package/transpiled/directives/octets/workflow/Workflow.js.map +1 -0
  250. package/transpiled/directives/octets/workflow/index.d.ts +1 -0
  251. package/transpiled/directives/octets/workflow/index.js +6 -0
  252. package/transpiled/directives/octets/workflow/index.js.map +1 -0
  253. package/transpiled/directives/vary/Directive.d.ts +5 -0
  254. package/transpiled/directives/vary/Directive.js +3 -0
  255. package/transpiled/directives/vary/Directive.js.map +1 -0
  256. package/transpiled/directives/vary/Embed.d.ts +10 -0
  257. package/transpiled/directives/vary/Embed.js +49 -0
  258. package/transpiled/directives/vary/Embed.js.map +1 -0
  259. package/transpiled/directives/vary/Properties.d.ts +9 -0
  260. package/transpiled/directives/vary/Properties.js +16 -0
  261. package/transpiled/directives/vary/Properties.js.map +1 -0
  262. package/transpiled/directives/vary/Vary.d.ts +10 -0
  263. package/transpiled/directives/vary/Vary.js +36 -0
  264. package/transpiled/directives/vary/Vary.js.map +1 -0
  265. package/transpiled/directives/vary/embeddings/Embedding.d.ts +5 -0
  266. package/transpiled/directives/vary/embeddings/Embedding.js +3 -0
  267. package/transpiled/directives/vary/embeddings/Embedding.js.map +1 -0
  268. package/transpiled/directives/vary/embeddings/Header.d.ts +7 -0
  269. package/transpiled/directives/vary/embeddings/Header.js +26 -0
  270. package/transpiled/directives/vary/embeddings/Header.js.map +1 -0
  271. package/transpiled/directives/vary/embeddings/Language.d.ts +7 -0
  272. package/transpiled/directives/vary/embeddings/Language.js +28 -0
  273. package/transpiled/directives/vary/embeddings/Language.js.map +1 -0
  274. package/transpiled/directives/vary/embeddings/index.d.ts +5 -0
  275. package/transpiled/directives/vary/embeddings/index.js +10 -0
  276. package/transpiled/directives/vary/embeddings/index.js.map +1 -0
  277. package/transpiled/directives/vary/index.d.ts +2 -0
  278. package/transpiled/directives/vary/index.js +6 -0
  279. package/transpiled/directives/vary/index.js.map +1 -0
  280. package/transpiled/io.d.ts +3 -0
  281. package/transpiled/io.js +3 -0
  282. package/transpiled/io.js.map +1 -0
  283. package/transpiled/manifest.js +10 -5
  284. package/transpiled/manifest.js.map +1 -1
  285. package/transpiled/schemas.d.ts +7 -3
  286. package/transpiled/schemas.js.map +1 -1
  287. package/transpiled/tsconfig.tsbuildinfo +1 -1
  288. package/transpiled/directives/auth/Family.js.map +0 -1
  289. package/transpiled/directives/dev/Family.js.map +0 -1
  290. package/transpiled/directives/octets/Family.d.ts +0 -12
  291. package/transpiled/directives/octets/Family.js.map +0 -1
@@ -1,199 +1,126 @@
1
+ import { randomUUID } from 'node:crypto'
1
2
  import { Connector } from '@toa.io/core'
2
- import { immediate } from '@toa.io/generic'
3
- import { generate } from 'randomstring'
4
3
  import { type Processing, Server } from './Server'
5
4
  import { type OutgoingMessage } from './messages'
6
- import { express, cors, createRequest, res, next } from './Server.fixtures'
7
5
  import { BadRequest } from './exceptions'
8
- import type { Express, Request, RequestHandler } from 'express'
9
- import type { CorsOptions } from 'cors'
10
- import type http from 'node:http'
11
-
12
- jest.mock('express', () => () => express())
13
- jest.mock('cors', () => (options: CorsOptions) => cors(options))
14
6
 
15
7
  let server: Server
16
- let app: jest.MockedObject<Express>
17
-
18
- beforeEach(() => {
19
- jest.clearAllMocks()
20
8
 
21
- server = Server.create()
22
- app = express.mock.results[0]?.value
9
+ beforeAll(() => {
10
+ server = Server.create({ port: 0 })
23
11
  })
24
12
 
25
13
  it('should instance of connector', async () => {
26
14
  expect(server).toBeInstanceOf(Connector)
27
15
  })
28
16
 
29
- it('should create express app', async () => {
30
- expect(express).toHaveBeenCalled()
31
- expect(app.disable).toHaveBeenCalledWith('x-powered-by')
32
- })
33
-
34
- it('should support cors', async () => {
35
- expect(cors).toHaveBeenCalledWith({ allowedHeaders: ['content-type'] } satisfies CorsOptions)
36
-
37
- const middleware = cors.mock.results[0].value
38
-
39
- expect(app.use).toHaveBeenCalledWith(middleware)
40
- })
41
-
42
17
  it('should start HTTP server', async () => {
43
- const stared = server.connect()
44
-
45
- await immediate()
46
-
47
- expect(app.listen).toHaveBeenCalledWith(8000, expect.anything())
48
-
49
- const done = app.listen.mock.calls[0][1]
50
-
51
- if (done !== undefined) done()
18
+ await server.connect()
52
19
 
53
- await stared
54
- })
55
-
56
- it('should stop HTTP server', async () => {
57
- const started = server.connect()
58
-
59
- await immediate()
60
-
61
- app.listen.mock.calls[0][1]?.() // `listen` callback
62
-
63
- await started
64
-
65
- const stopped = server.disconnect()
66
- const httpServer: jest.MockedObject<http.Server> = app.listen.mock.results[0].value
67
-
68
- await immediate()
69
-
70
- expect(httpServer.close).toHaveBeenCalled()
71
-
72
- httpServer.close.mock.calls[0][0]?.() // `close` callback
73
-
74
- await stopped
20
+ expect(server.connected).toBeTruthy()
21
+ expect(server.port).toBeGreaterThan(0)
75
22
  })
76
23
 
77
24
  it('should register request handler', async () => {
78
- const process = jest.fn(async () => ({})) as unknown as Processing
79
- const req = createRequest()
25
+ const process: Processing = jest.fn().mockResolvedValue(undefined)
80
26
 
81
27
  server.attach(process)
82
28
 
83
- await use(req)
29
+ const res = await fetch(`http://localhost:${server.port}`)
30
+
31
+ await res.text()
84
32
 
85
- expect(process).toHaveBeenCalled()
33
+ expect(process).toHaveBeenCalledTimes(1)
86
34
  })
87
35
 
88
36
  it('should send 501 on unknown method', async () => {
89
- const req = createRequest({ method: generate() })
37
+ const head = await fetch(`http://localhost:${server.port}/`, { method: 'COPY' })
90
38
 
91
- await use(req)
39
+ await head.text()
40
+ expect(head.status).toBe(501)
41
+ })
92
42
 
93
- expect(res.sendStatus).toHaveBeenCalledWith(501)
43
+ it('should stop HTTP server', async () => {
44
+ await server.disconnect()
45
+ expect(server.port).toBe(0)
46
+ expect(server.connected).toBeFalsy()
94
47
  })
95
48
 
96
49
  describe('result', () => {
97
- it('should send status code 200 if the result has a value', async () => {
98
- const req = createRequest()
50
+ beforeEach(async () => {
51
+ server = Server.create({ port: 0 })
52
+ await server.connect()
53
+ })
54
+
55
+ afterEach(async () => {
56
+ await server.disconnect()
57
+ })
99
58
 
59
+ it('should send status code 200 if the result has a value', async () => {
100
60
  server.attach(async (): Promise<OutgoingMessage> => ({
101
- headers: new Headers(), body: generate()
61
+ headers: new Headers(), body: randomUUID()
102
62
  }))
103
- await use(req)
104
63
 
105
- expect(res.status).toHaveBeenCalledWith(200)
64
+ const res = await fetch(`http://localhost:${server.port}/`)
65
+
66
+ await res.text()
67
+ expect(res.status).toBe(200)
106
68
  })
107
69
 
108
70
  it('should send status code 204 if the result has no value', async () => {
109
- const req = createRequest()
110
-
111
71
  server.attach(async (): Promise<OutgoingMessage> => ({ headers: new Headers() }))
112
- await use(req)
113
72
 
114
- expect(res.status).toHaveBeenCalledWith(204)
73
+ const res = await fetch(`http://localhost:${server.port}/`)
74
+
75
+ await res.text()
76
+ expect(res.status).toBe(204)
115
77
  })
116
78
 
117
79
  it('should send result', async () => {
118
- const body = { [generate()]: generate() }
119
- const json = JSON.stringify(body)
120
- const buf = Buffer.from(json)
121
- const req = createRequest({ headers: { accept: 'application/json' } })
80
+ const body = { [randomUUID()]: randomUUID() }
122
81
 
123
- server.attach(async (): Promise<OutgoingMessage> => ({ headers: new Headers(), body }))
124
- await use(req)
82
+ server.attach(async (): Promise<OutgoingMessage> =>
83
+ ({ headers: new Headers(), body }))
125
84
 
126
- expect(res.end).toHaveBeenCalledWith(buf)
127
- })
85
+ const res = await fetch(`http://localhost:${server.port}/`,
86
+ { headers: { accept: 'application/json' } })
128
87
 
129
- it('should return 500 on exception', async () => {
130
- async function process (): Promise<OutgoingMessage> {
131
- throw new Error('Bad')
132
- }
88
+ const result = await res.json()
133
89
 
134
- const req = createRequest()
135
-
136
- server.attach(process)
137
- await use(req)
138
-
139
- expect(res.status).toHaveBeenCalledWith(500)
90
+ expect(result).toEqual(body)
140
91
  })
141
92
 
142
- it('should output exception message if debug is enabled', async () => {
143
- jest.clearAllMocks()
144
-
145
- server = Server.create({ debug: true })
146
- app = express.mock.results[0]?.value
147
-
148
- const message = generate()
149
- const req = createRequest()
150
-
151
- async function process (): Promise<OutgoingMessage> {
152
- throw new Error(message)
153
- }
93
+ it('should return 500 on exception', async () => {
94
+ server.attach(jest.fn().mockRejectedValue(new Error('Bad')))
154
95
 
155
- server.attach(process)
156
- await use(req)
96
+ const res = await fetch(`http://localhost:${server.port}/`)
157
97
 
158
- expect(res.status).toHaveBeenCalledWith(500)
98
+ await res.text()
99
+ expect(res.status).toBe(500)
159
100
  })
160
101
 
161
102
  it('should send client error', async () => {
162
- const req = createRequest()
163
- const message = generate()
103
+ const message = randomUUID()
164
104
 
165
- async function process (): Promise<OutgoingMessage> {
166
- throw new BadRequest(message)
167
- }
105
+ server.attach(jest.fn().mockRejectedValueOnce(new BadRequest(message)))
168
106
 
169
- server.attach(process)
170
- await use(req)
107
+ const res = await fetch(`http://localhost:${server.port}/`)
108
+ const text = await res.text()
171
109
 
172
- expect(res.status).toHaveBeenCalledWith(400)
110
+ expect(res.status).toBe(400)
111
+ expect(text).toContain(message)
173
112
  })
174
113
  })
175
114
 
176
115
  describe('options', () => {
177
116
  it('should send 501 on unspecified method', async () => {
178
- jest.clearAllMocks()
117
+ server = Server.create({ methods: new Set(['COPY']), port: 0 })
118
+ await server.connect()
179
119
 
180
- server = Server.create({ methods: new Set(['COPY']) })
181
- app = express.mock.results[0]?.value
120
+ const res = await fetch(`http://localhost:${server.port}/`)
182
121
 
183
- const req = createRequest({ method: 'GET' })
184
-
185
- await use(req)
186
-
187
- expect(res.sendStatus).toHaveBeenCalledWith(501)
122
+ await res.text()
123
+ await server.disconnect()
124
+ expect(res.status).toBe(501)
188
125
  })
189
126
  })
190
-
191
- async function use (req: Request): Promise<void> {
192
- for (const call of app.use.mock.calls) {
193
- const usage = call[0] as unknown as RequestHandler
194
-
195
- usage(req, res, next)
196
- }
197
-
198
- await immediate()
199
- }
@@ -1,56 +1,67 @@
1
1
  import fs from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import express from 'express'
4
- import cors from 'cors'
5
4
  import { Connector } from '@toa.io/core'
6
5
  import { promex } from '@toa.io/generic'
7
6
  import Negotiator from 'negotiator'
8
7
  import { read, write, type IncomingMessage, type OutgoingMessage } from './messages'
9
8
  import { ClientError, Exception } from './exceptions'
10
9
  import { formats, types } from './formats'
11
- import type { Format } from './formats'
12
10
  import type * as http from 'node:http'
13
11
  import type { Express, Request, Response, NextFunction } from 'express'
14
12
 
15
13
  export class Server extends Connector {
16
- private readonly debug: boolean
17
- private readonly app: Express
18
14
  private server?: http.Server
15
+ private readonly app: Express
16
+ private readonly debug: boolean
17
+ private readonly requestedPort: number
19
18
 
20
- private constructor (app: Express, debug: boolean) {
19
+ private constructor (app: Express, debug: boolean, port: number) {
21
20
  super()
22
21
 
23
22
  this.app = app
24
23
  this.debug = debug
24
+ this.requestedPort = port
25
+ }
26
+
27
+ public get port (): number {
28
+ if (this.server === undefined) return this.requestedPort
29
+
30
+ const address = this.server.address()
31
+
32
+ if (address === null || typeof address === 'string')
33
+ throw new Error('Server is not listening on a port.')
34
+
35
+ return address.port
25
36
  }
26
37
 
27
38
  public static create (options?: Partial<Properties>): Server {
28
- const properties: Properties = options === undefined
39
+ const properties = options === undefined
29
40
  ? DEFAULTS
30
41
  : { ...DEFAULTS, ...options }
31
42
 
32
43
  const app = express()
33
44
 
34
45
  app.disable('x-powered-by')
35
- app.use(cors({ allowedHeaders: ['content-type'] }))
36
46
  app.use(supportedMethods(properties.methods))
37
47
 
38
- return new Server(app, properties.debug)
48
+ return new Server(app, properties.debug, properties.port)
39
49
  }
40
50
 
41
51
  public attach (process: Processing): void {
42
- this.app.use((request: any, response: Response) => {
43
- this.extend(request)
44
- .then(process)
45
- .then(this.success(request, response))
46
- .catch(this.fail(request, response))
52
+ this.app.use((request: Request, response: Response) => {
53
+ const message = this.extend(request)
54
+
55
+ process(message)
56
+ .then(this.success(message, response))
57
+ .catch(this.fail(message, response))
47
58
  })
48
59
  }
49
60
 
50
61
  protected override async open (): Promise<void> {
51
62
  const listening = promex()
52
63
 
53
- this.server = this.app.listen(8000, listening.callback)
64
+ this.server = this.app.listen(this.requestedPort, listening.callback)
54
65
 
55
66
  await listening
56
67
 
@@ -63,17 +74,27 @@ export class Server extends Connector {
63
74
  this.server?.close(stopped.callback)
64
75
 
65
76
  await stopped
66
- }
67
77
 
68
- protected override dispose (): void {
78
+ this.server = undefined
79
+
69
80
  console.info('HTTP Server has been stopped.')
70
81
  }
71
82
 
72
- private async extend (request: IncomingMessage): Promise<IncomingMessage> {
73
- const message = request as unknown as IncomingMessage
83
+ private extend (request: Request): IncomingMessage {
84
+ const message = request as IncomingMessage
74
85
 
75
- message.encoder = negotiate(request)
76
- message.parse = async <T> (): Promise<T> => await read(request)
86
+ negotiate(request, message)
87
+
88
+ message.pipelines = { body: [], response: [] }
89
+
90
+ message.parse = async <T> (): Promise<T> => {
91
+ const value = await read(request)
92
+
93
+ if (message.pipelines.body.length === 0)
94
+ return value
95
+
96
+ return message.pipelines.body.reduce((value, transform) => transform(value), value)
97
+ }
77
98
 
78
99
  return message
79
100
  }
@@ -89,12 +110,7 @@ export class Server extends Connector {
89
110
  else status = 200
90
111
 
91
112
  response.status(status)
92
- message.headers?.forEach((value, key) => response.set(key, value))
93
-
94
- if (message.body !== undefined && message.body !== null)
95
- write(request, response, message)
96
- else
97
- response.end()
113
+ write(request, response, message)
98
114
  }
99
115
  }
100
116
 
@@ -109,16 +125,15 @@ export class Server extends Connector {
109
125
 
110
126
  response.status(status)
111
127
 
112
- const outputAllowed = exception instanceof ClientError || this.debug
128
+ const message: OutgoingMessage = {}
129
+ const verbose = exception instanceof ClientError || this.debug
113
130
 
114
- if (outputAllowed) {
115
- const body = exception instanceof Exception
131
+ if (verbose)
132
+ message.body = exception instanceof Exception
116
133
  ? exception.body
117
134
  : (exception.stack ?? exception.message)
118
135
 
119
- write(request, response, { body })
120
- } else
121
- response.end()
136
+ write(request, response, message)
122
137
  }
123
138
  }
124
139
  }
@@ -130,11 +145,23 @@ function supportedMethods (methods: Set<string>) {
130
145
  }
131
146
  }
132
147
 
133
- function negotiate (request: Request): Format | null {
148
+ function negotiate (request: Request, message: IncomingMessage): void {
149
+ if (request.headers.accept !== undefined) {
150
+ const match = SUBTYPE.exec(request.headers.accept)
151
+
152
+ if (match !== null) {
153
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
154
+ const { type, subtype, suffix } = match.groups!
155
+
156
+ request.headers.accept = `${type}/${suffix}`
157
+ message.subtype = subtype
158
+ }
159
+ }
160
+
134
161
  const negotiator = new Negotiator(request)
135
162
  const mediaType = negotiator.mediaType(types)
136
163
 
137
- return mediaType === undefined ? null : formats[mediaType]
164
+ message.encoder = mediaType === undefined ? null : formats[mediaType]
138
165
  }
139
166
 
140
167
  // https://github.com/whatwg/fetch/issues/1254
@@ -149,14 +176,18 @@ async function adam (request: Request): Promise<void> {
149
176
  return await completed
150
177
  }
151
178
 
152
- const DEFAULTS = {
153
- methods: new Set<string>(['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']),
154
- debug: false
179
+ const DEFAULTS: Properties = {
180
+ methods: new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']),
181
+ debug: false,
182
+ port: 8000
155
183
  }
156
184
 
157
185
  interface Properties {
158
186
  methods: Set<string>
159
187
  debug: boolean
188
+ port: number
160
189
  }
161
190
 
162
191
  export type Processing = (input: IncomingMessage) => Promise<OutgoingMessage>
192
+
193
+ const SUBTYPE = /^(?<type>\w{1,32})\/(vnd\.toa\.(?<subtype>\S{1,32})\+)(?<suffix>\S{1,32})$/
@@ -1,13 +1,16 @@
1
1
  import { type Buffer } from 'node:buffer'
2
- import * as msgpack from 'msgpackr'
3
-
4
- export const type = 'application/msgpack'
5
- export const multipart = 'multipart/msgpack'
2
+ import { pack, unpack } from 'msgpackr'
6
3
 
7
4
  export function decode (buffer: Buffer): any {
8
- return msgpack.decode(buffer)
5
+ return unpack(buffer)
9
6
  }
10
7
 
11
8
  export function encode (value: any): Buffer {
12
- return msgpack.encode(value)
9
+ if (typeof value === 'object' && value !== null)
10
+ Object.setPrototypeOf(value, null)
11
+
12
+ return pack(value)
13
13
  }
14
+
15
+ export const type = 'application/msgpack'
16
+ export const multipart = 'multipart/msgpack'
@@ -7,6 +7,6 @@ export function decode (buffer: Buffer): any {
7
7
  return buffer.toString()
8
8
  }
9
9
 
10
- export function encode (value: any): Buffer {
10
+ export function encode (value: { toString: () => string }): Buffer {
11
11
  return Buffer.from(value.toString())
12
12
  }
@@ -11,7 +11,7 @@ export function decode (buffer: Buffer): any {
11
11
  }
12
12
 
13
13
  export function encode (value: any): Buffer {
14
- const text = yaml.dump(value)
14
+ const text = yaml.dump(value, { lineWidth: -1, noRefs: true })
15
15
 
16
16
  return Buffer.from(text)
17
17
  }
@@ -9,6 +9,11 @@ import type { Format } from './formats'
9
9
 
10
10
  export function write
11
11
  (request: IncomingMessage, response: Response, message: OutgoingMessage): void {
12
+ for (const transform of request.pipelines.response)
13
+ transform(message)
14
+
15
+ message.headers?.forEach((value, key) => response.set(key, value))
16
+
12
17
  if (message.body instanceof Readable)
13
18
  stream(message, request, response)
14
19
  else
@@ -36,7 +41,7 @@ export async function read (request: Request): Promise<any> {
36
41
  }
37
42
 
38
43
  function send (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
39
- if (message.body === undefined) {
44
+ if (message.body === undefined || message.body === null) {
40
45
  response.end()
41
46
 
42
47
  return
@@ -47,8 +52,10 @@ function send (message: OutgoingMessage, request: IncomingMessage, response: Res
47
52
 
48
53
  const buf = request.encoder.encode(message.body)
49
54
 
50
- response.set('content-type', request.encoder.type)
51
- response.end(buf)
55
+ response
56
+ .set('content-type', request.encoder.type)
57
+ .append('vary', 'accept')
58
+ .end(buf)
52
59
  }
53
60
 
54
61
  function stream
@@ -97,6 +104,11 @@ export interface IncomingMessage extends Request {
97
104
  query: Query
98
105
  parse: <T> () => Promise<T>
99
106
  encoder: Format | null
107
+ subtype: string | null
108
+ pipelines: {
109
+ body: Array<(input: unknown) => unknown>
110
+ response: Array<(output: OutgoingMessage) => void>
111
+ }
100
112
  }
101
113
 
102
114
  export interface OutgoingMessage {
@@ -0,0 +1,24 @@
1
+ import type { Input, Output } from './io'
2
+
3
+ export class Interception implements Interceptor {
4
+ private readonly interceptors: Interceptor[]
5
+
6
+ public constructor (interceptors: Interceptor[]) {
7
+ this.interceptors = interceptors
8
+ }
9
+
10
+ public async intercept (input: Input): Promise<Output> {
11
+ for (const interceptor of this.interceptors) {
12
+ const output = await interceptor.intercept(input)
13
+
14
+ if (output !== null)
15
+ return output
16
+ }
17
+
18
+ return null
19
+ }
20
+ }
21
+
22
+ export interface Interceptor {
23
+ intercept: (input: Input) => Output | Promise<Output>
24
+ }
@@ -1,7 +1,7 @@
1
1
  import type * as syntax from './syntax'
2
2
 
3
- export interface Directives<T = any> {
4
- merge: (directive: T) => void
3
+ export interface Directives<TDirective = any> {
4
+ merge: (directive: TDirective) => void
5
5
  }
6
6
 
7
7
  export interface DirectivesFactory<T = any> {
@@ -25,6 +25,9 @@ export class Tree<
25
25
  }
26
26
 
27
27
  public match (path: string): Match<TEndpoint, TDirectives> | null {
28
+ if (path === '/')
29
+ return { node: this.trunk, parameters: [] }
30
+
28
31
  const fragments = fragment(path)
29
32
 
30
33
  return this.trunk.match(fragments)
@@ -28,7 +28,7 @@ function parseNode (input: object, shortcuts?: Shortcuts): Node {
28
28
  }
29
29
 
30
30
  if (key[0] === '/') {
31
- const route = parseRoute(key, value, shortcuts)
31
+ const route = parseRoute(key, value as Node, shortcuts)
32
32
 
33
33
  node.routes.push(route)
34
34
 
@@ -36,7 +36,7 @@ function parseNode (input: object, shortcuts?: Shortcuts): Node {
36
36
  }
37
37
 
38
38
  if (verbs.has(key)) {
39
- const method = parseMethod(key, value, shortcuts)
39
+ const method = parseMethod(key, value as Mapping, shortcuts)
40
40
 
41
41
  node.methods.push(method)
42
42
 
@@ -65,7 +65,7 @@ export function createNode (): Node {
65
65
  }
66
66
  }
67
67
 
68
- function parseRoute (path: string, value: object, shortcuts?: Shortcuts): Route {
68
+ function parseRoute (path: string, value: Node, shortcuts?: Shortcuts): Route {
69
69
  const node = parse(value, shortcuts)
70
70
 
71
71
  return createRoute(path, node)
@@ -106,10 +106,10 @@ function parseQuery (mapping: any): void {
106
106
  return
107
107
 
108
108
  if (typeof query.limit === 'number')
109
- query.limit = expandRange(query.limit)
109
+ query.limit = expandRange(query.limit as number)
110
110
 
111
111
  if (typeof query.omit === 'number')
112
- query.omit = expandRange(query.omit)
112
+ query.omit = expandRange(query.omit as number)
113
113
  }
114
114
 
115
115
  function parseDirectives (mapping: Record<string, any>, shortcuts?: Shortcuts): Directive[] {
@@ -132,7 +132,7 @@ function parseDirectives (mapping: Record<string, any>, shortcuts?: Shortcuts):
132
132
 
133
133
  function parseDirective (key: string, value: any, shortcuts?: Shortcuts): Directive | null {
134
134
  if (shortcuts?.has(key) === true)
135
- key = shortcuts.get(key) as string
135
+ key = shortcuts.get(key)! // eslint-disable-line @typescript-eslint/no-non-null-assertion
136
136
 
137
137
  const match = key.match(DIRECTIVE_RX)
138
138
 
@@ -45,4 +45,4 @@ export interface Range {
45
45
  range: [number, number]
46
46
  }
47
47
 
48
- export const verbs = new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
48
+ export const verbs = new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'])
@@ -27,8 +27,7 @@ export function deployment (_: unknown, annotation: Annotation | undefined): Dep
27
27
  }
28
28
 
29
29
  if (annotation?.['/'] !== undefined) {
30
- const node = { '/': annotation['/'] }
31
- const tree = parse(node, shortcuts)
30
+ const tree = parse(annotation['/'], shortcuts)
32
31
 
33
32
  service.variables.push({
34
33
  name: 'TOA_EXPOSITION',