@toa.io/extensions.exposition 0.24.0-alpha.22 → 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.
Files changed (62) hide show
  1. package/components/context.toa.yaml +1 -1
  2. package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
  3. package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
  4. package/components/octets.storage/manifest.toa.yaml +1 -0
  5. package/components/octets.storage/operations/store.js +2 -2
  6. package/documentation/octets.md +89 -37
  7. package/features/octets.entries.feature +121 -0
  8. package/features/octets.feature +1 -27
  9. package/features/octets.meta.feature +65 -0
  10. package/features/octets.workflows.feature +105 -4
  11. package/features/steps/Captures.ts +3 -2
  12. package/features/steps/HTTP.ts +1 -1
  13. package/features/steps/Parameters.ts +1 -1
  14. package/features/steps/components/octets.tester/manifest.toa.yaml +1 -0
  15. package/features/steps/components/octets.tester/operations/echo.js +7 -0
  16. package/features/steps/tsconfig.json +1 -1
  17. package/package.json +8 -8
  18. package/schemas/octets/delete.cos.yaml +2 -1
  19. package/schemas/octets/list.cos.yaml +2 -1
  20. package/source/HTTP/Server.ts +18 -4
  21. package/source/HTTP/messages.ts +1 -0
  22. package/source/directives/octets/Delete.ts +45 -6
  23. package/source/directives/octets/Fetch.ts +17 -18
  24. package/source/directives/octets/List.ts +36 -6
  25. package/source/directives/octets/Permute.ts +2 -2
  26. package/source/directives/octets/Store.ts +36 -94
  27. package/source/directives/octets/schemas.ts +11 -6
  28. package/source/directives/octets/workflow/Execution.ts +77 -0
  29. package/source/directives/octets/workflow/Workflow.ts +28 -0
  30. package/source/directives/octets/workflow/index.ts +1 -0
  31. package/source/schemas.ts +7 -3
  32. package/transpiled/HTTP/Server.js +13 -3
  33. package/transpiled/HTTP/Server.js.map +1 -1
  34. package/transpiled/HTTP/messages.d.ts +1 -0
  35. package/transpiled/directives/octets/Delete.d.ts +9 -1
  36. package/transpiled/directives/octets/Delete.js +30 -6
  37. package/transpiled/directives/octets/Delete.js.map +1 -1
  38. package/transpiled/directives/octets/Fetch.d.ts +4 -5
  39. package/transpiled/directives/octets/Fetch.js +11 -12
  40. package/transpiled/directives/octets/Fetch.js.map +1 -1
  41. package/transpiled/directives/octets/List.d.ts +6 -1
  42. package/transpiled/directives/octets/List.js +22 -4
  43. package/transpiled/directives/octets/List.js.map +1 -1
  44. package/transpiled/directives/octets/Permute.js +2 -2
  45. package/transpiled/directives/octets/Permute.js.map +1 -1
  46. package/transpiled/directives/octets/Store.d.ts +7 -19
  47. package/transpiled/directives/octets/Store.js +21 -66
  48. package/transpiled/directives/octets/Store.js.map +1 -1
  49. package/transpiled/directives/octets/schemas.d.ts +11 -6
  50. package/transpiled/directives/octets/schemas.js.map +1 -1
  51. package/transpiled/directives/octets/workflow/Execution.d.ts +24 -0
  52. package/transpiled/directives/octets/workflow/Execution.js +55 -0
  53. package/transpiled/directives/octets/workflow/Execution.js.map +1 -0
  54. package/transpiled/directives/octets/workflow/Workflow.d.ts +11 -0
  55. package/transpiled/directives/octets/workflow/Workflow.js +21 -0
  56. package/transpiled/directives/octets/workflow/Workflow.js.map +1 -0
  57. package/transpiled/directives/octets/workflow/index.d.ts +1 -0
  58. package/transpiled/directives/octets/workflow/index.js +6 -0
  59. package/transpiled/directives/octets/workflow/index.js.map +1 -0
  60. package/transpiled/schemas.d.ts +7 -3
  61. package/transpiled/schemas.js.map +1 -1
  62. package/transpiled/tsconfig.tsbuildinfo +1 -1
@@ -70,17 +70,6 @@ Feature: Octets directive family
70
70
  """
71
71
  304 Not Modified
72
72
  """
73
- When the following request is received:
74
- """
75
- GET /10cf16b458f759e0d617f2f3d83599ff:meta HTTP/1.1
76
- accept: text/plain
77
- """
78
- Then the following reply is sent:
79
- """
80
- 403 Forbidden
81
-
82
- Metadata is not accessible.
83
- """
84
73
  When the following request is received:
