@toa.io/extensions.exposition 0.22.1 → 0.23.0-dev.0

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 (66) hide show
  1. package/components/identity.basic/source/authenticate.ts +3 -2
  2. package/components/identity.basic/source/transit.ts +4 -3
  3. package/components/octets.storage/manifest.toa.yaml +26 -0
  4. package/components/octets.storage/operations/delete.js +7 -0
  5. package/components/octets.storage/operations/fetch.js +46 -0
  6. package/components/octets.storage/operations/get.js +7 -0
  7. package/components/octets.storage/operations/list.js +7 -0
  8. package/components/octets.storage/operations/permute.js +7 -0
  9. package/components/octets.storage/operations/store.js +11 -0
  10. package/cucumber.js +0 -1
  11. package/documentation/octets.md +196 -0
  12. package/documentation/protocol.md +49 -5
  13. package/features/access.feature +1 -0
  14. package/features/errors.feature +18 -0
  15. package/features/identity.basic.feature +2 -0
  16. package/features/octets.feature +295 -0
  17. package/features/octets.workflows.feature +114 -0
  18. package/features/routes.feature +40 -0
  19. package/features/steps/HTTP.ts +47 -5
  20. package/features/steps/Parameters.ts +5 -1
  21. package/features/steps/Workspace.ts +3 -2
  22. package/features/steps/components/octets.tester/manifest.toa.yaml +15 -0
  23. package/features/steps/components/octets.tester/operations/bar.js +12 -0
  24. package/features/steps/components/octets.tester/operations/baz.js +11 -0
  25. package/features/steps/components/octets.tester/operations/diversify.js +14 -0
  26. package/features/steps/components/octets.tester/operations/err.js +16 -0
  27. package/features/steps/components/octets.tester/operations/foo.js +7 -0
  28. package/features/steps/components/octets.tester/operations/lenna.png +0 -0
  29. package/features/steps/components/pots/manifest.toa.yaml +1 -1
  30. package/features/streams.feature +5 -1
  31. package/package.json +11 -7
  32. package/readme.md +7 -5
  33. package/schemas/octets/context.cos.yaml +1 -0
  34. package/schemas/octets/delete.cos.yaml +1 -0
  35. package/schemas/octets/fetch.cos.yaml +3 -0
  36. package/schemas/octets/list.cos.yaml +1 -0
  37. package/schemas/octets/permute.cos.yaml +1 -0
  38. package/schemas/octets/store.cos.yaml +3 -0
  39. package/source/Gateway.ts +9 -4
  40. package/source/HTTP/Server.fixtures.ts +2 -6
  41. package/source/HTTP/Server.test.ts +7 -31
  42. package/source/HTTP/Server.ts +31 -16
  43. package/source/HTTP/exceptions.ts +2 -12
  44. package/source/HTTP/formats/index.ts +7 -4
  45. package/source/HTTP/formats/json.ts +3 -0
  46. package/source/HTTP/formats/msgpack.ts +3 -0
  47. package/source/HTTP/formats/text.ts +3 -0
  48. package/source/HTTP/formats/yaml.ts +3 -0
  49. package/source/HTTP/messages.test.ts +3 -49
  50. package/source/HTTP/messages.ts +58 -33
  51. package/source/RTD/Route.ts +1 -1
  52. package/source/RTD/segment.ts +2 -1
  53. package/source/Remotes.ts +8 -0
  54. package/source/directives/auth/Family.ts +24 -20
  55. package/source/directives/auth/Rule.ts +1 -1
  56. package/source/directives/index.ts +2 -1
  57. package/source/directives/octets/Context.ts +18 -0
  58. package/source/directives/octets/Delete.ts +32 -0
  59. package/source/directives/octets/Family.ts +68 -0
  60. package/source/directives/octets/Fetch.ts +85 -0
  61. package/source/directives/octets/List.ts +32 -0
  62. package/source/directives/octets/Permute.ts +37 -0
  63. package/source/directives/octets/Store.ts +158 -0
  64. package/source/directives/octets/index.ts +3 -0
  65. package/source/directives/octets/schemas.ts +12 -0
  66. package/source/directives/octets/types.ts +13 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "0.22.1",
