@toa.io/extensions.exposition 0.9.0-canary.2 → 0.20.0-alpha.1

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 (363) hide show
  1. package/components/context.toa.yaml +15 -0
  2. package/components/identity.bans/manifest.toa.yaml +18 -0
  3. package/components/identity.basic/events/principal.js +9 -0
  4. package/components/identity.basic/manifest.toa.yaml +50 -0
  5. package/components/identity.basic/source/authenticate.ts +29 -0
  6. package/components/identity.basic/source/create.ts +19 -0
  7. package/components/identity.basic/source/transit.ts +64 -0
  8. package/components/identity.basic/source/types.ts +42 -0
  9. package/components/identity.basic/tsconfig.json +9 -0
  10. package/components/identity.roles/manifest.toa.yaml +31 -0
  11. package/components/identity.roles/source/list.ts +7 -0
  12. package/components/identity.roles/source/principal.ts +20 -0
  13. package/components/identity.roles/tsconfig.json +9 -0
  14. package/components/identity.tokens/manifest.toa.yaml +39 -0
  15. package/components/identity.tokens/receivers/identity.bans.updated.js +3 -0
  16. package/components/identity.tokens/source/authenticate.test.ts +56 -0
  17. package/components/identity.tokens/source/authenticate.ts +38 -0
  18. package/components/identity.tokens/source/decrypt.test.ts +59 -0
  19. package/components/identity.tokens/source/decrypt.ts +25 -0
  20. package/components/identity.tokens/source/encrypt.test.ts +35 -0
  21. package/components/identity.tokens/source/encrypt.ts +25 -0
  22. package/components/identity.tokens/source/revoke.ts +5 -0
  23. package/components/identity.tokens/source/types.ts +48 -0
  24. package/components/identity.tokens/tsconfig.json +9 -0
  25. package/cucumber.js +9 -0
  26. package/documentation/.assets/ia3-dark.jpg +0 -0
  27. package/documentation/.assets/ia3-light.jpg +0 -0
  28. package/documentation/.assets/overview-dark.jpg +0 -0
  29. package/documentation/.assets/overview-light.jpg +0 -0
  30. package/documentation/.assets/role-scopes-dark.jpg +0 -0
  31. package/documentation/.assets/role-scopes-light.jpg +0 -0
  32. package/documentation/.assets/rtd-dark.jpg +0 -0
  33. package/documentation/.assets/rtd-light.jpg +0 -0
  34. package/documentation/access.md +256 -0
  35. package/documentation/components.md +276 -0
  36. package/documentation/identity.md +156 -0
  37. package/documentation/protocol.md +15 -0
  38. package/documentation/query.md +226 -0
  39. package/documentation/tree.md +169 -0
  40. package/features/access.feature +442 -0
  41. package/features/annotation.feature +28 -0
  42. package/features/body.feature +21 -0
  43. package/features/directives.feature +56 -0
  44. package/features/dynamic.feature +89 -0
  45. package/features/errors.feature +208 -0
  46. package/features/identity.basic.feature +235 -0
  47. package/features/identity.feature +61 -0
  48. package/features/identity.roles.feature +51 -0
  49. package/features/identity.tokens.feature +116 -0
  50. package/features/queries.feature +214 -0
  51. package/features/routes.feature +49 -0
  52. package/features/steps/Common.ts +10 -0
  53. package/features/steps/Components.ts +43 -0
  54. package/features/steps/Database.ts +58 -0
  55. package/features/steps/Gateway.ts +105 -0
  56. package/features/steps/HTTP.ts +71 -0
  57. package/features/steps/Parameters.ts +12 -0
  58. package/features/steps/Workspace.ts +40 -0
  59. package/features/steps/components/greeter/manifest.toa.yaml +9 -0
  60. package/features/steps/components/greeter/operations/greet.js +7 -0
  61. package/features/steps/components/pots/manifest.toa.yaml +20 -0
  62. package/features/steps/components/users/manifest.toa.yaml +11 -0
  63. package/features/steps/tsconfig.json +9 -0
  64. package/package.json +31 -17
  65. package/readme.md +181 -0
  66. package/schemas/annotation.cos.yaml +4 -0
  67. package/schemas/directive.cos.yaml +3 -0
  68. package/schemas/method.cos.yaml +9 -0
  69. package/schemas/node.cos.yaml +5 -0
  70. package/schemas/query.cos.yaml +16 -0
  71. package/schemas/querystring.cos.yaml +5 -0
  72. package/schemas/range.cos.yaml +2 -0
  73. package/schemas/route.cos.yaml +2 -0
  74. package/source/Annotation.ts +6 -0
  75. package/source/Branch.ts +8 -0
  76. package/source/Composition.ts +58 -0
  77. package/source/Context.ts +6 -0
  78. package/source/Directive.test.ts +91 -0
  79. package/source/Directive.ts +120 -0
  80. package/source/Endpoint.ts +57 -0
  81. package/source/Factory.ts +53 -0
  82. package/source/Gateway.ts +93 -0
  83. package/source/HTTP/Server.fixtures.ts +45 -0
  84. package/source/HTTP/Server.test.ts +221 -0
  85. package/source/HTTP/Server.ts +135 -0
  86. package/source/HTTP/exceptions.ts +77 -0
  87. package/source/HTTP/formats/index.ts +19 -0
  88. package/source/HTTP/formats/json.ts +13 -0
  89. package/source/HTTP/formats/msgpack.ts +10 -0
  90. package/source/HTTP/formats/text.ts +9 -0
  91. package/source/HTTP/formats/yaml.ts +14 -0
  92. package/source/HTTP/index.ts +3 -0
  93. package/source/HTTP/messages.test.ts +116 -0
  94. package/source/HTTP/messages.ts +77 -0
  95. package/source/Mapping.ts +48 -0
  96. package/source/Query.test.ts +37 -0
  97. package/source/Query.ts +105 -0
  98. package/source/RTD/Context.ts +16 -0
  99. package/source/RTD/Directives.ts +9 -0
  100. package/source/RTD/Endpoint.ts +11 -0
  101. package/source/RTD/Match.ts +16 -0
  102. package/source/RTD/Method.ts +24 -0
  103. package/source/RTD/Node.ts +85 -0
  104. package/source/RTD/Route.ts +58 -0
  105. package/source/RTD/Tree.ts +57 -0
  106. package/source/RTD/factory.ts +47 -0
  107. package/source/RTD/index.ts +8 -0
  108. package/source/RTD/segment.test.ts +32 -0
  109. package/source/RTD/segment.ts +25 -0
  110. package/source/RTD/syntax/index.ts +2 -0
  111. package/source/RTD/syntax/parse.test.ts +188 -0
  112. package/source/RTD/syntax/parse.ts +153 -0
  113. package/source/RTD/syntax/types.ts +48 -0
  114. package/source/Remotes.test.ts +41 -0
  115. package/source/Remotes.ts +20 -0
  116. package/source/Tenant.ts +38 -0
  117. package/source/deployment.ts +42 -0
  118. package/source/directives/auth/Anonymous.ts +14 -0
  119. package/source/directives/auth/Echo.ts +12 -0
  120. package/source/directives/auth/Family.ts +145 -0
  121. package/source/directives/auth/Id.ts +19 -0
  122. package/source/directives/auth/Incept.ts +42 -0
  123. package/source/directives/auth/Role.test.ts +62 -0
  124. package/source/directives/auth/Role.ts +56 -0
  125. package/source/directives/auth/Rule.ts +28 -0
  126. package/source/directives/auth/Scheme.ts +26 -0
  127. package/source/directives/auth/index.ts +3 -0
  128. package/source/directives/auth/schemes.ts +8 -0
  129. package/source/directives/auth/split.ts +15 -0
  130. package/source/directives/auth/types.ts +37 -0
  131. package/source/directives/dev/Family.ts +34 -0
  132. package/source/directives/dev/Stub.ts +14 -0
  133. package/source/directives/dev/index.ts +3 -0
  134. package/source/directives/dev/types.ts +5 -0
  135. package/source/directives/index.ts +5 -0
  136. package/source/discovery.ts +1 -0
  137. package/source/exceptions.ts +17 -0
  138. package/source/index.test.ts +9 -0
  139. package/source/index.ts +6 -0
  140. package/source/manifest.test.ts +57 -0
  141. package/source/manifest.ts +35 -0
  142. package/source/root.ts +38 -0
  143. package/source/schemas.ts +9 -0
  144. package/transpiled/Annotation.d.ts +6 -0
  145. package/transpiled/Annotation.js +3 -0
  146. package/transpiled/Annotation.js.map +1 -0
  147. package/transpiled/Branch.d.ts +7 -0
  148. package/transpiled/Branch.js +3 -0
  149. package/transpiled/Branch.js.map +1 -0
  150. package/transpiled/Composition.d.ts +14 -0
  151. package/transpiled/Composition.js +43 -0
  152. package/transpiled/Composition.js.map +1 -0
  153. package/transpiled/Context.d.ts +5 -0
  154. package/transpiled/Context.js +3 -0
  155. package/transpiled/Context.js.map +1 -0
  156. package/transpiled/Directive.d.ts +32 -0
  157. package/transpiled/Directive.js +76 -0
  158. package/transpiled/Directive.js.map +1 -0
  159. package/transpiled/Endpoint.d.ts +20 -0
  160. package/transpiled/Endpoint.js +44 -0
  161. package/transpiled/Endpoint.js.map +1 -0
  162. package/transpiled/Factory.d.ts +11 -0
  163. package/transpiled/Factory.js +67 -0
  164. package/transpiled/Factory.js.map +1 -0
  165. package/transpiled/Gateway.d.ts +19 -0
  166. package/transpiled/Gateway.js +90 -0
  167. package/transpiled/Gateway.js.map +1 -0
  168. package/transpiled/HTTP/Server.d.ts +22 -0
  169. package/transpiled/HTTP/Server.fixtures.d.ts +12 -0
  170. package/transpiled/HTTP/Server.fixtures.js +36 -0
  171. package/transpiled/HTTP/Server.fixtures.js.map +1 -0
  172. package/transpiled/HTTP/Server.js +111 -0
  173. package/transpiled/HTTP/Server.js.map +1 -0
  174. package/transpiled/HTTP/exceptions.d.ts +39 -0
  175. package/transpiled/HTTP/exceptions.js +78 -0
  176. package/transpiled/HTTP/exceptions.js.map +1 -0
  177. package/transpiled/HTTP/formats/index.d.ts +8 -0
  178. package/transpiled/HTTP/formats/index.js +38 -0
  179. package/transpiled/HTTP/formats/index.js.map +1 -0
  180. package/transpiled/HTTP/formats/json.d.ts +4 -0
  181. package/transpiled/HTTP/formats/json.js +15 -0
  182. package/transpiled/HTTP/formats/json.js.map +1 -0
  183. package/transpiled/HTTP/formats/msgpack.d.ts +4 -0
  184. package/transpiled/HTTP/formats/msgpack.js +36 -0
  185. package/transpiled/HTTP/formats/msgpack.js.map +1 -0
  186. package/transpiled/HTTP/formats/text.d.ts +4 -0
  187. package/transpiled/HTTP/formats/text.js +13 -0
  188. package/transpiled/HTTP/formats/text.js.map +1 -0
  189. package/transpiled/HTTP/formats/yaml.d.ts +4 -0
  190. package/transpiled/HTTP/formats/yaml.js +39 -0
  191. package/transpiled/HTTP/formats/yaml.js.map +1 -0
  192. package/transpiled/HTTP/index.d.ts +3 -0
  193. package/transpiled/HTTP/index.js +20 -0
  194. package/transpiled/HTTP/index.js.map +1 -0
  195. package/transpiled/HTTP/messages.d.ts +27 -0
  196. package/transpiled/HTTP/messages.js +49 -0
  197. package/transpiled/HTTP/messages.js.map +1 -0
  198. package/transpiled/Mapping.d.ts +8 -0
  199. package/transpiled/Mapping.js +35 -0
  200. package/transpiled/Mapping.js.map +1 -0
  201. package/transpiled/Query.d.ts +13 -0
  202. package/transpiled/Query.js +107 -0
  203. package/transpiled/Query.js.map +1 -0
  204. package/transpiled/RTD/Context.d.ts +11 -0
  205. package/transpiled/RTD/Context.js +3 -0
  206. package/transpiled/RTD/Context.js.map +1 -0
  207. package/transpiled/RTD/Directives.d.ts +7 -0
  208. package/transpiled/RTD/Directives.js +3 -0
  209. package/transpiled/RTD/Directives.js.map +1 -0
  210. package/transpiled/RTD/Endpoint.d.ts +9 -0
  211. package/transpiled/RTD/Endpoint.js +3 -0
  212. package/transpiled/RTD/Endpoint.js.map +1 -0
  213. package/transpiled/RTD/Match.d.ts +11 -0
  214. package/transpiled/RTD/Match.js +3 -0
  215. package/transpiled/RTD/Match.js.map +1 -0
  216. package/transpiled/RTD/Method.d.ts +9 -0
  217. package/transpiled/RTD/Method.js +16 -0
  218. package/transpiled/RTD/Method.js.map +1 -0
  219. package/transpiled/RTD/Node.d.ts +21 -0
  220. package/transpiled/RTD/Node.js +61 -0
  221. package/transpiled/RTD/Node.js.map +1 -0
  222. package/transpiled/RTD/Route.d.ts +14 -0
  223. package/transpiled/RTD/Route.js +48 -0
  224. package/transpiled/RTD/Route.js.map +1 -0
  225. package/transpiled/RTD/Tree.d.ts +14 -0
  226. package/transpiled/RTD/Tree.js +45 -0
  227. package/transpiled/RTD/Tree.js.map +1 -0
  228. package/transpiled/RTD/factory.d.ts +6 -0
  229. package/transpiled/RTD/factory.js +36 -0
  230. package/transpiled/RTD/factory.js.map +1 -0
  231. package/transpiled/RTD/index.d.ts +8 -0
  232. package/transpiled/RTD/index.js +38 -0
  233. package/transpiled/RTD/index.js.map +1 -0
  234. package/transpiled/RTD/segment.d.ts +8 -0
  235. package/transpiled/RTD/segment.js +23 -0
  236. package/transpiled/RTD/segment.js.map +1 -0
  237. package/transpiled/RTD/syntax/index.d.ts +2 -0
  238. package/transpiled/RTD/syntax/index.js +19 -0
  239. package/transpiled/RTD/syntax/index.js.map +1 -0
  240. package/transpiled/RTD/syntax/parse.d.ts +4 -0
  241. package/transpiled/RTD/syntax/parse.js +128 -0
  242. package/transpiled/RTD/syntax/parse.js.map +1 -0
  243. package/transpiled/RTD/syntax/types.d.ts +41 -0
  244. package/transpiled/RTD/syntax/types.js +5 -0
  245. package/transpiled/RTD/syntax/types.js.map +1 -0
  246. package/transpiled/Remotes.d.ts +7 -0
  247. package/transpiled/Remotes.js +19 -0
  248. package/transpiled/Remotes.js.map +1 -0
  249. package/transpiled/Tenant.d.ts +12 -0
  250. package/transpiled/Tenant.js +30 -0
  251. package/transpiled/Tenant.js.map +1 -0
  252. package/transpiled/deployment.d.ts +3 -0
  253. package/transpiled/deployment.js +61 -0
  254. package/transpiled/deployment.js.map +1 -0
  255. package/transpiled/directives/auth/Anonymous.d.ts +6 -0
  256. package/transpiled/directives/auth/Anonymous.js +17 -0
  257. package/transpiled/directives/auth/Anonymous.js.map +1 -0
  258. package/transpiled/directives/auth/Echo.d.ts +6 -0
  259. package/transpiled/directives/auth/Echo.js +13 -0
  260. package/transpiled/directives/auth/Echo.js.map +1 -0
  261. package/transpiled/directives/auth/Family.d.ts +20 -0
  262. package/transpiled/directives/auth/Family.js +125 -0
  263. package/transpiled/directives/auth/Family.js.map +1 -0
  264. package/transpiled/directives/auth/Id.d.ts +7 -0
  265. package/transpiled/directives/auth/Id.js +17 -0
  266. package/transpiled/directives/auth/Id.js.map +1 -0
  267. package/transpiled/directives/auth/Incept.d.ts +10 -0
  268. package/transpiled/directives/auth/Incept.js +59 -0
  269. package/transpiled/directives/auth/Incept.js.map +1 -0
  270. package/transpiled/directives/auth/Role.d.ts +11 -0
  271. package/transpiled/directives/auth/Role.js +44 -0
  272. package/transpiled/directives/auth/Role.js.map +1 -0
  273. package/transpiled/directives/auth/Rule.d.ts +9 -0
  274. package/transpiled/directives/auth/Rule.js +22 -0
  275. package/transpiled/directives/auth/Rule.js.map +1 -0
  276. package/transpiled/directives/auth/Scheme.d.ts +7 -0
  277. package/transpiled/directives/auth/Scheme.js +47 -0
  278. package/transpiled/directives/auth/Scheme.js.map +1 -0
  279. package/transpiled/directives/auth/index.d.ts +2 -0
  280. package/transpiled/directives/auth/index.js +7 -0
  281. package/transpiled/directives/auth/index.js.map +1 -0
  282. package/transpiled/directives/auth/schemes.d.ts +3 -0
  283. package/transpiled/directives/auth/schemes.js +9 -0
  284. package/transpiled/directives/auth/schemes.js.map +1 -0
  285. package/transpiled/directives/auth/split.d.ts +2 -0
  286. package/transpiled/directives/auth/split.js +38 -0
  287. package/transpiled/directives/auth/split.js.map +1 -0
  288. package/transpiled/directives/auth/types.d.ts +31 -0
  289. package/transpiled/directives/auth/types.js +3 -0
  290. package/transpiled/directives/auth/types.js.map +1 -0
  291. package/transpiled/directives/dev/Family.d.ts +10 -0
  292. package/transpiled/directives/dev/Family.js +25 -0
  293. package/transpiled/directives/dev/Family.js.map +1 -0
  294. package/transpiled/directives/dev/Stub.d.ts +7 -0
  295. package/transpiled/directives/dev/Stub.js +14 -0
  296. package/transpiled/directives/dev/Stub.js.map +1 -0
  297. package/transpiled/directives/dev/index.d.ts +2 -0
  298. package/transpiled/directives/dev/index.js +7 -0
  299. package/transpiled/directives/dev/index.js.map +1 -0
  300. package/transpiled/directives/dev/types.d.ts +4 -0
  301. package/transpiled/directives/dev/types.js +3 -0
  302. package/transpiled/directives/dev/types.js.map +1 -0
  303. package/transpiled/directives/index.d.ts +2 -0
  304. package/transpiled/directives/index.js +10 -0
  305. package/transpiled/directives/index.js.map +1 -0
  306. package/transpiled/discovery.d.ts +1 -0
  307. package/transpiled/discovery.js +3 -0
  308. package/transpiled/discovery.js.map +1 -0
  309. package/transpiled/exceptions.d.ts +2 -0
  310. package/transpiled/exceptions.js +39 -0
  311. package/transpiled/exceptions.js.map +1 -0
  312. package/transpiled/index.d.ts +5 -0
  313. package/transpiled/index.js +12 -0
  314. package/transpiled/index.js.map +1 -0
  315. package/transpiled/manifest.d.ts +3 -0
  316. package/transpiled/manifest.js +30 -0
  317. package/transpiled/manifest.js.map +1 -0
  318. package/transpiled/root.d.ts +2 -0
  319. package/transpiled/root.js +39 -0
  320. package/transpiled/root.js.map +1 -0
  321. package/transpiled/schemas.d.ts +3 -0
  322. package/transpiled/schemas.js +14 -0
  323. package/transpiled/schemas.js.map +1 -0
  324. package/transpiled/tsconfig.tsbuildinfo +1 -0
  325. package/tsconfig.json +12 -0
  326. package/src/.manifest/index.js +0 -7
  327. package/src/.manifest/normalize.js +0 -58
  328. package/src/.manifest/schema.yaml +0 -71
  329. package/src/.manifest/validate.js +0 -17
  330. package/src/constants.js +0 -3
  331. package/src/deployment.js +0 -23
  332. package/src/exposition.js +0 -68
  333. package/src/factory.js +0 -76
  334. package/src/index.js +0 -9
  335. package/src/manifest.js +0 -12
  336. package/src/query/criteria.js +0 -55
  337. package/src/query/enum.js +0 -35
  338. package/src/query/index.js +0 -17
  339. package/src/query/query.js +0 -60
  340. package/src/query/range.js +0 -28
  341. package/src/query/sort.js +0 -19
  342. package/src/remote.js +0 -88
  343. package/src/server.js +0 -83
  344. package/src/tenant.js +0 -29
  345. package/src/translate/etag.js +0 -14
  346. package/src/translate/index.js +0 -7
  347. package/src/translate/request.js +0 -68
  348. package/src/translate/response.js +0 -62
  349. package/src/tree.js +0 -109
  350. package/test/manifest.normalize.fixtures.js +0 -37
  351. package/test/manifest.normalize.test.js +0 -37
  352. package/test/manifest.validate.test.js +0 -40
  353. package/test/query.range.test.js +0 -18
  354. package/test/tree.fixtures.js +0 -21
  355. package/test/tree.test.js +0 -44
  356. package/types/annotations.d.ts +0 -10
  357. package/types/declarations.d.ts +0 -31
  358. package/types/exposition.d.ts +0 -13
  359. package/types/http.d.ts +0 -13
  360. package/types/query.d.ts +0 -16
  361. package/types/remote.d.ts +0 -19
  362. package/types/server.d.ts +0 -13
  363. package/types/tree.d.ts +0 -33