85
74
  """
86
75
  GET / HTTP/1.1
@@ -245,7 +234,7 @@ Feature: Octets directive family
245
234
  Trailing slash is redundant.
246
235
  """
247
236
 
248
- Scenario: Accessing an Entry and the original BLOLB
237
+ Scenario: Original BLOLB is not accessible
249
238
  Given the annotation:
250
239
  """yaml
251
240
  /:
@@ -267,21 +256,6 @@ Feature: Octets directive family
267
256
  """
268
257
  201 Created
269
258
  """
270
- When the following request is received:
271
- """
272
- GET /10cf16b458f759e0d617f2f3d83599ff:meta HTTP/1.1
273
- accept: application/yaml
274
- """
275
- Then the following reply is sent:
276
- """
277
- 200 OK
278
- content-type: application/yaml
279
- content-length: 124
280
-
281
- id: 10cf16b458f759e0d617f2f3d83599ff
282
- type: application/octet-stream
283
- size: 8169
284
- """
285
259
  When the following request is received:
286
260
  """
287
261
  GET /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
@@ -0,0 +1,65 @@
1
+ Feature: Octets `content-meta` header
2
+
3
+ Scenario: Sending `content-meta` header
4
+ Given the `octets.tester` is running
5
+ And the annotation:
6
+ """yaml
7
+ /:
8
+ auth:anonymous: true
9
+ octets:context: octets
10
+ /*:
11
+ POST:
12
+ octets:store: ~
13
+ /*:
14
+ GET:
15
+ octets:fetch:
16
+ meta: true
17
+ """
18
+ When the stream of `lenna.ascii` is received with the following headers:
19
+ """
20
+ POST /meta-header/ HTTP/1.1
21
+ content-type: application/octet-stream
22
+ content-meta: foo, bar=baz=1
23
+ content-meta: baz=1
24
+ """
25
+ Then the following reply is sent:
26
+ """
27
+ 201 Created
28
+ """
29
+ When the following request is received:
30
+ """
31
+ GET /meta-header/10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
32
+ accept: application/vnd.toa.octets.entry+yaml
33
+ """
34
+ Then the following reply is sent:
35
+ """
36
+ 200 OK
37
+
38
+ id: 10cf16b458f759e0d617f2f3d83599ff
39
+ type: application/octet-stream
40
+ size: 8169
41
+ meta:
42
+ foo: 'true'
43
+ bar: baz=1
44
+ baz: '1'
45
+ """
46
+
47
+ Scenario: CORS allows `content-meta` header
48
+ Given the annotation:
49
+ """yaml
50
+ /:
51
+ octets:context: octets
52
+ POST:
53
+ octets:store: ~
54
+ """
55
+ When the following request is received:
56
+ """
57
+ OPTIONS / HTTP/1.1
58
+ origin: http://example.com
59
+ """
60
+ Then the following reply is sent:
61
+ """
62
+ 204 No Content
63
+ access-control-allow-origin: http://example.com
64
+ access-control-allow-headers: accept, authorization, content-type, content-meta
65
+ """
@@ -2,7 +2,7 @@ Feature: Octets storage workflows
2
2
 
3
3
  Scenario: Running a workflow
4
4
  Given the `octets.tester` is running
5
- Given the annotation:
5
+ And the annotation:
6
6
  """yaml
7
7
  /:
8
8
  auth:anonymous: true
@@ -47,8 +47,8 @@ Feature: Octets storage workflows
47
47
  """
48
48
  When the following request is received:
49
49
  """
50
- GET /10cf16b458f759e0d617f2f3d83599ff:meta HTTP/1.1
51
- accept: application/yaml
50
+ GET /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
51
+ accept: application/vnd.toa.octets.entry+yaml
52
52
  """
53
53
  Then the following reply is sent:
54
54
  """
@@ -74,7 +74,7 @@ Feature: Octets storage workflows
74
74
  content-length: 473831
75
75
  """
76
76
 
77
- Scenario: Getting error when adding metadata to a file
77
+ Scenario: Getting error when running workflow on `store`
78
78
  Given the `octets.tester` is running
79
79
  Given the annotation:
80
80
  """yaml
@@ -112,3 +112,104 @@ Feature: Octets storage workflows
112
112
  message: Something went wrong
113
113
  --cut--
114
114
  """
