@toa.io/extensions.exposition 1.0.0-alpha.3 → 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 (152) 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 +1 -0
  5. package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
  6. package/components/identity.roles/manifest.toa.yaml +1 -0
  7. package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
  8. package/components/identity.tokens/manifest.toa.yaml +1 -0
  9. package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
  10. package/components/octets.storage/manifest.toa.yaml +1 -0
  11. package/components/octets.storage/operations/store.js +1 -1
  12. package/documentation/components.md +5 -5
  13. package/documentation/query.md +45 -2
  14. package/features/body.feature +1 -1
  15. package/features/errors.feature +1 -1
  16. package/features/etag.feature +86 -0
  17. package/features/octets.entries.feature +1 -1
  18. package/features/steps/Gateway.ts +3 -0
  19. package/features/steps/components/echo/manifest.toa.yaml +1 -0
  20. package/features/steps/components/greeter/manifest.toa.yaml +1 -0
  21. package/features/steps/components/octets.tester/manifest.toa.yaml +1 -0
  22. package/features/steps/components/pots/manifest.toa.yaml +10 -3
  23. package/features/steps/components/sequences/manifest.toa.yaml +1 -0
  24. package/features/timing.feature +43 -0
  25. package/package.json +7 -10
  26. package/readme.md +7 -6
  27. package/schemas/annotation.cos.yaml +1 -0
  28. package/schemas/querystring.cos.yaml +1 -0
  29. package/source/Annotation.ts +1 -0
  30. package/source/Directive.test.ts +3 -3
  31. package/source/Directive.ts +11 -11
  32. package/source/Endpoint.ts +18 -4
  33. package/source/Factory.ts +8 -4
  34. package/source/Gateway.ts +55 -42
  35. package/source/HTTP/Context.ts +67 -0
  36. package/source/HTTP/Server.test.ts +1 -1
  37. package/source/HTTP/Server.ts +60 -95
  38. package/source/HTTP/Timing.ts +40 -0
  39. package/source/HTTP/index.ts +1 -0
  40. package/source/HTTP/messages.test.ts +27 -8
  41. package/source/HTTP/messages.ts +32 -48
  42. package/source/Mapping.ts +7 -8
  43. package/source/deployment.ts +6 -0
  44. package/source/directives/auth/Anonymous.ts +3 -2
  45. package/source/directives/auth/Authorization.ts +5 -3
  46. package/source/directives/auth/Incept.ts +11 -6
  47. package/source/directives/auth/Role.ts +5 -3
  48. package/source/directives/auth/Scheme.ts +2 -2
  49. package/source/directives/cache/Cache.ts +2 -2
  50. package/source/directives/cache/Control.ts +5 -5
  51. package/source/directives/cache/types.ts +1 -1
  52. package/source/directives/cors/CORS.ts +5 -3
  53. package/source/directives/octets/Context.ts +1 -1
  54. package/source/directives/octets/Delete.ts +19 -9
  55. package/source/directives/octets/Fetch.ts +29 -14
  56. package/source/directives/octets/List.ts +14 -6
  57. package/source/directives/octets/Octets.ts +4 -2
  58. package/source/directives/octets/Permute.ts +12 -6
  59. package/source/directives/octets/Store.ts +18 -16
  60. package/source/directives/octets/Workflow.ts +3 -3
  61. package/source/directives/octets/workflows/Workflow.ts +2 -2
  62. package/source/directives/vary/Vary.ts +1 -1
  63. package/source/directives/vary/embeddings/Header.ts +1 -1
  64. package/source/directives/vary/embeddings/Language.ts +1 -1
  65. package/source/io.ts +2 -2
  66. package/transpiled/Annotation.d.ts +1 -0
  67. package/transpiled/Directive.d.ts +6 -6
  68. package/transpiled/Directive.js +8 -8
  69. package/transpiled/Directive.js.map +1 -1
  70. package/transpiled/Endpoint.d.ts +3 -3
  71. package/transpiled/Endpoint.js +34 -1
  72. package/transpiled/Endpoint.js.map +1 -1
  73. package/transpiled/Factory.js +4 -2
  74. package/transpiled/Factory.js.map +1 -1
  75. package/transpiled/Gateway.d.ts +5 -6
  76. package/transpiled/Gateway.js +38 -32
  77. package/transpiled/Gateway.js.map +1 -1
  78. package/transpiled/HTTP/Context.d.ts +24 -0
  79. package/transpiled/HTTP/Context.js +47 -0
  80. package/transpiled/HTTP/Context.js.map +1 -0
  81. package/transpiled/HTTP/Server.d.ts +8 -7
  82. package/transpiled/HTTP/Server.js +68 -76
  83. package/transpiled/HTTP/Server.js.map +1 -1
  84. package/transpiled/HTTP/Timing.d.ts +10 -0
  85. package/transpiled/HTTP/Timing.js +29 -0
  86. package/transpiled/HTTP/Timing.js.map +1 -0
  87. package/transpiled/HTTP/index.d.ts +1 -0
  88. package/transpiled/HTTP/index.js +1 -0
  89. package/transpiled/HTTP/index.js.map +1 -1
  90. package/transpiled/HTTP/messages.d.ts +7 -21
  91. package/transpiled/HTTP/messages.js +24 -26
  92. package/transpiled/HTTP/messages.js.map +1 -1
  93. package/transpiled/Mapping.js +7 -7
  94. package/transpiled/Mapping.js.map +1 -1
  95. package/transpiled/deployment.js +5 -0
  96. package/transpiled/deployment.js.map +1 -1
  97. package/transpiled/directives/auth/Anonymous.js +3 -4
  98. package/transpiled/directives/auth/Anonymous.js.map +1 -1
  99. package/transpiled/directives/auth/Authorization.js +1 -1
  100. package/transpiled/directives/auth/Authorization.js.map +1 -1
  101. package/transpiled/directives/auth/Incept.d.ts +1 -1
  102. package/transpiled/directives/auth/Incept.js +11 -6
  103. package/transpiled/directives/auth/Incept.js.map +1 -1
  104. package/transpiled/directives/auth/Role.js +5 -3
  105. package/transpiled/directives/auth/Role.js.map +1 -1
  106. package/transpiled/directives/auth/Scheme.js +2 -2
  107. package/transpiled/directives/auth/Scheme.js.map +1 -1
  108. package/transpiled/directives/cache/Cache.d.ts +1 -1
  109. package/transpiled/directives/cache/Cache.js +2 -2
  110. package/transpiled/directives/cache/Cache.js.map +1 -1
  111. package/transpiled/directives/cache/Control.d.ts +3 -3
  112. package/transpiled/directives/cache/Control.js +3 -3
  113. package/transpiled/directives/cache/Control.js.map +1 -1
  114. package/transpiled/directives/cache/types.d.ts +1 -1
  115. package/transpiled/directives/cors/CORS.js +4 -3
  116. package/transpiled/directives/cors/CORS.js.map +1 -1
  117. package/transpiled/directives/octets/Context.d.ts +1 -1
  118. package/transpiled/directives/octets/Context.js.map +1 -1
  119. package/transpiled/directives/octets/Delete.d.ts +1 -1
  120. package/transpiled/directives/octets/Delete.js +19 -9
  121. package/transpiled/directives/octets/Delete.js.map +1 -1
  122. package/transpiled/directives/octets/Fetch.d.ts +1 -1
  123. package/transpiled/directives/octets/Fetch.js +28 -14
  124. package/transpiled/directives/octets/Fetch.js.map +1 -1
  125. package/transpiled/directives/octets/List.d.ts +1 -1
  126. package/transpiled/directives/octets/List.js +13 -6
  127. package/transpiled/directives/octets/List.js.map +1 -1
  128. package/transpiled/directives/octets/Octets.js +4 -2
  129. package/transpiled/directives/octets/Octets.js.map +1 -1
  130. package/transpiled/directives/octets/Permute.d.ts +1 -1
  131. package/transpiled/directives/octets/Permute.js +11 -6
  132. package/transpiled/directives/octets/Permute.js.map +1 -1
  133. package/transpiled/directives/octets/Store.d.ts +1 -1
  134. package/transpiled/directives/octets/Store.js +11 -11
  135. package/transpiled/directives/octets/Store.js.map +1 -1
  136. package/transpiled/directives/octets/Workflow.d.ts +1 -1
  137. package/transpiled/directives/octets/Workflow.js +3 -3
  138. package/transpiled/directives/octets/Workflow.js.map +1 -1
  139. package/transpiled/directives/octets/workflows/Workflow.d.ts +1 -1
  140. package/transpiled/directives/octets/workflows/Workflow.js +2 -2
  141. package/transpiled/directives/octets/workflows/Workflow.js.map +1 -1
  142. package/transpiled/directives/vary/Vary.js +1 -1
  143. package/transpiled/directives/vary/embeddings/Header.js +1 -1
  144. package/transpiled/directives/vary/embeddings/Header.js.map +1 -1
  145. package/transpiled/directives/vary/embeddings/Language.js +1 -1
  146. package/transpiled/directives/vary/embeddings/Language.js.map +1 -1
  147. package/transpiled/io.d.ts +2 -2
  148. package/transpiled/tsconfig.tsbuildinfo +1 -1
  149. package/source/HTTP/Server.fixtures.ts +0 -40
  150. package/transpiled/HTTP/Server.fixtures.d.ts +0 -10
  151. package/transpiled/HTTP/Server.fixtures.js +0 -31
  152. package/transpiled/HTTP/Server.fixtures.js.map +0 -1
