@toa.io/extensions.exposition 0.22.1 → 1.0.0-alpha.0

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