@@ -0,0 +1,53 @@
1
+ import { Tenant } from './Tenant'
2
+ import { Gateway } from './Gateway'
3
+ import { Remotes } from './Remotes'
4
+ import { Tree, syntax } from './RTD'
5
+ import { Server } from './HTTP'
6
+ import { type Endpoint, EndpointFactory } from './Endpoint'
7
+ import * as directives from './directives'
8
+ import { type Directives, DirectivesFactory, type Family } from './Directive'
9
+ import { Composition } from './Composition'
10
+ import * as root from './root'
11
+ import type { Connector, Locator, extensions } from '@toa.io/core'
12
+
13
+ export class Factory implements extensions.Factory {
14
+ private readonly boot: Bootloader
15
+ private readonly families: Family[]
16
+
17
+ public constructor (boot: Bootloader) {
18
+ this.boot = boot
19
+ this.families = directives.families
20
+ }
21
+
22
+ public tenant (locator: Locator, manifest: syntax.Node): Connector {
23
+ const broadcast = this.boot.bindings.broadcast(CHANNEL, locator.id)
24
+
25
+ return new Tenant(broadcast, locator, manifest)
26
+ }
27
+
28
+ public service (): Connector | null {
29
+ return this.gateway()
30
+ }
31
+
32
+ private gateway (): Gateway {
33
+ const broadcast = this.boot.bindings.broadcast(CHANNEL)
34
+ const server = Server.create({ methods: syntax.verbs })
35
+ const remotes = new Remotes(this.boot)
36
+ const methods = new EndpointFactory(remotes)
37
+ const directives = new DirectivesFactory(this.families, remotes)
38
+ const node = root.resolve()
39
+ const tree = new Tree<Endpoint, Directives>(node, methods, directives)
40
+
41
+ const composition = new Composition(this.boot)
42
+ const gateway = new Gateway(broadcast, server, tree)
43
+
44
+ gateway.depends(composition)
45
+
46
+ return gateway
47
+ }
48
+ }
49
+
50
+ const CHANNEL = 'exposition'
51
+
52
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
53
+ export type Bootloader = typeof import('@toa.io/boot')
@@ -0,0 +1,93 @@
1
+ import { type bindings, Connector } from '@toa.io/core'
2
+ import { Nope, type Nopeable } from 'nopeable'
3
+ import * as http from './HTTP'
4
+ import { rethrow } from './exceptions'
5
+ import { type Method, type Parameter, type Tree } from './RTD'
6
+ import { type Label } from './discovery'
7
+ import { type Branch } from './Branch'
8
+ import { type Endpoint } from './Endpoint'
9
+ import { type Directives } from './Directive'
10
+
11
+ export class Gateway extends Connector {
12
+ private readonly broadcast: Broadcast
13
+ private readonly tree: Tree<Endpoint, Directives>
14
+
15
+ public constructor (broadcast: Broadcast, server: http.Server, tree: Tree<Endpoint, Directives>) {
16
+ super()
17
+
18
+ this.broadcast = broadcast
19
+ this.tree = tree
20
+
21
+ this.depends(broadcast)
22
+ this.depends(server)
23
+
24
+ server.attach(this.process.bind(this))
25
+ }
26
+
27
+ protected override async open (): Promise<void> {
28
+ await this.discover()
29
+
30
+ console.info('Gateway has started and is awaiting resource branches.')
31
+ }
32
+
33
+ protected override dispose (): void {
34
+ console.info('Gateway is closed.')
35
+ }
36
+
37
+ private async process (request: http.IncomingMessage): Promise<http.OutgoingMessage> {
38
+ if (request.path[request.path.length - 1] !== '/')
39
+ throw new http.NotFound('Trailing slash is required.')
40
+
41
+ const match = this.tree.match(request.path)
42
+
43
+ if (match === null)
44
+ throw new http.NotFound()
45
+
46
+ const { node, parameters } = match
47
+
48
+ if (!(request.method in node.methods))
49
+ throw new http.MethodNotAllowed()
50
+
51
+ const method = node.methods[request.method]
52
+ const interruption = await method.directives.preflight(request, parameters)
53
+ const response = interruption ?? await this.call(method, request, parameters)
54
+
55
+ await method.directives.settle(request, response)
56
+
57
+ return response
58
+ }
59
+
60
+ private async call
61
+ (method: Method<Endpoint, Directives>, request: http.IncomingMessage, parameters: Parameter[]):
62
+ Promise<http.OutgoingMessage> {
63
+ if (method.endpoint === null)
64
+ throw new http.MethodNotAllowed()
65
+
66
+ const reply = await method.endpoint
67
+ .call(request.body, request.query, parameters)
68
+ .catch(rethrow) as Nopeable<unknown>
69
+
70
+ if (reply instanceof Nope)
71
+ throw new http.Conflict(reply)
72
+
73
+ return { body: reply }
74
+ }
75
+
76
+ private async discover (): Promise<void> {
77
+ await this.broadcast.receive<Branch>('expose', this.merge.bind(this))
78
+ await this.broadcast.transmit<null>('ping', null)
79
+ }
80
+
81
+ private merge (branch: Branch): void {
82
+ try {
83
+ this.tree.merge(branch.node, branch)
84
+
85
+ console.info('Resource branch of ' +
86
+ `'${branch.namespace}.${branch.component}' has been merged.`)
87
+ } catch (exception) {
88
+ console.error(exception)
89
+ }
90
+ }
91
+ }
92
+
93
+ type Broadcast = bindings.Broadcast<Label>
@@ -0,0 +1,45 @@
1
+ import { Buffer } from 'buffer'
2
+ import { Readable } from 'stream'
3
+ import type { IncomingMessage } from './messages'
4
+ import type * as http from 'node:http'
5
+ import type { NextFunction, Response, Express, Request } from 'express'
6
+ import type { CorsOptions } from 'cors'
7
+
8
+ const server = {
9
+ close: jest.fn()
10
+ } as unknown as jest.Mock<http.Server>
11
+
12
+ const app = {
13
+ enable: jest.fn(),
14
+ disable: jest.fn(),
15
+ use: jest.fn(),
16
+ listen: jest.fn(() => server)
17
+ } as unknown as jest.Mock<Express>
18
+
19
+ export function createRequest (req: Partial<Request> = {}, content: string | Buffer = ''):
20
+ jest.MockedObject<Request> {
21
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content)
22
+ const stream = Readable.from(buffer)
23
+
24
+ Object.assign(stream, { headers: {} }, req)
25
+
26
+ return stream as unknown as jest.MockedObject<Request>
27
+ }
28
+
29
+ export function createIncomingMessage (path: string, method: string = 'GET'): IncomingMessage {
30
+ return { method, path, headers: {}, body: undefined, query: {} }
31
+ }
32
+
33
+ export const res = {
34
+ status: jest.fn(() => res),
35
+ sendStatus: jest.fn(() => res),
36
+ set: jest.fn(() => res),
37
+ send: jest.fn(() => res),
38
+ end: jest.fn(() => res)
39
+ } as unknown as jest.MockedObject<Response>
40
+
41
+ export const next = jest.fn() as unknown as NextFunction
42
+
43
+ export const express = jest.fn(() => app)
44
+
45
+ export const cors = jest.fn((_: CorsOptions) => () => undefined)
@@ -0,0 +1,221 @@
1
+ import { Connector } from '@toa.io/core'
2
+ import { immediate } from '@toa.io/generic'
3
+ import { generate } from 'randomstring'
4
+ import { type Processing, Server } from './Server'
5
+ import { type OutgoingMessage } from './messages'
6
+ import { express, cors, createRequest, res, next } from './Server.fixtures'
7
+ 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
+
15
+ let server: Server
16
+ let app: jest.MockedObject<Express>
17
+
18
+ beforeEach(() => {
19
+ jest.clearAllMocks()
20
+
21
+ server = Server.create()
22
+ app = express.mock.results[0]?.value
23
+ })
24
+
25
+ it('should instance of connector', async () => {
26
+ expect(server).toBeInstanceOf(Connector)
27
+ })
28
+
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
+ 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()
52
+
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
75
+ })
76
+
77
+ it('should register request handler', async () => {
78
+ const process = jest.fn(async () => ({})) as unknown as Processing
79
+ const req = createRequest()
80
+
81
+ server.attach(process)
82
+
83
+ await use(req)
84
+
85
+ expect(process).toHaveBeenCalled()
86
+ })
87
+
88
+ it('should send 501 on unknown method', async () => {
89
+ const req = createRequest({ method: generate() })
90
+
91
+ await use(req)
92
+
93
+ expect(res.sendStatus).toHaveBeenCalledWith(501)
94
+ })
95
+
96
+ describe('request', () => {
97
+ const process = jest.fn(async () => ({})) as unknown as Processing
98
+
99
+ beforeEach(() => {
100
+ server.attach(process)
101
+ })
102
+
103
+ it('should pass decoded request', async () => {
104
+ const path = generate()
105
+ const method = generate()
106
+ const headers = { 'content-type': 'application/json' }
107
+ const body = { [generate()]: generate() }
108
+ const json = JSON.stringify(body)
109
+ const req = createRequest({ path, method, headers }, json)
110
+
111
+ await use(req)
112
+
113
+ expect(process).toHaveBeenCalledWith(expect.objectContaining({ path, method, headers, body }))
114
+ })
115
+ })
116
+
117
+ describe('result', () => {
118
+ it('should send status code 200 if the result has a value', async () => {
119
+ const process = async (): Promise<OutgoingMessage> => ({ headers: {}, body: generate() })
120
+ const req = createRequest()
121
+
122
+ server.attach(process)
123
+ await use(req)
124
+
125
+ expect(res.status).toHaveBeenCalledWith(200)
126
+ })
127
+
128
+ it('should send status code 204 if the result has no value', async () => {
129
+ const process = async (): Promise<OutgoingMessage> => ({ headers: {} })
130
+ const req = createRequest()
131
+
132
+ server.attach(process)
133
+ await use(req)
134
+
135
+ expect(res.status).toHaveBeenCalledWith(204)
136
+ })
137
+
138
+ it('should send result', async () => {
139
+ const body = { [generate()]: generate() }
140
+ const json = JSON.stringify(body)
141
+ const buf = Buffer.from(json)
142
+ const process = async (): Promise<OutgoingMessage> => ({ headers: {}, body })
143
+ const req = createRequest({ headers: { accept: 'application/json' } })
144
+
145
+ server.attach(process)
146
+ await use(req)
147
+
148
+ expect(res.send).toHaveBeenCalledWith(buf)
149
+ })
150
+
151
+ it('should return 500 on exception', async () => {
152
+ const process = async (): Promise<OutgoingMessage> => {
153
+ throw new Error('Bad')
154
+ }
155
+
156
+ const req = createRequest()
157
+
158
+ server.attach(process)
159
+ await use(req)
160
+
161
+ expect(res.status).toHaveBeenCalledWith(500)
162
+ })
163
+
164
+ it('should output exception message if debug is enabled', async () => {
165
+ jest.clearAllMocks()
166
+
167
+ server = Server.create({ debug: true })
168
+ app = express.mock.results[0]?.value
169
+
170
+ const message = generate()
171
+ const req = createRequest()
172
+
173
+ const process = async (): Promise<OutgoingMessage> => {
174
+ throw new Error(message)
175
+ }
176
+
177
+ server.attach(process)
178
+ await use(req)
179
+
180
+ expect(res.status).toHaveBeenCalledWith(500)
181
+ })
182
+
183
+ it('should send client error', async () => {
184
+ const req = createRequest()
185
+ const message = generate()
186
+
187
+ const process = async (): Promise<OutgoingMessage> => {
188
+ throw new BadRequest(message)
189
+ }
190
+
191
+ server.attach(process)
192
+ await use(req)
193
+
194
+ expect(res.status).toHaveBeenCalledWith(400)
195
+ })
196
+ })
197
+
198
+ describe('options', () => {
199
+ it('should send 501 on unspecified method', async () => {
200
+ jest.clearAllMocks()
201
+
202
+ server = Server.create({ methods: new Set(['COPY']) })
203
+ app = express.mock.results[0]?.value
204
+
205
+ const req = createRequest({ method: 'GET' })
206
+
207
+ await use(req)
208
+
209
+ expect(res.sendStatus).toHaveBeenCalledWith(501)
210
+ })
211
+ })
212
+
213
+ async function use (req: Request): Promise<void> {
214
+ for (const call of app.use.mock.calls) {
215
+ const usage = call[0] as unknown as RequestHandler
216
+
217
+ usage(req, res, next)
218
+ }
219
+
220
+ await immediate()
221
+ }
@@ -0,0 +1,135 @@
1
+ import express from 'express'
2
+ import cors from 'cors'
3
+ import { Connector } from '@toa.io/core'
4
+ import { promex } from '@toa.io/generic'
5
+ import { read, write, type IncomingMessage, type OutgoingMessage } from './messages'
6
+ import { ClientError, Exception } from './exceptions'
7
+ import type * as http from 'node:http'
8
+ import type { Express, Request, Response, NextFunction } from 'express'
9
+
10
+ export class Server extends Connector {
11
+ private readonly debug: boolean
12
+ private readonly app: Express
13
+ private server?: http.Server
14
+
15
+ private constructor (app: Express, debug: boolean) {
16
+ super()
17
+
18
+ this.app = app
19
+ this.debug = debug
20
+ }
21
+
22
+ public static create (options: Partial<Properties> = {}): Server {
23
+ const properties: Properties = Object.assign({}, defaults(), options)
24
+
25
+ const app = express()
26
+
27
+ app.disable('x-powered-by')
28
+ app.use(cors({ allowedHeaders: ['content-type'] }))
29
+ app.use(supportedMethods(properties.methods))
30
+
31
+ return new Server(app, properties.debug)
32
+ }
33
+
34
+ public attach (process: Processing): void {
35
+ this.app.use((request: Request, response: Response): void => {
36
+ this.read(request)
37
+ .then(process)
38
+ .then(this.success(request, response))
39
+ .catch(this.fail(request, response))
40
+ })
41
+ }
42
+
43
+ protected override async open (): Promise<void> {
44
+ const listening = promex()
45
+
46
+ this.server = this.app.listen(8000, listening.callback)
47
+
48
+ await listening
49
+
50
+ console.info('HTTP Server is listening.')
51
+ }
52
+
53
+ protected override async close (): Promise<void> {
54
+ const stopped = promex()
55
+
56
+ this.server?.close(stopped.callback)
57
+
58
+ await stopped
59
+ }
60
+
61
+ protected override dispose (): void {
62
+ console.info('HTTP Server has been stopped.')
63
+ }
64
+
65
+ private async read (request: Request): Promise<IncomingMessage> {
66
+ const { method, path, headers, query } = request
67
+ const body = await read(request)
68
+
69
+ return { method, path, headers, query, body }
70
+ }
71
+
72
+ private success (request: Request, response: Response) {
73
+ return (message: OutgoingMessage) => {
74
+ let status = message.status
75
+
76
+ if (status === undefined)
77
+ if (message.body === null) status = 404
78
+ else if (request.method === 'POST') status = 201
79
+ else if (message.body === undefined) status = 204
80
+ else status = 200
81
+
82
+ response
83
+ .status(status)
84
+ .set(message.headers)
85
+
86
+ if (message.body !== undefined && message.body !== null)
87
+ write(request, response, message.body)
88
+ else
89
+ response.end()
90
+ }
91
+ }
92
+
93
+ private fail (request: Request, response: Response) {
94
+ return (exception: Error) => {
95
+ let status = 500
96
+
97
+ if (exception instanceof Exception)
98
+ status = exception.status
99
+
100
+ const outputAllowed = exception instanceof ClientError || this.debug
101
+
102
+ response.status(status)
103
+
104
+ if (outputAllowed) {
105
+ const body = exception instanceof Exception
106
+ ? exception.body
107
+ : (exception.stack ?? exception.message)
108
+
109
+ write(request, response, body)
110
+ } else
111
+ response.end()
112
+ }
113
+ }
114
+ }
115
+
116
+ function supportedMethods (methods: Set<string>) {
117
+ return (req: Request, res: Response, next: NextFunction): void => {
118
+ if (methods.has(req.method)) next()
119
+ else res.sendStatus(501)
120
+ }
121
+ }
122
+
123
+ interface Properties {
124
+ methods: Set<string>
125
+ debug: boolean
126
+ }
127
+
128
+ function defaults (): Properties {
129
+ return {
130
+ methods: new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),
131
+ debug: process.env.TOA_DEV === '1'
132
+ }
133
+ }
134
+
135
+ export type Processing = (input: IncomingMessage) => Promise<OutgoingMessage>
@@ -0,0 +1,77 @@
1
+ import { types } from './formats'
2
+
3
+ export class Exception extends Error {
4
+ public readonly status: number
5
+ public readonly body?: any
6
+
7
+ protected constructor (status: number, body?: any) {
8
+ super()
9
+ this.status = status
10
+ this.body = body
11
+ }
12
+ }
13
+
14
+ export class ClientError extends Exception {
15
+ }
16
+
17
+ export class BadRequest extends ClientError {
18
+ public constructor (body?: any) {
19
+ super(400, body)
20
+ }
21
+ }
22
+
23
+ export class Unauthorized extends ClientError {
24
+ public constructor (body?: any) {
25
+ super(401, body)
26
+ }
27
+ }
28
+
29
+ export class Forbidden extends ClientError {
30
+ public constructor (body?: any) {
31
+ super(403, body)
32
+ }
33
+ }
34
+
35
+ export class NotFound extends ClientError {
36
+ public constructor (body?: any) {
37
+ super(404, body)
38
+ }
39
+ }
40
+
41
+ export class Conflict extends ClientError {
42
+ public constructor (body: any) {
43
+ super(409, body)
44
+ }
45
+ }
46
+
47
+ export class MethodNotAllowed extends ClientError {
48
+ public constructor () {
49
+ super(405)
50
+ }
51
+ }
52
+
53
+ class MediaTypeException extends ClientError {
54
+ private static readonly message = 'Supported media types:\n- ' + types.join('\n- ')
55
+
56
+ protected constructor (status: number) {
57
+ super(status, MediaTypeException.message)
58
+ }
59
+ }
60
+
61
+ export class NotAcceptable extends MediaTypeException {
62
+ public constructor () {
63
+ super(406)
64
+ }
65
+ }
66
+
67
+ export class UnsupportedMediaType extends MediaTypeException {
68
+ public constructor () {
69
+ super(415)
70
+ }
71
+ }
72
+
73
+ export class PreconditionFailed extends ClientError {
74
+ public constructor () {
75
+ super(412)
76
+ }
77
+ }
@@ -0,0 +1,19 @@
1
+ import { type Buffer } from 'node:buffer'
2
+ import * as json from './json'
3
+ import * as yaml from './yaml'
4
+ import * as msgpack from './msgpack'
5
+ import * as text from './text'
6
+
7
+ export const formats: Record<string, Format> = {
8
+ 'application/yaml': yaml,
9
+ 'application/msgpack': msgpack,
10
+ 'application/json': json,
11
+ 'text/plain': text
12
+ }
13
+
14
+ export const types = Object.keys(formats)
15
+
16
+ export interface Format {
17
+ encode: (value: any) => Buffer
18
+ decode: (buffer: Buffer) => any
19
+ }
@@ -0,0 +1,13 @@
1
+ import { Buffer } from 'node:buffer'
2
+
3
+ export function decode (buffer: Buffer): any {
4
+ const text = buffer.toString()
5
+
6
+ return JSON.parse(text)
7
+ }
8
+
9
+ export function encode (value: any): Buffer {
10
+ const text = JSON.stringify(value)
11
+
12
+ return Buffer.from(text)
13
+ }
@@ -0,0 +1,10 @@
1
+ import { type Buffer } from 'node:buffer'
2
+ import * as msgpack from 'msgpackr'
3
+
4
+ export function decode (buffer: Buffer): any {
5
+ return msgpack.decode(buffer)
6
+ }
7
+
8
+ export function encode (value: any): Buffer {
9
+ return msgpack.encode(value)
10
+ }
@@ -0,0 +1,9 @@
1
+ import { Buffer } from 'node:buffer'
2
+
3
+ export function decode (buffer: Buffer): any {
4
+ return buffer.toString()
5
+ }
6
+
7
+ export function encode (value: any): Buffer {
8
+ return Buffer.from(value.toString())
9
+ }
@@ -0,0 +1,14 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import * as yaml from 'js-yaml'
3
+
4
+ export function decode (buffer: Buffer): any {
5
+ const text = buffer.toString()
6
+
7
+ return yaml.load(text)
8
+ }
9
+
10
+ export function encode (value: any): Buffer {
11
+ const text = yaml.dump(value)
12
+
13
+ return Buffer.from(text)
14
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Server'
2
+ export * from './messages'
3
+ export * from './exceptions'