@toa.io/extensions.exposition 0.24.0-alpha.17 → 0.24.0-alpha.19

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 (234) 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/cucumber.js +0 -1
  43. package/documentation/components.md +24 -1
  44. package/documentation/identity.md +7 -7
  45. package/documentation/protocol.md +20 -1
  46. package/documentation/query.md +1 -1
  47. package/documentation/vary.md +69 -0
  48. package/features/cors.feature +6 -26
  49. package/features/identity.feature +17 -1
  50. package/features/identity.federation.feature +125 -0
  51. package/features/response.feature +0 -0
  52. package/features/steps/Captures.ts +5 -0
  53. package/features/steps/Components.ts +5 -0
  54. package/features/steps/HTTP.ts +33 -5
  55. package/features/steps/IdP.ts +120 -0
  56. package/features/steps/Parameters.ts +8 -2
  57. package/features/steps/Workspace.ts +5 -7
  58. package/features/vary.feature +120 -0
  59. package/package.json +17 -18
  60. package/source/Directive.test.ts +8 -2
  61. package/source/Directive.ts +19 -16
  62. package/source/Factory.ts +8 -7
  63. package/source/Gateway.ts +22 -8
  64. package/source/HTTP/Server.fixtures.ts +0 -1
  65. package/source/HTTP/Server.test.ts +61 -138
  66. package/source/HTTP/Server.ts +45 -31
  67. package/source/HTTP/formats/text.ts +1 -1
  68. package/source/HTTP/formats/yaml.ts +1 -1
  69. package/source/HTTP/messages.ts +8 -2
  70. package/source/Interception.ts +24 -0
  71. package/source/RTD/Directives.ts +2 -2
  72. package/source/RTD/syntax/parse.ts +6 -6
  73. package/source/RTD/syntax/types.ts +1 -1
  74. package/source/directives/auth/{Family.ts → Authorization.ts} +29 -33
  75. package/source/directives/auth/Incept.ts +1 -1
  76. package/source/directives/auth/Rule.ts +2 -2
  77. package/source/directives/auth/index.ts +2 -2
  78. package/source/directives/auth/schemes.ts +2 -1
  79. package/source/directives/auth/types.ts +9 -6
  80. package/source/directives/cache/{Family.ts → Cache.ts} +4 -5
  81. package/source/directives/cache/index.ts +2 -2
  82. package/source/directives/cache/types.ts +1 -1
  83. package/source/directives/cors/CORS.ts +52 -0
  84. package/source/directives/cors/index.ts +3 -0
  85. package/source/directives/dev/{Family.ts → Development.ts} +3 -4
  86. package/source/directives/dev/Stub.ts +4 -4
  87. package/source/directives/dev/Throw.ts +4 -4
  88. package/source/directives/dev/index.ts +2 -2
  89. package/source/directives/dev/types.ts +1 -1
  90. package/source/directives/index.ts +10 -6
  91. package/source/directives/octets/Context.ts +1 -1
  92. package/source/directives/octets/Delete.ts +1 -2
  93. package/source/directives/octets/Fetch.ts +1 -1
  94. package/source/directives/octets/List.ts +1 -1
  95. package/source/directives/octets/{Family.ts → Octets.ts} +3 -4
  96. package/source/directives/octets/Permute.ts +1 -1
  97. package/source/directives/octets/Store.ts +3 -3
  98. package/source/directives/octets/index.ts +2 -2
  99. package/source/directives/octets/types.ts +3 -3
  100. package/source/directives/vary/Directive.ts +6 -0
  101. package/source/directives/vary/Embed.ts +62 -0
  102. package/source/directives/vary/Properties.ts +17 -0
  103. package/source/directives/vary/Vary.ts +48 -0
  104. package/source/directives/vary/embeddings/Embedding.ts +6 -0
  105. package/source/directives/vary/embeddings/Header.ts +30 -0
  106. package/source/directives/vary/embeddings/Language.ts +31 -0
  107. package/source/directives/vary/embeddings/index.ts +11 -0
  108. package/source/directives/vary/index.ts +3 -0
  109. package/source/io.ts +4 -0
  110. package/transpiled/Composition.js.map +1 -1
  111. package/transpiled/Directive.d.ts +6 -7
  112. package/transpiled/Directive.js +12 -10
  113. package/transpiled/Directive.js.map +1 -1
  114. package/transpiled/Factory.d.ts +0 -1
  115. package/transpiled/Factory.js +7 -6
  116. package/transpiled/Factory.js.map +1 -1
  117. package/transpiled/Gateway.d.ts +8 -5
  118. package/transpiled/Gateway.js +12 -2
  119. package/transpiled/Gateway.js.map +1 -1
  120. package/transpiled/HTTP/Server.d.ts +4 -2
  121. package/transpiled/HTTP/Server.fixtures.d.ts +0 -1
  122. package/transpiled/HTTP/Server.fixtures.js +1 -2
  123. package/transpiled/HTTP/Server.fixtures.js.map +1 -1
  124. package/transpiled/HTTP/Server.js +33 -22
  125. package/transpiled/HTTP/Server.js.map +1 -1
  126. package/transpiled/HTTP/formats/text.d.ts +3 -1
  127. package/transpiled/HTTP/formats/text.js.map +1 -1
  128. package/transpiled/HTTP/formats/yaml.js +1 -1
  129. package/transpiled/HTTP/formats/yaml.js.map +1 -1
  130. package/transpiled/HTTP/messages.d.ts +4 -0
  131. package/transpiled/HTTP/messages.js +4 -2
  132. package/transpiled/HTTP/messages.js.map +1 -1
  133. package/transpiled/Interception.d.ts +9 -0
  134. package/transpiled/Interception.js +19 -0
  135. package/transpiled/Interception.js.map +1 -0
  136. package/transpiled/Query.js.map +1 -1
  137. package/transpiled/RTD/Directives.d.ts +2 -2
  138. package/transpiled/RTD/Node.js.map +1 -1
  139. package/transpiled/RTD/Route.js.map +1 -1
  140. package/transpiled/RTD/syntax/parse.js +1 -1
  141. package/transpiled/RTD/syntax/parse.js.map +1 -1
  142. package/transpiled/RTD/syntax/types.js +1 -1
  143. package/transpiled/RTD/syntax/types.js.map +1 -1
  144. package/transpiled/deployment.js.map +1 -1
  145. package/transpiled/directives/auth/{Family.d.ts → Authorization.d.ts} +4 -4
  146. package/transpiled/directives/auth/{Family.js → Authorization.js} +15 -8
  147. package/transpiled/directives/auth/Authorization.js.map +1 -0
  148. package/transpiled/directives/auth/Incept.js.map +1 -1
  149. package/transpiled/directives/auth/Role.js.map +1 -1
  150. package/transpiled/directives/auth/Rule.d.ts +2 -2
  151. package/transpiled/directives/auth/Rule.js.map +1 -1
  152. package/transpiled/directives/auth/index.d.ts +2 -2
  153. package/transpiled/directives/auth/index.js +4 -5
  154. package/transpiled/directives/auth/index.js.map +1 -1
  155. package/transpiled/directives/auth/schemes.js +2 -1
  156. package/transpiled/directives/auth/schemes.js.map +1 -1
  157. package/transpiled/directives/auth/types.d.ts +4 -4
  158. package/transpiled/directives/cache/{Family.d.ts → Cache.d.ts} +4 -5
  159. package/transpiled/directives/cache/{Family.js → Cache.js} +4 -2
  160. package/transpiled/directives/cache/{Family.js.map → Cache.js.map} +1 -1
  161. package/transpiled/directives/cache/index.d.ts +2 -2
  162. package/transpiled/directives/cache/index.js +4 -5
  163. package/transpiled/directives/cache/index.js.map +1 -1
  164. package/transpiled/directives/cache/types.d.ts +1 -1
  165. package/transpiled/directives/cors/CORS.d.ts +14 -0
  166. package/transpiled/directives/cors/CORS.js +42 -0
  167. package/transpiled/directives/cors/CORS.js.map +1 -0
  168. package/transpiled/directives/cors/index.d.ts +2 -0
  169. package/transpiled/directives/cors/index.js +6 -0
  170. package/transpiled/directives/cors/index.js.map +1 -0
  171. package/transpiled/directives/dev/{Family.d.ts → Development.d.ts} +3 -4
  172. package/transpiled/directives/dev/{Family.js → Development.js} +4 -2
  173. package/transpiled/directives/dev/Development.js.map +1 -0
  174. package/transpiled/directives/dev/Stub.d.ts +3 -3
  175. package/transpiled/directives/dev/Stub.js.map +1 -1
  176. package/transpiled/directives/dev/Throw.d.ts +3 -3
  177. package/transpiled/directives/dev/Throw.js.map +1 -1
  178. package/transpiled/directives/dev/index.d.ts +2 -2
  179. package/transpiled/directives/dev/index.js +4 -5
  180. package/transpiled/directives/dev/index.js.map +1 -1
  181. package/transpiled/directives/dev/types.d.ts +1 -1
  182. package/transpiled/directives/index.d.ts +3 -1
  183. package/transpiled/directives/index.js +9 -9
  184. package/transpiled/directives/index.js.map +1 -1
  185. package/transpiled/directives/octets/Context.d.ts +1 -1
  186. package/transpiled/directives/octets/Delete.d.ts +1 -1
  187. package/transpiled/directives/octets/Delete.js.map +1 -1
  188. package/transpiled/directives/octets/Fetch.d.ts +1 -1
  189. package/transpiled/directives/octets/List.d.ts +1 -1
  190. package/transpiled/directives/octets/{Family.d.ts → Octets.d.ts} +3 -4
  191. package/transpiled/directives/octets/{Family.js → Octets.js} +4 -2
  192. package/transpiled/directives/octets/Octets.js.map +1 -0
  193. package/transpiled/directives/octets/Permute.d.ts +1 -1
  194. package/transpiled/directives/octets/Store.d.ts +1 -1
  195. package/transpiled/directives/octets/Store.js.map +1 -1
  196. package/transpiled/directives/octets/index.d.ts +2 -2
  197. package/transpiled/directives/octets/index.js +4 -5
  198. package/transpiled/directives/octets/index.js.map +1 -1
  199. package/transpiled/directives/octets/types.d.ts +3 -3
  200. package/transpiled/directives/vary/Directive.d.ts +5 -0
  201. package/transpiled/directives/vary/Directive.js +3 -0
  202. package/transpiled/directives/vary/Directive.js.map +1 -0
  203. package/transpiled/directives/vary/Embed.d.ts +10 -0
  204. package/transpiled/directives/vary/Embed.js +49 -0
  205. package/transpiled/directives/vary/Embed.js.map +1 -0
  206. package/transpiled/directives/vary/Properties.d.ts +9 -0
  207. package/transpiled/directives/vary/Properties.js +16 -0
  208. package/transpiled/directives/vary/Properties.js.map +1 -0
  209. package/transpiled/directives/vary/Vary.d.ts +10 -0
  210. package/transpiled/directives/vary/Vary.js +36 -0
  211. package/transpiled/directives/vary/Vary.js.map +1 -0
  212. package/transpiled/directives/vary/embeddings/Embedding.d.ts +5 -0
  213. package/transpiled/directives/vary/embeddings/Embedding.js +3 -0
  214. package/transpiled/directives/vary/embeddings/Embedding.js.map +1 -0
  215. package/transpiled/directives/vary/embeddings/Header.d.ts +7 -0
  216. package/transpiled/directives/vary/embeddings/Header.js +26 -0
  217. package/transpiled/directives/vary/embeddings/Header.js.map +1 -0
  218. package/transpiled/directives/vary/embeddings/Language.d.ts +7 -0
  219. package/transpiled/directives/vary/embeddings/Language.js +28 -0
  220. package/transpiled/directives/vary/embeddings/Language.js.map +1 -0
  221. package/transpiled/directives/vary/embeddings/index.d.ts +5 -0
  222. package/transpiled/directives/vary/embeddings/index.js +10 -0
  223. package/transpiled/directives/vary/embeddings/index.js.map +1 -0
  224. package/transpiled/directives/vary/index.d.ts +2 -0
  225. package/transpiled/directives/vary/index.js +6 -0
  226. package/transpiled/directives/vary/index.js.map +1 -0
  227. package/transpiled/io.d.ts +3 -0
  228. package/transpiled/io.js +3 -0
  229. package/transpiled/io.js.map +1 -0
  230. package/transpiled/manifest.js.map +1 -1
  231. package/transpiled/tsconfig.tsbuildinfo +1 -1
  232. package/transpiled/directives/auth/Family.js.map +0 -1
  233. package/transpiled/directives/dev/Family.js.map +0 -1
  234. package/transpiled/directives/octets/Family.js.map +0 -1