115
+
116
+ Scenario: Running a workflow on `delete`
117
+ Given the `octets.tester` is running
118
+ And the annotation:
119
+ """yaml
120
+ /:
121
+ auth:anonymous: true
122
+ octets:context: octets
123
+ POST:
124
+ octets:store: ~
125
+ /*:
126
+ GET:
127
+ octets:fetch: ~
128
+ DELETE:
129
+ octets:delete:
130
+ workflow:
131
+ echo: octets.tester.echo
132
+ """
133
+ When the stream of `lenna.ascii` is received with the following headers:
134
+ """
135
+ POST / HTTP/1.1
136
+ content-type: application/octet-stream
137
+ """
138
+ Then the following reply is sent:
139
+ """
140
+ 201 Created
141
+ """
142
+ When the following request is received:
143
+ """
144
+ DELETE /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
145
+ accept: application/yaml
146
+ """
147
+ Then the following reply is sent:
148
+ """
149
+ 202 Accepted
150
+ content-type: multipart/yaml; boundary=cut
151
+
152
+ --cut
153
+ echo: 10cf16b458f759e0d617f2f3d83599ff
154
+ --cut--
155
+ """
156
+ When the following request is received:
157
+ """
158
+ GET /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
159
+ """
160
+ Then the following reply is sent:
161
+ """
162
+ 404 Not Found
163
+ """
164
+
165
+ Scenario: Error in the workflow on `delete`
166
+ Given the `octets.tester` is running
167
+ And the annotation:
168
+ """yaml
169
+ /:
170
+ auth:anonymous: true
171
+ octets:context: octets
172
+ POST:
173
+ octets:store: ~
174
+ /*:
175
+ GET:
176
+ octets:fetch: ~
177
+ DELETE:
178
+ octets:delete:
179
+ workflow:
180
+ err: octets.tester.err
181
+ """
182
+ When the stream of `lenna.ascii` is received with the following headers:
183
+ """
184
+ POST / HTTP/1.1
185
+ content-type: application/octet-stream
186
+ """
187
+ Then the following reply is sent:
188
+ """
189
+ 201 Created
190
+ """
191
+ When the following request is received:
192
+ """
193
+ DELETE /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
194
+ accept: application/yaml
195
+ """
196
+ Then the following reply is sent:
197
+ """
198
+ 202 Accepted
199
+ content-type: multipart/yaml; boundary=cut
200
+
201
+ --cut
202
+ error:
203
+ step: err
204
+ code: ERROR
205
+ message: Something went wrong
206
+ --cut--
207
+ """
208
+ When the following request is received:
209
+ """
210
+ GET /10cf16b458f759e0d617f2f3d83599ff HTTP/1.1
211
+ """
212
+ Then the following reply is sent:
213
+ """
214
+ 200 OK
215
+ """
@@ -1,5 +1,6 @@
1
- import * as http from '@toa.io/http'
1
+ import * as http from '@toa.io/agent'
2
2
  import { binding } from 'cucumber-tsflow'
3
3
 
4
4
  @binding()
5
- export class Captures extends http.Captures {}
5
+ export class Captures extends http.Captures {
6
+ }
@@ -2,7 +2,7 @@ import * as assert from 'node:assert'
2
2
  import * as fs from 'node:fs'
3
3
  import * as path from 'node:path'
4
4
  import { binding, then, when } from 'cucumber-tsflow'
5
- import * as http from '@toa.io/http'
5
+ import * as http from '@toa.io/agent'
6
6
  import * as msgpack from 'msgpackr'
7
7
  import * as YAML from 'js-yaml'
8
8
  import { Captures } from './Captures'
@@ -16,6 +16,6 @@ process.env.TOA_DEV = '1'
16
16
  process.env.TOA_STORAGES = encode({
17
17
  octets: {
18
18
  provider: 'tmp',
19
- prefix: 'test'
19
+ directory: 'exposition'
20
20
  }
21
21
  })
@@ -12,4 +12,5 @@ operations:
12
12
  bar: *operation
13
13
  baz: *operation
14
14
  err: *operation
15
+ echo: *operation
15
16
  diversify: *operation
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ async function baz (input) {
4
+ return input.entry.id
5
+ }
6
+
7
+ exports.computation = baz
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ESNext",
4
- "outDir": "/dev/null",
4
+ "noEmit": true,
5
5
  "moduleResolution": "node",
6
6
  "experimentalDecorators": true,
7
7
  "emitDecoratorMetadata": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "0.24.0-alpha.22",
3
+ "version": "0.24.0-alpha.23",
4
4
  "description": "Toa Exposition",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -17,10 +17,10 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@toa.io/core": "0.24.0-alpha.22",
