@toa.io/extensions.exposition 0.22.0 → 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.
- package/components/identity.basic/source/authenticate.ts +9 -6
- package/components/identity.basic/source/create.ts +1 -2
- package/components/identity.basic/source/transit.ts +10 -6
- package/components/identity.basic/source/types.ts +2 -2
- package/components/identity.tokens/source/authenticate.ts +6 -5
- package/components/identity.tokens/source/decrypt.ts +12 -4
- package/components/identity.tokens/source/encrypt.test.ts +1 -1
- package/components/identity.tokens/source/types.ts +2 -2
- 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/octets.md +196 -0
- package/documentation/protocol.md +49 -5
- package/features/access.feature +1 -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 +47 -5
- package/features/steps/Parameters.ts +5 -1
- package/features/steps/Workspace.ts +3 -2
- 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 +11 -10
- package/readme.md +7 -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 +12 -7
- package/source/HTTP/Server.fixtures.ts +2 -6
- package/source/HTTP/Server.test.ts +7 -31
- package/source/HTTP/Server.ts +31 -16
- 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 +58 -33
- package/source/RTD/Route.ts +1 -1
- package/source/RTD/segment.ts +2 -1
- package/source/Remotes.ts +8 -0
- package/source/directives/auth/Family.ts +25 -22
- package/source/directives/auth/Incept.ts +3 -3
- package/source/directives/auth/Rule.ts +1 -1
- package/source/directives/auth/types.ts +2 -2
- package/source/directives/index.ts +2 -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/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)
|
|
@@ -1,32 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Nope } from 'nopeable'
|
|
3
|
-
import { type Parameter } from '../../RTD'
|
|
4
|
-
import { type Family, type Output } from '../../Directive'
|
|
5
|
-
import { type Remotes } from '../../Remotes'
|
|
1
|
+
import { match } from 'matchacho'
|
|
6
2
|
import * as http from '../../HTTP'
|
|
7
|
-
import {
|
|
8
|
-
type AuthenticationResult,
|
|
9
|
-
type Ban,
|
|
10
|
-
type Directive,
|
|
11
|
-
type Discovery,
|
|
12
|
-
type Extension,
|
|
13
|
-
type Identity,
|
|
14
|
-
type Input, type Remote,
|
|
15
|
-
type Schemes
|
|
16
|
-
} from './types'
|
|
17
3
|
import { Anonymous } from './Anonymous'
|
|
18
4
|
import { Id } from './Id'
|
|
19
5
|
import { Role } from './Role'
|
|
20
6
|
import { Rule } from './Rule'
|
|
21
7
|
import { Incept } from './Incept'
|
|
8
|
+
import { Echo } from './Echo'
|
|
22
9
|
import { split } from './split'
|
|
23
|
-
import { PRIMARY, PROVIDERS } from './schemes'
|
|
24
10
|
import { Scheme } from './Scheme'
|
|
25
|
-
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'
|
|
26
27
|
|
|
27
28
|
class Authorization implements Family<Directive, Extension> {
|
|
28
29
|
public readonly name: string = 'auth'
|
|
29
30
|
public readonly mandatory: boolean = true
|
|
31
|
+
|
|
30
32
|
private readonly schemes = {} as unknown as Schemes
|
|
31
33
|
private readonly discovery = {} as unknown as Discovery
|
|
32
34
|
private tokens: Component | null = null
|
|
@@ -41,10 +43,11 @@ class Authorization implements Family<Directive, Extension> {
|
|
|
41
43
|
for (const name of REMOTES)
|
|
42
44
|
this.discovery[name] ??= remotes.discover('identity', name)
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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))
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
public async preflight
|
|
@@ -107,7 +110,7 @@ class Authorization implements Family<Directive, Extension> {
|
|
|
107
110
|
const result = await this.schemes[scheme]
|
|
108
111
|
.invoke<AuthenticationResult>('authenticate', { input: credentials })
|
|
109
112
|
|
|
110
|
-
if (result instanceof
|
|
113
|
+
if (result instanceof Error)
|
|
111
114
|
return null
|
|
112
115
|
|
|
113
116
|
const identity = result.identity
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Maybe } from '@toa.io/types'
|
|
2
2
|
import * as http from '../../HTTP'
|
|
3
3
|
import { type Directive, type Discovery, type Identity, type Input, type Schemes } from './types'
|
|
4
4
|
import { split } from './split'
|
|
@@ -31,9 +31,9 @@ export class Incept implements Directive {
|
|
|
31
31
|
this.schemes[scheme] ??= await this.discovery[provider]
|
|
32
32
|
|
|
33
33
|
const identity = await this.schemes[scheme]
|
|
34
|
-
.invoke<
|
|
34
|
+
.invoke<Maybe<Identity>>('create', { input: { id, credentials } })
|
|
35
35
|
|
|
36
|
-
if (identity instanceof
|
|
36
|
+
if (identity instanceof Error)
|
|
37
37
|
throw new http.Conflict(identity)
|
|
38
38
|
|
|
39
39
|
request.identity = identity
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Component } from '@toa.io/core'
|
|
2
|
-
import { type
|
|
2
|
+
import { type Maybe } from '@toa.io/types'
|
|
3
3
|
import { type Parameter } from '../../RTD'
|
|
4
4
|
import type * as http from '../../HTTP'
|
|
5
5
|
import type * as directive from '../../Directive'
|
|
@@ -29,7 +29,7 @@ export interface Ban {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export type Input = directive.Input & Extension
|
|
32
|
-
export type AuthenticationResult =
|
|
32
|
+
export type AuthenticationResult = Maybe<{ identity: Identity, refresh: boolean }>
|
|
33
33
|
|
|
34
34
|
export type Scheme = 'basic' | 'token'
|
|
35
35
|
export type Remote = 'basic' | 'tokens' | 'roles' | 'bans'
|
|
@@ -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 = {
|
|
52
|
+
'content-type': result.type,
|
|
53
|
+
'content-length': result.size,
|
|
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NotAcceptable, 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 Permute 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
|
+
if (request.encoder === null)
|
|
25
|
+
throw new NotAcceptable()
|
|
26
|
+
|
|
27
|
+
const path = request.path
|
|
28
|
+
const list = await request.parse()
|
|
29
|
+
const input = { storage, path, list }
|
|
30
|
+
const error = await this.storage.invoke<Maybe<unknown>>('permute', { input })
|
|
31
|
+
|
|
32
|
+
if (error instanceof Error)
|
|
33
|
+
throw new NotFound()
|
|
34
|
+
|
|
35
|
+
return {}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Readable } from 'node:stream'
|
|
2
|
+
import { posix } from 'node:path'
|
|
3
|
+
import { match } from 'matchacho'
|
|
4
|
+
import { promex } from '@toa.io/generic'
|
|
5
|
+
import { BadRequest, UnsupportedMediaType } from '../../HTTP'
|
|
6
|
+
import * as schemas from './schemas'
|
|
7
|
+
import type { Maybe } from '@toa.io/types'
|
|
8
|
+
import type { Entry } from '@toa.io/extensions.storages'
|
|
9
|
+
import type { Remotes } from '../../Remotes'
|
|
10
|
+
import type { ErrorType } from 'error-value'
|
|
11
|
+
import type { Component } from '@toa.io/core'
|
|
12
|
+
import type { Output } from '../../Directive'
|
|
13
|
+
import type { Directive, Input } from './types'
|
|
14
|
+
|
|
15
|
+
export class Store implements Directive {
|
|
16
|
+
public readonly targeted = false
|
|
17
|
+
|
|
18
|
+
private readonly accept: string | undefined
|
|
19
|
+
private readonly workflow: Workflow | undefined
|
|
20
|
+
private readonly discovery: Record<string, Promise<Component>> = {}
|
|
21
|
+
private readonly remotes: Remotes
|
|
22
|
+
private readonly components: Record<string, Component> = {}
|
|
23
|
+
private storage: Component | null = null
|
|
24
|
+
|
|
25
|
+
public constructor
|
|
26
|
+
(options: Options | null, discovery: Promise<Component>, remotes: Remotes) {
|
|
27
|
+
schemas.store.validate(options)
|
|
28
|
+
|
|
29
|
+
this.accept = match(options?.accept,
|
|
30
|
+
String, (value: string) => value,
|
|
31
|
+
Array, (types: string[]) => types.join(','),
|
|
32
|
+
undefined)
|
|
33
|
+
|
|
34
|
+
this.workflow = match(options?.workflow,
|
|
35
|
+
Array, (units: Unit[]) => units,
|
|
36
|
+
Object, (unit: Unit) => [unit],
|
|
37
|
+
undefined)
|
|
38
|
+
|
|
39
|
+
this.discovery.storage = discovery
|
|
40
|
+
this.remotes = remotes
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public async apply (storage: string, request: Input): Promise<Output> {
|
|
44
|
+
this.storage ??= await this.discovery.storage
|
|
45
|
+
|
|
46
|
+
const input = { storage, request, accept: this.accept }
|
|
47
|
+
const entry = await this.storage.invoke('store', { input })
|
|
48
|
+
|
|
49
|
+
return match<Output>(entry,
|
|
50
|
+
Error, (error: ErrorType) => this.throw(error),
|
|
51
|
+
() => this.reply(request, storage, entry))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private reply (request: Input, storage: string, entry: Entry): Output {
|
|
55
|
+
const body = this.workflow === undefined
|
|
56
|
+
? entry
|
|
57
|
+
: Readable.from(this.execute(request, storage, entry))
|
|
58
|
+
|
|
59
|
+
return { body }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private throw (error: ErrorType): never {
|
|
63
|
+
throw match(error.code,
|
|
64
|
+
'NOT_ACCEPTABLE', () => new UnsupportedMediaType(),
|
|
65
|
+
'TYPE_MISMATCH', () => new BadRequest(),
|
|
66
|
+
error)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* eslint-disable no-useless-return, max-depth */
|
|
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 as 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}`
|
|
132
|
+
|
|
133
|
+
this.components[key] ??= await this.discover(key, namespace, component)
|
|
134
|
+
|
|
135
|
+
return await this.components[key].invoke(operation, { input: context })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async discover (key: string, namespace: string, component: string): Promise<Component> {
|
|
139
|
+
if (this.discovery[key] === undefined)
|
|
140
|
+
this.discovery[key] = this.remotes.discover(namespace, component)
|
|
141
|
+
|
|
142
|
+
return await this.discovery[key]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
type Unit = Record<string, string>
|
|
147
|
+
type Workflow = Unit[]
|
|
148
|
+
|
|
149
|
+
interface Options {
|
|
150
|
+
accept: string | string[]
|
|
151
|
+
workflow: Workflow | Unit
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface Context {
|
|
155
|
+
storage: string
|
|
156
|
+
path: string
|
|
157
|
+
entry: Entry
|
|
158
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import schemas from '@toa.io/schemas'
|
|
3
|
+
|
|
4
|
+
const path = resolve(__dirname, '../../../schemas/octets')
|
|
5
|
+
const namespace = schemas.namespace(path)
|
|
6
|
+
|
|
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')
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type * as directive from '../../Directive'
|
|
2
|
+
|
|
3
|
+
export interface Directive {
|
|
4
|
+
readonly targeted: boolean
|
|
5
|
+
|
|
6
|
+
apply: (storage: string, input: Input) => directive.Output | Promise<directive.Output>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Extension {
|
|
10
|
+
octets?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Input = directive.Input & Extension
|