@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/package.json +13 -18
  3. package/readme.md +100 -47
  4. package/schemas/annotation.cos.yaml +3 -2
  5. package/schemas/cloudinary.cos.yaml +38 -0
  6. package/schemas/fs.cos.yaml +5 -1
  7. package/schemas/s3.cos.yaml +4 -3
  8. package/schemas/spaces.cos.yaml +16 -0
  9. package/schemas/test.cos.yaml +3 -1
  10. package/schemas/tmp.cos.yaml +3 -2
  11. package/source/Annotation.ts +28 -15
  12. package/source/Aspect.ts +13 -6
  13. package/source/Entry.ts +13 -11
  14. package/source/Factory.ts +37 -31
  15. package/source/Provider.ts +28 -25
  16. package/source/Scanner.ts +61 -44
  17. package/source/Secrets.ts +6 -0
  18. package/source/Storage.test.ts +141 -297
  19. package/source/Storage.ts +96 -202
  20. package/source/context.ts +14 -0
  21. package/source/deployment.ts +56 -22
  22. package/source/errors.ts +3 -0
  23. package/source/index.ts +9 -4
  24. package/source/manifest.ts +5 -6
  25. package/source/providers/Cloudinary.ts +318 -0
  26. package/source/providers/Declaration.ts +8 -5
  27. package/source/providers/FileSystem.ts +69 -33
  28. package/source/providers/S3.ts +109 -131
  29. package/source/providers/Spaces.ts +25 -0
  30. package/source/providers/Temporary.ts +3 -4
  31. package/source/providers/Test.ts +6 -8
  32. package/source/providers/index.test.ts +115 -90
  33. package/source/providers/index.ts +22 -13
  34. package/source/providers/index.types.ts +5 -0
  35. package/source/providers/readme.md +13 -11
  36. package/source/providers/secrets.ts +19 -0
  37. package/source/schemas.test.ts +15 -10
  38. package/source/schemas.ts +5 -4
  39. package/source/test/.env.example +8 -0
  40. package/source/test/arny.jpg +0 -0
  41. package/source/test/lenna.48x48.jpeg +0 -0
  42. package/source/test/plank.mp4 +0 -0
  43. package/source/test/sport.mp4 +0 -0
  44. package/source/test/util.ts +82 -14
  45. package/transpiled/Annotation.d.ts +1 -1
  46. package/transpiled/Annotation.js +13 -39
  47. package/transpiled/Annotation.js.map +1 -1
  48. package/transpiled/Aspect.d.ts +8 -4
  49. package/transpiled/Aspect.js +12 -14
  50. package/transpiled/Aspect.js.map +1 -1
  51. package/transpiled/Entry.d.ts +14 -11
  52. package/transpiled/Entry.js +1 -2
  53. package/transpiled/Factory.d.ts +1 -1
  54. package/transpiled/Factory.js +28 -30
  55. package/transpiled/Factory.js.map +1 -1
  56. package/transpiled/Provider.d.ts +17 -19
  57. package/transpiled/Provider.js +8 -33
  58. package/transpiled/Provider.js.map +1 -1
  59. package/transpiled/Scanner.d.ts +5 -4
  60. package/transpiled/Scanner.js +33 -26
  61. package/transpiled/Scanner.js.map +1 -1
  62. package/transpiled/Secrets.d.ts +5 -0
  63. package/transpiled/Secrets.js +2 -0
  64. package/transpiled/Secrets.js.map +1 -0
  65. package/transpiled/Storage.d.ts +23 -22
  66. package/transpiled/Storage.js +77 -142
  67. package/transpiled/Storage.js.map +1 -1
  68. package/transpiled/context.d.ts +3 -0
  69. package/transpiled/context.js +12 -0
  70. package/transpiled/context.js.map +1 -0
  71. package/transpiled/deployment.d.ts +1 -1
  72. package/transpiled/deployment.js +39 -43
  73. package/transpiled/deployment.js.map +1 -1
  74. package/transpiled/errors.d.ts +7 -0
  75. package/transpiled/errors.js +4 -0
  76. package/transpiled/errors.js.map +1 -0
  77. package/transpiled/index.d.ts +7 -4
  78. package/transpiled/index.js +5 -9
  79. package/transpiled/index.js.map +1 -1
  80. package/transpiled/manifest.js +6 -7
  81. package/transpiled/manifest.js.map +1 -1
  82. package/transpiled/providers/Cloudinary.d.ts +50 -0
  83. package/transpiled/providers/Cloudinary.js +223 -0
  84. package/transpiled/providers/Cloudinary.js.map +1 -0
  85. package/transpiled/providers/Declaration.d.ts +9 -5
  86. package/transpiled/providers/Declaration.js +1 -2
  87. package/transpiled/providers/FileSystem.d.ts +16 -11
  88. package/transpiled/providers/FileSystem.js +64 -43
  89. package/transpiled/providers/FileSystem.js.map +1 -1
  90. package/transpiled/providers/S3.d.ts +14 -19
  91. package/transpiled/providers/S3.js +72 -123
  92. package/transpiled/providers/S3.js.map +1 -1
  93. package/transpiled/providers/Spaces.d.ts +12 -0
  94. package/transpiled/providers/Spaces.js +13 -0
  95. package/transpiled/providers/Spaces.js.map +1 -0
  96. package/transpiled/providers/Temporary.d.ts +2 -3
  97. package/transpiled/providers/Temporary.js +7 -11
  98. package/transpiled/providers/Temporary.js.map +1 -1
  99. package/transpiled/providers/Test.d.ts +4 -4
  100. package/transpiled/providers/Test.js +6 -12
  101. package/transpiled/providers/Test.js.map +1 -1
  102. package/transpiled/providers/index.d.ts +18 -11
  103. package/transpiled/providers/index.js +12 -14
  104. package/transpiled/providers/index.js.map +1 -1
  105. package/transpiled/providers/index.types.d.ts +5 -0
  106. package/transpiled/providers/index.types.js +2 -0
  107. package/transpiled/providers/index.types.js.map +1 -0
  108. package/transpiled/providers/secrets.d.ts +8 -0
  109. package/transpiled/providers/secrets.js +16 -0
  110. package/transpiled/providers/secrets.js.map +1 -0
  111. package/transpiled/schemas.d.ts +4 -3
  112. package/transpiled/schemas.js +11 -13
  113. package/transpiled/schemas.js.map +1 -1
  114. package/transpiled/test/util.d.ts +113 -6
  115. package/transpiled/test/util.js +68 -15
  116. package/transpiled/test/util.js.map +1 -1
  117. package/tsconfig.json +2 -1
  118. package/tsconfig.tsbuildinfo +1 -0
  119. package/schemas/mem.cos.yaml +0 -6
  120. package/source/providers/FileSystem.test.ts +0 -5
  121. package/source/providers/Memory.ts +0 -55
  122. package/source/providers/S3.test.ts +0 -134
  123. package/transpiled/providers/Memory.d.ts +0 -15
  124. package/transpiled/providers/Memory.js +0 -72
  125. package/transpiled/providers/Memory.js.map +0 -1
  126. 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.29",
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": "jest",
21
+ "test": "echo \"Error: run tests from root\" && exit 1",
25
22
  "transpile": "npx tsc"
