@toa.io/extensions.storages 1.0.0-alpha.29 → 1.0.0-alpha.291
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/CHANGELOG.md +27 -0
- package/package.json +13 -18
- package/readme.md +100 -47
- package/schemas/annotation.cos.yaml +3 -2
- package/schemas/cloudinary.cos.yaml +38 -0
- package/schemas/fs.cos.yaml +5 -1
- package/schemas/s3.cos.yaml +4 -3
- package/schemas/spaces.cos.yaml +16 -0
- package/schemas/test.cos.yaml +3 -1
- package/schemas/tmp.cos.yaml +3 -2
- package/source/Annotation.ts +28 -15
- package/source/Aspect.ts +13 -6
- package/source/Entry.ts +13 -11
- package/source/Factory.ts +37 -31
- package/source/Provider.ts +28 -25
- package/source/Scanner.ts +61 -44
- package/source/Secrets.ts +6 -0
- package/source/Storage.test.ts +141 -297
- package/source/Storage.ts +96 -202
- package/source/context.ts +14 -0
- package/source/deployment.ts +56 -22
- package/source/errors.ts +3 -0
- package/source/index.ts +9 -4
- package/source/manifest.ts +5 -6
- package/source/providers/Cloudinary.ts +318 -0
- package/source/providers/Declaration.ts +8 -5
- package/source/providers/FileSystem.ts +69 -33
- package/source/providers/S3.ts +109 -131
- package/source/providers/Spaces.ts +25 -0
- package/source/providers/Temporary.ts +3 -4
- package/source/providers/Test.ts +6 -8
- package/source/providers/index.test.ts +115 -90
- package/source/providers/index.ts +22 -13
- package/source/providers/index.types.ts +5 -0
- package/source/providers/readme.md +13 -11
- package/source/providers/secrets.ts +19 -0
- package/source/schemas.test.ts +15 -10
- package/source/schemas.ts +5 -4
- package/source/test/.env.example +8 -0
- package/source/test/arny.jpg +0 -0
- package/source/test/lenna.48x48.jpeg +0 -0
- package/source/test/plank.mp4 +0 -0
- package/source/test/sport.mp4 +0 -0
- package/source/test/util.ts +82 -14
- package/transpiled/Annotation.d.ts +1 -1
- package/transpiled/Annotation.js +13 -39
- package/transpiled/Annotation.js.map +1 -1
- package/transpiled/Aspect.d.ts +8 -4
- package/transpiled/Aspect.js +12 -14
- package/transpiled/Aspect.js.map +1 -1
- package/transpiled/Entry.d.ts +14 -11
- package/transpiled/Entry.js +1 -2
- package/transpiled/Factory.d.ts +1 -1
- package/transpiled/Factory.js +28 -30
- package/transpiled/Factory.js.map +1 -1
- package/transpiled/Provider.d.ts +17 -19
- package/transpiled/Provider.js +8 -33
- package/transpiled/Provider.js.map +1 -1
- package/transpiled/Scanner.d.ts +5 -4
- package/transpiled/Scanner.js +33 -26
- package/transpiled/Scanner.js.map +1 -1
- package/transpiled/Secrets.d.ts +5 -0
- package/transpiled/Secrets.js +2 -0
- package/transpiled/Secrets.js.map +1 -0
- package/transpiled/Storage.d.ts +23 -22
- package/transpiled/Storage.js +77 -142
- package/transpiled/Storage.js.map +1 -1
- package/transpiled/context.d.ts +3 -0
- package/transpiled/context.js +12 -0
- package/transpiled/context.js.map +1 -0
- package/transpiled/deployment.d.ts +1 -1
- package/transpiled/deployment.js +39 -43
- package/transpiled/deployment.js.map +1 -1
- package/transpiled/errors.d.ts +7 -0
- package/transpiled/errors.js +4 -0
- package/transpiled/errors.js.map +1 -0
- package/transpiled/index.d.ts +7 -4
- package/transpiled/index.js +5 -9
- package/transpiled/index.js.map +1 -1
- package/transpiled/manifest.js +6 -7
- package/transpiled/manifest.js.map +1 -1
- package/transpiled/providers/Cloudinary.d.ts +50 -0
- package/transpiled/providers/Cloudinary.js +223 -0
- package/transpiled/providers/Cloudinary.js.map +1 -0
- package/transpiled/providers/Declaration.d.ts +9 -5
- package/transpiled/providers/Declaration.js +1 -2
- package/transpiled/providers/FileSystem.d.ts +16 -11
- package/transpiled/providers/FileSystem.js +64 -43
- package/transpiled/providers/FileSystem.js.map +1 -1
- package/transpiled/providers/S3.d.ts +14 -19
- package/transpiled/providers/S3.js +72 -123
- package/transpiled/providers/S3.js.map +1 -1
- package/transpiled/providers/Spaces.d.ts +12 -0
- package/transpiled/providers/Spaces.js +13 -0
- package/transpiled/providers/Spaces.js.map +1 -0
- package/transpiled/providers/Temporary.d.ts +2 -3
- package/transpiled/providers/Temporary.js +7 -11
- package/transpiled/providers/Temporary.js.map +1 -1
- package/transpiled/providers/Test.d.ts +4 -4
- package/transpiled/providers/Test.js +6 -12
- package/transpiled/providers/Test.js.map +1 -1
- package/transpiled/providers/index.d.ts +18 -11
- package/transpiled/providers/index.js +12 -14
- package/transpiled/providers/index.js.map +1 -1
- package/transpiled/providers/index.types.d.ts +5 -0
- package/transpiled/providers/index.types.js +2 -0
- package/transpiled/providers/index.types.js.map +1 -0
- package/transpiled/providers/secrets.d.ts +8 -0
- package/transpiled/providers/secrets.js +16 -0
- package/transpiled/providers/secrets.js.map +1 -0
- package/transpiled/schemas.d.ts +4 -3
- package/transpiled/schemas.js +11 -13
- package/transpiled/schemas.js.map +1 -1
- package/transpiled/test/util.d.ts +113 -6
- package/transpiled/test/util.js +68 -15
- package/transpiled/test/util.js.map +1 -1
- package/tsconfig.json +2 -1
- package/tsconfig.tsbuildinfo +1 -0
- package/schemas/mem.cos.yaml +0 -6
- package/source/providers/FileSystem.test.ts +0 -5
- package/source/providers/Memory.ts +0 -55
- package/source/providers/S3.test.ts +0 -134
- package/transpiled/providers/Memory.d.ts +0 -15
- package/transpiled/providers/Memory.js +0 -72
- package/transpiled/providers/Memory.js.map +0 -1
- package/transpiled/tsconfig.tsbuildinfo +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
# [1.0.0-alpha.291](https://github.com/toa-io/toa/compare/v1.0.0-alpha.290...v1.0.0-alpha.291) (2026-09-07)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @toa.io/extensions.storages
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [1.0.0-alpha.289](https://github.com/toa-io/toa/compare/v1.0.0-alpha.288...v1.0.0-alpha.289) (2026-09-07)
|
|
15
|
+
|
|
16
|
+
* A deploy moves only what changed, and a component sees none of the runtime's environment (#1073) ([f38e3db](https://github.com/toa-io/toa/commit/f38e3db533f24db866c57bfa1ef294eff1eaf499)), closes [#1073](https://github.com/toa-io/toa/issues/1073) [#1064](https://github.com/toa-io/toa/issues/1064) [#1066](https://github.com/toa-io/toa/issues/1066) [#1067](https://github.com/toa-io/toa/issues/1067) [#1068](https://github.com/toa-io/toa/issues/1068) [#1069](https://github.com/toa-io/toa/issues/1069) [#1071](https://github.com/toa-io/toa/issues/1071) [#1070](https://github.com/toa-io/toa/issues/1070) [#1072](https://github.com/toa-io/toa/issues/1072)
|
|
17
|
+
|
|
18
|
+
### BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* a component that read `process.env.TOA_*` reads `context` instead;
|
|
21
|
+
`echo(input)` no longer substitutes from the environment; a bash operation sees no
|
|
22
|
+
`TOA_*`; images no longer set `USER node` — see migrations/289.md.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# [1.0.0-alpha.287](https://github.com/toa-io/toa/compare/v1.0.0-alpha.286...v1.0.0-alpha.287) (2026-09-06)
|
|
26
|
+
|
|
27
|
+
**Note:** Version bump only for package @toa.io/extensions.storages
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/extensions.storages",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.291",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Toa Storages",
|
|
5
6
|
"author": "temich <tema.gurtovoy@gmail.com>",
|
|
6
7
|
"homepage": "https://github.com/toa-io/toa#readme",
|
|
@@ -16,27 +17,21 @@
|
|
|
16
17
|
"publishConfig": {
|
|
17
18
|
"access": "public"
|
|
18
19
|
},
|
|
19
|
-
"jest": {
|
|
20
|
-
"preset": "ts-jest",
|
|
21
|
-
"testEnvironment": "node"
|
|
22
|
-
},
|
|
23
20
|
"scripts": {
|
|
24
|
-
"test": "
|
|
21
|
+
"test": "echo \"Error: run tests from root\" && exit 1",
|
|
25
22
|
"transpile": "npx tsc"
|
|
26
23
|
},
|
|
27
|
-
"
|
|
28
|
-
"dotenv": "
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"dotenv": "17.4.2"
|
|
29
26
|
},
|
|
30
27
|
"dependencies": {
|
|
31
|
-
"@aws-sdk/client-s3": "3.
|
|
32
|
-
"@aws-sdk/lib-storage": "3.
|
|
33
|
-
"@toa.io/
|
|
34
|
-
"@toa.io/
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"msgpackr": "1.10.1",
|
|
39
|
-
"smithy-node-native-fetch": "1.0.6"
|
|
28
|
+
"@aws-sdk/client-s3": "3.1125.0",
|
|
29
|
+
"@aws-sdk/lib-storage": "3.1125.0",
|
|
30
|
+
"@toa.io/generic": "1.0.0-alpha.291",
|
|
31
|
+
"@toa.io/schemas": "1.0.0-alpha.291",
|
|
32
|
+
"cloudinary": "2.11.0",
|
|
33
|
+
"negotiator": "1.1.0",
|
|
34
|
+
"openspan": "1.0.0-alpha.286"
|
|
40
35
|
},
|
|
41
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "bcd8d8d07bd3e64fc826af8719472404662cc37f"
|
|
42
37
|
}
|
package/readme.md
CHANGED
|
@@ -6,15 +6,11 @@ Shared BLOB storage.
|
|
|
6
6
|
|
|
7
7
|
BLOBs are stored with the meta-information object (Entry) having the following properties:
|
|
8
8
|
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
11
|
-
- `
|
|
12
|
-
- `created
|
|
13
|
-
- `
|
|
14
|
-
- `name` - unique name
|
|
15
|
-
- `size` - size in bytes
|
|
16
|
-
- `type` - variant MIME type
|
|
17
|
-
- `meta` - object with application-specific information, empty by default
|
|
9
|
+
- `size`: size in bytes
|
|
10
|
+
- `type`: MIME type
|
|
11
|
+
- `checksum`: content checksum
|
|
12
|
+
- `created`: creation timestamp (ISO 8601)
|
|
13
|
+
- `attributes`: `Record<string, string>` with application-specific information, empty by default
|
|
18
14
|
|
|
19
15
|
### Example
|
|
20
16
|
|
|
@@ -22,12 +18,7 @@ BLOBs are stored with the meta-information object (Entry) having the following p
|
|
|
22
18
|
id: eecd837c
|
|
23
19
|
type: image/jpeg
|
|
24
20
|
created: 1698004822358
|
|
25
|
-
|
|
26
|
-
- name: thumbnail.jpeg
|
|
27
|
-
type: image/jpeg
|
|
28
|
-
- name: thumbnail.webp
|
|
29
|
-
type: image/webp
|
|
30
|
-
meta:
|
|
21
|
+
attributes:
|
|
31
22
|
face: true
|
|
32
23
|
nudity: false
|
|
33
24
|
```
|
|
@@ -38,8 +29,8 @@ The Storages extension provides `storages` aspect,
|
|
|
38
29
|
containing named Storage instances, according to the annotation.
|
|
39
30
|
|
|
40
31
|
```javascript
|
|
41
|
-
async function effect
|
|
42
|
-
await context.storages.photos.
|
|
32
|
+
async function effect(_, context) {
|
|
33
|
+
await context.storages.photos.get('/path/to/b4f577e0.thumbnail.jpeg')
|
|
43
34
|
}
|
|
44
35
|
```
|
|
45
36
|
|
|
@@ -49,11 +40,11 @@ async function effect (_, context) {
|
|
|
49
40
|
|
|
50
41
|
#### `async put(path: string, stream: Readable, options?: Options): Maybe<Entry>`
|
|
51
42
|
|
|
52
|
-
```
|
|
43
|
+
```ts
|
|
53
44
|
interface Options {
|
|
54
45
|
claim?: string
|
|
55
46
|
accept?: string
|
|
56
|
-
|
|
47
|
+
attributes?: Record<string, string>
|
|
57
48
|
}
|
|
58
49
|
```
|
|
59
50
|
|
|
@@ -75,13 +66,13 @@ are: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/heic`, `image/
|
|
|
75
66
|
|
|
76
67
|
See [source](source/Scanner.ts).
|
|
77
68
|
|
|
78
|
-
#### `async
|
|
69
|
+
#### `async head(path: string): Maybe<Entry>`
|
|
79
70
|
|
|
80
71
|
Get an entry.
|
|
81
72
|
|
|
82
73
|
If the entry does not exist, a `NOT_FOUND` error is returned.
|
|
83
74
|
|
|
84
|
-
#### `async
|
|
75
|
+
#### `async get(path: string): Maybe<Readable>`
|
|
85
76
|
|
|
86
77
|
Fetch the BLOB specified by `path`. If the path does not exist, a `NOT_FOUND` error is returned.
|
|
87
78
|
|
|
@@ -110,18 +101,6 @@ await storage.move('/path/to/eecd837c', './sub/eecd837c')
|
|
|
110
101
|
await storage.move('/path/to/eecd837c', './sub/')
|
|
111
102
|
```
|
|
112
103
|
|
|
113
|
-
#### `async entries(path: string): Entry[]`
|
|
114
|
-
|
|
115
|
-
Get a list of entries under the `path`.
|
|
116
|
-
|
|
117
|
-
#### `async diversify(path: string, name: string, stream: Readable): Maybe<void>`
|
|
118
|
-
|
|
119
|
-
Add or replace a `name` variant of the entry specified by `path`.
|
|
120
|
-
|
|
121
|
-
#### `async annotate(path: string, key: string, value: any): Maybe<void>`
|
|
122
|
-
|
|
123
|
-
Set a `key` property in the `meta` of the entry specified by `path`.
|
|
124
|
-
|
|
125
104
|
## Providers
|
|
126
105
|
|
|
127
106
|
Storage uses underlying providers to store BLOBs and entries.
|
|
@@ -152,40 +131,114 @@ and [`toa env`](/runtime/cli/readme.md#env)
|
|
|
152
131
|
for local environment.
|
|
153
132
|
`endpoint` parameter is optional.
|
|
154
133
|
|
|
155
|
-
###
|
|
134
|
+
### DigitalOcean Spaces
|
|
156
135
|
|
|
157
136
|
Annotation format is:
|
|
158
137
|
|
|
159
138
|
```yaml
|
|
160
139
|
storages:
|
|
161
|
-
photos
|
|
162
|
-
provider:
|
|
163
|
-
|
|
140
|
+
photos:
|
|
141
|
+
provider: spaces
|
|
142
|
+
space: my-space
|
|
143
|
+
region: fra1
|
|
164
144
|
```
|
|
165
145
|
|
|
166
|
-
|
|
146
|
+
`region` is the datacenter slug, the endpoint is derived from it.
|
|
167
147
|
|
|
168
|
-
|
|
148
|
+
Secrets `ACCESS_KEY_ID` and `SECRET_ACCESS_KEY` are required.
|
|
149
|
+
Keys are created in the DigitalOcean control panel,
|
|
150
|
+
under Spaces Object Storage, on the Access Keys tab.
|
|
151
|
+
|
|
152
|
+
### Cloudinary
|
|
153
|
+
|
|
154
|
+
[Cloudinary](https://cloudinary.com) provider is used to store and transform media files.
|
|
155
|
+
|
|
156
|
+
Objects stored in the Cloudinary storage can be requested with transformations,
|
|
157
|
+
using dot-separated path extensions: `/path/to/eecd837c.{extension}.{extension}`.
|
|
158
|
+
|
|
159
|
+
Transformation is defined by the `extension` property, which is a regular expression with named
|
|
160
|
+
groups.
|
|
161
|
+
The named groups are used to replace the placeholders in the `transform` object.
|
|
162
|
+
|
|
163
|
+
`transform` object is an element of
|
|
164
|
+
the [Cloudinary `transformation` array](https://cloudinary.com/documentation/node_image_manipulation#apply_common_image_transformations).
|
|
165
|
+
|
|
166
|
+
> - `zoom` should be specified as integer [0–100].
|
|
167
|
+
> - `format` value `jpeg` is converted to `jpg`.
|
|
168
|
+
> - `^` and `$` are added to the regular expression automatically, unless they are already present.
|
|
169
169
|
|
|
170
170
|
Annotation format is:
|
|
171
171
|
|
|
172
172
|
```yaml
|
|
173
173
|
storages:
|
|
174
|
-
|
|
175
|
-
provider:
|
|
176
|
-
|
|
174
|
+
media:
|
|
175
|
+
provider: cloudinary
|
|
176
|
+
environment: my-cloud
|
|
177
|
+
type: image # image or video
|
|
178
|
+
prefix: my-app
|
|
179
|
+
transformations:
|
|
180
|
+
- extension: (?<width>\d*)x(?<height>\d*)(z(?<zoom>\d*))?
|
|
181
|
+
transformation:
|
|
182
|
+
width: <width>
|
|
183
|
+
height: <height>
|
|
184
|
+
zoom: <zoom>
|
|
185
|
+
crop: thumb
|
|
186
|
+
gravity: face
|
|
187
|
+
optional: true
|
|
188
|
+
- extension: (?<format>jpeg|webp)
|
|
189
|
+
transformation:
|
|
190
|
+
fetch_format: <format>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Path extensions must be specified in the same order as in the annotation.
|
|
194
|
+
|
|
195
|
+
For the example above:
|
|
196
|
+
|
|
197
|
+
- `/path/to/eecd837c.100x100.jpeg`: matches
|
|
198
|
+
- `/path/to/eecd837c.jpeg.100x100`: does not match
|
|
199
|
+
|
|
200
|
+
If a path extension is not matched,
|
|
201
|
+
or if at least one of the transformations is not matched (unless it is `optional`),
|
|
202
|
+
then an error is returned.
|
|
203
|
+
|
|
204
|
+
Secrets:
|
|
205
|
+
|
|
206
|
+
- `API_KEY`
|
|
207
|
+
- `API_SECRET`
|
|
208
|
+
|
|
209
|
+
### Filesystem
|
|
210
|
+
|
|
211
|
+
Annotation format is:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
storages:
|
|
215
|
+
photos:
|
|
216
|
+
provider: fs
|
|
217
|
+
path: /var/my-photos
|
|
177
218
|
```
|
|
178
219
|
|
|
179
|
-
|
|
220
|
+
[Kubernetes PVC](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) can be mounted to
|
|
221
|
+
the storage:
|
|
180
222
|
|
|
181
|
-
|
|
223
|
+
```yaml
|
|
224
|
+
storages:
|
|
225
|
+
photos:
|
|
226
|
+
provider: fs
|
|
227
|
+
path: /var/my-photos
|
|
228
|
+
claim: photos-pvc
|
|
229
|
+
```
|
|
182
230
|
|
|
183
|
-
|
|
231
|
+
### Temporary
|
|
232
|
+
|
|
233
|
+
Filesystem using OS temporary directory.
|
|
234
|
+
|
|
235
|
+
Annotation format is:
|
|
184
236
|
|
|
185
237
|
```yaml
|
|
186
238
|
storages:
|
|
187
239
|
photos@dev:
|
|
188
|
-
provider:
|
|
240
|
+
provider: tmp
|
|
241
|
+
directory: my-app-tmp
|
|
189
242
|
```
|
|
190
243
|
|
|
191
244
|
## Deduplication
|
|
@@ -196,7 +249,7 @@ Variants, on the other hand, are not deduplicated across different entries.
|
|
|
196
249
|
|
|
197
250
|
Underlying directory structure:
|
|
198
251
|
|
|
199
|
-
```
|
|
252
|
+
```text
|
|
200
253
|
/temp
|
|
201
254
|
c28f4dfd # random id
|
|
202
255
|
/blobs
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type: object
|
|
2
|
+
properties:
|
|
3
|
+
provider:
|
|
4
|
+
const: cloudinary
|
|
5
|
+
environment:
|
|
6
|
+
type: string
|
|
7
|
+
type:
|
|
8
|
+
enum:
|
|
9
|
+
- image
|
|
10
|
+
- video
|
|
11
|
+
prefix:
|
|
12
|
+
type: string
|
|
13
|
+
eager:
|
|
14
|
+
type: array
|
|
15
|
+
items:
|
|
16
|
+
type: object
|
|
17
|
+
transformations:
|
|
18
|
+
type: array
|
|
19
|
+
items:
|
|
20
|
+
type: object
|
|
21
|
+
properties:
|
|
22
|
+
extension:
|
|
23
|
+
type: string
|
|
24
|
+
transformation:
|
|
25
|
+
anyOf:
|
|
26
|
+
- type: object
|
|
27
|
+
- type: array
|
|
28
|
+
items:
|
|
29
|
+
type: object
|
|
30
|
+
optional:
|
|
31
|
+
type: boolean
|
|
32
|
+
required:
|
|
33
|
+
- extension
|
|
34
|
+
- transformation
|
|
35
|
+
required:
|
|
36
|
+
- environment
|
|
37
|
+
- type
|
|
38
|
+
additionalProperties: false
|
package/schemas/fs.cos.yaml
CHANGED
package/schemas/s3.cos.yaml
CHANGED
package/schemas/test.cos.yaml
CHANGED
package/schemas/tmp.cos.yaml
CHANGED
package/source/Annotation.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
-
import { providers } from './providers'
|
|
3
|
-
import * as schemas from './schemas'
|
|
4
|
-
import type { Declaration } from './providers'
|
|
2
|
+
import { providers } from './providers/index.js'
|
|
3
|
+
import * as schemas from './schemas.js'
|
|
4
|
+
import type { Declaration } from './providers/index.js'
|
|
5
5
|
import type { Schema } from '@toa.io/schemas'
|
|
6
6
|
|
|
7
7
|
export type Annotation = Record<string, Declaration>
|
|
8
8
|
|
|
9
|
-
export function validateAnnotation
|
|
9
|
+
export function validateAnnotation(
|
|
10
|
+
annotation: unknown
|
|
11
|
+
): asserts annotation is Annotation {
|
|
10
12
|
try {
|
|
11
13
|
schemas.annotation.validate(annotation)
|
|
12
14
|
} catch (error) {
|
|
@@ -20,19 +22,30 @@ export function validateAnnotation (annotation: unknown): asserts annotation is
|
|
|
20
22
|
/*
|
|
21
23
|
It is required because `oneOf` schema is used for the annotation validation.
|
|
22
24
|
*/
|
|
23
|
-
function explain
|
|
24
|
-
assert.ok(
|
|
25
|
-
'
|
|
25
|
+
function explain(annotation: unknown): void {
|
|
26
|
+
assert.ok(
|
|
27
|
+
typeof annotation === 'object' && annotation !== null,
|
|
28
|
+
'TOA_STORAGES is not an object'
|
|
29
|
+
)
|
|
26
30
|
|
|
27
31
|
for (const declaration of Object.values(annotation)) {
|
|
28
|
-
assert.ok(
|
|
29
|
-
declaration
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
assert.ok(
|
|
33
|
+
typeof declaration === 'object' &&
|
|
34
|
+
declaration !== null &&
|
|
35
|
+
declaration.provider in providers,
|
|
36
|
+
`Unknown provider '${declaration.provider}'`
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
assert.ok(
|
|
40
|
+
declaration.provider in schemas,
|
|
41
|
+
`No schema for provider '${declaration.provider}'`
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const provider = declaration.provider as keyof typeof providers
|
|
45
|
+
|
|
46
|
+
// the declaration names the provider
|
|
47
|
+
// oxlint-disable-next-line import/namespace
|
|
48
|
+
const schema: Schema<Declaration> = schemas[provider]
|
|
36
49
|
|
|
37
50
|
schema.validate(declaration, `Storage '${declaration.provider}' annotation`)
|
|
38
51
|
}
|
package/source/Aspect.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
-
import { Connector
|
|
3
|
-
import
|
|
2
|
+
import { Connector } from '@toa.io/core'
|
|
3
|
+
import type { extensions } from '@toa.io/core/types'
|
|
4
|
+
import { type Storage, type Storages } from './Storage.js'
|
|
4
5
|
|
|
5
6
|
export class Aspect extends Connector implements extensions.Aspect {
|
|
6
7
|
public readonly name = 'storages'
|
|
7
8
|
|
|
8
|
-
private readonly
|
|
9
|
+
private readonly create: () => Promise<Storages>
|
|
10
|
+
private storages: Storages = {}
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
/** The storages are made as the aspect connects: their providers load then, not before. */
|
|
13
|
+
public constructor(create: () => Promise<Storages>) {
|
|
11
14
|
super()
|
|
12
15
|
|
|
13
|
-
this.
|
|
16
|
+
this.create = create
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
protected override async open(): Promise<void> {
|
|
20
|
+
this.storages = await this.create()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public invoke(name: string, method: keyof Storage, ...args: unknown[]): unknown {
|
|
17
24
|
const storage = this.storages[name]
|
|
18
25
|
|
|
19
26
|
assert.ok(storage !== undefined, `Storage '${name}' is not defined`)
|
package/source/Entry.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
id: string
|
|
3
|
-
size: number
|
|
4
|
-
type: string
|
|
5
|
-
created: number
|
|
6
|
-
variants: Variant[]
|
|
7
|
-
meta: Record<string, unknown>
|
|
8
|
-
}
|
|
1
|
+
import type { Readable } from 'node:stream'
|
|
9
2
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
export type Entry = { id: string } & Metadata
|
|
4
|
+
export type Stream = { stream: Readable } & Metadata
|
|
5
|
+
|
|
6
|
+
export interface Metadata {
|
|
13
7
|
type: string
|
|
8
|
+
size: number | null
|
|
9
|
+
checksum: string
|
|
10
|
+
created: string
|
|
11
|
+
attributes: Attributes
|
|
12
|
+
range?: string
|
|
13
|
+
partial?: boolean
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
export type Attributes = Record<string, string>
|
package/source/Factory.ts
CHANGED
|
@@ -1,64 +1,70 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
-
import {
|
|
3
|
-
import { providers } from './providers'
|
|
4
|
-
import { Storage, type Storages } from './Storage'
|
|
5
|
-
import { Aspect } from './Aspect'
|
|
6
|
-
import {
|
|
7
|
-
import { validateAnnotation } from './Annotation'
|
|
8
|
-
import
|
|
9
|
-
import type {
|
|
10
|
-
import type {
|
|
2
|
+
import { console } from 'openspan'
|
|
3
|
+
import { providers } from './providers/index.js'
|
|
4
|
+
import { Storage, type Storages } from './Storage.js'
|
|
5
|
+
import { Aspect } from './Aspect.js'
|
|
6
|
+
import { ENV_PREFIX } from './deployment.js'
|
|
7
|
+
import { validateAnnotation } from './Annotation.js'
|
|
8
|
+
import { environment } from '@toa.io/generic'
|
|
9
|
+
import type { Constructor } from './Provider.js'
|
|
10
|
+
import type { Declaration } from './providers/index.js'
|
|
11
|
+
import type { Annotation } from './Annotation.js'
|
|
12
|
+
import type { Secrets } from './Secrets.js'
|
|
11
13
|
|
|
12
14
|
export class Factory {
|
|
13
15
|
private readonly annotation: Annotation
|
|
14
16
|
|
|
15
|
-
public constructor
|
|
16
|
-
const env =
|
|
17
|
+
public constructor() {
|
|
18
|
+
const env = environment.get(ENV_PREFIX)
|
|
17
19
|
|
|
18
|
-
assert.ok(env !== undefined,
|
|
20
|
+
assert.ok(env !== undefined, `${ENV_PREFIX} is not defined`)
|
|
19
21
|
|
|
20
|
-
this.annotation =
|
|
22
|
+
this.annotation = JSON.parse(env)
|
|
21
23
|
|
|
22
24
|
validateAnnotation(this.annotation)
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
public aspect
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return new Aspect(storages)
|
|
27
|
+
public aspect(): Aspect {
|
|
28
|
+
return new Aspect(() => this.createStorages())
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
private createStorages
|
|
31
|
+
private async createStorages(): Promise<Storages> {
|
|
32
32
|
const storages: Storages = {}
|
|
33
33
|
|
|
34
34
|
for (const [name, declaration] of Object.entries(this.annotation))
|
|
35
|
-
storages[name] = this.createStorage(name, declaration)
|
|
35
|
+
storages[name] = await this.createStorage(name, declaration)
|
|
36
36
|
|
|
37
37
|
return storages
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
private createStorage
|
|
41
|
-
const { provider:
|
|
42
|
-
const Provider:
|
|
40
|
+
private async createStorage(name: string, declaration: Declaration): Promise<Storage> {
|
|
41
|
+
const { provider: id, ...options } = declaration
|
|
42
|
+
const Provider: Constructor = await providers[id]()
|
|
43
43
|
const secrets = this.resolveSecrets(name, Provider)
|
|
44
44
|
const provider = new Provider(options, secrets)
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
console.debug('Storage created', {
|
|
47
|
+
name,
|
|
48
|
+
provider: id,
|
|
49
|
+
...(provider.root === undefined ? undefined : { root: provider.root })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return new Storage(provider, { name, provider: id })
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
private resolveSecrets
|
|
50
|
-
Class
|
|
51
|
-
if (Class.SECRETS === undefined)
|
|
52
|
-
return {}
|
|
55
|
+
private resolveSecrets(storageName: string, Class: Constructor): Secrets {
|
|
56
|
+
if (Class.SECRETS === undefined) return {}
|
|
53
57
|
|
|
54
58
|
const secrets: Record<string, string | undefined> = {}
|
|
55
59
|
|
|
56
60
|
for (const secret of Class.SECRETS) {
|
|
57
|
-
const variable = `${
|
|
58
|
-
const value =
|
|
61
|
+
const variable = `${ENV_PREFIX}_${storageName}_${secret.name}`.toUpperCase()
|
|
62
|
+
const value = environment.get(variable)
|
|
59
63
|
|
|
60
|
-
assert.ok(
|
|
61
|
-
|
|
64
|
+
assert.ok(
|
|
65
|
+
secret.optional === true || value !== undefined,
|
|
66
|
+
`'${variable}' is not defined`
|
|
67
|
+
)
|
|
62
68
|
|
|
63
69
|
secrets[secret.name] = value
|
|
64
70
|
}
|