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

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 (195) hide show
  1. package/components/identity.bans/manifest.toa.yaml +1 -0
  2. package/components/identity.basic/manifest.toa.yaml +1 -0
  3. package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
  4. package/components/identity.federation/manifest.toa.yaml +13 -0
  5. package/components/identity.federation/operations/authenticate.js +4 -4
  6. package/components/identity.federation/operations/authenticate.js.map +1 -1
  7. package/components/identity.federation/operations/create.js +4 -4
  8. package/components/identity.federation/operations/create.js.map +1 -1
  9. package/components/identity.federation/operations/{assertions-as-values.cjs → lib/assertions-as-values.js} +1 -1
  10. package/components/identity.federation/operations/lib/assertions-as-values.js.map +1 -0
  11. package/components/identity.federation/operations/{jwt.d.cts → lib/jwt.d.ts} +5 -4
  12. package/components/identity.federation/operations/{jwt.cjs → lib/jwt.js} +35 -11
  13. package/components/identity.federation/operations/lib/jwt.js.map +1 -0
  14. package/components/identity.federation/operations/schemas.d.ts +16 -0
  15. package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
  16. package/components/identity.federation/operations/types.d.ts +1 -1
  17. package/components/identity.federation/source/authenticate.ts +2 -2
  18. package/components/identity.federation/source/create.ts +2 -2
  19. package/components/identity.federation/source/{assertions-as-values.cts → lib/assertions-as-values.ts} +1 -2
  20. package/components/identity.federation/source/lib/jwt.test.ts +56 -0
  21. package/components/identity.federation/source/{jwt.cts → lib/jwt.ts} +57 -29
  22. package/components/identity.federation/source/schemas.ts +16 -0
  23. package/components/identity.federation/source/types.ts +1 -1
  24. package/components/identity.roles/manifest.toa.yaml +1 -0
  25. package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
  26. package/components/identity.tokens/manifest.toa.yaml +1 -0
  27. package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
  28. package/components/octets.storage/manifest.toa.yaml +1 -0
  29. package/components/octets.storage/operations/store.js +1 -1
  30. package/documentation/components.md +12 -5
  31. package/documentation/identity.md +7 -0
  32. package/documentation/octets.md +12 -0
  33. package/documentation/query.md +45 -2
  34. package/features/body.feature +1 -1
  35. package/features/errors.feature +1 -1
  36. package/features/etag.feature +86 -0
  37. package/features/identity.federation.feature +31 -1
  38. package/features/octets.entries.feature +1 -1
  39. package/features/octets.workflows.feature +38 -0
  40. package/features/steps/Gateway.ts +3 -0
  41. package/features/steps/IdP.ts +29 -0
  42. package/features/steps/components/echo/manifest.toa.yaml +1 -0
  43. package/features/steps/components/greeter/manifest.toa.yaml +1 -0
  44. package/features/steps/components/octets.tester/manifest.toa.yaml +1 -0
  45. package/features/steps/components/pots/manifest.toa.yaml +10 -3
  46. package/features/steps/components/sequences/manifest.toa.yaml +1 -0
  47. package/features/timing.feature +43 -0
  48. package/package.json +7 -10
  49. package/readme.md +7 -6
  50. package/schemas/annotation.cos.yaml +1 -0
  51. package/schemas/octets/workflow.cos.yaml +12 -0
  52. package/schemas/querystring.cos.yaml +1 -0
  53. package/source/Annotation.ts +1 -0
  54. package/source/Directive.test.ts +3 -3
  55. package/source/Directive.ts +11 -11
  56. package/source/Endpoint.ts +18 -4
  57. package/source/Factory.ts +8 -4
  58. package/source/Gateway.ts +55 -42
  59. package/source/HTTP/Context.ts +67 -0
  60. package/source/HTTP/Server.test.ts +1 -1
  61. package/source/HTTP/Server.ts +60 -95
  62. package/source/HTTP/Timing.ts +40 -0
  63. package/source/HTTP/index.ts +1 -0
  64. package/source/HTTP/messages.test.ts +27 -8
  65. package/source/HTTP/messages.ts +32 -48
  66. package/source/Mapping.ts +7 -8
  67. package/source/deployment.ts +6 -0
  68. package/source/directives/auth/Anonymous.ts +3 -2
  69. package/source/directives/auth/Authorization.ts +5 -3
  70. package/source/directives/auth/Incept.ts +11 -6
  71. package/source/directives/auth/Role.ts +5 -3
  72. package/source/directives/auth/Scheme.ts +2 -2
  73. package/source/directives/cache/Cache.ts +2 -2
  74. package/source/directives/cache/Control.ts +5 -5
  75. package/source/directives/cache/types.ts +1 -1
  76. package/source/directives/cors/CORS.ts +5 -3
  77. package/source/directives/octets/Context.ts +1 -1
  78. package/source/directives/octets/Delete.ts +21 -11
  79. package/source/directives/octets/Fetch.ts +29 -14
  80. package/source/directives/octets/List.ts +14 -6
  81. package/source/directives/octets/Octets.ts +7 -3
  82. package/source/directives/octets/Permute.ts +12 -6
  83. package/source/directives/octets/Store.ts +32 -16
  84. package/source/directives/octets/Workflow.ts +41 -0
  85. package/source/directives/octets/schemas.test.ts +21 -0
  86. package/source/directives/octets/schemas.ts +2 -0
  87. package/source/directives/octets/{workflow → workflows}/Execution.ts +0 -2
  88. package/source/directives/octets/{workflow → workflows}/Workflow.ts +2 -2
  89. package/source/directives/vary/Vary.ts +1 -1
  90. package/source/directives/vary/embeddings/Header.ts +1 -1
  91. package/source/directives/vary/embeddings/Language.ts +1 -1
  92. package/source/io.ts +2 -2
  93. package/transpiled/Annotation.d.ts +1 -0
  94. package/transpiled/Directive.d.ts +6 -6
  95. package/transpiled/Directive.js +8 -8
  96. package/transpiled/Directive.js.map +1 -1
  97. package/transpiled/Endpoint.d.ts +3 -3
  98. package/transpiled/Endpoint.js +34 -1
  99. package/transpiled/Endpoint.js.map +1 -1
  100. package/transpiled/Factory.js +4 -2
  101. package/transpiled/Factory.js.map +1 -1
  102. package/transpiled/Gateway.d.ts +5 -6
  103. package/transpiled/Gateway.js +38 -32
  104. package/transpiled/Gateway.js.map +1 -1
  105. package/transpiled/HTTP/Context.d.ts +24 -0
  106. package/transpiled/HTTP/Context.js +47 -0
  107. package/transpiled/HTTP/Context.js.map +1 -0
  108. package/transpiled/HTTP/Server.d.ts +8 -7
  109. package/transpiled/HTTP/Server.js +68 -76
  110. package/transpiled/HTTP/Server.js.map +1 -1
  111. package/transpiled/HTTP/Timing.d.ts +10 -0
  112. package/transpiled/HTTP/Timing.js +29 -0
  113. package/transpiled/HTTP/Timing.js.map +1 -0
  114. package/transpiled/HTTP/index.d.ts +1 -0
  115. package/transpiled/HTTP/index.js +1 -0
  116. package/transpiled/HTTP/index.js.map +1 -1
  117. package/transpiled/HTTP/messages.d.ts +7 -21
  118. package/transpiled/HTTP/messages.js +24 -26
  119. package/transpiled/HTTP/messages.js.map +1 -1
  120. package/transpiled/Mapping.js +7 -7
  121. package/transpiled/Mapping.js.map +1 -1
  122. package/transpiled/deployment.js +5 -0
  123. package/transpiled/deployment.js.map +1 -1
  124. package/transpiled/directives/auth/Anonymous.js +3 -4
  125. package/transpiled/directives/auth/Anonymous.js.map +1 -1
  126. package/transpiled/directives/auth/Authorization.js +1 -1
  127. package/transpiled/directives/auth/Authorization.js.map +1 -1
  128. package/transpiled/directives/auth/Incept.d.ts +1 -1
  129. package/transpiled/directives/auth/Incept.js +11 -6
  130. package/transpiled/directives/auth/Incept.js.map +1 -1
  131. package/transpiled/directives/auth/Role.js +5 -3
  132. package/transpiled/directives/auth/Role.js.map +1 -1
  133. package/transpiled/directives/auth/Scheme.js +2 -2
  134. package/transpiled/directives/auth/Scheme.js.map +1 -1
  135. package/transpiled/directives/cache/Cache.d.ts +1 -1
  136. package/transpiled/directives/cache/Cache.js +2 -2
  137. package/transpiled/directives/cache/Cache.js.map +1 -1
  138. package/transpiled/directives/cache/Control.d.ts +3 -3
  139. package/transpiled/directives/cache/Control.js +3 -3
  140. package/transpiled/directives/cache/Control.js.map +1 -1
  141. package/transpiled/directives/cache/types.d.ts +1 -1
  142. package/transpiled/directives/cors/CORS.js +4 -3
  143. package/transpiled/directives/cors/CORS.js.map +1 -1
  144. package/transpiled/directives/octets/Context.d.ts +1 -1
  145. package/transpiled/directives/octets/Context.js.map +1 -1
  146. package/transpiled/directives/octets/Delete.d.ts +2 -2
  147. package/transpiled/directives/octets/Delete.js +21 -11
  148. package/transpiled/directives/octets/Delete.js.map +1 -1
  149. package/transpiled/directives/octets/Fetch.d.ts +1 -1
  150. package/transpiled/directives/octets/Fetch.js +28 -14
  151. package/transpiled/directives/octets/Fetch.js.map +1 -1
  152. package/transpiled/directives/octets/List.d.ts +1 -1
  153. package/transpiled/directives/octets/List.js +13 -6
  154. package/transpiled/directives/octets/List.js.map +1 -1
  155. package/transpiled/directives/octets/Octets.js +7 -3
  156. package/transpiled/directives/octets/Octets.js.map +1 -1
  157. package/transpiled/directives/octets/Permute.d.ts +1 -1
  158. package/transpiled/directives/octets/Permute.js +11 -6
  159. package/transpiled/directives/octets/Permute.js.map +1 -1
  160. package/transpiled/directives/octets/Store.d.ts +3 -2
  161. package/transpiled/directives/octets/Store.js +19 -11
  162. package/transpiled/directives/octets/Store.js.map +1 -1
  163. package/transpiled/directives/octets/Workflow.d.ts +14 -0
  164. package/transpiled/directives/octets/Workflow.js +52 -0
  165. package/transpiled/directives/octets/Workflow.js.map +1 -0
  166. package/transpiled/directives/octets/schemas.d.ts +2 -0
  167. package/transpiled/directives/octets/schemas.js +2 -1
  168. package/transpiled/directives/octets/schemas.js.map +1 -1
  169. package/transpiled/directives/octets/{workflow → workflows}/Execution.js +0 -1
  170. package/transpiled/directives/octets/workflows/Execution.js.map +1 -0
  171. package/transpiled/directives/octets/{workflow → workflows}/Workflow.d.ts +1 -1
  172. package/transpiled/directives/octets/{workflow → workflows}/Workflow.js +2 -2
  173. package/transpiled/directives/octets/workflows/Workflow.js.map +1 -0
  174. package/transpiled/directives/octets/workflows/index.js.map +1 -0
  175. package/transpiled/directives/vary/Vary.js +1 -1
  176. package/transpiled/directives/vary/embeddings/Header.js +1 -1
  177. package/transpiled/directives/vary/embeddings/Header.js.map +1 -1
  178. package/transpiled/directives/vary/embeddings/Language.js +1 -1
  179. package/transpiled/directives/vary/embeddings/Language.js.map +1 -1
  180. package/transpiled/io.d.ts +2 -2
  181. package/transpiled/tsconfig.tsbuildinfo +1 -1
  182. package/components/identity.federation/operations/assertions-as-values.cjs.map +0 -1
  183. package/components/identity.federation/operations/jwt.cjs.map +0 -1
  184. package/source/HTTP/Server.fixtures.ts +0 -40
  185. package/transpiled/HTTP/Server.fixtures.d.ts +0 -10
  186. package/transpiled/HTTP/Server.fixtures.js +0 -31
  187. package/transpiled/HTTP/Server.fixtures.js.map +0 -1
  188. package/transpiled/directives/octets/workflow/Execution.js.map +0 -1
  189. package/transpiled/directives/octets/workflow/Workflow.js.map +0 -1
  190. package/transpiled/directives/octets/workflow/index.js.map +0 -1
  191. /package/components/identity.federation/operations/{assertions-as-values.d.cts → lib/assertions-as-values.d.ts} +0 -0
  192. /package/source/directives/octets/{workflow → workflows}/index.ts +0 -0
  193. /package/transpiled/directives/octets/{workflow → workflows}/Execution.d.ts +0 -0
  194. /package/transpiled/directives/octets/{workflow → workflows}/index.d.ts +0 -0
  195. /package/transpiled/directives/octets/{workflow → workflows}/index.js +0 -0
