@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.
- package/components/identity.bans/manifest.toa.yaml +1 -0
- package/components/identity.basic/manifest.toa.yaml +1 -0
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/manifest.toa.yaml +13 -0
- package/components/identity.federation/operations/authenticate.js +4 -4
- package/components/identity.federation/operations/authenticate.js.map +1 -1
- package/components/identity.federation/operations/create.js +4 -4
- package/components/identity.federation/operations/create.js.map +1 -1
- package/components/identity.federation/operations/{assertions-as-values.cjs → lib/assertions-as-values.js} +1 -1
- package/components/identity.federation/operations/lib/assertions-as-values.js.map +1 -0
- package/components/identity.federation/operations/{jwt.d.cts → lib/jwt.d.ts} +5 -4
- package/components/identity.federation/operations/{jwt.cjs → lib/jwt.js} +35 -11
- package/components/identity.federation/operations/lib/jwt.js.map +1 -0
- package/components/identity.federation/operations/schemas.d.ts +16 -0
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/operations/types.d.ts +1 -1
- package/components/identity.federation/source/authenticate.ts +2 -2
- package/components/identity.federation/source/create.ts +2 -2
- package/components/identity.federation/source/{assertions-as-values.cts → lib/assertions-as-values.ts} +1 -2
- package/components/identity.federation/source/lib/jwt.test.ts +56 -0
- package/components/identity.federation/source/{jwt.cts → lib/jwt.ts} +57 -29
- package/components/identity.federation/source/schemas.ts +16 -0
- package/components/identity.federation/source/types.ts +1 -1
- package/components/identity.roles/manifest.toa.yaml +1 -0
- package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/manifest.toa.yaml +1 -0
- package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
- package/components/octets.storage/manifest.toa.yaml +1 -0
- package/components/octets.storage/operations/store.js +1 -1
- package/documentation/components.md +12 -5
- package/documentation/identity.md +7 -0
- package/documentation/octets.md +12 -0
- package/documentation/query.md +45 -2
- package/features/body.feature +1 -1
- package/features/errors.feature +1 -1
- package/features/etag.feature +86 -0
- package/features/identity.federation.feature +31 -1
- package/features/octets.entries.feature +1 -1
- package/features/octets.workflows.feature +38 -0
- package/features/steps/Gateway.ts +3 -0
- package/features/steps/IdP.ts +29 -0
- package/features/steps/components/echo/manifest.toa.yaml +1 -0
- package/features/steps/components/greeter/manifest.toa.yaml +1 -0
- package/features/steps/components/octets.tester/manifest.toa.yaml +1 -0
- package/features/steps/components/pots/manifest.toa.yaml +10 -3
- package/features/steps/components/sequences/manifest.toa.yaml +1 -0
- package/features/timing.feature +43 -0
- package/package.json +7 -10
- package/readme.md +7 -6
- package/schemas/annotation.cos.yaml +1 -0
- package/schemas/octets/workflow.cos.yaml +12 -0
- package/schemas/querystring.cos.yaml +1 -0
- package/source/Annotation.ts +1 -0
- package/source/Directive.test.ts +3 -3
- package/source/Directive.ts +11 -11
- package/source/Endpoint.ts +18 -4
- package/source/Factory.ts +8 -4
- package/source/Gateway.ts +55 -42
- package/source/HTTP/Context.ts +67 -0
- package/source/HTTP/Server.test.ts +1 -1
- package/source/HTTP/Server.ts +60 -95
- package/source/HTTP/Timing.ts +40 -0
- package/source/HTTP/index.ts +1 -0
- package/source/HTTP/messages.test.ts +27 -8
- package/source/HTTP/messages.ts +32 -48
- package/source/Mapping.ts +7 -8
- package/source/deployment.ts +6 -0
- package/source/directives/auth/Anonymous.ts +3 -2
- package/source/directives/auth/Authorization.ts +5 -3
- package/source/directives/auth/Incept.ts +11 -6
- package/source/directives/auth/Role.ts +5 -3
- package/source/directives/auth/Scheme.ts +2 -2
- package/source/directives/cache/Cache.ts +2 -2
- package/source/directives/cache/Control.ts +5 -5
- package/source/directives/cache/types.ts +1 -1
- package/source/directives/cors/CORS.ts +5 -3
- package/source/directives/octets/Context.ts +1 -1
- package/source/directives/octets/Delete.ts +21 -11
- package/source/directives/octets/Fetch.ts +29 -14
- package/source/directives/octets/List.ts +14 -6
- package/source/directives/octets/Octets.ts +7 -3
- package/source/directives/octets/Permute.ts +12 -6
- package/source/directives/octets/Store.ts +32 -16
- package/source/directives/octets/Workflow.ts +41 -0
- package/source/directives/octets/schemas.test.ts +21 -0
- package/source/directives/octets/schemas.ts +2 -0
- package/source/directives/octets/{workflow → workflows}/Execution.ts +0 -2
- package/source/directives/octets/{workflow → workflows}/Workflow.ts +2 -2
- package/source/directives/vary/Vary.ts +1 -1
- package/source/directives/vary/embeddings/Header.ts +1 -1
- package/source/directives/vary/embeddings/Language.ts +1 -1
- package/source/io.ts +2 -2
- package/transpiled/Annotation.d.ts +1 -0
- package/transpiled/Directive.d.ts +6 -6
- package/transpiled/Directive.js +8 -8
- package/transpiled/Directive.js.map +1 -1
- package/transpiled/Endpoint.d.ts +3 -3
- package/transpiled/Endpoint.js +34 -1
- package/transpiled/Endpoint.js.map +1 -1
- package/transpiled/Factory.js +4 -2
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Gateway.d.ts +5 -6
- package/transpiled/Gateway.js +38 -32
- package/transpiled/Gateway.js.map +1 -1
- package/transpiled/HTTP/Context.d.ts +24 -0
- package/transpiled/HTTP/Context.js +47 -0
- package/transpiled/HTTP/Context.js.map +1 -0
- package/transpiled/HTTP/Server.d.ts +8 -7
- package/transpiled/HTTP/Server.js +68 -76
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/Timing.d.ts +10 -0
- package/transpiled/HTTP/Timing.js +29 -0
- package/transpiled/HTTP/Timing.js.map +1 -0
- package/transpiled/HTTP/index.d.ts +1 -0
- package/transpiled/HTTP/index.js +1 -0
- package/transpiled/HTTP/index.js.map +1 -1
- package/transpiled/HTTP/messages.d.ts +7 -21
- package/transpiled/HTTP/messages.js +24 -26
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/Mapping.js +7 -7
- package/transpiled/Mapping.js.map +1 -1
- package/transpiled/deployment.js +5 -0
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/directives/auth/Anonymous.js +3 -4
- package/transpiled/directives/auth/Anonymous.js.map +1 -1
- package/transpiled/directives/auth/Authorization.js +1 -1
- package/transpiled/directives/auth/Authorization.js.map +1 -1
- package/transpiled/directives/auth/Incept.d.ts +1 -1
- package/transpiled/directives/auth/Incept.js +11 -6
- package/transpiled/directives/auth/Incept.js.map +1 -1
- package/transpiled/directives/auth/Role.js +5 -3
- package/transpiled/directives/auth/Role.js.map +1 -1
- package/transpiled/directives/auth/Scheme.js +2 -2
- package/transpiled/directives/auth/Scheme.js.map +1 -1
- package/transpiled/directives/cache/Cache.d.ts +1 -1
- package/transpiled/directives/cache/Cache.js +2 -2
- package/transpiled/directives/cache/Cache.js.map +1 -1
- package/transpiled/directives/cache/Control.d.ts +3 -3
- package/transpiled/directives/cache/Control.js +3 -3
- package/transpiled/directives/cache/Control.js.map +1 -1
- package/transpiled/directives/cache/types.d.ts +1 -1
- package/transpiled/directives/cors/CORS.js +4 -3
- package/transpiled/directives/cors/CORS.js.map +1 -1
- package/transpiled/directives/octets/Context.d.ts +1 -1
- package/transpiled/directives/octets/Context.js.map +1 -1
- package/transpiled/directives/octets/Delete.d.ts +2 -2
- package/transpiled/directives/octets/Delete.js +21 -11
- package/transpiled/directives/octets/Delete.js.map +1 -1
- package/transpiled/directives/octets/Fetch.d.ts +1 -1
- package/transpiled/directives/octets/Fetch.js +28 -14
- package/transpiled/directives/octets/Fetch.js.map +1 -1
- package/transpiled/directives/octets/List.d.ts +1 -1
- package/transpiled/directives/octets/List.js +13 -6
- package/transpiled/directives/octets/List.js.map +1 -1
- package/transpiled/directives/octets/Octets.js +7 -3
- package/transpiled/directives/octets/Octets.js.map +1 -1
- package/transpiled/directives/octets/Permute.d.ts +1 -1
- package/transpiled/directives/octets/Permute.js +11 -6
- package/transpiled/directives/octets/Permute.js.map +1 -1
- package/transpiled/directives/octets/Store.d.ts +3 -2
- package/transpiled/directives/octets/Store.js +19 -11
- package/transpiled/directives/octets/Store.js.map +1 -1
- package/transpiled/directives/octets/Workflow.d.ts +14 -0
- package/transpiled/directives/octets/Workflow.js +52 -0
- package/transpiled/directives/octets/Workflow.js.map +1 -0
- package/transpiled/directives/octets/schemas.d.ts +2 -0
- package/transpiled/directives/octets/schemas.js +2 -1
- package/transpiled/directives/octets/schemas.js.map +1 -1
- package/transpiled/directives/octets/{workflow → workflows}/Execution.js +0 -1
- package/transpiled/directives/octets/workflows/Execution.js.map +1 -0
- package/transpiled/directives/octets/{workflow → workflows}/Workflow.d.ts +1 -1
- package/transpiled/directives/octets/{workflow → workflows}/Workflow.js +2 -2
- package/transpiled/directives/octets/workflows/Workflow.js.map +1 -0
- package/transpiled/directives/octets/workflows/index.js.map +1 -0
- package/transpiled/directives/vary/Vary.js +1 -1
- package/transpiled/directives/vary/embeddings/Header.js +1 -1
- package/transpiled/directives/vary/embeddings/Header.js.map +1 -1
- package/transpiled/directives/vary/embeddings/Language.js +1 -1
- package/transpiled/directives/vary/embeddings/Language.js.map +1 -1
- package/transpiled/io.d.ts +2 -2
- package/transpiled/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/operations/assertions-as-values.cjs.map +0 -1
- package/components/identity.federation/operations/jwt.cjs.map +0 -1
- package/source/HTTP/Server.fixtures.ts +0 -40
- package/transpiled/HTTP/Server.fixtures.d.ts +0 -10
- package/transpiled/HTTP/Server.fixtures.js +0 -31
- package/transpiled/HTTP/Server.fixtures.js.map +0 -1
- package/transpiled/directives/octets/workflow/Execution.js.map +0 -1
- package/transpiled/directives/octets/workflow/Workflow.js.map +0 -1
- package/transpiled/directives/octets/workflow/index.js.map +0 -1
- /package/components/identity.federation/operations/{assertions-as-values.d.cts → lib/assertions-as-values.d.ts} +0 -0
- /package/source/directives/octets/{workflow → workflows}/index.ts +0 -0
- /package/transpiled/directives/octets/{workflow → workflows}/Execution.d.ts +0 -0
- /package/transpiled/directives/octets/{workflow → workflows}/index.d.ts +0 -0
- /package/transpiled/directives/octets/{workflow → workflows}/index.js +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Negotiator from 'negotiator'
|
|
2
|
+
import { Timing } from './Timing'
|
|
3
|
+
import { type Format, formats, types } from './formats'
|
|
4
|
+
import { read } from './messages'
|
|
5
|
+
import type { OutgoingMessage } from './messages'
|
|
6
|
+
import type * as http from 'node:http'
|
|
7
|
+
|
|
8
|
+
export class Context {
|
|
9
|
+
public readonly request: IncomingMessage
|
|
10
|
+
public readonly url: URL
|
|
11
|
+
public readonly subtype: string | null = null
|
|
12
|
+
public readonly encoder: Format | null = null
|
|
13
|
+
public readonly timing: Timing
|
|
14
|
+
|
|
15
|
+
public readonly pipelines: Pipelines = {
|
|
16
|
+
body: [],
|
|
17
|
+
response: []
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public constructor (request: IncomingMessage, trace = false) {
|
|
21
|
+
this.request = request
|
|
22
|
+
|
|
23
|
+
this.url = new URL(request.url, `https://${request.headers.host}`)
|
|
24
|
+
this.timing = new Timing(trace)
|
|
25
|
+
|
|
26
|
+
if (this.request.headers.accept !== undefined) {
|
|
27
|
+
const match = SUBTYPE.exec(this.request.headers.accept)
|
|
28
|
+
|
|
29
|
+
if (match !== null) {
|
|
30
|
+
const {
|
|
31
|
+
type,
|
|
32
|
+
subtype,
|
|
33
|
+
suffix
|
|
34
|
+
} = match.groups!
|
|
35
|
+
|
|
36
|
+
this.request.headers.accept = `${type}/${suffix}`
|
|
37
|
+
this.subtype = subtype
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const negotiator = new Negotiator(this.request)
|
|
42
|
+
const mediaType = negotiator.mediaType(types)
|
|
43
|
+
|
|
44
|
+
if (mediaType !== undefined)
|
|
45
|
+
this.encoder = formats[mediaType]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public async body<T> (): Promise<T> {
|
|
49
|
+
const value = await read(this)
|
|
50
|
+
|
|
51
|
+
return this.pipelines.body.length === 0
|
|
52
|
+
? value
|
|
53
|
+
: this.pipelines.body.reduce((value, transform) => transform(value), value)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface IncomingMessage extends http.IncomingMessage {
|
|
58
|
+
url: string
|
|
59
|
+
method: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface Pipelines {
|
|
63
|
+
body: Array<(input: unknown) => unknown>
|
|
64
|
+
response: Array<(output: OutgoingMessage) => void>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SUBTYPE = /^(?<type>\w{1,32})\/(vnd\.toa\.(?<subtype>\S{1,32})\+)(?<suffix>\S{1,32})$/
|
package/source/HTTP/Server.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import os from 'node:os'
|
|
3
|
-
import
|
|
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
|
|
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 {
|
|
10
|
-
import type
|
|
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
|
|
15
|
-
private readonly
|
|
16
|
-
private
|
|
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 (
|
|
18
|
+
private constructor (properties: Properties) {
|
|
20
19
|
super()
|
|
21
20
|
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
this.server?.close(stopped.callback)
|
|
59
|
+
this.server.close()
|
|
75
60
|
|
|
76
|
-
|
|
61
|
+
console.info('HTTP Server stopped accepting new connections.')
|
|
77
62
|
|
|
78
|
-
this.server
|
|
63
|
+
await once(this.server, 'close')
|
|
79
64
|
|
|
80
65
|
console.info('HTTP Server has been stopped.')
|
|
81
66
|
}
|
|
82
67
|
|
|
83
|
-
private
|
|
84
|
-
|
|
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
|
-
|
|
72
|
+
return
|
|
73
|
+
}
|
|
87
74
|
|
|
88
|
-
|
|
75
|
+
if (request.url === undefined) {
|
|
76
|
+
response.writeHead(400).end()
|
|
89
77
|
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
return
|
|
79
|
+
}
|
|
92
80
|
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
assert.ok(this.process !== undefined,
|
|
82
|
+
'No processing function has been attached to the server.')
|
|
95
83
|
|
|
96
|
-
|
|
97
|
-
}
|
|
84
|
+
const context = new Context(request as IncomingMessage, this.properties.trace)
|
|
98
85
|
|
|
99
|
-
|
|
86
|
+
this.process(context)
|
|
87
|
+
.then(this.success(context, response))
|
|
88
|
+
.catch(this.fail(context, response))
|
|
100
89
|
}
|
|
101
90
|
|
|
102
|
-
private success (
|
|
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)
|
|
108
|
-
|
|
109
|
-
else if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
|
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:
|
|
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
|
+
}
|
package/source/HTTP/index.ts
CHANGED
|
@@ -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
|
|
18
|
-
const output = await read(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
+
}
|
package/source/HTTP/messages.ts
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
|
-
import { type IncomingHttpHeaders } from 'node:http'
|
|
2
1
|
import { Readable } from 'node:stream'
|
|
3
|
-
import {
|
|
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 {
|
|
8
|
-
import type
|
|
5
|
+
import type { Context } from './Context'
|
|
6
|
+
import type * as http from 'node:http'
|
|
9
7
|
|
|
10
8
|
export function write
|
|
11
|
-
(
|
|
12
|
-
for (const transform of
|
|
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.
|
|
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,
|
|
17
|
+
stream(message, context, response)
|
|
19
18
|
else
|
|
20
|
-
send(message,
|
|
19
|
+
send(message, context, response)
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
export async function read (
|
|
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
|
|
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 (
|
|
49
|
+
if (context.encoder === null)
|
|
51
50
|
throw new NotAcceptable()
|
|
52
51
|
|
|
53
|
-
const buf =
|
|
52
|
+
const buf = context.encoder.encode(message.body)
|
|
54
53
|
|
|
55
54
|
response
|
|
56
|
-
.
|
|
57
|
-
.
|
|
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,
|
|
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(
|
|
65
|
+
message.body.pipe(response)
|
|
67
66
|
else
|
|
68
|
-
multipart(message,
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
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 =
|
|
80
|
+
const encoder = context.encoder
|
|
86
81
|
|
|
87
|
-
response.
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|
package/source/deployment.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
11
|
+
return 'authorization' in input.request.headers
|
|
12
|
+
? false
|
|
13
|
+
: this.allow
|
|
13
14
|
}
|
|
14
15
|
}
|