3
+ "version": "0.23.0-dev.0",
4
4
  "description": "Toa Exposition",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -17,15 +17,17 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@toa.io/core": "0.22.1",
21
- "@toa.io/generic": "0.22.0",
22
- "@toa.io/http": "0.22.0",
23
- "@toa.io/schemas": "0.22.0",
24
- "@toa.io/streams": "0.22.0",
20
+ "@toa.io/core": "0.23.0-dev.0",
21
+ "@toa.io/generic": "0.23.0-dev.0",
22
+ "@toa.io/http": "0.23.0-dev.0",
23
+ "@toa.io/schemas": "0.23.0-dev.0",
24
+ "@toa.io/streams": "0.23.0-dev.0",
25
25
  "bcryptjs": "2.4.3",
26
26
  "cors": "2.8.5",
27
+ "error-value": "0.3.0",
27
28
  "express": "4.18.2",
28
29
  "js-yaml": "4.1.0",
30
+ "matchacho": "0.3.5",
29
31
  "msgpackr": "1.9.5",
30
32
  "negotiator": "0.6.3",
31
33
  "paseto": "3.1.4"
@@ -35,16 +37,18 @@
35
37
  "testEnvironment": "node"
36
38
  },
37
39
  "scripts": {
40
+ "test": "jest",
38
41
  "transpile": "rm -rf transpiled && npx tsc && npm run transpile:basic && npm run transpile:tokens",
39
42
  "transpile:basic": "rm -rf ./components/identity.basic/operations && npx tsc -p ./components/identity.basic",
40
43
  "transpile:tokens": "rm -rf ./components/identity.tokens/operations && npx tsc -p ./components/identity.tokens",
41
44
  "features": "npx cucumber-js"
42
45
  },
43
46
  "devDependencies": {
47
+ "@toa.io/extensions.storages": "0.23.0-dev.0",
44
48
  "@types/bcryptjs": "2.4.3",
45
49
  "@types/cors": "2.8.13",
46
50
  "@types/express": "4.17.17",
47
51
  "@types/negotiator": "0.6.1"
48
52
  },
49
- "gitHead": "d1116e1a3c3d881d2204989851596f0715abcb0f"
53
+ "gitHead": "19d4af8ff3b8a1a8f191644f44113c88de4bb404"
50
54
  }
package/readme.md CHANGED
@@ -39,11 +39,12 @@ See [features](features) for more examples.
39
39
  </picture>
40
40
  </a>
41
41
 
42
- The Exposition extension includes a Service which is an HTTP server with ingress and a Tenant. The
43
- Service communicates
44
- with Tenants to discover their resource declarations and exposes them as HTTP resources. An instance
45
- of the Tenant is
46
- running within each Composition that has at least one Component with a resource declaration.
42
+ The Exposition extension includes a Service, which is an HTTP server with ingress and a Tenant.
43
+ The Service communicates with Tenants to discover their resource declarations and exposes them as
44
+ HTTP resources.
45
+ An instance of the Tenant is running within each Composition that has at least one Component with a
46
+ resource
47
+ declaration.
47
48
 
48
49
  ## Resource tree discovery
49
50
 
@@ -179,5 +180,6 @@ exposition:
179
180
  - [Resource Tree Definition](documentation/tree.md)
180
181
  - [Identity authentication](documentation/identity.md)
181
182
  - [Access authorization](documentation/access.md)
183
+ - [BLOBs](documentation/octets.md)
182
184
  - [Components and resources](documentation/components.md)
183
185
  - [Features](features)
@@ -0,0 +1 @@
1
+ string
@@ -0,0 +1 @@
1
+ ~
@@ -0,0 +1,3 @@
1
+ blob: boolean
2
+ meta: boolean
3
+ _: true
@@ -0,0 +1 @@
1
+ ~
@@ -0,0 +1 @@
1
+ ~
@@ -0,0 +1,3 @@
1
+ accept+: string
2
+ workflow+: <string>
3
+ _: true
package/source/Gateway.ts CHANGED
@@ -35,9 +35,6 @@ export class Gateway extends Connector {
35
35
  }
36
36
 
