@toa.io/extensions.exposition 0.24.0-alpha.21 → 0.24.0-alpha.23
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/context.toa.yaml +1 -1
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/components/octets.storage/manifest.toa.yaml +1 -0
- package/components/octets.storage/operations/store.js +2 -2
- package/documentation/octets.md +89 -37
- package/features/cors.feature +33 -0
- package/features/octets.entries.feature +121 -0
- package/features/octets.feature +1 -27
- package/features/octets.meta.feature +65 -0
- package/features/octets.workflows.feature +105 -4
- package/features/routes.feature +37 -0
- package/features/steps/Captures.ts +3 -2
- package/features/steps/Components.ts +15 -8
- package/features/steps/HTTP.ts +1 -1
- package/features/steps/Parameters.ts +1 -1
- package/features/steps/components/octets.tester/manifest.toa.yaml +1 -0
- package/features/steps/components/octets.tester/operations/echo.js +7 -0
- package/features/steps/components/users/manifest.toa.yaml +3 -0
- package/features/steps/components/users.properties/manifest.toa.yaml +13 -0
- package/features/steps/tsconfig.json +1 -1
- package/features/vary.feature +1 -1
- package/package.json +8 -8
- package/schemas/octets/delete.cos.yaml +2 -1
- package/schemas/octets/list.cos.yaml +2 -1
- package/source/HTTP/Server.ts +32 -23
- package/source/HTTP/messages.ts +7 -1
- package/source/directives/cors/CORS.ts +25 -23
- package/source/directives/index.ts +1 -1
- package/source/directives/octets/Delete.ts +45 -6
- package/source/directives/octets/Fetch.ts +17 -18
- package/source/directives/octets/List.ts +36 -6
- package/source/directives/octets/Permute.ts +2 -2
- package/source/directives/octets/Store.ts +36 -94
- package/source/directives/octets/schemas.ts +11 -6
- package/source/directives/octets/workflow/Execution.ts +77 -0
- package/source/directives/octets/workflow/Workflow.ts +28 -0
- package/source/directives/octets/workflow/index.ts +1 -0
- package/source/manifest.test.ts +6 -14
- package/source/manifest.ts +9 -6
- package/source/schemas.ts +7 -3
- package/transpiled/HTTP/Server.d.ts +1 -1
- package/transpiled/HTTP/Server.js +22 -21
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/messages.d.ts +1 -0
- package/transpiled/HTTP/messages.js +4 -1
- package/transpiled/HTTP/messages.js.map +1 -1
- package/transpiled/directives/cors/CORS.d.ts +2 -5
- package/transpiled/directives/cors/CORS.js +18 -16
- package/transpiled/directives/cors/CORS.js.map +1 -1
- package/transpiled/directives/index.js +1 -1
- package/transpiled/directives/index.js.map +1 -1
- package/transpiled/directives/octets/Delete.d.ts +9 -1
- package/transpiled/directives/octets/Delete.js +30 -6
- package/transpiled/directives/octets/Delete.js.map +1 -1
- package/transpiled/directives/octets/Fetch.d.ts +4 -5
- package/transpiled/directives/octets/Fetch.js +11 -12
- package/transpiled/directives/octets/Fetch.js.map +1 -1
- package/transpiled/directives/octets/List.d.ts +6 -1
- package/transpiled/directives/octets/List.js +22 -4
- package/transpiled/directives/octets/List.js.map +1 -1
- package/transpiled/directives/octets/Permute.js +2 -2
- package/transpiled/directives/octets/Permute.js.map +1 -1
- package/transpiled/directives/octets/Store.d.ts +7 -19
- package/transpiled/directives/octets/Store.js +21 -66
- package/transpiled/directives/octets/Store.js.map +1 -1
- package/transpiled/directives/octets/schemas.d.ts +11 -6
- package/transpiled/directives/octets/schemas.js.map +1 -1
- package/transpiled/directives/octets/workflow/Execution.d.ts +24 -0
- package/transpiled/directives/octets/workflow/Execution.js +55 -0
- package/transpiled/directives/octets/workflow/Execution.js.map +1 -0
- package/transpiled/directives/octets/workflow/Workflow.d.ts +11 -0
- package/transpiled/directives/octets/workflow/Workflow.js +21 -0
- package/transpiled/directives/octets/workflow/Workflow.js.map +1 -0
- package/transpiled/directives/octets/workflow/index.d.ts +1 -0
- package/transpiled/directives/octets/workflow/index.js +6 -0
- package/transpiled/directives/octets/workflow/index.js.map +1 -0
- package/transpiled/manifest.js +10 -5
- package/transpiled/manifest.js.map +1 -1
- package/transpiled/schemas.d.ts +7 -3
- package/transpiled/schemas.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
package/source/HTTP/Server.ts
CHANGED
|
@@ -7,17 +7,21 @@ import Negotiator from 'negotiator'
|
|
|
7
7
|
import { read, write, type IncomingMessage, type OutgoingMessage } from './messages'
|
|
8
8
|
import { ClientError, Exception } from './exceptions'
|
|
9
9
|
import { formats, types } from './formats'
|
|
10
|
-
import type { Format } from './formats'
|
|
11
10
|
import type * as http from 'node:http'
|
|
12
11
|
import type { Express, Request, Response, NextFunction } from 'express'
|
|
13
12
|
|
|
14
13
|
export class Server extends Connector {
|
|
15
14
|
private server?: http.Server
|
|
15
|
+
private readonly app: Express
|
|
16
|
+
private readonly debug: boolean
|
|
17
|
+
private readonly requestedPort: number
|
|
16
18
|
|
|
17
|
-
private constructor (
|
|
18
|
-
private readonly debug: boolean,
|
|
19
|
-
private readonly requestedPort: number) {
|
|
19
|
+
private constructor (app: Express, debug: boolean, port: number) {
|
|
20
20
|
super()
|
|
21
|
+
|
|
22
|
+
this.app = app
|
|
23
|
+
this.debug = debug
|
|
24
|
+
this.requestedPort = port
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
public get port (): number {
|
|
@@ -39,7 +43,6 @@ export class Server extends Connector {
|
|
|
39
43
|
const app = express()
|
|
40
44
|
|
|
41
45
|
app.disable('x-powered-by')
|
|
42
|
-
// app.use(cors(CORS))
|
|
43
46
|
app.use(supportedMethods(properties.methods))
|
|
44
47
|
|
|
45
48
|
return new Server(app, properties.debug, properties.port)
|
|
@@ -80,7 +83,8 @@ export class Server extends Connector {
|
|
|
80
83
|
private extend (request: Request): IncomingMessage {
|
|
81
84
|
const message = request as IncomingMessage
|
|
82
85
|
|
|
83
|
-
|
|
86
|
+
negotiate(request, message)
|
|
87
|
+
|
|
84
88
|
message.pipelines = { body: [], response: [] }
|
|
85
89
|
|
|
86
90
|
message.parse = async <T> (): Promise<T> => {
|
|
@@ -97,9 +101,6 @@ export class Server extends Connector {
|
|
|
97
101
|
|
|
98
102
|
private success (request: IncomingMessage, response: Response) {
|
|
99
103
|
return (message: OutgoingMessage) => {
|
|
100
|
-
for (const transform of request.pipelines.response)
|
|
101
|
-
transform(message)
|
|
102
|
-
|
|
103
104
|
let status = message.status
|
|
104
105
|
|
|
105
106
|
if (status === undefined)
|
|
@@ -109,12 +110,7 @@ export class Server extends Connector {
|
|
|
109
110
|
else status = 200
|
|
110
111
|
|
|
111
112
|
response.status(status)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (message.body !== undefined && message.body !== null)
|
|
115
|
-
write(request, response, message)
|
|
116
|
-
else
|
|
117
|
-
response.end()
|
|
113
|
+
write(request, response, message)
|
|
118
114
|
}
|
|
119
115
|
}
|
|
120
116
|
|
|
@@ -129,16 +125,15 @@ export class Server extends Connector {
|
|
|
129
125
|
|
|
130
126
|
response.status(status)
|
|
131
127
|
|
|
132
|
-
const
|
|
128
|
+
const message: OutgoingMessage = {}
|
|
129
|
+
const verbose = exception instanceof ClientError || this.debug
|
|
133
130
|
|
|
134
|
-
if (
|
|
135
|
-
|
|
131
|
+
if (verbose)
|
|
132
|
+
message.body = exception instanceof Exception
|
|
136
133
|
? exception.body
|
|
137
134
|
: (exception.stack ?? exception.message)
|
|
138
135
|
|
|
139
|
-
|
|
140
|
-
} else
|
|
141
|
-
response.end()
|
|
136
|
+
write(request, response, message)
|
|
142
137
|
}
|
|
143
138
|
}
|
|
144
139
|
}
|
|
@@ -150,11 +145,23 @@ function supportedMethods (methods: Set<string>) {
|
|
|
150
145
|
}
|
|
151
146
|
}
|
|
152
147
|
|
|
153
|
-
function negotiate (request: Request):
|
|
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
|
+
|
|
154
161
|
const negotiator = new Negotiator(request)
|
|
155
162
|
const mediaType = negotiator.mediaType(types)
|
|
156
163
|
|
|
157
|
-
|
|
164
|
+
message.encoder = mediaType === undefined ? null : formats[mediaType]
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
// https://github.com/whatwg/fetch/issues/1254
|
|
@@ -182,3 +189,5 @@ interface Properties {
|
|
|
182
189
|
}
|
|
183
190
|
|
|
184
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})$/
|
package/source/HTTP/messages.ts
CHANGED
|
@@ -9,6 +9,11 @@ import type { Format } from './formats'
|
|
|
9
9
|
|
|
10
10
|
export function write
|
|
11
11
|
(request: IncomingMessage, response: Response, message: OutgoingMessage): void {
|
|
12
|
+
for (const transform of request.pipelines.response)
|
|
13
|
+
transform(message)
|
|
14
|
+
|
|
15
|
+
message.headers?.forEach((value, key) => response.set(key, value))
|
|
16
|
+
|
|
12
17
|
if (message.body instanceof Readable)
|
|
13
18
|
stream(message, request, response)
|
|
14
19
|
else
|
|
@@ -36,7 +41,7 @@ export async function read (request: Request): Promise<any> {
|
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
function send (message: OutgoingMessage, request: IncomingMessage, response: Response): void {
|
|
39
|
-
if (message.body === undefined) {
|
|
44
|
+
if (message.body === undefined || message.body === null) {
|
|
40
45
|
response.end()
|
|
41
46
|
|
|
42
47
|
return
|
|
@@ -99,6 +104,7 @@ export interface IncomingMessage extends Request {
|
|
|
99
104
|
query: Query
|
|
100
105
|
parse: <T> () => Promise<T>
|
|
101
106
|
encoder: Format | null
|
|
107
|
+
subtype: string | null
|
|
102
108
|
pipelines: {
|
|
103
109
|
body: Array<(input: unknown) => unknown>
|
|
104
110
|
response: Array<(output: OutgoingMessage) => void>
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type { OutgoingMessage } from '../../HTTP'
|
|
2
1
|
import type { Input, Output } from '../../io'
|
|
3
|
-
import type { Family } from '../../Directive'
|
|
4
2
|
import type { Interceptor } from '../../Interception'
|
|
5
3
|
|
|
6
|
-
export class CORS implements
|
|
4
|
+
export class CORS implements Interceptor {
|
|
7
5
|
public readonly name = 'cors'
|
|
8
6
|
public readonly mandatory = true
|
|
9
7
|
|
|
@@ -18,35 +16,39 @@ export class CORS implements Family, Interceptor {
|
|
|
18
16
|
vary: 'origin'
|
|
19
17
|
})
|
|
20
18
|
|
|
21
|
-
public
|
|
22
|
-
|
|
23
|
-
}
|
|
19
|
+
public intercept (input: Input): Output {
|
|
20
|
+
const origin = input.headers.origin
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return
|
|
22
|
+
if (origin === undefined)
|
|
23
|
+
return null
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
output.headers.set('access-control-expose-headers',
|
|
32
|
-
'authorization, content-type, content-length, etag')
|
|
33
|
-
output.headers.append('vary', 'origin')
|
|
34
|
-
}
|
|
25
|
+
if (input.method === 'OPTIONS')
|
|
26
|
+
return this.preflightResponse(origin)
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
input.pipelines.response.push((output) => {
|
|
29
|
+
output.headers ??= new Headers()
|
|
30
|
+
output.headers.set('access-control-allow-origin', origin)
|
|
31
|
+
output.headers.set('access-control-expose-headers',
|
|
32
|
+
'authorization, content-type, content-length, etag')
|
|
39
33
|
|
|
40
|
-
|
|
34
|
+
if (input.method === 'GET' || input.method === 'HEAD' || input.method === 'OPTIONS')
|
|
35
|
+
output.headers.append('vary', 'origin')
|
|
36
|
+
})
|
|
41
37
|
|
|
42
|
-
return
|
|
43
|
-
status: 204,
|
|
44
|
-
headers: this.headers
|
|
45
|
-
}
|
|
38
|
+
return null
|
|
46
39
|
}
|
|
47
40
|
|
|
48
41
|
public allowHeader (header: string): void {
|
|
49
42
|
this.allowedHeaders.add(header.toLowerCase())
|
|
50
43
|
this.headers.set('access-control-allow-headers', Array.from(this.allowedHeaders).join(', '))
|
|
51
44
|
}
|
|
45
|
+
|
|
46
|
+
private preflightResponse (origin: string): Output {
|
|
47
|
+
this.headers.set('access-control-allow-origin', origin)
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
status: 204,
|
|
51
|
+
headers: this.headers
|
|
52
|
+
}
|
|
53
|
+
}
|
|
52
54
|
}
|
|
@@ -7,5 +7,5 @@ import { vary } from './vary'
|
|
|
7
7
|
import type { Family } from '../Directive'
|
|
8
8
|
import type { Interceptor } from '../Interception'
|
|
9
9
|
|
|
10
|
-
export const families: Family[] = [authorization, cache, octets,
|
|
10
|
+
export const families: Family[] = [authorization, cache, octets, vary, dev]
|
|
11
11
|
export const interceptors: Interceptor[] = [cors]
|
|
@@ -1,18 +1,27 @@
|
|
|
1
|
+
import { Readable } from 'stream'
|
|
1
2
|
import { NotFound } from '../../HTTP'
|
|
2
3
|
import * as schemas from './schemas'
|
|
4
|
+
import { Workflow } from './workflow'
|
|
5
|
+
import type { Unit } from './workflow'
|
|
3
6
|
import type { Maybe } from '@toa.io/types'
|
|
4
7
|
import type { Component } from '@toa.io/core'
|
|
5
8
|
import type { Output } from '../../io'
|
|
6
9
|
import type { Directive, Input } from './types'
|
|
10
|
+
import type { Remotes } from '../../Remotes'
|
|
11
|
+
import type { Entry } from '@toa.io/extensions.storages'
|
|
7
12
|
|
|
8
13
|
export class Delete implements Directive {
|
|
9
14
|
public readonly targeted = true
|
|
10
15
|
|
|
16
|
+
private readonly workflow?: Workflow
|
|
11
17
|
private readonly discovery: Promise<Component>
|
|
12
18
|
private storage: Component | null = null
|
|
13
19
|
|
|
14
|
-
public constructor (
|
|
15
|
-
schemas.remove.validate(
|
|
20
|
+
public constructor (options: Options | null, discovery: Promise<Component>, remotes: Remotes) {
|
|
21
|
+
schemas.remove.validate(options)
|
|
22
|
+
|
|
23
|
+
if (options?.workflow !== undefined)
|
|
24
|
+
this.workflow = new Workflow(options.workflow, remotes)
|
|
16
25
|
|
|
17
26
|
this.discovery = discovery
|
|
18
27
|
}
|
|
@@ -20,12 +29,42 @@ export class Delete implements Directive {
|
|
|
20
29
|
public async apply (storage: string, request: Input): Promise<Output> {
|
|
21
30
|
this.storage ??= await this.discovery
|
|
22
31
|
|
|
23
|
-
const
|
|
24
|
-
|
|
32
|
+
const entry = await this.storage.invoke<Maybe<Entry>>('get',
|
|
33
|
+
{ input: { storage, path: request.url } })
|
|
25
34
|
|
|
26
|
-
if (
|
|
35
|
+
if (entry instanceof Error)
|
|
27
36
|
throw new NotFound()
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
const output: Output = {}
|
|
39
|
+
|
|
40
|
+
if (this.workflow !== undefined) {
|
|
41
|
+
output.status = 202
|
|
42
|
+
output.body = Readable.from(this.execute(request, storage, entry))
|
|
43
|
+
} else
|
|
44
|
+
await this.delete(storage, request)
|
|
45
|
+
|
|
46
|
+
return output
|
|
30
47
|
}
|
|
48
|
+
|
|
49
|
+
private async delete (storage: string, request: Input): Promise<void> {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
51
|
+
await this.storage!.invoke('delete',
|
|
52
|
+
{ input: { storage, path: request.url } })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async * execute (request: Input, storage: string, entry: Entry): AsyncGenerator {
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
57
|
+
for await (const chunk of this.workflow!.execute(request, storage, entry)) {
|
|
58
|
+
yield chunk
|
|
59
|
+
|
|
60
|
+
if (typeof chunk === 'object' && chunk !== null && 'error' in chunk)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await this.delete(storage, request)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Options {
|
|
69
|
+
workflow?: Unit[] | Unit
|
|
31
70
|
}
|
|
@@ -12,11 +12,11 @@ import type { Directive, Input } from './types'
|
|
|
12
12
|
export class Fetch implements Directive {
|
|
13
13
|
public readonly targeted = true
|
|
14
14
|
|
|
15
|
-
private readonly permissions: Permissions = { blob: true, meta: false }
|
|
15
|
+
private readonly permissions: Required<Permissions> = { blob: true, meta: false }
|
|
16
16
|
private readonly discovery: Promise<Component>
|
|
17
17
|
private storage: Component = null as unknown as Component
|
|
18
18
|
|
|
19
|
-
public constructor (permissions:
|
|
19
|
+
public constructor (permissions: Permissions | null, discovery: Promise<Component>) {
|
|
20
20
|
schemas.fetch.validate(permissions)
|
|
21
21
|
|
|
22
22
|
Object.assign(this.permissions, permissions)
|
|
@@ -26,19 +26,22 @@ export class Fetch implements Directive {
|
|
|
26
26
|
public async apply (storage: string, request: Input): Promise<Output> {
|
|
27
27
|
this.storage ??= await this.discovery
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
else
|
|
32
|
-
return await this.fetch(storage, request)
|
|
33
|
-
}
|
|
29
|
+
const variant = posix.basename(request.url).includes('.')
|
|
30
|
+
const metadata = request.subtype === 'octets.entry'
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
if (!variant && metadata)
|
|
33
|
+
if (this.permissions.meta)
|
|
34
|
+
return this.get(storage, request)
|
|
35
|
+
else
|
|
36
|
+
throw new Forbidden('Metadata is not accessible.')
|
|
38
37
|
|
|
39
38
|
if (!variant && !this.permissions.blob)
|
|
40
39
|
throw new Forbidden('BLOB variant must be specified.')
|
|
41
40
|
|
|
41
|
+
return await this.fetch(storage, request)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async fetch (storage: string, request: Input): Promise<Output> {
|
|
42
45
|
if ('if-none-match' in request.headers)
|
|
43
46
|
return { status: 304 }
|
|
44
47
|
|
|
@@ -58,11 +61,7 @@ export class Fetch implements Directive {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
private async get (storage: string, request: Input): Promise<Output> {
|
|
61
|
-
|
|
62
|
-
throw new Forbidden('Metadata is not accessible.')
|
|
63
|
-
|
|
64
|
-
const path = request.url.slice(0, -5)
|
|
65
|
-
const input = { storage, path }
|
|
64
|
+
const input = { storage, path: request.url }
|
|
66
65
|
const entry = await this.storage.invoke<Maybe<Entry>>('get', { input })
|
|
67
66
|
|
|
68
67
|
if (entry instanceof Error)
|
|
@@ -72,9 +71,9 @@ export class Fetch implements Directive {
|
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
interface Permissions {
|
|
76
|
-
blob
|
|
77
|
-
meta
|
|
74
|
+
export interface Permissions {
|
|
75
|
+
blob?: boolean
|
|
76
|
+
meta?: boolean
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
interface FetchResult {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { posix } from 'node:path'
|
|
2
|
+
import { Forbidden, NotFound } from '../../HTTP'
|
|
2
3
|
import * as schemas from './schemas'
|
|
4
|
+
import type { Entry } from '@toa.io/extensions.storages'
|
|
3
5
|
import type { Maybe } from '@toa.io/types'
|
|
4
6
|
import type { Component } from '@toa.io/core'
|
|
5
7
|
import type { Output } from '../../io'
|
|
@@ -9,24 +11,52 @@ import type { Directive, Input } from './types'
|
|
|
9
11
|
export class List implements Directive {
|
|
10
12
|
public readonly targeted = false
|
|
11
13
|
|
|
14
|
+
private readonly permissions: Required<Permissions> = { meta: false }
|
|
12
15
|
private readonly discovery: Promise<Component>
|
|
13
16
|
private storage: Component | null = null
|
|
14
17
|
|
|
15
|
-
public constructor (
|
|
16
|
-
schemas.
|
|
18
|
+
public constructor (permissions: Permissions | null, discovery: Promise<Component>) {
|
|
19
|
+
schemas.list.validate(permissions)
|
|
17
20
|
|
|
21
|
+
Object.assign(this.permissions, permissions)
|
|
18
22
|
this.discovery = discovery
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
public async apply (storage: string, request: Input): Promise<Output> {
|
|
22
26
|
this.storage ??= await this.discovery
|
|
23
27
|
|
|
24
|
-
const
|
|
25
|
-
|
|
28
|
+
const metadata = request.subtype === 'octets.entries'
|
|
29
|
+
|
|
30
|
+
if (metadata && !this.permissions.meta)
|
|
31
|
+
throw new Forbidden('Metadata is not accessible.')
|
|
32
|
+
|
|
33
|
+
const input = { storage, path: request.url }
|
|
34
|
+
const list = await this.storage.invoke<Maybe<string[]>>('list', { input })
|
|
26
35
|
|
|
27
36
|
if (list instanceof Error)
|
|
28
37
|
throw new NotFound()
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
const body = metadata
|
|
40
|
+
? await this.expand(storage, request.url, list)
|
|
41
|
+
: list
|
|
42
|
+
|
|
43
|
+
return { body }
|
|
31
44
|
}
|
|
45
|
+
|
|
46
|
+
private async expand (storage: string, prefix: string, list: string[]):
|
|
47
|
+
Promise<Array<Maybe<Entry>>> {
|
|
48
|
+
const promises = list.map(async (id) => {
|
|
49
|
+
const path = posix.join(prefix, id)
|
|
50
|
+
const input = { storage, path }
|
|
51
|
+
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- ensured in `apply`
|
|
53
|
+
return this.storage!.invoke<Maybe<Entry>>('get', { input })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return await Promise.all(promises)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Permissions {
|
|
61
|
+
meta?: boolean
|
|
32
62
|
}
|
|
@@ -13,7 +13,7 @@ export class Permute implements Directive {
|
|
|
13
13
|
private storage: Component | null = null
|
|
14
14
|
|
|
15
15
|
public constructor (value: null, discovery: Promise<Component>) {
|
|
16
|
-
schemas.
|
|
16
|
+
schemas.permute.validate(value)
|
|
17
17
|
|
|
18
18
|
this.discovery = discovery
|
|
19
19
|
}
|
|
@@ -24,7 +24,7 @@ export class Permute implements Directive {
|
|
|
24
24
|
if (request.encoder === null)
|
|
25
25
|
throw new NotAcceptable()
|
|
26
26
|
|
|
27
|
-
const path = request.
|
|
27
|
+
const path = request.url
|
|
28
28
|
const list = await request.parse()
|
|
29
29
|
const input = { storage, path, list }
|
|
30
30
|
const error = await this.storage.invoke<Maybe<unknown>>('permute', { input })
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { Readable } from 'node:stream'
|
|
2
|
-
import { posix } from 'node:path'
|
|
3
1
|
import { match } from 'matchacho'
|
|
4
|
-
import { promex } from '@toa.io/generic'
|
|
5
2
|
import { BadRequest, UnsupportedMediaType } from '../../HTTP'
|
|
3
|
+
import { cors } from '../cors'
|
|
6
4
|
import * as schemas from './schemas'
|
|
7
|
-
import
|
|
5
|
+
import { Workflow } from './workflow'
|
|
6
|
+
import type { Unit } from './workflow'
|
|
8
7
|
import type { Entry } from '@toa.io/extensions.storages'
|
|
9
8
|
import type { Remotes } from '../../Remotes'
|
|
10
9
|
import type { ErrorType } from 'error-value'
|
|
@@ -15,11 +14,9 @@ import type { Directive, Input } from './types'
|
|
|
15
14
|
export class Store implements Directive {
|
|
16
15
|
public readonly targeted = false
|
|
17
16
|
|
|
18
|
-
private readonly accept
|
|
19
|
-
private readonly workflow
|
|
17
|
+
private readonly accept?: string
|
|
18
|
+
private readonly workflow?: Workflow
|
|
20
19
|
private readonly discovery: Record<string, Promise<Component>> = {}
|
|
21
|
-
private readonly remotes: Remotes
|
|
22
|
-
private readonly components: Record<string, Component> = {}
|
|
23
20
|
private storage: Component | null = null
|
|
24
21
|
|
|
25
22
|
public constructor
|
|
@@ -31,19 +28,26 @@ export class Store implements Directive {
|
|
|
31
28
|
Array, (types: string[]) => types.join(','),
|
|
32
29
|
undefined)
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Object, (unit: Unit) => [unit],
|
|
37
|
-
undefined)
|
|
31
|
+
if (options?.workflow !== undefined)
|
|
32
|
+
this.workflow = new Workflow(options.workflow, remotes)
|
|
38
33
|
|
|
39
34
|
this.discovery.storage = discovery
|
|
40
|
-
|
|
35
|
+
|
|
36
|
+
cors.allowHeader('content-meta')
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
public async apply (storage: string, request: Input): Promise<Output> {
|
|
44
40
|
this.storage ??= await this.discovery.storage
|
|
45
41
|
|
|
46
|
-
const input = { storage, request
|
|
42
|
+
const input: StoreInput = { storage, request }
|
|
43
|
+
const meta = request.headers['content-meta']
|
|
44
|
+
|
|
45
|
+
if (this.accept !== undefined)
|
|
46
|
+
input.accept = this.accept
|
|
47
|
+
|
|
48
|
+
if (meta !== undefined)
|
|
49
|
+
input.meta = this.parseMeta(meta)
|
|
50
|
+
|
|
47
51
|
const entry = await this.storage.invoke<Entry>('store', { input })
|
|
48
52
|
|
|
49
53
|
return match<Output>(entry,
|
|
@@ -54,7 +58,7 @@ export class Store implements Directive {
|
|
|
54
58
|
private reply (request: Input, storage: string, entry: Entry): Output {
|
|
55
59
|
const body = this.workflow === undefined
|
|
56
60
|
? entry
|
|
57
|
-
:
|
|
61
|
+
: this.workflow.execute(request, storage, entry)
|
|
58
62
|
|
|
59
63
|
return { body }
|
|
60
64
|
}
|
|
@@ -66,93 +70,31 @@ export class Store implements Directive {
|
|
|
66
70
|
error)
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* Execute workflow units sequentially, steps within a unit in parallel.
|
|
73
|
-
* Yield results as soon as they come.
|
|
74
|
-
*
|
|
75
|
-
* If you need to change this, it may take a while.
|
|
76
|
-
*/
|
|
77
|
-
private async * execute (request: Input, storage: string, entry: Entry): AsyncGenerator {
|
|
78
|
-
yield entry
|
|
79
|
-
|
|
80
|
-
const path = posix.join(request.path, entry.id)
|
|
81
|
-
let interrupted = false
|
|
82
|
-
|
|
83
|
-
for (const unit of this.workflow!) {
|
|
84
|
-
if (interrupted)
|
|
85
|
-
break
|
|
86
|
-
|
|
87
|
-
const steps = Object.keys(unit)
|
|
88
|
-
|
|
89
|
-
// unit result promises queue
|
|
90
|
-
const results = Array.from(steps, promex<unknown>)
|
|
91
|
-
let next = 0
|
|
92
|
-
|
|
93
|
-
// execute steps in parallel
|
|
94
|
-
for (const step of steps)
|
|
95
|
-
// these promises are indirectly awaited in the yield loop
|
|
96
|
-
void (async () => {
|
|
97
|
-
const endpoint = unit[step]
|
|
98
|
-
const context: Context = { storage, path, entry }
|
|
99
|
-
const result = await this.call(endpoint, context)
|
|
100
|
-
|
|
101
|
-
if (interrupted)
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
// as a result is received, resolve the next promise from the queue
|
|
105
|
-
const promise = results[next++]
|
|
106
|
-
|
|
107
|
-
if (result instanceof Error) {
|
|
108
|
-
interrupted = true
|
|
109
|
-
promise.resolve({ error: { step, ...result } })
|
|
110
|
-
|
|
111
|
-
// cancel pending promises
|
|
112
|
-
results[next].resolve(null)
|
|
113
|
-
} else
|
|
114
|
-
promise.resolve({ [step]: result ?? null })
|
|
115
|
-
})().catch((e) => results[next].reject(e))
|
|
116
|
-
|
|
117
|
-
// yield results from the queue as they come
|
|
118
|
-
for (const promise of results) {
|
|
119
|
-
const result = await promise
|
|
120
|
-
|
|
121
|
-
if (result === null) // canceled promise
|
|
122
|
-
break
|
|
123
|
-
else
|
|
124
|
-
yield result
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private async call (endpoint: string, context: Context): Promise<Maybe<unknown>> {
|
|
130
|
-
const [operation, component, namespace = 'default'] = endpoint.split('.').reverse()
|
|
131
|
-
const key = `${namespace}.${component}`
|
|
73
|
+
private parseMeta (value: string | string[]): Record<string, string> {
|
|
74
|
+
if (Array.isArray(value))
|
|
75
|
+
value = value.join(',')
|
|
132
76
|
|
|
133
|
-
|
|
77
|
+
const meta: Record<string, string> = {}
|
|
134
78
|
|
|
135
|
-
|
|
136
|
-
|
|
79
|
+
for (const pair of value.split(',')) {
|
|
80
|
+
const eq = pair.indexOf('=')
|
|
81
|
+
const key = (eq === -1 ? pair : pair.slice(0, eq)).trim()
|
|
137
82
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
this.discovery[key] = this.remotes.discover(namespace, component)
|
|
83
|
+
meta[key] = eq === -1 ? 'true' : pair.slice(eq + 1).trim()
|
|
84
|
+
}
|
|
141
85
|
|
|
142
|
-
return
|
|
86
|
+
return meta
|
|
143
87
|
}
|
|
144
88
|
}
|
|
145
89
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
interface Options {
|
|
150
|
-
accept: string | string[]
|
|
151
|
-
workflow: Workflow | Unit
|
|
90
|
+
export interface Options {
|
|
91
|
+
accept?: string | string[]
|
|
92
|
+
workflow?: Unit[] | Unit
|
|
152
93
|
}
|
|
153
94
|
|
|
154
|
-
interface
|
|
95
|
+
interface StoreInput {
|
|
155
96
|
storage: string
|
|
156
|
-
|
|
157
|
-
|
|
97
|
+
request: Input
|
|
98
|
+
accept?: string
|
|
99
|
+
meta?: Record<string, string>
|
|
158
100
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import schemas from '@toa.io/schemas'
|
|
3
|
+
import type { Permissions as FetchPermissions } from './Fetch'
|
|
4
|
+
import type { Permissions as ListPermissions } from './List'
|
|
5
|
+
import type { Options as StoreOptions } from './Store'
|
|
6
|
+
import type { Options as DeleteOptions } from './Delete'
|
|
7
|
+
import type { Schema } from '@toa.io/schemas'
|
|
3
8
|
|
|
4
9
|
const path = resolve(__dirname, '../../../schemas/octets')
|
|
5
10
|
const namespace = schemas.namespace(path)
|
|
6
11
|
|
|
7
|
-
export const context = namespace.schema('context')
|
|
8
|
-
export const store = namespace.schema('store')
|
|
9
|
-
export const fetch = namespace.schema('fetch')
|
|
10
|
-
export const remove = namespace.schema('delete')
|
|
11
|
-
export const list = namespace.schema('list')
|
|
12
|
-
export const permute = namespace.schema('permute')
|
|
12
|
+
export const context: Schema<string> = namespace.schema('context')
|
|
13
|
+
export const store: Schema<StoreOptions | null> = namespace.schema('store')
|
|
14
|
+
export const fetch: Schema<FetchPermissions | null> = namespace.schema('fetch')
|
|
15
|
+
export const remove: Schema<DeleteOptions | null> = namespace.schema('delete')
|
|
16
|
+
export const list: Schema<ListPermissions | null> = namespace.schema('list')
|
|
17
|
+
export const permute: Schema<null> = namespace.schema('permute')
|