@@ -1,17 +1,21 @@
1
- import { type IncomingMessage, type OutgoingMessage } from './HTTP'
2
- import { type Remotes } from './Remotes'
1
+ import type { IncomingMessage, OutgoingMessage } from './HTTP'
2
+ import type { Remotes } from './Remotes'
3
+ import type { Output } from './io'
3
4
  import type * as RTD from './RTD'
4
5
 
5
6
  export class Directives implements RTD.Directives<Directives> {
6
- private readonly directives: DirectiveSet[]
7
+ private readonly sets: DirectiveSet[]
7
8
 
8
- public constructor (directives: DirectiveSet[]) {
9
- this.directives = directives
9
+ public constructor (sets: DirectiveSet[]) {
10
+ this.sets = sets
10
11
  }
11
12
 
12
13
  public async preflight (request: IncomingMessage, parameters: RTD.Parameter[]): Promise<Output> {
13
- for (const directive of this.directives) {
14
- const output = await directive.family.preflight(directive.directives, request, parameters)
14
+ for (const set of this.sets) {
15
+ if (set.family.preflight === undefined)
16
+ continue
17
+
18
+ const output = await set.family.preflight(set.directives, request, parameters)
15
19
 
16
20
  if (output !== null) {
17
21
  await this.settle(request, output)
@@ -24,13 +28,13 @@ export class Directives implements RTD.Directives<Directives> {
24
28
  }
25
29
 
26
30
  public async settle (request: IncomingMessage, response: OutgoingMessage): Promise<void> {
27
- for (const directive of this.directives)
28
- if (directive.family.settle !== undefined)
29
- await directive.family.settle(directive.directives, request, response)
31
+ for (const set of this.sets)
32
+ if (set.family.settle !== undefined)
33
+ await set.family.settle(set.directives, request, response)
30
34
  }
31
35
 
32
36
  public merge (directives: Directives): void {
33
- this.directives.push(...directives.directives)
37
+ this.sets.push(...directives.sets)
34
38
  }
35
39
  }
36
40
 
@@ -61,7 +65,7 @@ export class DirectivesFactory implements RTD.DirectivesFactory<Directives> {
61
65
  const family = this.families[declaration.family]
62
66
 
63
67
  if (family === undefined)
64
- throw new Error(`Directive family '${declaration.family}' not found.`)
68
+ throw new Error(`Directive family '${declaration.family}' is not found.`)
65
69
 
66
70
  const directive = family.create(declaration.name, declaration.value, this.remtoes)
67
71
 
@@ -100,9 +104,11 @@ export interface Family<TDirective = any, TExtension = any> {
100
104
  readonly name: string
101
105
  readonly mandatory: boolean
102
106
 
107
+ // produce: (declarations: RTD.syntax.Directive[], remotes: Remotes) => TDirective[]
108
+
103
109
  create: (name: string, value: any, remotes: Remotes) => TDirective
104
110
 
105
- preflight: (directives: TDirective[],
111
+ preflight?: (directives: TDirective[],
106
112
  request: IncomingMessage & TExtension,
107
113
  parameters: RTD.Parameter[]) => Output | Promise<Output>
108
114
 
@@ -115,6 +121,3 @@ interface DirectiveSet {
115
121
  family: Family
116
122
  directives: any[]
117
123
  }
118
-
119
- export type Input = IncomingMessage
120
- export type Output = OutgoingMessage | null
package/source/Factory.ts CHANGED
@@ -4,19 +4,18 @@ import { Remotes } from './Remotes'
4
4
  import { Tree, syntax } from './RTD'
5
5
  import { Server } from './HTTP'
6
6
  import { type Endpoint, EndpointsFactory } from './Endpoint'
7
- import * as directives from './directives'
8
- import { type Directives, DirectivesFactory, type Family } from './Directive'
7
+ import { families, interceptors } from './directives'
8
+ import { type Directives, DirectivesFactory } from './Directive'
9
9
  import { Composition } from './Composition'
10
10
  import * as root from './root'
11
+ import { Interception } from './Interception'
11
12
  import type { Connector, Locator, extensions } from '@toa.io/core'
12
13
 
13
14
  export class Factory implements extensions.Factory {
14
15
  private readonly boot: Bootloader
15
- private readonly families: Family[]
16
16
 
17
17
  public constructor (boot: Bootloader) {
18
18
  this.boot = boot
19
- this.families = directives.families
20
19
  }
21
20
 
22
21
  public tenant (locator: Locator, node: syntax.Node): Connector {
@@ -32,16 +31,18 @@ export class Factory implements extensions.Factory {
32
31
  const remotes = new Remotes(this.boot)
33
32
  const node = root.resolve()
34
33
  const methods = new EndpointsFactory(remotes)
35
- const directives = new DirectivesFactory(this.families, remotes)
34
+ const directives = new DirectivesFactory(families, remotes)
35
+ const interception = new Interception(interceptors)
36
36
  const tree = new Tree<Endpoint, Directives>(node, methods, directives)
37
37
 
38
38
  const composition = new Composition(this.boot)
39
- const gateway = new Gateway(broadcast, server, tree)
39
+ const gateway = new Gateway(broadcast, server, tree, interception)
40
40
 
41
41
  gateway.depends(remotes)
42
42
  gateway.depends(composition)
43
+ server.depends(gateway)
43
44
 
44
- return gateway
45
+ return server
45
46
  }
46
47
  }
47
48
 
package/source/Gateway.ts CHANGED
@@ -1,25 +1,31 @@
1
1
  import { type bindings, Connector } from '@toa.io/core'
2
- import { type Maybe } from '@toa.io/types'
3
2
  import * as http from './HTTP'
4
3
  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'
4
+ import type { Interceptor } from './Interception'
5
+ import type { Maybe } from '@toa.io/types'
6
+ import type { Method, Parameter, Tree } from './RTD'
7
+ import type { Label } from './discovery'
8
+ import type { Branch } from './Branch'
9
+ import type { Endpoint } from './Endpoint'
10
+ import type { Directives } from './Directive'
10
11
 
11
12
  export class Gateway extends Connector {
12
13
  private readonly broadcast: Broadcast
13
14
  private readonly tree: Tree<Endpoint, Directives>
15
+ private readonly interceptor: Interceptor
16
+ private readonly server: Connector
14
17
 
15
- public constructor (broadcast: Broadcast, server: http.Server, tree: Tree<Endpoint, Directives>) {
18
+ // eslint-disable-next-line max-params, max-len
19
+ public constructor (broadcast: Broadcast, server: http.Server, tree: Tree<Endpoint, Directives>, interception: Interceptor) {
16
20
  super()
17
21
 
18
22
  this.broadcast = broadcast
19
23
  this.tree = tree
24
+ this.interceptor = interception
25
+ this.server = server
20
26
 
21
27
  this.depends(broadcast)
22
- this.depends(server)
28
+ // this.depends(server)
23
29
 
24
30
  server.attach(this.process.bind(this))
25
31
  }
@@ -35,6 +41,11 @@ export class Gateway extends Connector {
35
41
  }
36
42
 
37
43
  private async process (request: http.IncomingMessage): Promise<http.OutgoingMessage> {
44
+ const interception = await this.interceptor.intercept(request)
45
+
46
+ if (interception !== null)
47
+ return interception
48
+
38
49
  const match = this.tree.match(request.path)
39
50
 
40
51
  if (match === null)
@@ -68,6 +79,9 @@ export class Gateway extends Connector {
68
79
 
69
80
  const body = await request.parse()
70
81
 
82
+ if ('embed' in request && typeof body === 'object' && body !== null)
83
+ Object.assign(body, request.embed)
84
+
71
85
  const reply = await method.endpoint
72
86
  .call(body, request.query, parameters)
73
87
  .catch(rethrow) as Maybe<unknown>
@@ -1,4 +1,3 @@
1
- import { Buffer } from 'buffer'
2
1
  import { Readable } from 'stream'
3
2
  import type { IncomingMessage } from './messages'
4
3
  import type * as http from 'node:http'
@@ -1,203 +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(expect.objectContaining({
36
- credentials: true,
37
- maxAge: 86400,
38
- allowedHeaders: ['accept', 'authorization', 'content-type']
39
- } satisfies CorsOptions))
40
-
41
- const middleware = cors.mock.results[0].value
42
-
43
- expect(app.use).toHaveBeenCalledWith(middleware)
44
- })
45
-
46
17
  it('should start HTTP server', async () => {
47
- const stared = server.connect()
48
-
49
- await immediate()
50
-
51
- expect(app.listen).toHaveBeenCalledWith(8000, expect.anything())
52
-
53
- const done = app.listen.mock.calls[0][1]
54
-
55
- if (done !== undefined) done()
18
+ await server.connect()
56
19
 
57
- await stared
58
- })
59
-
60
- it('should stop HTTP server', async () => {
61
- const started = server.connect()
62
-
63
- await immediate()
64
-
65
- app.listen.mock.calls[0][1]?.() // `listen` callback
66
-
67
- await started
68
-
69
- const stopped = server.disconnect()
70
- const httpServer: jest.MockedObject<http.Server> = app.listen.mock.results[0].value
71
-
72
- await immediate()
73
-
74
- expect(httpServer.close).toHaveBeenCalled()
75
-
76
- httpServer.close.mock.calls[0][0]?.() // `close` callback
77
-
78
- await stopped
20
+ expect(server.connected).toBeTruthy()
21
+ expect(server.port).toBeGreaterThan(0)
79
22
  })
80
23
 
81
24
  it('should register request handler', async () => {
82
- const process = jest.fn(async () => ({})) as unknown as Processing
83
- const req = createRequest()
25
+ const process: Processing = jest.fn().mockResolvedValue(undefined)
84
26
 
85
27
  server.attach(process)
86
28
 
87
- await use(req)
29
+ const res = await fetch(`http://localhost:${server.port}`)
30
+
31
+ await res.text()
88
32
 
89
- expect(process).toHaveBeenCalled()
33
+ expect(process).toHaveBeenCalledTimes(1)
90
34
  })
91
35
 
92
36
  it('should send 501 on unknown method', async () => {
93
- const req = createRequest({ method: generate() })
37
+ const head = await fetch(`http://localhost:${server.port}/`, { method: 'COPY' })
94
38
 
95
- await use(req)
39
+ await head.text()
40
+ expect(head.status).toBe(501)
41
+ })
96
42
 
97
- 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()
98
47
  })
99
48
 
100
49
  describe('result', () => {
101
- it('should send status code 200 if the result has a value', async () => {
102
- 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
+ })
103
58
 
59
+ it('should send status code 200 if the result has a value', async () => {
104
60
  server.attach(async (): Promise<OutgoingMessage> => ({
105
- headers: new Headers(), body: generate()
61
+ headers: new Headers(), body: randomUUID()
106
62
  }))
107
- await use(req)
108
63
 
109
- 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)
110
68
  })
111
69
 
112
70
  it('should send status code 204 if the result has no value', async () => {
113
- const req = createRequest()
114
-
115
71
  server.attach(async (): Promise<OutgoingMessage> => ({ headers: new Headers() }))
116
- await use(req)
117
72
 
118
- 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)
119
77
  })
120
78
 
121
79
  it('should send result', async () => {
122
- const body = { [generate()]: generate() }
123
- const json = JSON.stringify(body)
124
- const buf = Buffer.from(json)
125
- const req = createRequest({ headers: { accept: 'application/json' } })
80
+ const body = { [randomUUID()]: randomUUID() }
126
81
 
127
- server.attach(async (): Promise<OutgoingMessage> => ({ headers: new Headers(), body }))
128
- await use(req)
82
+ server.attach(async (): Promise<OutgoingMessage> =>
83
+ ({ headers: new Headers(), body }))
129
84
 
130
- expect(res.end).toHaveBeenCalledWith(buf)
131
- })
85
+ const res = await fetch(`http://localhost:${server.port}/`,
86
+ { headers: { accept: 'application/json' } })
132
87
 
133
- it('should return 500 on exception', async () => {
134
- async function process (): Promise<OutgoingMessage> {
135
- throw new Error('Bad')
136
- }
88
+ const result = await res.json()
137
89
 
138
- const req = createRequest()
139
-
140
- server.attach(process)
141
- await use(req)
142
-
143
- expect(res.status).toHaveBeenCalledWith(500)
90
+ expect(result).toEqual(body)
144
91
  })
145
92
 
146
- it('should output exception message if debug is enabled', async () => {
147
- jest.clearAllMocks()
148
-
149
- server = Server.create({ debug: true })
150
- app = express.mock.results[0]?.value
151
-
152
- const message = generate()
153
- const req = createRequest()
154
-
155
- async function process (): Promise<OutgoingMessage> {
156
- throw new Error(message)
157
- }
93
+ it('should return 500 on exception', async () => {
94
+ server.attach(jest.fn().mockRejectedValue(new Error('Bad')))
158
95
 
159
- server.attach(process)
160
- await use(req)
96
+ const res = await fetch(`http://localhost:${server.port}/`)
161
97
 
162
- expect(res.status).toHaveBeenCalledWith(500)
98
+ await res.text()
99
+ expect(res.status).toBe(500)
163
100
  })
164
101
 
165
102
  it('should send client error', async () => {
166
- const req = createRequest()
167
- const message = generate()
103
+ const message = randomUUID()
168
104
 
169
- async function process (): Promise<OutgoingMessage> {
170
- throw new BadRequest(message)
171
- }
105
+ server.attach(jest.fn().mockRejectedValueOnce(new BadRequest(message)))
172
106
 
173
- server.attach(process)
174
- await use(req)
107
+ const res = await fetch(`http://localhost:${server.port}/`)
108
+ const text = await res.text()
175
109
 
176
- expect(res.status).toHaveBeenCalledWith(400)
110
+ expect(res.status).toBe(400)
111
+ expect(text).toContain(message)
177
112
  })
178
113
  })
179
114
 
180
115
  describe('options', () => {
181
116
  it('should send 501 on unspecified method', async () => {
182
- jest.clearAllMocks()
117
+ server = Server.create({ methods: new Set(['COPY']), port: 0 })
118
+ await server.connect()
183
119
 
184
- server = Server.create({ methods: new Set(['COPY']) })
185
- app = express.mock.results[0]?.value
120
+ const res = await fetch(`http://localhost:${server.port}/`)
186
121
 
187
- const req = createRequest({ method: 'GET' })
188
-
189
- await use(req)
190
-
191
- expect(res.sendStatus).toHaveBeenCalledWith(501)
122
+ await res.text()
123
+ await server.disconnect()
124
+ expect(res.status).toBe(501)
192
125
  })
193
126
  })
194
-
195
- async function use (req: Request): Promise<void> {
196
- for (const call of app.use.mock.calls) {
197
- const usage = call[0] as unknown as RequestHandler
198
-
199
- usage(req, res, next)
200
- }
201
-
202
- await immediate()
203
- }
@@ -1,57 +1,64 @@
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 { CorsOptions } from 'cors'
12
10
  import type { Format } from './formats'
13
11
  import type * as http from 'node:http'
14
12
  import type { Express, Request, Response, NextFunction } from 'express'
15
13
 
16
14
  export class Server extends Connector {
17
- private readonly debug: boolean
18
- private readonly app: Express
19
15
  private server?: http.Server
20
16
 
21
- private constructor (app: Express, debug: boolean) {
17
+ private constructor (private readonly app: Express,
18
+ private readonly debug: boolean,
19
+ private readonly requestedPort: number) {
22
20
  super()
21
+ }
22
+
23
+ public get port (): number {
24
+ if (this.server === undefined) return this.requestedPort
25
+
26
+ const address = this.server.address()
27
+
28
+ if (address === null || typeof address === 'string')
29
+ throw new Error('Server is not listening on a port.')
23
30
 
24
- this.app = app
25
- this.debug = debug
31
+ return address.port
26
32
  }
27
33
 
28
34
  public static create (options?: Partial<Properties>): Server {
29
- const properties: Properties = options === undefined
35
+ const properties = options === undefined
30
36
  ? DEFAULTS
31
37
  : { ...DEFAULTS, ...options }
32
38
 
33
39
  const app = express()
34
40
 
35
41
  app.disable('x-powered-by')
36
- app.use(cors(CORS))
42
+ // app.use(cors(CORS))
37
43
  app.use(supportedMethods(properties.methods))
38
44
 
39
- return new Server(app, properties.debug)
45
+ return new Server(app, properties.debug, properties.port)
40
46
  }
41
47
 
42
48
  public attach (process: Processing): void {
43
- this.app.use((request: any, response: Response) => {
44
- this.extend(request)
45
- .then(process)
46
- .then(this.success(request, response))
47
- .catch(this.fail(request, response))
49
+ this.app.use((request: Request, response: Response) => {
50
+ const message = this.extend(request)
51
+
52
+ process(message)
53
+ .then(this.success(message, response))
54
+ .catch(this.fail(message, response))
48
55
  })
49
56
  }
50
57
 
51
58
  protected override async open (): Promise<void> {
52
59
  const listening = promex()
53
60
 
54
- this.server = this.app.listen(8000, listening.callback)
61
+ this.server = this.app.listen(this.requestedPort, listening.callback)
55
62
 
56
63
  await listening
57
64
 
@@ -64,23 +71,35 @@ export class Server extends Connector {
64
71
  this.server?.close(stopped.callback)
65
72
 
66
73
  await stopped
67
- }
68
74
 
69
- protected override dispose (): void {
75
+ this.server = undefined
76
+
70
77
  console.info('HTTP Server has been stopped.')
71
78
  }
72
79
 
73
- private async extend (request: IncomingMessage): Promise<IncomingMessage> {
74
- const message = request as unknown as IncomingMessage
80
+ private extend (request: Request): IncomingMessage {
81
+ const message = request as IncomingMessage
75
82
 
76
83
  message.encoder = negotiate(request)
77
- message.parse = async <T> (): Promise<T> => await read(request)
84
+ message.pipelines = { body: [], response: [] }
85
+
86
+ message.parse = async <T> (): Promise<T> => {
87
+ const value = await read(request)
88
+
89
+ if (message.pipelines.body.length === 0)
90
+ return value
91
+
92
+ return message.pipelines.body.reduce((value, transform) => transform(value), value)
93
+ }
78
94
 
79
95
  return message
80
96
  }
81
97
 
82
98
  private success (request: IncomingMessage, response: Response) {
83
99
  return (message: OutgoingMessage) => {
100
+ for (const transform of request.pipelines.response)
101
+ transform(message)
102
+
84
103
  let status = message.status
85
104
 
86
105
  if (status === undefined)
@@ -150,21 +169,16 @@ async function adam (request: Request): Promise<void> {
150
169
  return await completed
151
170
  }
152
171
 
153
- const DEFAULTS = {
154
- methods: new Set<string>(['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']),
155
- debug: false
156
- }
157
-
158
- const CORS: CorsOptions = {
159
- credentials: true,
160
- maxAge: 86400,
161
- allowedHeaders: ['accept', 'authorization', 'content-type'],
162
- origin: (origin: string | undefined, callback) => callback(null, origin)
172
+ const DEFAULTS: Properties = {
173
+ methods: new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']),
174
+ debug: false,
175
+ port: 8000
163
176
  }
164
177
 
165
178
  interface Properties {
166
179
  methods: Set<string>
167
180
  debug: boolean
181
+ port: number
168
182
  }
169
183
 
170
184
  export type Processing = (input: IncomingMessage) => Promise<OutgoingMessage>
@@ -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
  }
@@ -47,8 +47,10 @@ function send (message: OutgoingMessage, request: IncomingMessage, response: Res
47
47
 
48
48
  const buf = request.encoder.encode(message.body)
49
49
 
50
- response.set('content-type', request.encoder.type)
51
- response.end(buf)
50
+ response
51
+ .set('content-type', request.encoder.type)
52
+ .append('vary', 'accept')
53
+ .end(buf)
52
54
  }
53
55
 
54
56
  function stream
@@ -97,6 +99,10 @@ export interface IncomingMessage extends Request {
97
99
  query: Query
98
100
  parse: <T> () => Promise<T>
99
101
  encoder: Format | null
102
+ pipelines: {
103
+ body: Array<(input: unknown) => unknown>
104
+ response: Array<(output: OutgoingMessage) => void>
105
+ }
100
106
  }
101
107
 
102
108
  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> {