37
37
  private async process (request: http.IncomingMessage): Promise<http.OutgoingMessage> {
38
- if (request.path[request.path.length - 1] !== '/')
39
- throw new http.NotFound('Trailing slash is required.')
40
-
41
38
  const match = this.tree.match(request.path)
42
39
 
43
40
  if (match === null)
@@ -60,11 +57,19 @@ export class Gateway extends Connector {
60
57
  private async call
61
58
  (method: Method<Endpoint, Directives>, request: http.IncomingMessage, parameters: Parameter[]):
62
59
  Promise<http.OutgoingMessage> {
60
+ if (request.path[request.path.length - 1] !== '/')
61
+ throw new http.NotFound('Trailing slash is required.')
62
+
63
+ if (request.encoder === null)
64
+ throw new http.NotAcceptable()
65
+
63
66
  if (method.endpoint === null)
64
67
  throw new http.MethodNotAllowed()
65
68
 
69
+ const body = await request.parse()
70
+
66
71
  const reply = await method.endpoint
67
- .call(request.body, request.query, parameters)
72
+ .call(body, request.query, parameters)
68
73
  .catch(rethrow) as Maybe<unknown>
69
74
 
70
75
  if (reply instanceof Error)
@@ -17,17 +17,13 @@ const app = {
17
17
  } as unknown as jest.Mock<Express>
18
18
 
19
19
  export function createRequest (req: Partial<Request> = {}, content: string | Buffer = ''):
20
- jest.MockedObject<Request> {
20
+ jest.MockedObject<IncomingMessage> {
21
21
  const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content)
22
22
  const stream = Readable.from(buffer)
23
23
 
24
24
  Object.assign(stream, { headers: {} }, req)
25
25
 
26
- return stream as unknown as jest.MockedObject<Request>
27
- }
28
-
29
- export function createIncomingMessage (path: string, method: string = 'GET'): IncomingMessage {
30
- return { method, path, headers: {}, body: undefined, query: {} }
26
+ return stream as unknown as jest.MockedObject<IncomingMessage>
31
27
  }
32
28
 
33
29
  export const res = {
@@ -93,43 +93,20 @@ it('should send 501 on unknown method', async () => {
93
93
  expect(res.sendStatus).toHaveBeenCalledWith(501)
94
94
  })
95
95
 
96
- describe('request', () => {
97
- const process = jest.fn(async () => ({})) as unknown as Processing
98
-
99
- beforeEach(() => {
100
- server.attach(process)
101
- })
102
-
103
- it('should pass decoded request', async () => {
104
- const path = generate()
105
- const method = generate()
106
- const headers = { 'content-type': 'application/json' }
107
- const body = { [generate()]: generate() }
108
- const json = JSON.stringify(body)
109
- const req = createRequest({ path, method, headers }, json)
110
-
111
- await use(req)
112
-
113
- expect(process).toHaveBeenCalledWith(expect.objectContaining({ path, method, headers, body }))
114
- })
115
- })
116
-
117
96
  describe('result', () => {
118
97
  it('should send status code 200 if the result has a value', async () => {
119
- const process = async (): Promise<OutgoingMessage> => ({ headers: {}, body: generate() })
120
98
  const req = createRequest()
121
99
 
122
- server.attach(process)
100
+ server.attach(async (): Promise<OutgoingMessage> => ({ headers: {}, body: generate() }))
123
101
  await use(req)
124
102
 
125
103
  expect(res.status).toHaveBeenCalledWith(200)
126
104
  })
127
105
 
128
106
  it('should send status code 204 if the result has no value', async () => {
129
- const process = async (): Promise<OutgoingMessage> => ({ headers: {} })
130
107
  const req = createRequest()
131
108
 
132
- server.attach(process)
109
+ server.attach(async (): Promise<OutgoingMessage> => ({ headers: {} }))
133
110
  await use(req)
134
111
 
135
112
  expect(res.status).toHaveBeenCalledWith(204)
@@ -139,17 +116,16 @@ describe('result', () => {
139
116
  const body = { [generate()]: generate() }
140
117
  const json = JSON.stringify(body)
141
118
  const buf = Buffer.from(json)
142
- const process = async (): Promise<OutgoingMessage> => ({ headers: {}, body })
143
119
  const req = createRequest({ headers: { accept: 'application/json' } })
144
120
 
145
- server.attach(process)
121
+ server.attach(async (): Promise<OutgoingMessage> => ({ headers: {}, body }))
146
122
  await use(req)
147
123
 
148
- expect(res.send).toHaveBeenCalledWith(buf)
124
+ expect(res.end).toHaveBeenCalledWith(buf)
149
125
  })
150
126
 
151
127
  it('should return 500 on exception', async () => {
152
- const process = async (): Promise<OutgoingMessage> => {
128
+ async function process (): Promise<OutgoingMessage> {
153
129
  throw new Error('Bad')
154
130
  }
155
131
 
@@ -170,7 +146,7 @@ describe('result', () => {
170
146
  const message = generate()
171
147
  const req = createRequest()
172
148
 
173
- const process = async (): Promise<OutgoingMessage> => {
149
+ async function process (): Promise<OutgoingMessage> {
174
150
  throw new Error(message)
175
151
  }
176
152
 
@@ -184,7 +160,7 @@ describe('result', () => {
184
160
  const req = createRequest()
185
161
  const message = generate()
186
162
 
187
- const process = async (): Promise<OutgoingMessage> => {
163
+ async function process (): Promise<OutgoingMessage> {
188
164
  throw new BadRequest(message)
189
165
  }
190
166
 
@@ -2,8 +2,11 @@ import express from 'express'
2
2
  import cors from 'cors'
3
3
  import { Connector } from '@toa.io/core'
4
4
  import { promex } from '@toa.io/generic'
5
+ import Negotiator from 'negotiator'
5
6
  import { read, write, type IncomingMessage, type OutgoingMessage } from './messages'
6
7
  import { ClientError, Exception } from './exceptions'
8
+ import { formats, types } from './formats'
9
+ import type { Format } from './formats'
7
10
  import type * as http from 'node:http'
8
11
  import type { Express, Request, Response, NextFunction } from 'express'
9
12
 
@@ -32,8 +35,8 @@ export class Server extends Connector {
32
35
  }
33
36
 
34
37
  public attach (process: Processing): void {
35
- this.app.use((request: Request, response: Response): void => {
36
- this.read(request)
38
+ this.app.use((request: any, response: Response): void => {
39
+ this.extend(request)
37
40
  .then(process)
38
41
  .then(this.success(request, response))
39
42
  .catch(this.fail(request, response))
@@ -62,14 +65,16 @@ export class Server extends Connector {
62
65
  console.info('HTTP Server has been stopped.')
63
66
  }
64
67
 
65
- private async read (request: Request): Promise<IncomingMessage> {
66
- const { method, path, headers, query } = request
67
- const body = await read(request)
68
+ private async extend (request: IncomingMessage): Promise<IncomingMessage> {
69
+ const message = request as unknown as IncomingMessage
68
70
 
69
- return { method, path, headers, query, body }
71
+ message.encoder = negotiate(request)
72
+ message.parse = async <T> (): Promise<T> => await read(request)
73
+
74
+ return message
70
75
  }
71
76
 
72
- private success (request: Request, response: Response) {
77
+ private success (request: IncomingMessage, response: Response) {
73
78
  return (message: OutgoingMessage) => {
74
79
  let status = message.status
75
80
 
@@ -84,31 +89,34 @@ export class Server extends Connector {
84
89
  .set(message.headers)
85
90
 
86
91
  if (message.body !== undefined && message.body !== null)
87
- write(request, response, message.body)
92
+ write(request, response, message)
88
93
  else
89
94
  response.end()
90
95
  }
91
96
  }
92
97
 
93
- private fail (request: Request, response: Response) {
98
+ private fail (request: IncomingMessage, response: Response) {
94
99
  return (exception: Error) => {
95
- let status = 500
100
+ const status = exception instanceof Exception
101
+ ? exception.status
102
+ : 500
96
103
 
97
- if (exception instanceof Exception)
98
- status = exception.status
104
+ response.status(status)
99
105
 
100
106
  const outputAllowed = exception instanceof ClientError || this.debug
101
107
 
102
- response.status(status)
103
-
104
108
  if (outputAllowed) {
105
109
  const body = exception instanceof Exception
106
110
  ? exception.body
107
111
  : (exception.stack ?? exception.message)
108
112
 
109
- write(request, response, body)
113
+ write(request, response, { body })
110
114
  } else
111
115
  response.end()
116
+
117
+ // stop accepting request
118
+ if (!request.complete)
119
+ request.destroy()
112
120
  }
113
121
  }
114
122
  }
@@ -120,6 +128,13 @@ function supportedMethods (methods: Set<string>) {
120
128
  }
121
129
  }
122
130
 
131
+ function negotiate (request: Request): Format | null {
132
+ const negotiator = new Negotiator(request)
133
+ const mediaType = negotiator.mediaType(types)
134
+
135
+ return mediaType === undefined ? null : formats[mediaType]
136
+ }
137
+
123
138
  interface Properties {
124
139
  methods: Set<string>
125
140
  debug: boolean
@@ -127,7 +142,7 @@ interface Properties {
127
142
 
128
143
  function defaults (): Properties {
129
144
  return {
130
- methods: new Set<string>(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']),
145
+ methods: new Set<string>(['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']),
131
146
  debug: false
132
147
  }
133
148
  }
@@ -1,5 +1,3 @@
1
- import { types } from './formats'
2
-
3
1
  export class Exception extends Error {
4
2
  public readonly status: number
5
3
  public readonly body?: any
@@ -50,21 +48,13 @@ export class MethodNotAllowed extends ClientError {
50
48
  }
51
49
  }
52
50
 
53
- class MediaTypeException extends ClientError {
54
- private static readonly message = 'Supported media types:\n- ' + types.join('\n- ')
55
-
56
- protected constructor (status: number) {
57
- super(status, MediaTypeException.message)
58
- }
59
- }
60
-
61
- export class NotAcceptable extends MediaTypeException {
51
+ export class NotAcceptable extends ClientError {
62
52
  public constructor () {
63
53
  super(406)
64
54
  }
65
55
  }
66
56
 
67
- export class UnsupportedMediaType extends MediaTypeException {
57
+ export class UnsupportedMediaType extends ClientError {
68
58
  public constructor () {
69
59
  super(415)
70
60
  }
@@ -5,15 +5,18 @@ import * as msgpack from './msgpack'
5
5
  import * as text from './text'
6
6
 
7
7
  export const formats: Record<string, Format> = {
8
- 'application/yaml': yaml,
9
- 'application/msgpack': msgpack,
10
- 'application/json': json,
11
- 'text/plain': text
8
+ [msgpack.type]: msgpack,
9
+ [yaml.type]: yaml,
10
+ [json.type]: json,
11
+ [text.type]: text
12
12
  }
13
13
 
14
14
  export const types = Object.keys(formats)
15
15
 
16
16
  export interface Format {
17
+ readonly type: string
18
+ readonly multipart: string
19
+
17
20
  encode: (value: any) => Buffer
18
21
  decode: (buffer: Buffer) => any
19
22
  }
@@ -1,5 +1,8 @@
1
1
  import { Buffer } from 'node:buffer'
2
2
 
3
+ export const type = 'application/json'
4
+ export const multipart = 'multipart/json'
5
+
3
6
  export function decode (buffer: Buffer): any {
4
7
  const text = buffer.toString()
5
8
 
@@ -1,6 +1,9 @@
1
1
  import { type Buffer } from 'node:buffer'
2
2
  import * as msgpack from 'msgpackr'
3
3
 
4
+ export const type = 'application/msgpack'
5
+ export const multipart = 'multipart/msgpack'
6
+
4
7
  export function decode (buffer: Buffer): any {
5
8
  return msgpack.decode(buffer)
6
9
  }
@@ -1,5 +1,8 @@
1
1
  import { Buffer } from 'node:buffer'
2
2
 
3
+ export const type = 'text/plain'
4
+ export const multipart = 'multipart/text'
5
+
3
6
  export function decode (buffer: Buffer): any {
4
7
  return buffer.toString()
5
8
  }
@@ -1,6 +1,9 @@
1
1
  import { Buffer } from 'node:buffer'
2
2
  import * as yaml from 'js-yaml'
3
3
 
4
+ export const type = 'application/yaml'
5
+ export const multipart = 'multipart/yaml'
6
+
4
7
  export function decode (buffer: Buffer): any {
5
8
  const text = buffer.toString()
6
9
 
@@ -1,9 +1,8 @@
1
- import { Buffer } from 'node:buffer'
2
1
  import { generate } from 'randomstring'
3
2
  import * as msgpack from 'msgpackr'
4
- import { type OutgoingMessage, read, write } from './messages'
5
- import { createRequest, res } from './Server.fixtures'
6
- import { BadRequest, NotAcceptable, UnsupportedMediaType } from './exceptions'
3
+ import { read } from './messages'
4
+ import { createRequest } from './Server.fixtures'
5
+ import { BadRequest, UnsupportedMediaType } from './exceptions'
7
6
 
8
7
  beforeEach(() => {
9
8
  jest.clearAllMocks()
@@ -69,48 +68,3 @@ describe('read', () => {
69
68
  await expect(read(request)).rejects.toThrow(BadRequest)
70
69
  })
71
70
  })
72
-
73
- describe('write', () => {
74
- it('should write encoded response', async () => {
75
- const value = { [generate()]: generate() }
76
- const json = JSON.stringify(value)
77
- const buf = Buffer.from(json)
78
- const headers = { accept: 'application/json' }
79
- const request = createRequest({ headers }, buf)
80
-
81
- write(request, res, value)
82
-
83
- expect(res.set).toHaveBeenCalledWith('content-type', 'application/json')
84
- expect(res.send).toHaveBeenCalledWith(buf)
85
- })
86
-
87
- it('should throw on unsupported response media type', async () => {
88
- const headers = { accept: 'wtf/' + generate() }
89
- const request = createRequest({ headers })
90
- const value = generate()
91
-
92
- expect(() => {
93
- write(request, res, value)
94
- }).toThrow(NotAcceptable)
95
- })
96
-
97
- it('should use application/yaml as default', async () => {
98
- const request = createRequest()
99
- const message: OutgoingMessage = { headers: {}, body: 'hello' }
100
-
101
- write(request, res, message)
102
-
103
- expect(res.set).toHaveBeenCalledWith('content-type', 'application/yaml')
104
- expect(res.send).toHaveBeenCalled()
105
- })
106
-
107
- it('should negotiate', async () => {
108
- const headers = { accept: 'text/html, application/*;q=0.2, image/jpeg;q=0.8' }
109
- const request = createRequest({ headers })
110
- const message: OutgoingMessage = { headers: {}, body: 'hello' }
111
-
112
- write(request, res, message)
113
-
114
- expect(res.set).toHaveBeenCalledWith('content-type', 'application/yaml')
115
- })
116
- })
@@ -1,23 +1,28 @@
1
1
  import { type IncomingHttpHeaders, type OutgoingHttpHeaders } from 'node:http'
2
2
  import { Readable } from 'node:stream'
3
3
  import { type Request, type Response } from 'express'
4
- import Negotiator from 'negotiator'
5
- import { buffer } from '@toa.io/generic'
6
- import { map } from '@toa.io/streams'
7
- import { formats, types } from './formats'
4
+ import { buffer } from '@toa.io/streams'
5
+ import { formats } from './formats'
8
6
  import { BadRequest, NotAcceptable, UnsupportedMediaType } from './exceptions'
9
-
10
- export function write (request: Request, response: Response, body: any): void {
11
- if (body instanceof Readable) void pipe(body, request, response)
12
- else send(body, request, response)
7
+ import type { ParsedQs } from 'qs'
8
+ import type { Format } from './formats'
9
+
10
+ export function write
11
+ (request: IncomingMessage, response: Response, message: OutgoingMessage): void {
12
+ if (message.body instanceof Readable)
13
+ stream(message, request, response)
14
+ else
15
+ send(message, request, response)
13
16
  }
14
17
 
15
18
  export async function read (request: Request): Promise<any> {
16
19
  const type = request.headers['content-type']
17
20
 
18
- if (type === undefined) return undefined
21
+ if (type === undefined)
22
+ return undefined
19
23
 
20
- if (!(type in formats)) throw new UnsupportedMediaType()
24
+ if (!(type in formats))
25
+ throw new UnsupportedMediaType()
21
26
 
22
27
  const format = formats[type]
23
28
 
@@ -30,48 +35,68 @@ export async function read (request: Request): Promise<any> {
30
35
  }
31
36
  }
32
37
 
33
- function send (body: any, request: Request, response: Response): void {
34
- if (body === undefined || body?.length === 0) {
38
+ function send (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
39
+ if (message.body === undefined) {
35
40
  response.end()
36
41
 
37
42
  return
38
43
  }
39
44
 
40
- const type = negotiate(request)
41
- const format = formats[type]
42
- const buf = format.encode(body)
45
+ if (request.encoder === null)
46
+ throw new NotAcceptable()
47
+
48
+ const buf = request.encoder.encode(message.body)
43
49
 
44
- // content-length and etag are set by Express
45
- response.set('content-type', type)
46
- response.send(buf)
50
+ response.set('content-type', request.encoder.type)
51
+ response.end(buf)
47
52
  }
48
53
 
49
- async function pipe (stream: Readable, request: Request, response: Response): Promise<void> {
50
- const type = negotiate(request)
51
- const format = formats[type]
54
+ function stream
55
+ (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
56
+ const encoded = message.headers !== undefined && 'content-type' in message.headers
57
+
58
+ if (encoded)
59
+ pipe(message, response)
60
+ else
61
+ multipart(message, request, response)
52
62
 
53
- response.set('content-type', type)
54
- map(stream, format.encode).pipe(response)
63
+ message.body.on('error', (e: Error) => {
64
+ console.error(e)
65
+ response.end()
66
+ })
55
67
  }
56
68
 
57
- function negotiate (request: Request): string {
58
- const negotiator = new Negotiator(request)
59
- const mediaType = negotiator.mediaType(types)
69
+ function pipe (message: OutgoingMessage, response: Response): void {
70
+ response.set(message.headers)
71
+ message.body.pipe(response)
72
+ }
60
73
 
61
- if (mediaType === undefined) throw new NotAcceptable()
74
+ function multipart (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
75
+ if (request.encoder === null)
76
+ throw new NotAcceptable()
62
77
 
63
- return mediaType
64
- }
78
+ const encoder = request.encoder
65
79
 
66
- interface Message {
67
- body: any
80
+ response.set('content-type', `${encoder.multipart}; boundary=${BOUNDARY}`)
81
+
82
+ message.body
83
+ .map((part: unknown) => Buffer.concat([CUT, encoder.encode(part)]))
84
+ .on('end', () => response.end(FINALCUT))
85
+ .pipe(response)
68
86
  }
69
87
 
70
- export interface IncomingMessage extends Message {
88
+ const BOUNDARY = 'cut'
89
+ const CUT = Buffer.from(`--${BOUNDARY}\r\n`)
90
+ const FINALCUT = Buffer.from(`--${BOUNDARY}--`)
91
+
92
+ export interface IncomingMessage extends Request {
71
93
  method: string
72
94
  path: string
95
+ url: string
73
96
  headers: IncomingHttpHeaders
74
97
  query: Query
98
+ parse: <T> () => Promise<T>
99
+ encoder: Format | null
75
100
  }
76
101
 
77
102
  export interface OutgoingMessage {
@@ -80,7 +105,7 @@ export interface OutgoingMessage {
80
105
  body?: any
81
106
  }
82
107
 
83
- export interface Query {
108
+ export interface Query extends ParsedQs {
84
109
  id?: string
85
110
  criteria?: string
86
111
  sort?: string
@@ -25,7 +25,7 @@ export class Route {
25
25
  if (segment.fragment !== null && segment.fragment !== fragments[i])
26
26
  return null
27
27
 
28
- if (segment.fragment === null)
28
+ if (segment.fragment === null && segment.placeholder !== null)
29
29
  parameters.push({ name: segment.placeholder, value: fragments[i] })
30
30
  }
31
31
 
@@ -14,6 +14,7 @@ export function fragment (path: string): string[] {
14
14
 
15
15
  function parse (segment: string): Segment {
16
16
  if (segment[0] === ':') return { fragment: null, placeholder: segment.substring(1) }
17
+ else if (segment === '*') return { fragment: null, placeholder: null }
17
18
  else return { fragment: segment }
18
19
  }
19
20
 
@@ -21,5 +22,5 @@ export type Segment = {
21
22
  fragment: string
22
23
  } | {
23
24
  fragment: null
24
- placeholder: string
25
+ placeholder: string | null
25
26
  }