@toa.io/extensions.exposition 0.22.1 → 1.0.0-alpha.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.
- package/components/identity.basic/source/authenticate.ts +3 -2
- package/components/identity.basic/source/transit.ts +4 -3
- package/components/octets.storage/manifest.toa.yaml +26 -0
- package/components/octets.storage/operations/delete.js +7 -0
- package/components/octets.storage/operations/fetch.js +46 -0
- package/components/octets.storage/operations/get.js +7 -0
- package/components/octets.storage/operations/list.js +7 -0
- package/components/octets.storage/operations/permute.js +7 -0
- package/components/octets.storage/operations/store.js +11 -0
- package/cucumber.js +0 -1
- package/documentation/access.md +2 -3
- package/documentation/cache.md +42 -0
- package/documentation/octets.md +196 -0
- package/documentation/protocol.md +49 -5
- package/documentation/tree.md +1 -5
- package/features/access.feature +1 -0
- package/features/cache.feature +160 -0
- package/features/errors.feature +18 -0
- package/features/identity.basic.feature +2 -0
- package/features/octets.feature +295 -0
- package/features/octets.workflows.feature +114 -0
- package/features/routes.feature +40 -0
- package/features/steps/HTTP.ts +56 -6
- package/features/steps/Parameters.ts +5 -2
- package/features/steps/Workspace.ts +8 -5
- package/features/steps/components/octets.tester/manifest.toa.yaml +15 -0
- package/features/steps/components/octets.tester/operations/bar.js +12 -0
- package/features/steps/components/octets.tester/operations/baz.js +11 -0
- package/features/steps/components/octets.tester/operations/diversify.js +14 -0
- package/features/steps/components/octets.tester/operations/err.js +16 -0
- package/features/steps/components/octets.tester/operations/foo.js +7 -0
- package/features/steps/components/octets.tester/operations/lenna.png +0 -0
- package/features/steps/components/pots/manifest.toa.yaml +1 -1
- package/features/streams.feature +5 -1
- package/package.json +16 -9
- package/readme.md +8 -5
- package/schemas/octets/context.cos.yaml +1 -0
- package/schemas/octets/delete.cos.yaml +1 -0
- package/schemas/octets/fetch.cos.yaml +3 -0
- package/schemas/octets/list.cos.yaml +1 -0
- package/schemas/octets/permute.cos.yaml +1 -0
- package/schemas/octets/store.cos.yaml +3 -0
- package/source/Gateway.ts +9 -4
- package/source/HTTP/Server.fixtures.ts +2 -6
- package/source/HTTP/Server.test.ts +9 -31
- package/source/HTTP/Server.ts +33 -19
- package/source/HTTP/exceptions.ts +2 -12
- package/source/HTTP/formats/index.ts +7 -4
- package/source/HTTP/formats/json.ts +3 -0
- package/source/HTTP/formats/msgpack.ts +3 -0
- package/source/HTTP/formats/text.ts +3 -0
- package/source/HTTP/formats/yaml.ts +3 -0
- package/source/HTTP/messages.test.ts +3 -49
- package/source/HTTP/messages.ts +60 -35
- package/source/RTD/Route.ts +1 -1
- package/source/RTD/segment.ts +2 -1
- package/source/RTD/syntax/parse.ts +2 -1
- package/source/Remotes.ts +8 -0
- package/source/Tenant.ts +5 -0
- package/source/directives/auth/Family.ts +26 -22
- package/source/directives/auth/Rule.ts +1 -1
- package/source/directives/cache/Control.ts +59 -0
- package/source/directives/cache/Exact.ts +7 -0
- package/source/directives/cache/Family.ts +36 -0
- package/source/directives/cache/index.ts +3 -0
- package/source/directives/cache/types.ts +9 -0
- package/source/directives/index.ts +3 -1
- package/source/directives/octets/Context.ts +18 -0
- package/source/directives/octets/Delete.ts +32 -0
- package/source/directives/octets/Family.ts +68 -0
- package/source/directives/octets/Fetch.ts +85 -0
- package/source/directives/octets/List.ts +32 -0
- package/source/directives/octets/Permute.ts +37 -0
- package/source/directives/octets/Store.ts +158 -0
- package/source/directives/octets/index.ts +3 -0
- package/source/directives/octets/schemas.ts +12 -0
- package/source/directives/octets/types.ts +13 -0
package/source/HTTP/messages.ts
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
|
-
import { type IncomingHttpHeaders
|
|
1
|
+
import { type IncomingHttpHeaders } from 'node:http'
|
|
2
2
|
import { Readable } from 'node:stream'
|
|
3
3
|
import { type Request, type Response } from 'express'
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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)
|
|
21
|
+
if (type === undefined)
|
|
22
|
+
return undefined
|
|
19
23
|
|
|
20
|
-
if (!(type in formats))
|
|
24
|
+
if (!(type in formats))
|
|
25
|
+
throw new UnsupportedMediaType()
|
|
21
26
|
|
|
22
27
|
const format = formats[type]
|
|
23
28
|
|
|
@@ -30,57 +35,77 @@ export async function read (request: Request): Promise<any> {
|
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
function send (
|
|
34
|
-
if (body === undefined
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
if (request.encoder === null)
|
|
46
|
+
throw new NotAcceptable()
|
|
47
|
+
|
|
48
|
+
const buf = request.encoder.encode(message.body)
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
response.
|
|
46
|
-
response.send(buf)
|
|
50
|
+
response.set('content-type', request.encoder.type)
|
|
51
|
+
response.end(buf)
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
54
|
+
function stream
|
|
55
|
+
(message: OutgoingMessage, request: IncomingMessage, response: Response): void {
|
|
56
|
+
const encoded = message.headers !== undefined && message.headers.has('content-type')
|
|
57
|
+
|
|
58
|
+
if (encoded)
|
|
59
|
+
pipe(message, response)
|
|
60
|
+
else
|
|
61
|
+
multipart(message, request, response)
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
message.body.on('error', (e: Error) => {
|
|
64
|
+
console.error(e)
|
|
65
|
+
response.end()
|
|
66
|
+
})
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
function pipe (message: OutgoingMessage, response: Response): void {
|
|
70
|
+
message.headers?.forEach((value, key) => response.set(key, value))
|
|
71
|
+
message.body.pipe(response)
|
|
72
|
+
}
|
|
60
73
|
|
|
61
|
-
|
|
74
|
+
function multipart (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
|
|
75
|
+
if (request.encoder === null)
|
|
76
|
+
throw new NotAcceptable()
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
}
|
|
78
|
+
const encoder = request.encoder
|
|
65
79
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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 {
|
|
78
103
|
status?: number
|
|
79
|
-
headers?:
|
|
104
|
+
headers?: Headers
|
|
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
|
package/source/RTD/Route.ts
CHANGED
|
@@ -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
|
|
package/source/RTD/segment.ts
CHANGED
|
@@ -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
|
}
|
package/source/Remotes.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type Bootloader } from './Factory'
|
|
|
3
3
|
|
|
4
4
|
export class Remotes extends Connector {
|
|
5
5
|
private readonly boot: Bootloader
|
|
6
|
+
private readonly remotes: Record<string, Promise<Component>> = {}
|
|
6
7
|
|
|
7
8
|
public constructor (boot: Bootloader) {
|
|
8
9
|
super()
|
|
@@ -11,6 +12,13 @@ export class Remotes extends Connector {
|
|
|
11
12
|
|
|
12
13
|
public async discover (namespace: string, name: string): Promise<Component> {
|
|
13
14
|
const locator = new Locator(name, namespace)
|
|
15
|
+
|
|
16
|
+
this.remotes[locator.id] ??= this.create(locator)
|
|
17
|
+
|
|
18
|
+
return await this.remotes[locator.id]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private async create (locator: Locator): Promise<Component> {
|
|
14
22
|
const remote = await this.boot.remote(locator)
|
|
15
23
|
|
|
16
24
|
this.depends(remote)
|
package/source/Tenant.ts
CHANGED
|
@@ -30,6 +30,11 @@ export class Tenant extends Connector {
|
|
|
30
30
|
`'${this.branch.namespace}.${this.branch.component}' has started.`)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
public override async dispose (): Promise<void> {
|
|
34
|
+
console.info('Exposition Tenant for ' +
|
|
35
|
+
`'${this.branch.namespace}.${this.branch.component}' has been stopped.`)
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
private async expose (): Promise<void> {
|
|
34
39
|
await this.broadcast.transmit('expose', this.branch)
|
|
35
40
|
}
|
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type Parameter } from '../../RTD'
|
|
3
|
-
import { type Family, type Output } from '../../Directive'
|
|
4
|
-
import { type Remotes } from '../../Remotes'
|
|
1
|
+
import { match } from 'matchacho'
|
|
5
2
|
import * as http from '../../HTTP'
|
|
6
|
-
import {
|
|
7
|
-
type AuthenticationResult,
|
|
8
|
-
type Ban,
|
|
9
|
-
type Directive,
|
|
10
|
-
type Discovery,
|
|
11
|
-
type Extension,
|
|
12
|
-
type Identity,
|
|
13
|
-
type Input, type Remote,
|
|
14
|
-
type Schemes
|
|
15
|
-
} from './types'
|
|
16
3
|
import { Anonymous } from './Anonymous'
|
|
17
4
|
import { Id } from './Id'
|
|
18
5
|
import { Role } from './Role'
|
|
19
6
|
import { Rule } from './Rule'
|
|
20
7
|
import { Incept } from './Incept'
|
|
8
|
+
import { Echo } from './Echo'
|
|
21
9
|
import { split } from './split'
|
|
22
|
-
import { PRIMARY, PROVIDERS } from './schemes'
|
|
23
10
|
import { Scheme } from './Scheme'
|
|
24
|
-
import {
|
|
11
|
+
import { PRIMARY, PROVIDERS } from './schemes'
|
|
12
|
+
import type { Component } from '@toa.io/core'
|
|
13
|
+
import type { Remotes } from '../../Remotes'
|
|
14
|
+
import type { Family, Output } from '../../Directive'
|
|
15
|
+
import type { Parameter } from '../../RTD'
|
|
16
|
+
import type {
|
|
17
|
+
AuthenticationResult,
|
|
18
|
+
Ban,
|
|
19
|
+
Directive,
|
|
20
|
+
Discovery,
|
|
21
|
+
Extension,
|
|
22
|
+
Identity,
|
|
23
|
+
Input,
|
|
24
|
+
Remote,
|
|
25
|
+
Schemes
|
|
26
|
+
} from './types'
|
|
25
27
|
|
|
26
28
|
class Authorization implements Family<Directive, Extension> {
|
|
27
29
|
public readonly name: string = 'auth'
|
|
28
30
|
public readonly mandatory: boolean = true
|
|
31
|
+
|
|
29
32
|
private readonly schemes = {} as unknown as Schemes
|
|
30
33
|
private readonly discovery = {} as unknown as Discovery
|
|
31
34
|
private tokens: Component | null = null
|
|
@@ -40,10 +43,11 @@ class Authorization implements Family<Directive, Extension> {
|
|
|
40
43
|
for (const name of REMOTES)
|
|
41
44
|
this.discovery[name] ??= remotes.discover('identity', name)
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
return match(Class,
|
|
47
|
+
Role, () => new Role(value, this.discovery.roles),
|
|
48
|
+
Rule, () => new Rule(value, this.create.bind(this)),
|
|
49
|
+
Incept, () => new Incept(value, this.discovery),
|
|
50
|
+
() => new Class(value))
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
public async preflight
|
|
@@ -86,9 +90,9 @@ class Authorization implements Family<Directive, Extension> {
|
|
|
86
90
|
const authorization = `Token ${token}`
|
|
87
91
|
|
|
88
92
|
if (response.headers === undefined)
|
|
89
|
-
response.headers =
|
|
93
|
+
response.headers = new Headers()
|
|
90
94
|
|
|
91
|
-
response.headers.authorization
|
|
95
|
+
response.headers.set('authorization', authorization)
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
private async resolve (authorization: string | undefined): Promise<Identity | null> {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { match } from 'matchacho'
|
|
2
|
+
import type { AuthenticatedRequest, Directive } from './types'
|
|
3
|
+
|
|
4
|
+
export class Control implements Directive {
|
|
5
|
+
protected readonly value: string
|
|
6
|
+
private cache: string | null = null
|
|
7
|
+
|
|
8
|
+
public constructor (value: string) {
|
|
9
|
+
this.value = value
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public set (request: AuthenticatedRequest, headers: Headers): void {
|
|
13
|
+
if (!['GET', 'HEAD', 'OPTIONS'].includes(request.method))
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
this.cache ??= this.resolve(request)
|
|
17
|
+
|
|
18
|
+
headers.set('cache-control', this.cache)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
protected resolve (request: AuthenticatedRequest): string {
|
|
22
|
+
if (request.identity === null)
|
|
23
|
+
return this.value
|
|
24
|
+
|
|
25
|
+
const directives = this.mask()
|
|
26
|
+
|
|
27
|
+
if ((directives & (PUBLIC | NO_CACHE)) === PUBLIC)
|
|
28
|
+
return 'no-cache, ' + this.value
|
|
29
|
+
|
|
30
|
+
if ((directives & (PUBLIC | PRIVATE)) === 0)
|
|
31
|
+
return 'private, ' + this.value
|
|
32
|
+
|
|
33
|
+
return this.value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private mask (): number {
|
|
37
|
+
const directives = this.value.match(DIRECTIVES_RX)
|
|
38
|
+
|
|
39
|
+
if (directives === null)
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
let mask = 0
|
|
43
|
+
|
|
44
|
+
for (const directive of directives)
|
|
45
|
+
mask |= match<number>(directive,
|
|
46
|
+
'private', PRIVATE,
|
|
47
|
+
'public', PUBLIC,
|
|
48
|
+
'no-cache', NO_CACHE,
|
|
49
|
+
0)
|
|
50
|
+
|
|
51
|
+
return mask
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DIRECTIVES_RX = /\b(private|public|no-cache)\b/ig
|
|
56
|
+
|
|
57
|
+
const PUBLIC = 1
|
|
58
|
+
const PRIVATE = 2
|
|
59
|
+
const NO_CACHE = 4
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type Input, type Output, type Family } from '../../Directive'
|
|
2
|
+
import { Control } from './Control'
|
|
3
|
+
import { type Directive } from './types'
|
|
4
|
+
import { Exact } from './Exact'
|
|
5
|
+
import type * as http from '../../HTTP'
|
|
6
|
+
|
|
7
|
+
class Cache implements Family<Directive> {
|
|
8
|
+
public readonly name: string = 'cache'
|
|
9
|
+
public readonly mandatory: boolean = false
|
|
10
|
+
|
|
11
|
+
public create (name: string, value: any): Directive {
|
|
12
|
+
const Class = constructors[name]
|
|
13
|
+
|
|
14
|
+
if (Class === undefined)
|
|
15
|
+
throw new Error(`Directive '${name}' is not provided by the '${this.name}' family.`)
|
|
16
|
+
|
|
17
|
+
return new Class(value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public preflight (): Output {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async settle
|
|
25
|
+
(directives: Directive[], request: Input, response: http.OutgoingMessage): Promise<void> {
|
|
26
|
+
response.headers ??= new Headers()
|
|
27
|
+
directives[0]?.set(request, response.headers)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const constructors: Record<string, new (value: any) => Directive> = {
|
|
32
|
+
control: Control,
|
|
33
|
+
exact: Exact
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export = new Cache()
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Family } from '../Directive'
|
|
2
2
|
import Dev from './dev'
|
|
3
3
|
import Auth from './auth'
|
|
4
|
+
import Cache from './cache'
|
|
5
|
+
import Octets from './octets'
|
|
4
6
|
|
|
5
|
-
export const families: Family[] = [Dev,
|
|
7
|
+
export const families: Family[] = [Auth, Octets, Dev, Cache]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as schemas from './schemas'
|
|
2
|
+
import type { Output } from '../../Directive'
|
|
3
|
+
import type { Directive } from './types'
|
|
4
|
+
|
|
5
|
+
export class Context implements Directive {
|
|
6
|
+
public readonly targeted = false
|
|
7
|
+
public readonly storage: string
|
|
8
|
+
|
|
9
|
+
public constructor (value: any) {
|
|
10
|
+
schemas.context.validate(value)
|
|
11
|
+
|
|
12
|
+
this.storage = value
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public apply (): Output {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NotFound } from '../../HTTP'
|
|
2
|
+
import * as schemas from './schemas'
|
|
3
|
+
import type { Maybe } from '@toa.io/types'
|
|
4
|
+
import type { Component } from '@toa.io/core'
|
|
5
|
+
import type { Output } from '../../Directive'
|
|
6
|
+
|
|
7
|
+
import type { Directive, Input } from './types'
|
|
8
|
+
|
|
9
|
+
export class Delete implements Directive {
|
|
10
|
+
public readonly targeted = true
|
|
11
|
+
|
|
12
|
+
private readonly discovery: Promise<Component>
|
|
13
|
+
private storage: Component | null = null
|
|
14
|
+
|
|
15
|
+
public constructor (value: null, discovery: Promise<Component>) {
|
|
16
|
+
schemas.remove.validate(value)
|
|
17
|
+
|
|
18
|
+
this.discovery = discovery
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async apply (storage: string, request: Input): Promise<Output> {
|
|
22
|
+
this.storage ??= await this.discovery
|
|
23
|
+
|
|
24
|
+
const input = { storage, path: request.path }
|
|
25
|
+
const error = await this.storage.invoke<Maybe<unknown>>('delete', { input })
|
|
26
|
+
|
|
27
|
+
if (error instanceof Error)
|
|
28
|
+
throw new NotFound()
|
|
29
|
+
|
|
30
|
+
return {}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { NotFound } from '../../HTTP'
|
|
2
|
+
import { Context } from './Context'
|
|
3
|
+
import { Store } from './Store'
|
|
4
|
+
import { Fetch } from './Fetch'
|
|
5
|
+
import { List } from './List'
|
|
6
|
+
import { Delete } from './Delete'
|
|
7
|
+
import { Permute } from './Permute'
|
|
8
|
+
import type { Component } from '@toa.io/core'
|
|
9
|
+
import type { Remotes } from '../../Remotes'
|
|
10
|
+
import type { Output, Family } from '../../Directive'
|
|
11
|
+
import type { Directive, Input } from './types'
|
|
12
|
+
|
|
13
|
+
class Octets implements Family<Directive> {
|
|
14
|
+
public readonly name: string = 'octets'
|
|
15
|
+
public readonly mandatory: boolean = false
|
|
16
|
+
|
|
17
|
+
private discovery = null as unknown as Promise<Component>
|
|
18
|
+
|
|
19
|
+
public create (name: string, value: any, remotes: Remotes): Directive {
|
|
20
|
+
const Class = DIRECTIVES[name]
|
|
21
|
+
|
|
22
|
+
if (Class === undefined)
|
|
23
|
+
throw new Error(`Directive '${name}' is not provided by the '${this.name}' family.`)
|
|
24
|
+
|
|
25
|
+
this.discovery ??= remotes.discover('octets', 'storage')
|
|
26
|
+
|
|
27
|
+
return new Class(value, this.discovery, remotes)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async preflight (directives: Directive[], input: Input): Promise<Output> {
|
|
31
|
+
let context: Context | null = null
|
|
32
|
+
let action: Directive | null = null
|
|
33
|
+
|
|
34
|
+
for (const directive of directives)
|
|
35
|
+
if (directive instanceof Context)
|
|
36
|
+
context ??= directive
|
|
37
|
+
else if (action === null)
|
|
38
|
+
action = directive
|
|
39
|
+
else
|
|
40
|
+
throw new Error('Octets action is umbiguous.')
|
|
41
|
+
|
|
42
|
+
if (action === null)
|
|
43
|
+
return null
|
|
44
|
+
|
|
45
|
+
if (context === null)
|
|
46
|
+
throw new Error('Octets context is not defined.')
|
|
47
|
+
|
|
48
|
+
const targeted = input.path[input.path.length - 1] !== '/'
|
|
49
|
+
|
|
50
|
+
if (targeted !== action.targeted)
|
|
51
|
+
throw new NotFound(`Trailing slash is ${action.targeted ? 'redundant' : 'required'}.`)
|
|
52
|
+
|
|
53
|
+
return await action.apply(context.storage, input)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DIRECTIVES: Record<string, Constructor> = {
|
|
58
|
+
context: Context,
|
|
59
|
+
store: Store,
|
|
60
|
+
fetch: Fetch,
|
|
61
|
+
list: List,
|
|
62
|
+
delete: Delete,
|
|
63
|
+
permute: Permute
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Constructor = new (value: any, discovery: Promise<Component>, remotes: Remotes) => Directive
|
|
67
|
+
|
|
68
|
+
export = new Octets()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { posix } from 'node:path'
|
|
2
|
+
import { Forbidden, NotFound } from '../../HTTP'
|
|
3
|
+
import * as schemas from './schemas'
|
|
4
|
+
import type { Maybe } from '@toa.io/types'
|
|
5
|
+
import type { Entry } from '@toa.io/extensions.storages'
|
|
6
|
+
import type { Readable } from 'node:stream'
|
|
7
|
+
import type { Component } from '@toa.io/core'
|
|
8
|
+
import type { Output } from '../../Directive'
|
|
9
|
+
|
|
10
|
+
import type { Directive, Input } from './types'
|
|
11
|
+
|
|
12
|
+
export class Fetch implements Directive {
|
|
13
|
+
public readonly targeted = true
|
|
14
|
+
|
|
15
|
+
private readonly permissions: Permissions = { blob: true, meta: false }
|
|
16
|
+
private readonly discovery: Promise<Component>
|
|
17
|
+
private storage: Component = null as unknown as Component
|
|
18
|
+
|
|
19
|
+
public constructor (permissions: Partial<Permissions> | null, discovery: Promise<Component>) {
|
|
20
|
+
schemas.fetch.validate(permissions)
|
|
21
|
+
|
|
22
|
+
Object.assign(this.permissions, permissions)
|
|
23
|
+
this.discovery = discovery
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async apply (storage: string, request: Input): Promise<Output> {
|
|
27
|
+
this.storage ??= await this.discovery
|
|
28
|
+
|
|
29
|
+
if (request.url.slice(-5) === ':meta')
|
|
30
|
+
return await this.get(storage, request)
|
|
31
|
+
else
|
|
32
|
+
return await this.fetch(storage, request)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async fetch (storage: string, request: Input): Promise<Output> {
|
|
36
|
+
const filename = posix.basename(request.url)
|
|
37
|
+
const variant = filename.includes('.')
|
|
38
|
+
|
|
39
|
+
if (!variant && !this.permissions.blob)
|
|
40
|
+
throw new Forbidden('BLOB variant must be specified.')
|
|
41
|
+
|
|
42
|
+
if ('if-none-match' in request.headers)
|
|
43
|
+
return { status: 304 }
|
|
44
|
+
|
|
45
|
+
const input = { storage, path: request.url }
|
|
46
|
+
const result = await this.storage.invoke<Maybe<FetchResult>>('fetch', { input })
|
|
47
|
+
|
|
48
|
+
if (result instanceof Error)
|
|
49
|
+
throw new NotFound()
|
|
50
|
+
|
|
51
|
+
const headers = new Headers({
|
|
52
|
+
'content-type': result.type,
|
|
53
|
+
'content-length': result.size.toString(),
|
|
54
|
+
etag: result.checksum
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return { headers, body: result.stream }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private async get (storage: string, request: Input): Promise<Output> {
|
|
61
|
+
if (!this.permissions.meta)
|
|
62
|
+
throw new Forbidden('Metadata is not accessible.')
|
|
63
|
+
|
|
64
|
+
const path = request.url.slice(0, -5)
|
|
65
|
+
const input = { storage, path }
|
|
66
|
+
const entry = await this.storage.invoke<Maybe<Entry>>('get', { input })
|
|
67
|
+
|
|
68
|
+
if (entry instanceof Error)
|
|
69
|
+
throw new NotFound()
|
|
70
|
+
|
|
71
|
+
return { body: entry }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface Permissions {
|
|
76
|
+
blob: boolean
|
|
77
|
+
meta: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface FetchResult {
|
|
81
|
+
stream: Readable
|
|
82
|
+
checksum: string
|
|
83
|
+
size: number
|
|
84
|
+
type: string
|
|
85
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NotFound } from '../../HTTP'
|
|
2
|
+
import * as schemas from './schemas'
|
|
3
|
+
import type { Maybe } from '@toa.io/types'
|
|
4
|
+
import type { Component } from '@toa.io/core'
|
|
5
|
+
import type { Output } from '../../Directive'
|
|
6
|
+
|
|
7
|
+
import type { Directive, Input } from './types'
|
|
8
|
+
|
|
9
|
+
export class List implements Directive {
|
|
10
|
+
public readonly targeted = false
|
|
11
|
+
|
|
12
|
+
private readonly discovery: Promise<Component>
|
|
13
|
+
private storage: Component | null = null
|
|
14
|
+
|
|
15
|
+
public constructor (value: null, discovery: Promise<Component>) {
|
|
16
|
+
schemas.remove.validate(value)
|
|
17
|
+
|
|
18
|
+
this.discovery = discovery
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async apply (storage: string, request: Input): Promise<Output> {
|
|
22
|
+
this.storage ??= await this.discovery
|
|
23
|
+
|
|
24
|
+
const input = { storage, path: request.path }
|
|
25
|
+
const list = await this.storage.invoke<Maybe<unknown>>('list', { input })
|
|
26
|
+
|
|
27
|
+
if (list instanceof Error)
|
|
28
|
+
throw new NotFound()
|
|
29
|
+
|
|
30
|
+
return { body: list }
|
|
31
|
+
}
|
|
32
|
+
}
|