@@ -246,3 +246,41 @@ Feature: Octets storage workflows
246
246
  concat: hello world
247
247
  --cut--
248
248
  """
249
+
250
+ Scenario: Executing a workflow with `octets:workflow`
251
+ Given the `octets.tester` is running
252
+ And the annotation:
253
+ """yaml
254
+ /:
255
+ auth:anonymous: true
256
+ octets:context: octets
257
+ POST:
258
+ octets:store: ~
259
+ /*:
260
+ DELETE:
261
+ octets:workflow:
262
+ echo: octets.tester.echo
263
+ """
264
+ When the stream of `lenna.ascii` is received with the following headers:
265
+ """
266
+ POST / HTTP/1.1
267
+ content-type: application/octet-stream
268
+ """
269
+ Then the following reply is sent:
270
+ """
271
+ 201 Created
272
+ """
273
+ When the following request is received:
274
+ """
275
+ DELETE /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
276
+ accept: application/yaml
277
+ """
278
+ Then the following reply is sent:
279
+ """
280
+ 202 Accepted
281
+ content-type: multipart/yaml; boundary=cut
282
+
283
+ --cut
284
+ echo: 10cf16b458f759e0d617f2f3d83599ff
285
+ --cut--
286
+ """
@@ -26,6 +26,9 @@ export class Gateway {
26
26
  if (annotation.debug === true)
27
27
  process.env.TOA_EXPOSITION_DEBUG = '1'
28
28
 
29
+ if (annotation.trace === true)
30
+ process.env.TOA_EXPOSITION_TRACE = '1'
31
+
29
32
  await Gateway.stop()
30
33
 
31
34
  this.default = false
@@ -117,4 +117,33 @@ export class IdP {
117
117
 
118
118
  this.captures.set(`${user}.id_token`, idToken)
119
119
  }
120
+
121
+ @given('the IDP {word} token for {word} is issued with following secret:')
122
+ public async issueSymmetricToken (alg: string, user: string, secret: string): Promise<void> {
123
+ console.log('Sym token for %s with secret "%s"', user, secret)
124
+
125
+ const jwt = [
126
+ {
127
+ typ: 'JWT',
128
+ alg
129
+ },
130
+ {
131
+ iss: IdP.issuer,
132
+ sub: `${user}-mock-id`,
133
+ aud: 'test',
134
+ iat: Math.floor(Date.now() / 1000),
135
+ exp: Math.floor((Date.now() + 1000 * 60 * 5) / 1000)
136
+ }
137
+ ]
138
+ .map((v) => Buffer.from(JSON.stringify(v)).toString('base64url'))
139
+ .join('.')
140
+
141
+ const signature = crypto.createHmac(alg.replace(/^HS(\d{3})$/, 'sha$1'), secret)
142
+ .update(jwt)
143
+ .digest('base64url')
144
+
145
+ const idToken = `${jwt}.${signature}`
146
+
147
+ this.captures.set(`${user}.id_token`, idToken)
148
+ }
120
149
  }
@@ -1,4 +1,5 @@
1
1
  name: echo
2
+ version: 0.0.0
2
3
 
3
4
  operations:
4
5
  compute:
@@ -1,4 +1,5 @@
1
1
  name: greeter
2
+ version: 0.0.0
2
3
 
3
4
  exposition:
4
5
  /:
@@ -1,5 +1,6 @@
1
1
  namespace: octets
2
2
  name: tester
3
+ version: 0.0.0
3
4
 
4
5
  storages: octets
5
6
 
@@ -1,4 +1,5 @@
1
1
  name: pots
2
+ version: 0.0.0
2
3
 
3
4
  entity:
4
5
  schema:
@@ -7,11 +8,17 @@ entity:
7
8
  temperature?: number
8
9
 
9
10
  operations:
10
- transit:
11
+ create:
11
12
  query: false
13
+ forward: transit
14
+ input:
15
+ title*: .
16
+ volume*: .
17
+ transit:
18
+ concurrency: retry
12
19
  input:
13
- title*: ~
14
- volume*: ~
20
+ title: .
21
+ volume: .
15
22
  observe:
16
23
  output:
17
24
  id: string
@@ -1,4 +1,5 @@
1
1
  name: sequences
2
+ version: 0.0.0
2
3
 
3
4
  operations:
4
5
  numbers:
@@ -0,0 +1,43 @@
1
+ Feature: Server timing
2
+
3
+ Background:
4
+ Given the `pots` is running with the following manifest:
5
+ """yaml
6
+ exposition:
7
+ /:
8
+ POST: create
9
+ """
10
+
11
+ Scenario: Server timing is not available by default
12
+ When the following request is received:
13
+ """
14
+ POST /pots/ HTTP/1.1
15
+ content-type: application/yaml
16
+
17
+ title: Hello
18
+ volume: 1.5
19
+ """
20
+ Then the reply does not contain:
21
+ """
22
+ server-timing:
23
+ """
24
+
25
+ Scenario: Server timing is sent when debug is enabled
26
+ Given the annotation:
27
+ """
28
+ trace: true
29
+ """
30
+ When the following request is received:
31
+ """
32
+ POST /pots/ HTTP/1.1
33
+ content-type: application/yaml
34
+
35
+ title: Hello
36
+ volume: 1.5
37
+ """
38
+ # to debug, break it and look at the console
39
+ Then the following reply is sent:
40
+ """
41
+ 201 Created
42
+ server-timing:
43
+ """
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.4",
4
4
  "description": "Toa Exposition",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -17,13 +17,11 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@toa.io/core": "1.0.0-alpha.2",
21
- "@toa.io/generic": "1.0.0-alpha.2",
22
- "@toa.io/schemas": "1.0.0-alpha.2",
23
- "@toa.io/streams": "1.0.0-alpha.2",
20
+ "@toa.io/core": "1.0.0-alpha.4",
21
+ "@toa.io/generic": "1.0.0-alpha.4",
22
+ "@toa.io/schemas": "1.0.0-alpha.4",
24
23
  "bcryptjs": "2.4.3",
25
24
  "error-value": "0.3.0",
26
- "express": "4.18.2",
27
25
  "js-yaml": "4.1.0",
28
26
  "matchacho": "0.3.5",
29
27
  "msgpackr": "1.10.1",
@@ -45,12 +43,11 @@
45
43
  "features": "cucumber-js"
46
44
  },
47
45
  "devDependencies": {
48
- "@toa.io/agent": "1.0.0-alpha.2",
49
- "@toa.io/extensions.storages": "1.0.0-alpha.2",
46
+ "@toa.io/agent": "1.0.0-alpha.4",
47
+ "@toa.io/extensions.storages": "1.0.0-alpha.4",
50
48
  "@types/bcryptjs": "2.4.3",
51
49
  "@types/cors": "2.8.13",
52
- "@types/express": "4.17.17",
53
50
  "@types/negotiator": "0.6.1"
54
51
  },
55
- "gitHead": "7688e6e980a65c82ac2e459be4e355eebf406cd0"
52
+ "gitHead": "80a91c0d9c167484247a91e69a0c0a3c344f90d0"
56
53
  }
package/readme.md CHANGED
@@ -127,12 +127,13 @@ exposition:
127
127
  host: the.example.com
128
128
  ```
129
129
 
130
- | Option | Type | Description |
131
- |---------------|-----------|------------------------------------------------------------------|
132
- | `host` | `string` | Domain name to be used for the corresponding Kubernetes Ingress. |
133
- | `class` | `string` | Ingress class |
134
- | `annotations` | `object` | Ingress annotations |
135
- | `debug` | `boolean` | Output server errors. Default `false`. |
130
+ | Option | Type | Description |
131
+ |---------------|-----------|-------------------------------------------------------------------------------------------------------------------|
132
+ | `host` | `string` | Domain name to be used for the corresponding Kubernetes Ingress. |
133
+ | `class` | `string` | Ingress class |
134
+ | `annotations` | `object` | Ingress annotations |
135
+ | `debug` | `boolean` | Output server errors. Default `false`. |
136
+ | `trace` | `boolean` | Output [server timing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing). Default `false`. |
136
137
 
137
138
  ### Context resources
138
139
 
@@ -2,4 +2,5 @@ host: string
2
2
  class: string
3
3
  annotations: <string>
4
4
  debug: boolean
5
+ trace: boolean
5
6
  /: ~
@@ -0,0 +1,12 @@
1
+ definitions:
2
+ unit:
3
+ type: object
4
+ patternProperties:
5
+ ^[a-zA-Z0-9_]+$:
6
+ type: string
7
+
8
+ oneOf:
9
+ - $ref: '#/definitions/unit'
10
+ - type: array
11
+ items:
12
+ $ref: '#/definitions/unit'
@@ -3,3 +3,4 @@ criteria: string
3
3
  sort: string
4
4
  omit: number
5
5
  limit: number
6
+ version: number
@@ -3,5 +3,6 @@ export interface Annotation {
3
3
  class?: string
4
4
  annotations?: Record<string, string>
5
5
  debug: boolean
6
+ trace: boolean
6
7
  '/'?: object // parsed and validated by RTD.syntax.parse
7
8
  }
@@ -2,8 +2,8 @@ import assert from 'node:assert'
2
2
  import { generate } from 'randomstring'
3
3
  import { DirectivesFactory, type Family } from './Directive'
4
4
  import { type syntax } from './RTD'
5
- import { type IncomingMessage } from './HTTP'
6
5
  import { type Remotes } from './Remotes'
6
+ import type { Context } from './HTTP'
7
7
 
8
8
  const families: Array<jest.MockedObjectDeep<Family>> = [
9
9
  {
@@ -76,7 +76,7 @@ it('should apply directive', async () => {
76
76
  }
77
77
 
78
78
  const directives = factory.create([declaration])
79
- const request = generate() as unknown as IncomingMessage
79
+ const request = generate() as unknown as Context
80
80
  const directive = families[0].create.mock.results[0].value
81
81
 
82
82
  await directives.preflight(request, [])
@@ -89,7 +89,7 @@ it('should apply directive', async () => {
89
89
 
90
90
  it('should apply mandatory families', async () => {
91
91
  const directives = factory.create([])
92
- const request = generate() as unknown as IncomingMessage
92
+ const request = generate() as unknown as Context
93
93
 
94
94
  await directives.preflight(request, [])
95
95
 
@@ -1,4 +1,4 @@
1
- import type { IncomingMessage, OutgoingMessage } from './HTTP'
1
+ import type { Context, OutgoingMessage } from './HTTP'
2
2
  import type { Remotes } from './Remotes'
3
3
  import type { Output } from './io'
4
4
  import type * as RTD from './RTD'
@@ -10,15 +10,15 @@ export class Directives implements RTD.Directives<Directives> {
10
10
  this.sets = sets
11
11
  }
12
12
 
13
- public async preflight (request: IncomingMessage, parameters: RTD.Parameter[]): Promise<Output> {
13
+ public async preflight (context: Context, parameters: RTD.Parameter[]): Promise<Output> {
14
14
  for (const set of this.sets) {
15
15
  if (set.family.preflight === undefined)
16
16
  continue
17
17
 
18
- const output = await set.family.preflight(set.directives, request, parameters)
18
+ const output = await set.family.preflight(set.directives, context, parameters)
19
19
 
20
20
  if (output !== null) {
21
- await this.settle(request, output)
21
+ await this.settle(context, output)
22
22
 
23
23
  return output
24
24
  }
@@ -27,10 +27,10 @@ export class Directives implements RTD.Directives<Directives> {
27
27
  return null
28
28
  }
29
29
 
30
- public async settle (request: IncomingMessage, response: OutgoingMessage): Promise<void> {
30
+ public async settle (context: Context, response: OutgoingMessage): Promise<void> {
31
31
  for (const set of this.sets)
32
32
  if (set.family.settle !== undefined)
33
- await set.family.settle(set.directives, request, response)
33
+ await set.family.settle(set.directives, context, response)
34
34
  }
35
35
 
36
36
  public merge (directives: Directives): void {
@@ -39,7 +39,7 @@ export class Directives implements RTD.Directives<Directives> {
39
39
  }
40
40
 
41
41
  export class DirectivesFactory implements RTD.DirectivesFactory<Directives> {
42
- private readonly remtoes: Remotes
42
+ private readonly remotes: Remotes
43
43
  private readonly families: Record<string, Family> = {}
44
44
  private readonly mandatory: string[] = []
45
45
 
@@ -51,7 +51,7 @@ export class DirectivesFactory implements RTD.DirectivesFactory<Directives> {
51
51
  this.mandatory.push(family.name)
52
52
  }
53
53
 
54
- this.remtoes = remotes
54
+ this.remotes = remotes
55
55
  }
56
56
 
57
57
  public create (declarations: RTD.syntax.Directive[]): Directives {
@@ -67,7 +67,7 @@ export class DirectivesFactory implements RTD.DirectivesFactory<Directives> {
67
67
  if (family === undefined)
68
68
  throw new Error(`Directive family '${declaration.family}' is not found.`)
69
69
 
70
- const directive = family.create(declaration.name, declaration.value, this.remtoes)
70
+ const directive = family.create(declaration.name, declaration.value, this.remotes)
71
71
 
72
72
  groups[family.name] ??= []
73
73
  groups[family.name].push(directive)
@@ -109,11 +109,11 @@ export interface Family<TDirective = any, TExtension = any> {
109
109
  create: (name: string, value: any, remotes: Remotes) => TDirective
110
110
 
111
111
  preflight?: (directives: TDirective[],
112
- request: IncomingMessage & TExtension,
112
+ request: Context & TExtension,
113
113
  parameters: RTD.Parameter[]) => Output | Promise<Output>
114
114
 
115
115
  settle?: (directives: TDirective[],
116
- request: IncomingMessage & TExtension,
116
+ request: Context & TExtension,
117
117
  response: OutgoingMessage) => void | Promise<void>
118
118
  }
119
119
 
@@ -1,9 +1,9 @@
1
- import { type Component, type Reply } from '@toa.io/core'
1
+ import { type Component } from '@toa.io/core'
2
2
  import { type Remotes } from './Remotes'
3
3
  import { Mapping } from './Mapping'
4
4
  import { type Context } from './Context'
5
+ import * as http from './HTTP'
5
6
  import type * as RTD from './RTD'
6
- import type * as http from './HTTP'
7
7
 
8
8
  export class Endpoint implements RTD.Endpoint<Endpoint> {
9
9
  private readonly endpoint: string
@@ -17,12 +17,26 @@ export class Endpoint implements RTD.Endpoint<Endpoint> {
17
17
  this.discovery = discovery
18
18
  }
19
19
 
20
- public async call (body: any, query: http.Query, parameters: RTD.Parameter[]): Promise<Reply> {
20
+ public async call
21
+ (body: any, query: http.Query, parameters: RTD.Parameter[]): Promise<http.OutgoingMessage> {
21
22
  const request = this.mapping.fit(body, query, parameters)
22
23
 
23
24
  this.remote ??= await this.discovery
24
25
 
25
- return await this.remote.invoke(this.endpoint, request)
26
+ const reply = await this.remote.invoke(this.endpoint, request)
27
+
28
+ if (reply instanceof Error)
29
+ throw new http.Conflict(reply)
30
+
31
+ const message: http.OutgoingMessage = { body: reply }
32
+
33
+ if (typeof reply === 'object' && reply !== null && '_version' in reply) {
34
+ message.headers ??= new Headers()
35
+ message.headers.set('etag', `"${reply._version.toString()}"`)
36
+ delete reply._version
37
+ }
38
+
39
+ return message
26
40
  }
27
41
 
28
42
  public async close (): Promise<void> {
package/source/Factory.ts CHANGED
@@ -9,6 +9,7 @@ import { type Directives, DirectivesFactory } from './Directive'
9
9
  import { Composition } from './Composition'
10
10
  import * as root from './root'
11
11
  import { Interception } from './Interception'
12
+ import type { Broadcast } from './Gateway'
12
13
  import type { Connector, Locator, extensions } from '@toa.io/core'
13
14
 
14
15
  export class Factory implements extensions.Factory {
@@ -19,15 +20,16 @@ export class Factory implements extensions.Factory {
19
20
  }
20
21
 
21
22
  public tenant (locator: Locator, node: syntax.Node): Connector {
22
- const broadcast = this.boot.bindings.broadcast(CHANNEL, locator.id)
23
+ const broadcast: Broadcast = this.boot.bindings.broadcast(CHANNEL, locator.id)
23
24
 
24
25
  return new Tenant(broadcast, locator, node)
25
26
  }
26
27
 
27
28
  public service (): Connector | null {
28
29
  const debug = process.env.TOA_EXPOSITION_DEBUG === '1'
29
- const broadcast = this.boot.bindings.broadcast(CHANNEL)
30
- const server = Server.create({ methods: syntax.verbs, debug })
30
+ const trace = process.env.TOA_EXPOSITION_TRACE === '1'
31
+ const broadcast: Broadcast = this.boot.bindings.broadcast(CHANNEL)
32
+ const server = Server.create({ methods: syntax.verbs, debug, trace })
31
33
  const remotes = new Remotes(this.boot)
32
34
  const node = root.resolve()
33
35
  const methods = new EndpointsFactory(remotes)
@@ -36,10 +38,12 @@ export class Factory implements extensions.Factory {
36
38
  const tree = new Tree<Endpoint, Directives>(node, methods, directives)
37
39
 
38
40
  const composition = new Composition(this.boot)
39
- const gateway = new Gateway(broadcast, server, tree, interception)
41
+ const gateway = new Gateway(broadcast, tree, interception)
40
42
 
41
43
  gateway.depends(remotes)
42
44
  gateway.depends(composition)
45
+
46
+ server.attach(gateway.process.bind(gateway))
43
47
  server.depends(gateway)
44
48
 
45
49
  return server
package/source/Gateway.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { type bindings, Connector } from '@toa.io/core'
2
2
  import * as http from './HTTP'
3
3
  import { rethrow } from './exceptions'
4
- import type { Interceptor } from './Interception'
5
- import type { Maybe } from '@toa.io/types'
4
+ import type { Interception } from './Interception'
6
5
  import type { Method, Parameter, Tree } from './RTD'
7
6
  import type { Label } from './discovery'
8
7
  import type { Branch } from './Branch'
@@ -12,84 +11,96 @@ import type { Directives } from './Directive'
12
11
  export class Gateway extends Connector {
13
12
  private readonly broadcast: Broadcast
14
13
  private readonly tree: Tree<Endpoint, Directives>
15
- private readonly interceptor: Interceptor
16
- private readonly server: Connector
14
+ private readonly interceptor: Interception
17
15
 
18
- // eslint-disable-next-line max-params, max-len
19
- public constructor (broadcast: Broadcast, server: http.Server, tree: Tree<Endpoint, Directives>, interception: Interceptor) {
16
+ // eslint-disable-next-line max-len
17
+ public constructor (broadcast: Broadcast, tree: Tree<Endpoint, Directives>, interception: Interception) {
20
18
  super()
21
19
 
22
20
  this.broadcast = broadcast
23
21
  this.tree = tree
24
22
  this.interceptor = interception
25
- this.server = server
26
23
 
27
24
  this.depends(broadcast)
28
- // this.depends(server)
29
-
30
- server.attach(this.process.bind(this))
31
- }
32
-
33
- protected override async open (): Promise<void> {
34
- await this.discover()
35
-
36
- console.info('Gateway has started and is awaiting resource branches.')
37
25
  }
38
26
 
39
- protected override dispose (): void {
40
- console.info('Gateway is closed.')
41
- }
42
-
43
- private async process (request: http.IncomingMessage): Promise<http.OutgoingMessage> {
44
- const interception = await this.interceptor.intercept(request)
27
+ public async process (context: http.Context): Promise<http.OutgoingMessage> {
28
+ const interception = await context.timing.capture('gate:intercept',
29
+ this.interceptor.intercept(context))
45
30
 
46
31
  if (interception !== null)
47
32
  return interception
48
33
 
49
- const match = this.tree.match(request.path)
34
+ const match = this.tree.match(context.url.pathname)
50
35
 
51
36
  if (match === null)
52
37
  throw new http.NotFound()
53
38
 
54
- const { node, parameters } = match
39
+ const {
40
+ node,
41
+ parameters
42
+ } = match
55
43
 
56
- if (!(request.method in node.methods))
44
+ if (!(context.request.method in node.methods))
57
45
  throw new http.MethodNotAllowed()
58
46
 
59
- const method = node.methods[request.method]
60
- const interruption = await method.directives.preflight(request, parameters)
61
- const response = interruption ?? await this.call(method, request, parameters)
47
+ const method = node.methods[context.request.method]
62
48
 
63
- await method.directives.settle(request, response)
49
+ const interruption = await context.timing.capture('gate:preflight',
50
+ method.directives.preflight(context, parameters))
51
+
52
+ const response = interruption ??
53
+ await context.timing.capture('gate:call', this.call(method, context, parameters))
54
+
55
+ await context.timing.capture('gate:settle', method.directives.settle(context, response))
64
56
 
65
57
  return response
66
58
  }
67
59
 
60
+ protected override async open (): Promise<void> {
61
+ await this.discover()
62
+
63
+ console.info('Gateway has started and is awaiting resource branches.')
64
+ }
65
+
66
+ protected override dispose (): void {
67
+ console.info('Gateway is closed.')
68
+ }
69
+
68
70
  private async call
69
- (method: Method<Endpoint, Directives>, request: http.IncomingMessage, parameters: Parameter[]):
71
+ (method: Method<Endpoint, Directives>, context: http.Context, parameters: Parameter[]):
70
72
  Promise<http.OutgoingMessage> {
71
- if (request.path[request.path.length - 1] !== '/')
73
+ if (context.url.pathname[context.url.pathname.length - 1] !== '/')
72
74
  throw new http.NotFound('Trailing slash is required.')
73
75
 
74
- if (request.encoder === null)
76
+ if (context.encoder === null)
75
77
  throw new http.NotAcceptable()
76
78
 
77
79
  if (method.endpoint === null)
78
80
  throw new http.MethodNotAllowed()
79
81
 
80
- const body = await request.parse()
82
+ const body = await context.body()
83
+ const query = this.query(context)
84
+
85
+ return await method.endpoint
86
+ .call(body, query, parameters)
87
+ .catch(rethrow) as http.OutgoingMessage
88
+ }
81
89
 
82
- if ('embed' in request && typeof body === 'object' && body !== null)
83
- Object.assign(body, request.embed)
90
+ private query (context: http.Context): http.Query {
91
+ const query: http.Query = Object.fromEntries(context.url.searchParams)
92
+ const etag = context.request.headers['if-match']
84
93
 
85
- const reply = await method.endpoint
86
- .call(body, request.query, parameters)
87
- .catch(rethrow) as Maybe<unknown>
94
+ if (etag !== undefined) {
95
+ const match = etag.match(ETAG)
88
96
 
89
- if (reply instanceof Error)
90
- throw new http.Conflict(reply)
97
+ if (match === null)
98
+ throw new http.BadRequest('Invalid ETag.')
99
+ else
100
+ query.version = parseInt(match.groups!.version)
101
+ }
91
102
 
92
- return { body: reply }
103
+ return query
93
104
  }
94
105
 
95
106
  private async discover (): Promise<void> {
@@ -109,4 +120,6 @@ export class Gateway extends Connector {
109
120
  }
110
121
  }
111
122
 
112
- type Broadcast = bindings.Broadcast<Label>
123
+ const ETAG = /^"(?<version>\d{1,32})"$/
124
+
125
+ export type Broadcast = bindings.Broadcast<Label>