@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
|
@@ -56,7 +56,7 @@ export class Authorization implements Family<Directive, Extension> {
|
|
|
56
56
|
public async preflight (directives: Directive[],
|
|
57
57
|
input: Input,
|
|
58
58
|
parameters: Parameter[]): Promise<Output> {
|
|
59
|
-
const identity = await this.resolve(input.headers.authorization)
|
|
59
|
+
const identity = await this.resolve(input.request.headers.authorization)
|
|
60
60
|
|
|
61
61
|
input.identity = identity
|
|
62
62
|
|
|
@@ -67,8 +67,10 @@ export class Authorization implements Family<Directive, Extension> {
|
|
|
67
67
|
return directive.reply?.(identity) ?? null
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
if (identity === null)
|
|
71
|
-
|
|
70
|
+
if (identity === null)
|
|
71
|
+
throw new http.Unauthorized()
|
|
72
|
+
else
|
|
73
|
+
throw new http.Forbidden()
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
public async settle (directives: Directive[],
|
|
@@ -15,28 +15,33 @@ export class Incept implements Directive {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
public authorize (identity: Identity | null, input: Input): boolean {
|
|
18
|
-
return identity === null && 'authorization' in input.headers
|
|
18
|
+
return identity === null && 'authorization' in input.request.headers
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
public async settle (
|
|
21
|
+
public async settle (input: Input, response: http.OutgoingMessage): Promise<void> {
|
|
22
22
|
const id = response.body?.[this.property]
|
|
23
23
|
|
|
24
24
|
if (id === undefined)
|
|
25
25
|
throw new http.Conflict('Identity inception has failed as the response body ' +
|
|
26
26
|
` does not contain the '${this.property}' property.`)
|
|
27
27
|
|
|
28
|
-
const [scheme, credentials] = split(request.headers.authorization!)
|
|
28
|
+
const [scheme, credentials] = split(input.request.headers.authorization!)
|
|
29
29
|
const provider = PROVIDERS[scheme]
|
|
30
30
|
|
|
31
31
|
this.schemes[scheme] ??= await this.discovery[provider]
|
|
32
32
|
|
|
33
33
|
const identity = await this.schemes[scheme]
|
|
34
|
-
.invoke<Maybe<Identity>>('create', {
|
|
34
|
+
.invoke<Maybe<Identity>>('create', {
|
|
35
|
+
input: {
|
|
36
|
+
id,
|
|
37
|
+
credentials
|
|
38
|
+
}
|
|
39
|
+
})
|
|
35
40
|
|
|
36
41
|
if (identity instanceof Error)
|
|
37
42
|
throw new http.Conflict(identity)
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
input.identity = identity
|
|
45
|
+
input.identity.scheme = scheme
|
|
41
46
|
}
|
|
42
47
|
}
|
|
@@ -14,10 +14,12 @@ export class Role implements Directive {
|
|
|
14
14
|
public static async set (identity: Identity, discovery: Promise<Component>): Promise<void> {
|
|
15
15
|
this.remote ??= await discovery
|
|
16
16
|
|
|
17
|
-
const query: Query = {
|
|
18
|
-
|
|
17
|
+
const query: Query = {
|
|
18
|
+
criteria: `identity==${identity.id}`,
|
|
19
|
+
limit: 1024
|
|
20
|
+
}
|
|
19
21
|
|
|
20
|
-
identity.roles =
|
|
22
|
+
identity.roles = await this.remote.invoke('list', { query })
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
public async authorize (identity: Identity | null): Promise<boolean> {
|
|
@@ -12,10 +12,10 @@ export class Scheme implements Directive {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
public authorize (_: Identity | null, input: Input): boolean {
|
|
15
|
-
if (input.headers.authorization === undefined)
|
|
15
|
+
if (input.request.headers.authorization === undefined)
|
|
16
16
|
return false
|
|
17
17
|
|
|
18
|
-
const [scheme] = split(input.headers.authorization)
|
|
18
|
+
const [scheme] = split(input.request.headers.authorization)
|
|
19
19
|
|
|
20
20
|
if (scheme !== this.scheme)
|
|
21
21
|
throw new http.Forbidden(this.Scheme +
|
|
@@ -23,9 +23,9 @@ export class Cache implements Family<Directive> {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
public async settle
|
|
26
|
-
(directives: Directive[],
|
|
26
|
+
(directives: Directive[], input: Input, response: http.OutgoingMessage): Promise<void> {
|
|
27
27
|
response.headers ??= new Headers()
|
|
28
|
-
directives[0]?.set(
|
|
28
|
+
directives[0]?.set(input, response.headers)
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { match } from 'matchacho'
|
|
2
|
-
import type {
|
|
2
|
+
import type { AuthenticatedContext, Directive } from './types'
|
|
3
3
|
|
|
4
4
|
export class Control implements Directive {
|
|
5
5
|
protected readonly value: string
|
|
@@ -9,16 +9,16 @@ export class Control implements Directive {
|
|
|
9
9
|
this.value = value
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
public set (
|
|
13
|
-
if (!['GET', 'HEAD', 'OPTIONS'].includes(request.method))
|
|
12
|
+
public set (context: AuthenticatedContext, headers: Headers): void {
|
|
13
|
+
if (!['GET', 'HEAD', 'OPTIONS'].includes(context.request.method))
|
|
14
14
|
return
|
|
15
15
|
|
|
16
|
-
this.cache ??= this.resolve(
|
|
16
|
+
this.cache ??= this.resolve(context)
|
|
17
17
|
|
|
18
18
|
headers.set('cache-control', this.cache)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
protected resolve (request:
|
|
21
|
+
protected resolve (request: AuthenticatedContext): string {
|
|
22
22
|
if (request.identity === null)
|
|
23
23
|
return this.value
|
|
24
24
|
|
|
@@ -17,12 +17,12 @@ export class CORS implements Interceptor {
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
public intercept (input: Input): Output {
|
|
20
|
-
const origin = input.headers.origin
|
|
20
|
+
const origin = input.request.headers.origin
|
|
21
21
|
|
|
22
22
|
if (origin === undefined)
|
|
23
23
|
return null
|
|
24
24
|
|
|
25
|
-
if (input.method === 'OPTIONS')
|
|
25
|
+
if (input.request.method === 'OPTIONS')
|
|
26
26
|
return this.preflightResponse(origin)
|
|
27
27
|
|
|
28
28
|
input.pipelines.response.push((output) => {
|
|
@@ -31,7 +31,9 @@ export class CORS implements Interceptor {
|
|
|
31
31
|
output.headers.set('access-control-expose-headers',
|
|
32
32
|
'authorization, content-type, content-length, etag')
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
const method = input.request.method
|
|
35
|
+
|
|
36
|
+
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS')
|
|
35
37
|
output.headers.append('vary', 'origin')
|
|
36
38
|
})
|
|
37
39
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Readable } from 'stream'
|
|
2
2
|
import { NotFound } from '../../HTTP'
|
|
3
3
|
import * as schemas from './schemas'
|
|
4
|
-
import { Workflow } from './
|
|
4
|
+
import { Workflow } from './workflows'
|
|
5
5
|
import type { Parameter } from '../../RTD'
|
|
6
|
-
import type { Unit } from './
|
|
6
|
+
import type { Unit } from './workflows'
|
|
7
7
|
import type { Maybe } from '@toa.io/types'
|
|
8
8
|
import type { Component } from '@toa.io/core'
|
|
9
9
|
import type { Output } from '../../io'
|
|
@@ -27,11 +27,16 @@ export class Delete implements Directive {
|
|
|
27
27
|
this.discovery = discovery
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
public async apply (storage: string,
|
|
30
|
+
public async apply (storage: string, input: Input, parameters: Parameter[]): Promise<Output> {
|
|
31
31
|
this.storage ??= await this.discovery
|
|
32
32
|
|
|
33
33
|
const entry = await this.storage.invoke<Maybe<Entry>>('get',
|
|
34
|
-
{
|
|
34
|
+
{
|
|
35
|
+
input: {
|
|
36
|
+
storage,
|
|
37
|
+
path: input.request.url
|
|
38
|
+
}
|
|
39
|
+
})
|
|
35
40
|
|
|
36
41
|
if (entry instanceof Error)
|
|
37
42
|
throw new NotFound()
|
|
@@ -40,31 +45,36 @@ export class Delete implements Directive {
|
|
|
40
45
|
|
|
41
46
|
if (this.workflow !== undefined) {
|
|
42
47
|
output.status = 202
|
|
43
|
-
output.body = Readable.from(this.execute(
|
|
48
|
+
output.body = Readable.from(this.execute(input, storage, entry, parameters))
|
|
44
49
|
} else
|
|
45
|
-
await this.delete(storage,
|
|
50
|
+
await this.delete(storage, input)
|
|
46
51
|
|
|
47
52
|
return output
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
private async delete (storage: string,
|
|
55
|
+
private async delete (storage: string, input: Input): Promise<void> {
|
|
51
56
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
52
57
|
await this.storage!.invoke('delete',
|
|
53
|
-
{
|
|
58
|
+
{
|
|
59
|
+
input: {
|
|
60
|
+
storage,
|
|
61
|
+
path: input.request.url
|
|
62
|
+
}
|
|
63
|
+
})
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
// eslint-disable-next-line max-params
|
|
57
67
|
private async * execute
|
|
58
|
-
(
|
|
68
|
+
(input: Input, storage: string, entry: Entry, parameters: Parameter[]): AsyncGenerator {
|
|
59
69
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
60
|
-
for await (const chunk of this.workflow!.execute(
|
|
70
|
+
for await (const chunk of this.workflow!.execute(input, storage, entry, parameters)) {
|
|
61
71
|
yield chunk
|
|
62
72
|
|
|
63
73
|
if (typeof chunk === 'object' && chunk !== null && 'error' in chunk)
|
|
64
74
|
return
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
await this.delete(storage,
|
|
77
|
+
await this.delete(storage, input)
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
80
|
|
|
@@ -12,7 +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: Required<Permissions> = {
|
|
15
|
+
private readonly permissions: Required<Permissions> = {
|
|
16
|
+
blob: true,
|
|
17
|
+
meta: false
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
private readonly discovery: Promise<Component>
|
|
17
21
|
private storage: Component = null as unknown as Component
|
|
18
22
|
|
|
@@ -23,30 +27,34 @@ export class Fetch implements Directive {
|
|
|
23
27
|
this.discovery = discovery
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
public async apply (storage: string,
|
|
30
|
+
public async apply (storage: string, input: Input): Promise<Output> {
|
|
27
31
|
this.storage ??= await this.discovery
|
|
28
32
|
|
|
29
|
-
const variant = posix.basename(request.url).includes('.')
|
|
30
|
-
const metadata =
|
|
33
|
+
const variant = posix.basename(input.request.url).includes('.')
|
|
34
|
+
const metadata = input.subtype === 'octets.entry'
|
|
31
35
|
|
|
32
36
|
if (!variant && metadata)
|
|
33
37
|
if (this.permissions.meta)
|
|
34
|
-
return this.get(storage,
|
|
38
|
+
return this.get(storage, input)
|
|
35
39
|
else
|
|
36
40
|
throw new Forbidden('Metadata is not accessible.')
|
|
37
41
|
|
|
38
42
|
if (!variant && !this.permissions.blob)
|
|
39
43
|
throw new Forbidden('BLOB variant must be specified.')
|
|
40
44
|
|
|
41
|
-
return await this.fetch(storage,
|
|
45
|
+
return await this.fetch(storage, input)
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
private async fetch (storage: string,
|
|
45
|
-
if ('if-none-match' in request.headers)
|
|
48
|
+
private async fetch (storage: string, input: Input): Promise<Output> {
|
|
49
|
+
if ('if-none-match' in input.request.headers)
|
|
46
50
|
return { status: 304 }
|
|
47
51
|
|
|
48
|
-
const
|
|
49
|
-
|
|
52
|
+
const result = await this.storage.invoke<Maybe<FetchResult>>('fetch', {
|
|
53
|
+
input: {
|
|
54
|
+
storage,
|
|
55
|
+
path: input.request.url
|
|
56
|
+
}
|
|
57
|
+
})
|
|
50
58
|
|
|
51
59
|
if (result instanceof Error)
|
|
52
60
|
throw new NotFound()
|
|
@@ -57,12 +65,19 @@ export class Fetch implements Directive {
|
|
|
57
65
|
etag: result.checksum
|
|
58
66
|
})
|
|
59
67
|
|
|
60
|
-
return {
|
|
68
|
+
return {
|
|
69
|
+
headers,
|
|
70
|
+
body: result.stream
|
|
71
|
+
}
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
private async get (storage: string,
|
|
64
|
-
const
|
|
65
|
-
|
|
74
|
+
private async get (storage: string, input: Input): Promise<Output> {
|
|
75
|
+
const entry = await this.storage.invoke<Maybe<Entry>>('get', {
|
|
76
|
+
input: {
|
|
77
|
+
storage,
|
|
78
|
+
path: input.request.url
|
|
79
|
+
}
|
|
80
|
+
})
|
|
66
81
|
|
|
67
82
|
if (entry instanceof Error)
|
|
68
83
|
throw new NotFound()
|
|
@@ -22,22 +22,26 @@ export class List implements Directive {
|
|
|
22
22
|
this.discovery = discovery
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
public async apply (storage: string,
|
|
25
|
+
public async apply (storage: string, input: Input): Promise<Output> {
|
|
26
26
|
this.storage ??= await this.discovery
|
|
27
27
|
|
|
28
|
-
const metadata =
|
|
28
|
+
const metadata = input.subtype === 'octets.entries'
|
|
29
29
|
|
|
30
30
|
if (metadata && !this.permissions.meta)
|
|
31
31
|
throw new Forbidden('Metadata is not accessible.')
|
|
32
32
|
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const list = await this.storage.invoke<Maybe<string[]>>('list', {
|
|
34
|
+
input: {
|
|
35
|
+
storage,
|
|
36
|
+
path: input.request.url
|
|
37
|
+
}
|
|
38
|
+
})
|
|
35
39
|
|
|
36
40
|
if (list instanceof Error)
|
|
37
41
|
throw new NotFound()
|
|
38
42
|
|
|
39
43
|
const body = metadata
|
|
40
|
-
? await this.expand(storage, request.url, list)
|
|
44
|
+
? await this.expand(storage, input.request.url, list)
|
|
41
45
|
: list
|
|
42
46
|
|
|
43
47
|
return { body }
|
|
@@ -47,7 +51,11 @@ export class List implements Directive {
|
|
|
47
51
|
Promise<Array<Maybe<Entry>>> {
|
|
48
52
|
const promises = list.map(async (id) => {
|
|
49
53
|
const path = posix.join(prefix, id)
|
|
50
|
-
|
|
54
|
+
|
|
55
|
+
const input = {
|
|
56
|
+
storage,
|
|
57
|
+
path
|
|
58
|
+
}
|
|
51
59
|
|
|
52
60
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- ensured in `apply`
|
|
53
61
|
return this.storage!.invoke<Maybe<Entry>>('get', { input })
|
|
@@ -5,6 +5,7 @@ import { Fetch } from './Fetch'
|
|
|
5
5
|
import { List } from './List'
|
|
6
6
|
import { Delete } from './Delete'
|
|
7
7
|
import { Permute } from './Permute'
|
|
8
|
+
import { WorkflowDirective } from './Workflow'
|
|
8
9
|
import type { Output } from '../../io'
|
|
9
10
|
import type { Component } from '@toa.io/core'
|
|
10
11
|
import type { Remotes } from '../../Remotes'
|
|
@@ -45,15 +46,17 @@ export class Octets implements Family<Directive> {
|
|
|
45
46
|
if (action === null)
|
|
46
47
|
return null
|
|
47
48
|
|
|
49
|
+
// noinspection PointlessBooleanExpressionJS
|
|
48
50
|
if (context === null)
|
|
49
51
|
throw new Error('Octets context is not defined.')
|
|
50
52
|
|
|
51
|
-
const targeted = input.
|
|
53
|
+
const targeted = input.request.url[input.request.url.length - 1] !== '/'
|
|
52
54
|
|
|
53
55
|
if (targeted !== action.targeted)
|
|
54
56
|
throw new NotFound(`Trailing slash is ${action.targeted ? 'redundant' : 'required'}.`)
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
// noinspection JSObjectNullOrUndefined
|
|
59
|
+
return action.apply(context.storage, input, parameters)
|
|
57
60
|
}
|
|
58
61
|
}
|
|
59
62
|
|
|
@@ -63,7 +66,8 @@ const DIRECTIVES: Record<string, Constructor> = {
|
|
|
63
66
|
fetch: Fetch,
|
|
64
67
|
list: List,
|
|
65
68
|
delete: Delete,
|
|
66
|
-
permute: Permute
|
|
69
|
+
permute: Permute,
|
|
70
|
+
workflow: WorkflowDirective
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
type Constructor = new (value: any, discovery: Promise<Component>, remotes: Remotes) => Directive
|
|
@@ -18,16 +18,22 @@ export class Permute implements Directive {
|
|
|
18
18
|
this.discovery = discovery
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
public async apply (storage: string,
|
|
21
|
+
public async apply (storage: string, input: Input): Promise<Output> {
|
|
22
22
|
this.storage ??= await this.discovery
|
|
23
23
|
|
|
24
|
-
if (
|
|
24
|
+
if (input.encoder === null)
|
|
25
25
|
throw new NotAcceptable()
|
|
26
26
|
|
|
27
|
-
const path = request.url
|
|
28
|
-
const list = await
|
|
29
|
-
|
|
30
|
-
const error = await this.storage.invoke<Maybe<unknown>>('permute', {
|
|
27
|
+
const path = input.request.url
|
|
28
|
+
const list = await input.body()
|
|
29
|
+
|
|
30
|
+
const error = await this.storage.invoke<Maybe<unknown>>('permute', {
|
|
31
|
+
input: {
|
|
32
|
+
storage,
|
|
33
|
+
path,
|
|
34
|
+
list
|
|
35
|
+
}
|
|
36
|
+
})
|
|
31
37
|
|
|
32
38
|
if (error instanceof Error)
|
|
33
39
|
throw new NotFound()
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { PassThrough } from 'node:stream'
|
|
1
2
|
import { match } from 'matchacho'
|
|
2
3
|
import { BadRequest, UnsupportedMediaType } from '../../HTTP'
|
|
3
4
|
import { cors } from '../cors'
|
|
4
5
|
import * as schemas from './schemas'
|
|
5
|
-
import { Workflow } from './
|
|
6
|
+
import { Workflow } from './workflows'
|
|
7
|
+
import type { Readable } from 'stream'
|
|
6
8
|
import type { Parameter } from '../../RTD'
|
|
7
|
-
import type { Unit } from './
|
|
9
|
+
import type { Unit } from './workflows'
|
|
8
10
|
import type { Entry } from '@toa.io/extensions.storages'
|
|
9
11
|
import type { Remotes } from '../../Remotes'
|
|
10
12
|
import type { ErrorType } from 'error-value'
|
|
@@ -37,34 +39,46 @@ export class Store implements Directive {
|
|
|
37
39
|
cors.allowHeader('content-meta')
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
public async apply (storage: string,
|
|
42
|
+
public async apply (storage: string, input: Input, parameters: Parameter[]): Promise<Output> {
|
|
41
43
|
this.storage ??= await this.discovery.storage
|
|
42
44
|
|
|
43
|
-
const
|
|
44
|
-
const meta = request.headers['content-meta']
|
|
45
|
+
const request: StoreRequest = { input: { storage, request: input.request } }
|
|
46
|
+
const meta = input.request.headers['content-meta']
|
|
45
47
|
|
|
46
48
|
if (this.accept !== undefined)
|
|
47
|
-
input.accept = this.accept
|
|
49
|
+
request.input.accept = this.accept
|
|
48
50
|
|
|
49
51
|
if (meta !== undefined)
|
|
50
|
-
input.meta = this.parseMeta(meta)
|
|
52
|
+
request.input.meta = this.parseMeta(meta)
|
|
51
53
|
|
|
52
|
-
const entry = await this.storage.invoke<Entry>('store',
|
|
54
|
+
const entry = await this.storage.invoke<Entry>('store', request)
|
|
53
55
|
|
|
54
56
|
return match<Output>(entry,
|
|
55
57
|
Error, (error: ErrorType) => this.throw(error),
|
|
56
|
-
() => this.reply(
|
|
58
|
+
() => this.reply(input, storage, entry, parameters))
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
// eslint-disable-next-line max-params
|
|
60
|
-
private reply (
|
|
62
|
+
private reply (input: Input, storage: string, entry: Entry, parameters: Parameter[]): Output {
|
|
61
63
|
const body = this.workflow === undefined
|
|
62
64
|
? entry
|
|
63
|
-
: this.
|
|
65
|
+
: this.execute(input, storage, entry, parameters)
|
|
64
66
|
|
|
65
67
|
return { body }
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// eslint-disable-next-line max-params
|
|
71
|
+
private execute
|
|
72
|
+
(input: Input, storage: string, entry: Entry, parameters: Parameter[]): Readable {
|
|
73
|
+
const stream = new PassThrough({ objectMode: true })
|
|
74
|
+
|
|
75
|
+
stream.push(entry)
|
|
76
|
+
|
|
77
|
+
this.workflow!.execute(input, storage, entry, parameters).pipe(stream)
|
|
78
|
+
|
|
79
|
+
return stream
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
private throw (error: ErrorType): never {
|
|
69
83
|
throw match(error.code,
|
|
70
84
|
'NOT_ACCEPTABLE', () => new UnsupportedMediaType(),
|
|
@@ -94,9 +108,11 @@ export interface Options {
|
|
|
94
108
|
workflow?: Unit[] | Unit
|
|
95
109
|
}
|
|
96
110
|
|
|
97
|
-
interface
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
interface StoreRequest {
|
|
112
|
+
input: {
|
|
113
|
+
storage: string
|
|
114
|
+
request: Input['request']
|
|
115
|
+
accept?: string
|
|
116
|
+
meta?: Record<string, string>
|
|
117
|
+
}
|
|
102
118
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NotFound } from '../../HTTP'
|
|
2
|
+
import * as schemas from './schemas'
|
|
3
|
+
import { Workflow } from './workflows'
|
|
4
|
+
import type { Unit } from './workflows'
|
|
5
|
+
import type { Directive, Input } from './types'
|
|
6
|
+
import type { Component } from '@toa.io/core'
|
|
7
|
+
import type { Output } from '../../io'
|
|
8
|
+
import type { Remotes } from '../../Remotes'
|
|
9
|
+
import type { Maybe } from '@toa.io/types'
|
|
10
|
+
import type { Entry } from '@toa.io/extensions.storages'
|
|
11
|
+
import type { Parameter } from '../../RTD'
|
|
12
|
+
|
|
13
|
+
export class WorkflowDirective implements Directive {
|
|
14
|
+
public readonly targeted = true
|
|
15
|
+
|
|
16
|
+
private readonly workflow: Workflow
|
|
17
|
+
private readonly discovery: Promise<Component>
|
|
18
|
+
private storage: Component | null = null
|
|
19
|
+
|
|
20
|
+
public constructor (units: Unit[] | Unit, discovery: Promise<Component>, remotes: Remotes) {
|
|
21
|
+
schemas.workflow.validate(units)
|
|
22
|
+
|
|
23
|
+
this.workflow = new Workflow(units, remotes)
|
|
24
|
+
this.discovery = discovery
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async apply (storage: string, input: Input, parameters: Parameter[]): Promise<Output> {
|
|
28
|
+
this.storage ??= await this.discovery
|
|
29
|
+
|
|
30
|
+
const entry = await this.storage.invoke<Maybe<Entry>>('get',
|
|
31
|
+
{ input: { storage, path: input.request.url } })
|
|
32
|
+
|
|
33
|
+
if (entry instanceof Error)
|
|
34
|
+
throw new NotFound()
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
status: 202,
|
|
38
|
+
body: this.workflow.execute(input, storage, entry, parameters)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as schemas from './schemas'
|
|
2
|
+
|
|
3
|
+
describe('workflow', () => {
|
|
4
|
+
const ok = [
|
|
5
|
+
{ echo: 'hello world' },
|
|
6
|
+
[{ echo: 'hello world' }, { ok: 'ok' }]
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
const oh = [
|
|
10
|
+
{ echo: [] },
|
|
11
|
+
{ echo: 'hello world', ok: { not: 'ok' } }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
it.each(ok)('should be valid', (workflow) => {
|
|
15
|
+
expect(() => schemas.workflow.validate(workflow)).not.toThrow()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it.each(oh)('should not be valid', (workflow) => {
|
|
19
|
+
expect(() => schemas.workflow.validate(workflow)).toThrow()
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -5,6 +5,7 @@ import type { Permissions as ListPermissions } from './List'
|
|
|
5
5
|
import type { Options as StoreOptions } from './Store'
|
|
6
6
|
import type { Options as DeleteOptions } from './Delete'
|
|
7
7
|
import type { Schema } from '@toa.io/schemas'
|
|
8
|
+
import type { Unit } from './workflows'
|
|
8
9
|
|
|
9
10
|
const path = resolve(__dirname, '../../../schemas/octets')
|
|
10
11
|
const namespace = schemas.namespace(path)
|
|
@@ -15,3 +16,4 @@ export const fetch: Schema<FetchPermissions | null> = namespace.schema('fetch')
|
|
|
15
16
|
export const remove: Schema<DeleteOptions | null> = namespace.schema('delete')
|
|
16
17
|
export const list: Schema<ListPermissions | null> = namespace.schema('list')
|
|
17
18
|
export const permute: Schema<null> = namespace.schema('permute')
|
|
19
|
+
export const workflow: Schema<Unit[] | Unit> = namespace.schema('workflow')
|
|
@@ -21,8 +21,8 @@ export class Workflow {
|
|
|
21
21
|
|
|
22
22
|
// eslint-disable-next-line max-params
|
|
23
23
|
public execute
|
|
24
|
-
(
|
|
25
|
-
const path = posix.join(request.
|
|
24
|
+
(input: Input, storage: string, entry: Entry, params: Parameter[]): Execution {
|
|
25
|
+
const path = posix.join(input.request.url, entry.id)
|
|
26
26
|
const parameters: Record<string, string> = {}
|
|
27
27
|
|
|
28
28
|
for (const { name, value } of params)
|
|
@@ -21,7 +21,7 @@ export class Vary implements Family {
|
|
|
21
21
|
|
|
22
22
|
public preflight (instances: Array<Directive | Property>, request: Input): Output {
|
|
23
23
|
/*
|
|
24
|
-
To stop
|
|
24
|
+
To stop constructing `properties` object on each request, Directive Families must be refactored
|
|
25
25
|
from singleton factories to per-Node instances on the Tree.
|
|
26
26
|
*/
|
|
27
27
|
const properties: Properties = {}
|