21
- "@toa.io/generic": "0.24.0-alpha.22",
22
- "@toa.io/schemas": "0.24.0-alpha.22",
23
- "@toa.io/streams": "0.24.0-alpha.22",
20
+ "@toa.io/core": "0.24.0-alpha.23",
21
+ "@toa.io/generic": "0.24.0-alpha.23",
22
+ "@toa.io/schemas": "0.24.0-alpha.23",
23
+ "@toa.io/streams": "0.24.0-alpha.23",
24
24
  "bcryptjs": "2.4.3",
25
25
  "error-value": "0.3.0",
26
26
  "express": "4.18.2",
@@ -45,12 +45,12 @@
45
45
  "features": "cucumber-js"
46
46
  },
47
47
  "devDependencies": {
48
- "@toa.io/extensions.storages": "0.24.0-alpha.22",
49
- "@toa.io/http": "0.24.0-alpha.22",
48
+ "@toa.io/agent": "0.24.0-alpha.23",
49
+ "@toa.io/extensions.storages": "0.24.0-alpha.23",
50
50
  "@types/bcryptjs": "2.4.3",
51
51
  "@types/cors": "2.8.13",
52
52
  "@types/express": "4.17.17",
53
53
  "@types/negotiator": "0.6.1"
54
54
  },
55
- "gitHead": "9680eca8da28019924a4c7cac5d803954d6c6936"
55
+ "gitHead": "df9de3cbb530e8f985a660bca0bf65bd027dbb01"
56
56
  }
@@ -1 +1,2 @@
1
- ~
1
+ workflow+: <string>
2
+ _: true
@@ -1 +1,2 @@
1
- ~
1
+ meta: boolean
2
+ _: true
@@ -7,7 +7,6 @@ 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
 
@@ -84,7 +83,8 @@ export class Server extends Connector {
84
83
  private extend (request: Request): IncomingMessage {
85
84
  const message = request as IncomingMessage
86
85
 
87
- message.encoder = negotiate(request)
86
+ negotiate(request, message)
87
+
88
88
  message.pipelines = { body: [], response: [] }
89
89
 
90
90
  message.parse = async <T> (): Promise<T> => {
@@ -145,11 +145,23 @@ function supportedMethods (methods: Set<string>) {
145
145
  }
146
146
  }
147
147
 
148
- function negotiate (request: Request): Format | null {
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
+
149
161
  const negotiator = new Negotiator(request)
150
162
  const mediaType = negotiator.mediaType(types)
151
163
 
152
- return mediaType === undefined ? null : formats[mediaType]
164
+ message.encoder = mediaType === undefined ? null : formats[mediaType]
153
165
  }
154
166
 
155
167
  // https://github.com/whatwg/fetch/issues/1254
@@ -177,3 +189,5 @@ interface Properties {
177
189
  }
178
190
 
179
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})$/
@@ -104,6 +104,7 @@ export interface IncomingMessage extends Request {
104
104
  query: Query
105
105
  parse: <T> () => Promise<T>
106
106
  encoder: Format | null
107
+ subtype: string | null
107
108
  pipelines: {
108
109
  body: Array<(input: unknown) => unknown>
109
110
  response: Array<(output: OutgoingMessage) => void>
@@ -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 (value: null, discovery: Promise<Component>) {
15
- schemas.remove.validate(value)
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 input = { storage, path: request.path }
24
- const error = await this.storage.invoke<Maybe<unknown>>('delete', { input })
32
+ const entry = await this.storage.invoke<Maybe<Entry>>('get',
33
+ { input: { storage, path: request.url } })
25
34
 
26
- if (error instanceof Error)
35
+ if (entry instanceof Error)
27
36
  throw new NotFound()
28
37
 
29
- return {}
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: Partial<Permissions> | null, discovery: Promise<Component>) {
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
- if (request.url.slice(-5) === ':meta')
30
- return await this.get(storage, request)
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
- private async fetch (storage: string, request: Input): Promise<Output> {
36
- const filename = posix.basename(request.url)
37
- const variant = filename.includes('.')
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
- 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 }
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: boolean
77
- meta: boolean
74
+ export interface Permissions {
75
+ blob?: boolean
76
+ meta?: boolean
78
77
  }
79
78
 
80
79
  interface FetchResult {
@@ -1,5 +1,7 @@
1
- import { NotFound } from '../../HTTP'
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 (value: null, discovery: Promise<Component>) {
16
- schemas.remove.validate(value)
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 input = { storage, path: request.path }
25
- const list = await this.storage.invoke<Maybe<unknown>>('list', { input })
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
- return { body: list }
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.remove.validate(value)
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.path
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 })