@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.
- package/components/context.toa.yaml +1 -1
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/components/octets.storage/manifest.toa.yaml +1 -0
- package/components/octets.storage/operations/store.js +2 -2
- package/documentation/octets.md +89 -37
- package/features/octets.entries.feature +121 -0
- package/features/octets.feature +1 -27
- package/features/octets.meta.feature +65 -0
- package/features/octets.workflows.feature +105 -4
- package/features/steps/Captures.ts +3 -2
- package/features/steps/HTTP.ts +1 -1
- package/features/steps/Parameters.ts +1 -1
- package/features/steps/components/octets.tester/manifest.toa.yaml +1 -0
- package/features/steps/components/octets.tester/operations/echo.js +7 -0
- package/features/steps/tsconfig.json +1 -1
- package/package.json +8 -8
- package/schemas/octets/delete.cos.yaml +2 -1
- package/schemas/octets/list.cos.yaml +2 -1
- package/source/HTTP/Server.ts +18 -4
- package/source/HTTP/messages.ts +1 -0
- package/source/directives/octets/Delete.ts +45 -6
- package/source/directives/octets/Fetch.ts +17 -18
- package/source/directives/octets/List.ts +36 -6
- package/source/directives/octets/Permute.ts +2 -2
- package/source/directives/octets/Store.ts +36 -94
- package/source/directives/octets/schemas.ts +11 -6
- package/source/directives/octets/workflow/Execution.ts +77 -0
- package/source/directives/octets/workflow/Workflow.ts +28 -0
- package/source/directives/octets/workflow/index.ts +1 -0
- package/source/schemas.ts +7 -3
- package/transpiled/HTTP/Server.js +13 -3
- package/transpiled/HTTP/Server.js.map +1 -1
- package/transpiled/HTTP/messages.d.ts +1 -0
- package/transpiled/directives/octets/Delete.d.ts +9 -1
- package/transpiled/directives/octets/Delete.js +30 -6
- package/transpiled/directives/octets/Delete.js.map +1 -1
- package/transpiled/directives/octets/Fetch.d.ts +4 -5
- package/transpiled/directives/octets/Fetch.js +11 -12
- package/transpiled/directives/octets/Fetch.js.map +1 -1
- package/transpiled/directives/octets/List.d.ts +6 -1
- package/transpiled/directives/octets/List.js +22 -4
- package/transpiled/directives/octets/List.js.map +1 -1
- package/transpiled/directives/octets/Permute.js +2 -2
- package/transpiled/directives/octets/Permute.js.map +1 -1
- package/transpiled/directives/octets/Store.d.ts +7 -19
- package/transpiled/directives/octets/Store.js +21 -66
- package/transpiled/directives/octets/Store.js.map +1 -1
- package/transpiled/directives/octets/schemas.d.ts +11 -6
- package/transpiled/directives/octets/schemas.js.map +1 -1
- package/transpiled/directives/octets/workflow/Execution.d.ts +24 -0
- package/transpiled/directives/octets/workflow/Execution.js +55 -0
- package/transpiled/directives/octets/workflow/Execution.js.map +1 -0
- package/transpiled/directives/octets/workflow/Workflow.d.ts +11 -0
- package/transpiled/directives/octets/workflow/Workflow.js +21 -0
- package/transpiled/directives/octets/workflow/Workflow.js.map +1 -0
- package/transpiled/directives/octets/workflow/index.d.ts +1 -0
- package/transpiled/directives/octets/workflow/index.js +6 -0
- package/transpiled/directives/octets/workflow/index.js.map +1 -0
- package/transpiled/schemas.d.ts +7 -3
- package/transpiled/schemas.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
package/features/octets.feature
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
"""
|
package/features/steps/HTTP.ts
CHANGED
|
@@ -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/
|
|
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'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.exposition",
|
|
3
|
-
"version": "0.24.0-alpha.
|
|
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.
|
|
21
|
-
"@toa.io/generic": "0.24.0-alpha.
|
|
22
|
-
"@toa.io/schemas": "0.24.0-alpha.
|
|
23
|
-
"@toa.io/streams": "0.24.0-alpha.
|
|
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/
|
|
49
|
-
"@toa.io/
|
|
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": "
|
|
55
|
+
"gitHead": "df9de3cbb530e8f985a660bca0bf65bd027dbb01"
|
|
56
56
|
}
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
workflow+: <string>
|
|
2
|
+
_: true
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
meta: boolean
|
|
2
|
+
_: true
|
package/source/HTTP/Server.ts
CHANGED
|
@@ -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
|
-
|
|
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):
|
|
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
|
-
|
|
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})$/
|
package/source/HTTP/messages.ts
CHANGED
|
@@ -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 (
|
|
15
|
-
schemas.remove.validate(
|
|
20
|
+
public constructor (options: Options | null, discovery: Promise<Component>, remotes: Remotes) {
|
|
21
|
+
schemas.remove.validate(options)
|
|
22
|
+
|
|
23
|
+
if (options?.workflow !== undefined)
|
|
24
|
+
this.workflow = new Workflow(options.workflow, remotes)
|
|
16
25
|
|
|
17
26
|
this.discovery = discovery
|
|
18
27
|
}
|
|
@@ -20,12 +29,42 @@ export class Delete implements Directive {
|
|
|
20
29
|
public async apply (storage: string, request: Input): Promise<Output> {
|
|
21
30
|
this.storage ??= await this.discovery
|
|
22
31
|
|
|
23
|
-
const
|
|
24
|
-
|
|
32
|
+
const entry = await this.storage.invoke<Maybe<Entry>>('get',
|
|
33
|
+
{ input: { storage, path: request.url } })
|
|
25
34
|
|
|
26
|
-
if (
|
|
35
|
+
if (entry instanceof Error)
|
|
27
36
|
throw new NotFound()
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
const output: Output = {}
|
|
39
|
+
|
|
40
|
+
if (this.workflow !== undefined) {
|
|
41
|
+
output.status = 202
|
|
42
|
+
output.body = Readable.from(this.execute(request, storage, entry))
|
|
43
|
+
} else
|
|
44
|
+
await this.delete(storage, request)
|
|
45
|
+
|
|
46
|
+
return output
|
|
30
47
|
}
|
|
48
|
+
|
|
49
|
+
private async delete (storage: string, request: Input): Promise<void> {
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
51
|
+
await this.storage!.invoke('delete',
|
|
52
|
+
{ input: { storage, path: request.url } })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async * execute (request: Input, storage: string, entry: Entry): AsyncGenerator {
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
57
|
+
for await (const chunk of this.workflow!.execute(request, storage, entry)) {
|
|
58
|
+
yield chunk
|
|
59
|
+
|
|
60
|
+
if (typeof chunk === 'object' && chunk !== null && 'error' in chunk)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await this.delete(storage, request)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface Options {
|
|
69
|
+
workflow?: Unit[] | Unit
|
|
31
70
|
}
|
|
@@ -12,11 +12,11 @@ import type { Directive, Input } from './types'
|
|
|
12
12
|
export class Fetch implements Directive {
|
|
13
13
|
public readonly targeted = true
|
|
14
14
|
|
|
15
|
-
private readonly permissions: Permissions = { blob: true, meta: false }
|
|
15
|
+
private readonly permissions: Required<Permissions> = { blob: true, meta: false }
|
|
16
16
|
private readonly discovery: Promise<Component>
|
|
17
17
|
private storage: Component = null as unknown as Component
|
|
18
18
|
|
|
19
|
-
public constructor (permissions:
|
|
19
|
+
public constructor (permissions: Permissions | null, discovery: Promise<Component>) {
|
|
20
20
|
schemas.fetch.validate(permissions)
|
|
21
21
|
|
|
22
22
|
Object.assign(this.permissions, permissions)
|
|
@@ -26,19 +26,22 @@ export class Fetch implements Directive {
|
|
|
26
26
|
public async apply (storage: string, request: Input): Promise<Output> {
|
|
27
27
|
this.storage ??= await this.discovery
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
else
|
|
32
|
-
return await this.fetch(storage, request)
|
|
33
|
-
}
|
|
29
|
+
const variant = posix.basename(request.url).includes('.')
|
|
30
|
+
const metadata = request.subtype === 'octets.entry'
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
if (!variant && metadata)
|
|
33
|
+
if (this.permissions.meta)
|
|
34
|
+
return this.get(storage, request)
|
|
35
|
+
else
|
|
36
|
+
throw new Forbidden('Metadata is not accessible.')
|
|
38
37
|
|
|
39
38
|
if (!variant && !this.permissions.blob)
|
|
40
39
|
throw new Forbidden('BLOB variant must be specified.')
|
|
41
40
|
|
|
41
|
+
return await this.fetch(storage, request)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async fetch (storage: string, request: Input): Promise<Output> {
|
|
42
45
|
if ('if-none-match' in request.headers)
|
|
43
46
|
return { status: 304 }
|
|
44
47
|
|
|
@@ -58,11 +61,7 @@ export class Fetch implements Directive {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
private async get (storage: string, request: Input): Promise<Output> {
|
|
61
|
-
|
|
62
|
-
throw new Forbidden('Metadata is not accessible.')
|
|
63
|
-
|
|
64
|
-
const path = request.url.slice(0, -5)
|
|
65
|
-
const input = { storage, path }
|
|
64
|
+
const input = { storage, path: request.url }
|
|
66
65
|
const entry = await this.storage.invoke<Maybe<Entry>>('get', { input })
|
|
67
66
|
|
|
68
67
|
if (entry instanceof Error)
|
|
@@ -72,9 +71,9 @@ export class Fetch implements Directive {
|
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
interface Permissions {
|
|
76
|
-
blob
|
|
77
|
-
meta
|
|
74
|
+
export interface Permissions {
|
|
75
|
+
blob?: boolean
|
|
76
|
+
meta?: boolean
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
interface FetchResult {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { posix } from 'node:path'
|
|
2
|
+
import { Forbidden, NotFound } from '../../HTTP'
|
|
2
3
|
import * as schemas from './schemas'
|
|
4
|
+
import type { Entry } from '@toa.io/extensions.storages'
|
|
3
5
|
import type { Maybe } from '@toa.io/types'
|
|
4
6
|
import type { Component } from '@toa.io/core'
|
|
5
7
|
import type { Output } from '../../io'
|
|
@@ -9,24 +11,52 @@ import type { Directive, Input } from './types'
|
|
|
9
11
|
export class List implements Directive {
|
|
10
12
|
public readonly targeted = false
|
|
11
13
|
|
|
14
|
+
private readonly permissions: Required<Permissions> = { meta: false }
|
|
12
15
|
private readonly discovery: Promise<Component>
|
|
13
16
|
private storage: Component | null = null
|
|
14
17
|
|
|
15
|
-
public constructor (
|
|
16
|
-
schemas.
|
|
18
|
+
public constructor (permissions: Permissions | null, discovery: Promise<Component>) {
|
|
19
|
+
schemas.list.validate(permissions)
|
|
17
20
|
|
|
21
|
+
Object.assign(this.permissions, permissions)
|
|
18
22
|
this.discovery = discovery
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
public async apply (storage: string, request: Input): Promise<Output> {
|
|
22
26
|
this.storage ??= await this.discovery
|
|
23
27
|
|
|
24
|
-
const
|
|
25
|
-
|
|
28
|
+
const metadata = request.subtype === 'octets.entries'
|
|
29
|
+
|
|
30
|
+
if (metadata && !this.permissions.meta)
|
|
31
|
+
throw new Forbidden('Metadata is not accessible.')
|
|
32
|
+
|
|
33
|
+
const input = { storage, path: request.url }
|
|
34
|
+
const list = await this.storage.invoke<Maybe<string[]>>('list', { input })
|
|
26
35
|
|
|
27
36
|
if (list instanceof Error)
|
|
28
37
|
throw new NotFound()
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
const body = metadata
|
|
40
|
+
? await this.expand(storage, request.url, list)
|
|
41
|
+
: list
|
|
42
|
+
|
|
43
|
+
return { body }
|
|
31
44
|
}
|
|
45
|
+
|
|
46
|
+
private async expand (storage: string, prefix: string, list: string[]):
|
|
47
|
+
Promise<Array<Maybe<Entry>>> {
|
|
48
|
+
const promises = list.map(async (id) => {
|
|
49
|
+
const path = posix.join(prefix, id)
|
|
50
|
+
const input = { storage, path }
|
|
51
|
+
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- ensured in `apply`
|
|
53
|
+
return this.storage!.invoke<Maybe<Entry>>('get', { input })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return await Promise.all(promises)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Permissions {
|
|
61
|
+
meta?: boolean
|
|
32
62
|
}
|
|
@@ -13,7 +13,7 @@ export class Permute implements Directive {
|
|
|
13
13
|
private storage: Component | null = null
|
|
14
14
|
|
|
15
15
|
public constructor (value: null, discovery: Promise<Component>) {
|
|
16
|
-
schemas.
|
|
16
|
+
schemas.permute.validate(value)
|
|
17
17
|
|
|
18
18
|
this.discovery = discovery
|
|
19
19
|
}
|
|
@@ -24,7 +24,7 @@ export class Permute implements Directive {
|
|
|
24
24
|
if (request.encoder === null)
|
|
25
25
|
throw new NotAcceptable()
|
|
26
26
|
|
|
27
|
-
const path = request.
|
|
27
|
+
const path = request.url
|
|
28
28
|
const list = await request.parse()
|
|
29
29
|
const input = { storage, path, list }
|
|
30
30
|
const error = await this.storage.invoke<Maybe<unknown>>('permute', { input })
|