26
23
  },
27
- "peerDependencies": {
28
- "dotenv": "*"
24
+ "devDependencies": {
25
+ "dotenv": "17.4.2"
29
26
  },
30
27
  "dependencies": {
31
- "@aws-sdk/client-s3": "3.481.0",
32
- "@aws-sdk/lib-storage": "3.481.0",
33
- "@toa.io/agent": "1.0.0-alpha.29",
34
- "@toa.io/generic": "1.0.0-alpha.29",
35
- "@toa.io/schemas": "1.0.0-alpha.29",
36
- "error-value": "0.3.0",
37
- "matchacho": "0.6.0",
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": "186d0e17c12a9016b8076edf925963cbb32b9219"
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
- - `id` - checksum
10
- - `size` - size in bytes
11
- - `type` - MIME type
12
- - `created` - creation timestamp (UNIX time, ms)
13
- - `variants` - array of:
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
- variants:
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 (_, context) {
42
- await context.storages.photos.fetch('/path/to/b4f577e0.thumbnail.jpeg')
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
- meta?: Record<string, string>
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 get(path: string): Maybe<Entry>`
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 fetch(path: string): Maybe<Readable>`
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
- ### Filesystem
134
+ ### DigitalOcean Spaces
156
135
 
157
136
  Annotation format is:
158
137
 
159
138
  ```yaml
160
139
  storages:
161
- photos@dev:
162
- provider: fs
163
- path: /var/my-storage
140
+ photos:
141
+ provider: spaces
142
+ space: my-space
143
+ region: fra1
164
144
  ```
165
145
 
166
- ### Temporary
146
+ `region` is the datacenter slug, the endpoint is derived from it.
167
147
 
168
- Filesystem using OS temporary directory.
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
- photos@dev:
175
- provider: tmp
176
- directory: my-app-tmp
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
- ### Memory
220
+ [Kubernetes PVC](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) can be mounted to
221
+ the storage:
180
222
 
181
- In-memory non-persistent storage.
223
+ ```yaml
224
+ storages:
225
+ photos:
226
+ provider: fs
227
+ path: /var/my-photos
228
+ claim: photos-pvc
229
+ ```
182
230
 
183
- Annotation value format is:
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: mem
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
@@ -1,10 +1,11 @@
1
1
  type: object
2
2
  patternProperties:
3
- '^[a-z0-9_]{1,32}$':
3
+ ^[a-z0-9_]{1,32}$:
4
4
  oneOf:
5
5
  - $ref: s3
6
+ - $ref: spaces
7
+ - $ref: cloudinary
6
8
  - $ref: fs
7
9
  - $ref: tmp
8
- - $ref: mem
9
10
  - $ref: test
10
11
  additionalProperties: false
@@ -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
@@ -5,5 +5,9 @@ properties:
5
5
  path:
6
6
  type: string
7
7
  maxLength: 4096
8
- required: [provider, path]
8
+ claim:
9
+ type: string
10
+ required:
11
+ - provider
12
+ - path
9
13
  additionalProperties: false
@@ -8,9 +8,10 @@ properties:
8
8
  maxLength: 63
9
9
  region:
10
10
  type: string
11
- prefix:
12
- type: string
13
11
  endpoint:
14
12
  type: string
15
13
  format: uri
16
- required: [provider, bucket]
14
+ required:
15
+ - provider
16
+ - bucket
17
+ additionalProperties: false
@@ -0,0 +1,16 @@
1
+ type: object
2
+ properties:
3
+ provider:
4
+ const: spaces
5
+ space:
6
+ type: string
7
+ minLength: 3
8
+ maxLength: 63
9
+ region:
10
+ type: string
11
+ minLength: 1
12
+ required:
13
+ - provider
14
+ - space
15
+ - region
16
+ additionalProperties: false
@@ -4,5 +4,7 @@ properties:
4
4
  const: test
5
5
  directory:
6
6
  type: string
7
- required: [provider, directory]
7
+ required:
8
+ - provider
9
+ - directory
8
10
  additionalProperties: false
@@ -1,9 +1,10 @@
1
1
  type: object
2
2
  properties:
3
3
  provider:
4
- type: string
5
4
  const: tmp
6
5
  directory:
7
6
  type: string
8
- required: [provider, directory]
7
+ required:
8
+ - provider
9
+ - directory
9
10
  additionalProperties: false
@@ -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 (annotation: unknown): asserts annotation is Annotation {
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 (annotation: unknown): void {
24
- assert.ok(typeof annotation === 'object' && annotation !== null,
25
- 'TOA_STORAGES is not an object')
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(typeof declaration === 'object' && declaration !== null &&
29
- declaration.provider in providers,
30
- `Unknown provider '${declaration.provider}'`)
31
-
32
- assert.ok(declaration.provider in schemas,
33
- `No schema for provider '${declaration.provider}'`)
34
-
35
- const schema: Schema<Declaration> = schemas[declaration.provider as keyof typeof providers]
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, type extensions } from '@toa.io/core'
3
- import { type Storage, type Storages } from './Storage'
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 storages: Storages
9
+ private readonly create: () => Promise<Storages>
10
+ private storages: Storages = {}
9
11
 
10
- public constructor (storages: Storages) {
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.storages = storages
16
+ this.create = create
14
17
  }
15
18
 
16
- public invoke (name: string, method: keyof Storage, ...args: unknown[]): unknown {
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
- export interface Entry {
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
- interface Variant {
11
- name: string
12
- size: number
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 { decode } from '@toa.io/generic'
3
- import { providers } from './providers'
4
- import { Storage, type Storages } from './Storage'
5
- import { Aspect } from './Aspect'
6
- import { SERIALIZATION_PREFIX } from './deployment'
7
- import { validateAnnotation } from './Annotation'
8
- import type { Declaration } from './providers'
9
- import type { Annotation } from './Annotation'
10
- import type { ProviderConstructor, ProviderSecrets } from './Provider'
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 = process.env.TOA_STORAGES
17
+ public constructor() {
18
+ const env = environment.get(ENV_PREFIX)
17
19
 
18
- assert.ok(env !== undefined, 'TOA_STORAGES is not defined')
20
+ assert.ok(env !== undefined, `${ENV_PREFIX} is not defined`)
19
21
 
20
- this.annotation = decode(env)
22
+ this.annotation = JSON.parse(env)
21
23
 
22
24
  validateAnnotation(this.annotation)
23
25
  }
24
26
 
25
- public aspect (): Aspect {
26
- const storages = this.createStorages()
27
-
28
- return new Aspect(storages)
27
+ public aspect(): Aspect {
28
+ return new Aspect(() => this.createStorages())
29
29
  }
30
30
 
31
- private createStorages (): Storages {
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 (name: string, declaration: Declaration): Storage {
41
- const { provider: providerId, ...options } = declaration
42
- const Provider: ProviderConstructor = providers[providerId]
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
- return new Storage(provider)
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 (storageName: string,
50
- Class: ProviderConstructor): ProviderSecrets {
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 = `${SERIALIZATION_PREFIX}_${storageName}_${secret.name}`.toUpperCase()
58
- const value = process.env[variable]
61
+ const variable = `${ENV_PREFIX}_${storageName}_${secret.name}`.toUpperCase()
62
+ const value = environment.get(variable)
59
63
 
60
- assert.ok(secret.optional === true || value !== undefined,
61
- `'${variable}' is not defined`)
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
  }