@@ -1,31 +1,31 @@
1
1
  import fs from 'node:fs'
2
2
  import os from 'node:os'
3
- import express from 'express'
3
+ import * as http from 'node:http'
4
+ import assert from 'node:assert'
5
+ import { once } from 'node:events'
4
6
  import { Connector } from '@toa.io/core'
5
7
  import { promex } from '@toa.io/generic'
6
- import Negotiator from 'negotiator'
7
- import { read, write, type IncomingMessage, type OutgoingMessage } from './messages'
8
+ import { type OutgoingMessage, write } from './messages'
8
9
  import { ClientError, Exception } from './exceptions'
9
- import { formats, types } from './formats'
10
- import type * as http from 'node:http'
11
- import type { Express, Request, Response, NextFunction } from 'express'
10
+ import { Context } from './Context'
11
+ import type { IncomingMessage } from './Context'
12
12
 
13
13
  export class Server extends Connector {
14
- private server?: http.Server
15
- private readonly app: Express
16
- private readonly debug: boolean
17
- private readonly requestedPort: number
14
+ private readonly server: http.Server = http.createServer()
15
+ private readonly properties: Properties
16
+ private process?: Processing
18
17
 
19
- private constructor (app: Express, debug: boolean, port: number) {
18
+ private constructor (properties: Properties) {
20
19
  super()
21
20
 
22
- this.app = app
23
- this.debug = debug
24
- this.requestedPort = port
21
+ this.properties = properties
22
+
23
+ this.server.on('request', (req, res) => this.listener(req, res))
25
24
  }
26
25
 
27
26
  public get port (): number {
28
- if (this.server === undefined) return this.requestedPort
27
+ if (this.properties.port !== 0)
28
+ return this.properties.port
29
29
 
30
30
  const address = this.server.address()
31
31
 
@@ -40,132 +40,97 @@ export class Server extends Connector {
40
40
  ? DEFAULTS
41
41
  : { ...DEFAULTS, ...options }
42
42
 
43
- const app = express()
44
-
45
- app.disable('x-powered-by')
46
- app.use(supportedMethods(properties.methods))
47
-
48
- return new Server(app, properties.debug, properties.port)
43
+ return new Server(properties)
49
44
  }
50
45
 
51
46
  public attach (process: Processing): void {
52
- this.app.use((request: Request, response: Response) => {
53
- const message = this.extend(request)
54
-
55
- process(message)
56
- .then(this.success(message, response))
57
- .catch(this.fail(message, response))
58
- })
47
+ this.process = process
59
48
  }
60
49
 
61
50
  protected override async open (): Promise<void> {
62
- const listening = promex()
63
-
64
- this.server = this.app.listen(this.requestedPort, listening.callback)
51
+ this.server.listen(this.properties.port)
65
52
 
66
- await listening
53
+ await once(this.server, 'listening')
67
54
 
68
55
  console.info('HTTP Server is listening.')
69
56
  }
70
57
 
71
58
  protected override async close (): Promise<void> {
72
- const stopped = promex()
73
-
74
- this.server?.close(stopped.callback)
59
+ this.server.close()
75
60
 
76
- await stopped
61
+ console.info('HTTP Server stopped accepting new connections.')
77
62
 
78
- this.server = undefined
63
+ await once(this.server, 'close')
79
64
 
80
65
  console.info('HTTP Server has been stopped.')
81
66
  }
82
67
 
83
- private extend (request: Request): IncomingMessage {
84
- const message = request as IncomingMessage
68
+ private listener (request: http.IncomingMessage, response: http.ServerResponse): void {
69
+ if (request.method === undefined || !this.properties.methods.has(request.method)) {
70
+ response.writeHead(501).end()
85
71
 
86
- negotiate(request, message)
72
+ return
73
+ }
87
74
 
88
- message.pipelines = { body: [], response: [] }
75
+ if (request.url === undefined) {
76
+ response.writeHead(400).end()
89
77
 
90
- message.parse = async <T> (): Promise<T> => {
91
- const value = await read(request)
78
+ return
79
+ }
92
80
 
93
- if (message.pipelines.body.length === 0)
94
- return value
81
+ assert.ok(this.process !== undefined,
82
+ 'No processing function has been attached to the server.')
95
83
 
96
- return message.pipelines.body.reduce((value, transform) => transform(value), value)
97
- }
84
+ const context = new Context(request as IncomingMessage, this.properties.trace)
98
85
 
99
- return message
86
+ this.process(context)
87
+ .then(this.success(context, response))
88
+ .catch(this.fail(context, response))
100
89
  }
101
90
 
102
- private success (request: IncomingMessage, response: Response) {
91
+ private success (context: Context, response: http.ServerResponse) {
103
92
  return (message: OutgoingMessage) => {
104
93
  let status = message.status
105
94
 
106
95
  if (status === undefined)
107
- if (message.body === null) status = 404
108
- else if (request.method === 'POST') status = 201
109
- else if (message.body === undefined) status = 204
110
- else status = 200
111
-
112
- response.status(status)
113
- write(request, response, message)
96
+ if (message.body === null)
97
+ status = 404
98
+ else if (context.request.method === 'POST')
99
+ status = 201
100
+ else if (message.body === undefined)
101
+ status = 204
102
+ else
103
+ status = 200
104
+
105
+ response.statusCode = status
106
+ write(context, response, message)
114
107
  }
115
108
  }
116
109
 
117
- private fail (request: IncomingMessage, response: Response) {
110
+ private fail (context: Context, response: http.ServerResponse) {
118
111
  return async (exception: Error) => {
119
- if (!request.complete)
120
- await adam(request)
112
+ if (!context.request.complete)
113
+ await adam(context.request)
121
114
 
122
- const status = exception instanceof Exception
115
+ response.statusCode = exception instanceof Exception
123
116
  ? exception.status
124
117
  : 500
125
118
 
126
- response.status(status)
127
-
128
119
  const message: OutgoingMessage = {}
129
- const verbose = exception instanceof ClientError || this.debug
120
+ const verbose = exception instanceof ClientError || this.properties.debug
130
121
 
131
122
  if (verbose)
132
123
  message.body = exception instanceof Exception
133
124
  ? exception.body
134
125
  : (exception.stack ?? exception.message)
135
126
 
136
- write(request, response, message)
127
+ write(context, response, message)
137
128
  }
138
129
  }
139
130
  }
140
131
 
141
- function supportedMethods (methods: Set<string>) {
142
- return (req: Request, res: Response, next: NextFunction): void => {
143
- if (methods.has(req.method)) next()
144
- else res.sendStatus(501)
145
- }
146
- }
147
-
148
- function negotiate (request: Request, message: IncomingMessage): void {
149
- if (request.headers.accept !== undefined) {
150
- const match = SUBTYPE.exec(request.headers.accept)
151
-
152
- if (match !== null) {
153
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
154
- const { type, subtype, suffix } = match.groups!
155
-
156
- request.headers.accept = `${type}/${suffix}`
157
- message.subtype = subtype
158
- }
159
- }
160
-
161
- const negotiator = new Negotiator(request)
162
- const mediaType = negotiator.mediaType(types)
163
-
164
- message.encoder = mediaType === undefined ? null : formats[mediaType]
165
- }
166
-
167
132
  // https://github.com/whatwg/fetch/issues/1254
168
- async function adam (request: Request): Promise<void> {
133
+ async function adam (request: http.IncomingMessage): Promise<void> {
169
134
  const completed = promex()
170
135
  const devnull = fs.createWriteStream(os.devNull)
171
136
 
@@ -173,21 +138,21 @@ async function adam (request: Request): Promise<void> {
173
138
  .on('end', completed.callback)
174
139
  .pipe(devnull)
175
140
 
176
- return await completed
141
+ return completed
177
142
  }
178
143
 
179
144
  const DEFAULTS: Properties = {
180
145
  methods: new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']),
181
146
  debug: false,
147
+ trace: false,
182
148
  port: 8000
183
149
  }
184
150
 
185
151
  interface Properties {
186
152
  methods: Set<string>
187
153
  debug: boolean
154
+ trace: boolean
188
155
  port: number
189
156
  }
190
157
 
191
- export type Processing = (input: IncomingMessage) => Promise<OutgoingMessage>
192
-
193
- const SUBTYPE = /^(?<type>\w{1,32})\/(vnd\.toa\.(?<subtype>\S{1,32})\+)(?<suffix>\S{1,32})$/
158
+ export type Processing = (input: Context) => Promise<OutgoingMessage>
@@ -0,0 +1,40 @@
1
+ import { performance } from 'node:perf_hooks'
2
+ import type { ServerResponse } from 'node:http'
3
+
4
+ export class Timing {
5
+ private readonly skip: boolean
6
+ private readonly start = performance.now()
7
+ private readonly breakpoints: Breakpoint[] = []
8
+
9
+ public constructor (enabled: boolean) {
10
+ this.skip = !enabled
11
+ }
12
+
13
+ public async capture<T> (id: string, promise: Promise<T>): Promise<T> {
14
+ if (this.skip)
15
+ return promise
16
+
17
+ const start = performance.now()
18
+ const result = promise instanceof Promise ? await promise : promise
19
+
20
+ this.breakpoints.push({ id, duration: performance.now() - start })
21
+
22
+ return result
23
+ }
24
+
25
+ public append (response: ServerResponse): void {
26
+ if (this.skip)
27
+ return
28
+
29
+ this.breakpoints.push({ id: 'total', duration: performance.now() - this.start })
30
+
31
+ for (const breakpoint of this.breakpoints)
32
+ response.appendHeader('server-timing',
33
+ `${breakpoint.id};dur=${breakpoint.duration.toFixed(3)}`)
34
+ }
35
+ }
36
+
37
+ interface Breakpoint {
38
+ id: string
39
+ duration: number
40
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './Server'
2
2
  export * from './messages'
3
3
  export * from './exceptions'
4
+ export * from './Context'
@@ -1,8 +1,10 @@
1
+ import { Readable } from 'node:stream'
1
2
  import { generate } from 'randomstring'
2
3
  import * as msgpack from 'msgpackr'
3
4
  import { read } from './messages'
4
- import { createRequest } from './Server.fixtures'
5
5
  import { BadRequest, UnsupportedMediaType } from './exceptions'
6
+ import { Timing } from './Timing'
7
+ import type { Context } from './Context'
6
8
 
7
9
  beforeEach(() => {
8
10
  jest.clearAllMocks()
@@ -14,8 +16,8 @@ describe('read', () => {
14
16
  const headers = { 'content-type': 'application/json' }
15
17
  const input = { [generate()]: generate() }
16
18
  const json = JSON.stringify(input)
17
- const request = createRequest({ path, headers }, json)
18
- const output = await read(request)
19
+ const context = createContext(path, headers, json)
20
+ const output = await read(context)
19
21
 
20
22
  expect(output).toStrictEqual(input)
21
23
  })
@@ -24,7 +26,7 @@ describe('read', () => {
24
26
  const path = generate()
25
27
  const headers = { 'content-type': 'application/yaml' }
26
28
  const yaml = 'foo: 1'
27
- const request = createRequest({ path, headers }, yaml)
29
+ const request = createContext(path, headers, yaml)
28
30
  const value = await read(request)
29
31
 
30
32
  expect(value).toStrictEqual({ foo: 1 })
@@ -35,7 +37,7 @@ describe('read', () => {
35
37
  const headers = { 'content-type': 'application/msgpack' }
36
38
  const input = { [generate()]: generate() }
37
39
  const msg = msgpack.encode(input)
38
- const request = createRequest({ path, headers }, msg)
40
+ const request = createContext(path, headers, msg)
39
41
  const output = await read(request)
40
42
 
41
43
  expect(output).toStrictEqual(input)
@@ -45,7 +47,7 @@ describe('read', () => {
45
47
  const path = generate()
46
48
  const headers = { 'content-type': 'text/plain' }
47
49
  const input = generate()
48
- const request = createRequest({ path, headers }, input)
50
+ const request = createContext(path, headers, input)
49
51
  const output = await read(request)
50
52
 
51
53
  expect(output).toStrictEqual(input)
@@ -54,7 +56,7 @@ describe('read', () => {
54
56
  it('should throw on unsupported request media type', async () => {
55
57
  const path = generate()
56
58
  const headers = { 'content-type': 'wtf/' + generate() }
57
- const request = createRequest({ path, headers })
59
+ const request = createContext(path, headers)
58
60
 
59
61
  await expect(read(request)).rejects.toThrow(UnsupportedMediaType)
60
62
  })
@@ -63,8 +65,25 @@ describe('read', () => {
63
65
  const path = generate()
64
66
  const text = '{ "foo": "val... oops '
65
67
  const headers = { 'content-type': 'application/json' }
66
- const request = createRequest({ path, headers }, text)
68
+ const request = createContext(path, headers, text)
67
69
 
68
70
  await expect(read(request)).rejects.toThrow(BadRequest)
69
71
  })
70
72
  })
73
+
74
+ export function createContext
75
+ (url: string, headers: Record<string, string> = {}, content: string | Buffer = ''):
76
+ jest.MockedObject<Context> {
77
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content)
78
+ const stream = Readable.from(buffer)
79
+
80
+ const mock: Partial<Context> = {
81
+ request: Object.assign(stream, {
82
+ url,
83
+ headers
84
+ }) as unknown as Context['request'],
85
+ timing: new Timing(false)
86
+ }
87
+
88
+ return mock as unknown as jest.MockedObject<Context>
89
+ }
@@ -1,27 +1,26 @@
1
- import { type IncomingHttpHeaders } from 'node:http'
2
1
  import { Readable } from 'node:stream'
3
- import { type Request, type Response } from 'express'
4
- import { buffer } from '@toa.io/streams'
2
+ import { buffer } from 'node:stream/consumers'
5
3
  import { formats } from './formats'
6
4
  import { BadRequest, NotAcceptable, UnsupportedMediaType } from './exceptions'
7
- import type { ParsedQs } from 'qs'
8
- import type { Format } from './formats'
5
+ import type { Context } from './Context'
6
+ import type * as http from 'node:http'
9
7
 
10
8
  export function write
11
- (request: IncomingMessage, response: Response, message: OutgoingMessage): void {
12
- for (const transform of request.pipelines.response)
9
+ (context: Context, response: http.ServerResponse, message: OutgoingMessage): void {
10
+ for (const transform of context.pipelines.response)
13
11
  transform(message)
14
12
 
15
- message.headers?.forEach((value, key) => response.set(key, value))
13
+ message.headers?.forEach((value, key) => response.setHeader(key, value))
14
+ context.timing.append(response)
16
15
 
17
16
  if (message.body instanceof Readable)
18
- stream(message, request, response)
17
+ stream(message, context, response)
19
18
  else
20
- send(message, request, response)
19
+ send(message, context, response)
21
20
  }
22
21
 
23
- export async function read (request: Request): Promise<any> {
24
- const type = request.headers['content-type']
22
+ export async function read (context: Context): Promise<any> {
23
+ const type = context.request.headers['content-type']
25
24
 
26
25
  if (type === undefined)
27
26
  return undefined
@@ -30,8 +29,7 @@ export async function read (request: Request): Promise<any> {
30
29
  throw new UnsupportedMediaType()
31
30
 
32
31
  const format = formats[type]
33
-
34
- const buf = await buffer(request)
32
+ const buf = await context.timing.capture('req:buffer', buffer(context.request))
35
33
 
36
34
  try {
37
35
  return format.decode(buf)
@@ -40,32 +38,33 @@ export async function read (request: Request): Promise<any> {
40
38
  }
41
39
  }
42
40
 
43
- function send (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
41
+ function send
42
+ (message: OutgoingMessage, context: Context, response: http.ServerResponse): void {
44
43
  if (message.body === undefined || message.body === null) {
45
44
  response.end()
46
45
 
47
46
  return
48
47
  }
49
48
 
50
- if (request.encoder === null)
49
+ if (context.encoder === null)
51
50
  throw new NotAcceptable()
52
51
 
53
- const buf = request.encoder.encode(message.body)
52
+ const buf = context.encoder.encode(message.body)
54
53
 
55
54
  response
56
- .set('content-type', request.encoder.type)
57
- .append('vary', 'accept')
55
+ .setHeader('content-type', context.encoder.type)
56
+ .appendHeader('vary', 'accept')
58
57
  .end(buf)
59
58
  }
60
59
 
61
60
  function stream
62
- (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
61
+ (message: OutgoingMessage, context: Context, response: http.ServerResponse): void {
63
62
  const encoded = message.headers !== undefined && message.headers.has('content-type')
64
63
 
65
64
  if (encoded)
66
- pipe(message, response)
65
+ message.body.pipe(response)
67
66
  else
68
- multipart(message, request, response)
67
+ multipart(message, context, response)
69
68
 
70
69
  message.body.on('error', (e: Error) => {
71
70
  console.error(e)
@@ -73,54 +72,39 @@ function stream
73
72
  })
74
73
  }
75
74
 
76
- function pipe (message: OutgoingMessage, response: Response): void {
77
- message.headers?.forEach((value, key) => response.set(key, value))
78
- message.body.pipe(response)
79
- }
80
-
81
- function multipart (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
82
- if (request.encoder === null)
75
+ function multipart
76
+ (message: OutgoingMessage, context: Context, response: http.ServerResponse): void {
77
+ if (context.encoder === null)
83
78
  throw new NotAcceptable()
84
79
 
85
- const encoder = request.encoder
80
+ const encoder = context.encoder
86
81
 
87
- response.set('content-type', `${encoder.multipart}; boundary=${BOUNDARY}`)
82
+ response.setHeader('content-type', `${encoder.multipart}; boundary=${BOUNDARY}`)
88
83
 
89
84
  message.body
90
- .map((part: unknown) => Buffer.concat([CUT, encoder.encode(part)]))
85
+ .map((part: unknown) => Buffer.concat([CUT, encoder.encode(part), CRLF]))
91
86
  .on('end', () => response.end(FINALCUT))
92
87
  .pipe(response)
93
88
  }
94
89
 
95
90
  const BOUNDARY = 'cut'
96
91
  const CUT = Buffer.from(`--${BOUNDARY}\r\n`)
92
+ const CRLF = Buffer.from('\r\n')
97
93
  const FINALCUT = Buffer.from(`--${BOUNDARY}--`)
98
94
 
99
- export interface IncomingMessage extends Request {
100
- method: string
101
- path: string
102
- url: string
103
- headers: IncomingHttpHeaders
104
- query: Query
105
- parse: <T> () => Promise<T>
106
- encoder: Format | null
107
- subtype: string | null
108
- pipelines: {
109
- body: Array<(input: unknown) => unknown>
110
- response: Array<(output: OutgoingMessage) => void>
111
- }
112
- }
113
-
114
95
  export interface OutgoingMessage {
115
96
  status?: number
116
97
  headers?: Headers
117
98
  body?: any
118
99
  }
119
100
 
120
- export interface Query extends ParsedQs {
101
+ export interface Query {
102
+ [key: string]: string | number | undefined
103
+
121
104
  id?: string
122
105
  criteria?: string
123
106
  sort?: string
124
107
  omit?: string
125
108
  limit?: string
109
+ version?: number
126
110
  }
package/source/Mapping.ts CHANGED
@@ -29,7 +29,10 @@ class QueryableMapping extends Mapping {
29
29
  public fit (input: any, qs: http.Query, parameters: Parameter[]): core.Request {
30
30
  const query = this.query.fit(qs, parameters)
31
31
 
32
- return { input, query }
32
+ return {
33
+ input,
34
+ query
35
+ }
33
36
  }
34
37
  }
35
38
 
@@ -38,14 +41,10 @@ class InputMapping extends Mapping {
38
41
  if (input === undefined && parameters.length > 0)
39
42
  input = {}
40
43
 
41
- if (typeof input === 'object')
42
- this.assign(input, parameters)
44
+ if (typeof input === 'object' && input !== null)
45
+ for (const parameter of parameters)
46
+ input[parameter.name] = parameter.value
43
47
 
44
48
  return { input }
45
49
  }
46
-
47
- private assign (input: Record<string, any>, parameters: Parameter[]): void {
48
- for (const parameter of parameters)
49
- input[parameter.name] = parameter.value
50
- }
51
50
  }
@@ -41,6 +41,12 @@ export function deployment (_: unknown, annotation: Annotation | undefined): Dep
41
41
  value: '1'
42
42
  })
43
43
 
44
+ if (annotation?.trace === true)
45
+ service.variables.push({
46
+ name: 'TOA_EXPOSITION_TRACE',
47
+ value: '1'
48
+ })
49
+
44
50
  if (annotation !== undefined)
45
51
  schemas.annotaion.validate(annotation)
46
52
 
@@ -8,7 +8,8 @@ export class Anonymous implements Directive {
8
8
  }
9
9
 
10
10
  public authorize (_: any, input: Input): boolean {
11
- if ('authorization' in input.headers) return false
12
- else return this.allow
11
+ return 'authorization' in input.request.headers
12
+ ? false
13
+ : this.allow
13
14
  }
14
15
  }
@@ -56,7 +56,7 @@ export class Authorization implements Family<Directive, Extension> {
56
56
  public async preflight (directives: Directive[],
57
57
  input: Input,
58
58
  parameters: Parameter[]): Promise<Output> {
59
- const identity = await this.resolve(input.headers.authorization)
59
+ const identity = await this.resolve(input.request.headers.authorization)
60
60
 
61
61
  input.identity = identity
62
62
 
@@ -67,8 +67,10 @@ export class Authorization implements Family<Directive, Extension> {
67
67
  return directive.reply?.(identity) ?? null
68
68
  }
69
69
 
70
- if (identity === null) throw new http.Unauthorized()
71
- else throw new http.Forbidden()
70
+ if (identity === null)
71
+ throw new http.Unauthorized()
72
+ else
73
+ throw new http.Forbidden()
72
74
  }
73
75
 
74
76
  public async settle (directives: Directive[],
@@ -15,28 +15,33 @@ export class Incept implements Directive {
15
15
  }
16
16
 
17
17
  public authorize (identity: Identity | null, input: Input): boolean {
18
- return identity === null && 'authorization' in input.headers
18
+ return identity === null && 'authorization' in input.request.headers
19
19
  }
20
20
 
21
- public async settle (request: Input, response: http.OutgoingMessage): Promise<void> {
21
+ public async settle (input: Input, response: http.OutgoingMessage): Promise<void> {
22
22
  const id = response.body?.[this.property]
23
23
 
24
24
  if (id === undefined)
25
25
  throw new http.Conflict('Identity inception has failed as the response body ' +
26
26
  ` does not contain the '${this.property}' property.`)
27
27
 
28
- const [scheme, credentials] = split(request.headers.authorization!)
28
+ const [scheme, credentials] = split(input.request.headers.authorization!)
29
29
  const provider = PROVIDERS[scheme]
30
30
 
31
31
  this.schemes[scheme] ??= await this.discovery[provider]
32
32
 
33
33
  const identity = await this.schemes[scheme]
34
- .invoke<Maybe<Identity>>('create', { input: { id, credentials } })
34
+ .invoke<Maybe<Identity>>('create', {
35
+ input: {
36
+ id,
37
+ credentials
38
+ }
39
+ })
35
40
 
36
41
  if (identity instanceof Error)
37
42
  throw new http.Conflict(identity)
38
43
 
39
- request.identity = identity
40
- request.identity.scheme = scheme
44
+ input.identity = identity
45
+ input.identity.scheme = scheme
41
46
  }
42
47
  }
@@ -14,10 +14,12 @@ export class Role implements Directive {
14
14
  public static async set (identity: Identity, discovery: Promise<Component>): Promise<void> {
15
15
  this.remote ??= await discovery
16
16
 
17
- const query: Query = { criteria: `identity==${identity.id}`, limit: 1024 }
18
- const roles: string[] = await this.remote.invoke('list', { query })
17
+ const query: Query = {
18
+ criteria: `identity==${identity.id}`,
19
+ limit: 1024
20
+ }
19
21
 
20
- identity.roles = roles
22
+ identity.roles = await this.remote.invoke('list', { query })
21
23
  }
22
24
 
23
25
  public async authorize (identity: Identity | null): Promise<boolean> {