@toa.io/extensions.storages 1.0.0-alpha.0 → 1.0.0-alpha.2

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 (88) hide show
  1. package/package.json +12 -11
  2. package/readme.md +63 -19
  3. package/schemas/annotation.cos.yaml +10 -0
  4. package/schemas/fs.cos.yaml +9 -0
  5. package/schemas/mem.cos.yaml +6 -0
  6. package/schemas/s3.cos.yaml +16 -0
  7. package/schemas/test.cos.yaml +8 -0
  8. package/schemas/tmp.cos.yaml +9 -0
  9. package/source/Annotation.ts +39 -0
  10. package/source/Aspect.ts +6 -4
  11. package/source/Factory.ts +31 -28
  12. package/source/Provider.ts +30 -5
  13. package/source/Scanner.ts +3 -3
  14. package/source/Storage.test.ts +110 -105
  15. package/source/Storage.ts +13 -6
  16. package/source/deployment.ts +21 -29
  17. package/source/providers/Declaration.ts +10 -0
  18. package/source/providers/FileSystem.test.ts +1 -9
  19. package/source/providers/FileSystem.ts +20 -15
  20. package/source/providers/Memory.ts +41 -0
  21. package/source/providers/S3.test.ts +133 -0
  22. package/source/providers/S3.ts +114 -39
  23. package/source/providers/Temporary.ts +8 -6
  24. package/source/providers/Test.ts +8 -8
  25. package/source/providers/index.test.ts +24 -19
  26. package/source/providers/index.ts +10 -9
  27. package/source/providers/readme.md +1 -1
  28. package/source/schemas.test.ts +58 -0
  29. package/source/schemas.ts +15 -0
  30. package/source/test/util.ts +25 -54
  31. package/transpiled/Annotation.d.ts +3 -0
  32. package/transpiled/Annotation.js +57 -0
  33. package/transpiled/Annotation.js.map +1 -0
  34. package/transpiled/Aspect.d.ts +8 -0
  35. package/transpiled/Aspect.js +25 -0
  36. package/transpiled/Aspect.js.map +1 -0
  37. package/transpiled/Entry.d.ts +14 -0
  38. package/transpiled/Entry.js +3 -0
  39. package/transpiled/Entry.js.map +1 -0
  40. package/transpiled/Factory.d.ts +9 -0
  41. package/transpiled/Factory.js +53 -0
  42. package/transpiled/Factory.js.map +1 -0
  43. package/transpiled/Provider.d.ts +20 -0
  44. package/transpiled/Provider.js +36 -0
  45. package/transpiled/Provider.js.map +1 -0
  46. package/transpiled/Scanner.d.ts +26 -0
  47. package/transpiled/Scanner.js +98 -0
  48. package/transpiled/Scanner.js.map +1 -0
  49. package/transpiled/Storage.d.ts +32 -0
  50. package/transpiled/Storage.js +176 -0
  51. package/transpiled/Storage.js.map +1 -0
  52. package/transpiled/deployment.d.ts +5 -0
  53. package/transpiled/deployment.js +68 -0
  54. package/transpiled/deployment.js.map +1 -0
  55. package/transpiled/index.d.ts +4 -0
  56. package/transpiled/index.js +10 -0
  57. package/transpiled/index.js.map +1 -0
  58. package/transpiled/manifest.d.ts +1 -0
  59. package/transpiled/manifest.js +9 -0
  60. package/transpiled/manifest.js.map +1 -0
  61. package/transpiled/providers/Declaration.d.ts +14 -0
  62. package/transpiled/providers/Declaration.js +3 -0
  63. package/transpiled/providers/Declaration.js.map +1 -0
  64. package/transpiled/providers/FileSystem.d.ts +15 -0
  65. package/transpiled/providers/FileSystem.js +44 -0
  66. package/transpiled/providers/FileSystem.js.map +1 -0
  67. package/transpiled/providers/Memory.d.ts +13 -0
  68. package/transpiled/providers/Memory.js +60 -0
  69. package/transpiled/providers/Memory.js.map +1 -0
  70. package/transpiled/providers/S3.d.ts +27 -0
  71. package/transpiled/providers/S3.js +154 -0
  72. package/transpiled/providers/S3.js.map +1 -0
  73. package/transpiled/providers/Temporary.d.ts +8 -0
  74. package/transpiled/providers/Temporary.js +14 -0
  75. package/transpiled/providers/Temporary.js.map +1 -0
  76. package/transpiled/providers/Test.d.ts +6 -0
  77. package/transpiled/providers/Test.js +15 -0
  78. package/transpiled/providers/Test.js.map +1 -0
  79. package/transpiled/providers/index.d.ts +13 -0
  80. package/transpiled/providers/index.js +16 -0
  81. package/transpiled/providers/index.js.map +1 -0
  82. package/transpiled/schemas.d.ts +9 -0
  83. package/transpiled/schemas.js +14 -0
  84. package/transpiled/schemas.js.map +1 -0
  85. package/transpiled/test/util.d.ts +29 -0
  86. package/transpiled/test/util.js +38 -0
  87. package/transpiled/test/util.js.map +1 -0
  88. package/transpiled/tsconfig.tsbuildinfo +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.storages",
3
- "version": "1.0.0-alpha.0",
3
+ "version": "1.0.0-alpha.2",
4
4
  "description": "Toa Storages",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -22,21 +22,22 @@
22
22
  },
23
23
  "scripts": {
24
24
  "test": "jest",
25
- "transpile": "rm -rf transpiled && npx tsc"
25
+ "transpile": "npx tsc"
26
26
  },
27
27
  "devDependencies": {
28
- "@toa.io/streams": "1.0.0-alpha.0",
29
- "@types/fs-extra": "11.0.3",
28
+ "@toa.io/streams": "1.0.0-alpha.2",
30
29
  "dotenv": "16.3.1"
31
30
  },
32
31
  "dependencies": {
33
- "@aws-sdk/client-s3": "3.435.0",
34
- "@aws-sdk/lib-storage": "3.437.0",
35
- "@toa.io/generic": "0.20.0-alpha.2",
32
+ "@aws-sdk/client-s3": "3.481.0",
33
+ "@aws-sdk/lib-storage": "3.481.0",
34
+ "@toa.io/agent": "1.0.0-alpha.2",
35
+ "@toa.io/generic": "1.0.0-alpha.2",
36
+ "@toa.io/schemas": "1.0.0-alpha.2",
36
37
  "error-value": "0.3.0",
37
- "fs-extra": "11.1.1",
38
- "matchacho": "0.3.5",
39
- "msgpackr": "1.9.9"
38
+ "matchacho": "0.6.0",
39
+ "msgpackr": "1.10.1",
40
+ "smithy-node-native-fetch": "1.0.6"
40
41
  },
41
- "gitHead": "06c64546f6292cc07c52f74b31415101037f7616"
42
+ "gitHead": "7688e6e980a65c82ac2e459be4e355eebf406cd0"
42
43
  }
package/readme.md CHANGED
@@ -47,26 +47,28 @@ async function effect (_, context) {
47
47
 
48
48
  > `Maybe<T> = T | Error`
49
49
 
50
- #### `async put(path: string, stream: Readable, type?: TypeControl): Maybe<Entry>`
50
+ #### `async put(path: string, stream: Readable, options?: Options): Maybe<Entry>`
51
51
 
52
52
  ```
53
- interface TypeControl {
53
+ interface Options {
54
54
  claim?: string
55
55
  accept?: string
56
+ meta?: Record<string, string>
56
57
  }
57
58
  ```
58
59
 
59
- Add a BLOB to the storage and create an entry under specified `path`.
60
+ Add a BLOB to the storage and create an entry under specified `path`, with given meta-information.
60
61
 
61
62
  BLOB type is identified
62
63
  using [magick numbers](https://github.com/sindresorhus/file-type).
63
64
 
64
- If the `type` argument is specified and the value of the `claim` does not match the detected BLOB type, then
65
- a `TYPE_MISMATCH` error is returned.
66
- If the BLOB type cannot be identified and the value of the `claim` is not in the list of known types, then the given
67
- value is used.
68
- If the list of [acceptable types](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) is passed and the type of
69
- the BLOB does not match any of its values, then a `NOT_ACCEPTABLE` error is returned.
65
+ If the `type` argument is specified and the value of the `claim` does not match the detected BLOB
66
+ type, then a `TYPE_MISMATCH` error is returned.
67
+ If the BLOB type cannot be identified and the value of the `claim` is not in the list of known
68
+ types, then the given value is used.
69
+ If the list of [acceptable types](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) is
70
+ passed and the type of the BLOB does not match any of its values, then a `NOT_ACCEPTABLE` error is
71
+ returned.
70
72
 
71
73
  Known types
72
74
  are: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/heic`, `image/jxl`, `image/avif`.
@@ -130,29 +132,63 @@ Custom providers are not supported.
130
132
 
131
133
  ### Amazon S3
132
134
 
133
- Annotation value formats is `s3://{region}/{bucket}?endpoint={endpoint}`.
135
+ Annotation formats is like:
136
+
137
+ ```yaml
138
+ storages:
139
+ photos:
140
+ provider: s3
141
+ bucket: my-bucket
142
+ region: eu-west-1
143
+ tmp:
144
+ provider: s3
145
+ endpoint: http://localhost:9000
146
+ bucket: my-bucket
147
+ ```
134
148
 
135
- Requires secrets for the access key and secret key.
149
+ Secrets for the AWS access key and secret key can be provided via SECRETS constructs property. If
150
+ missed standard AWS SDK credentials resolve chain will be used (that means environment variable,
151
+ shared config file, EC2 metadata service, etc.).
136
152
  See [`toa conceal`](/runtime/cli/readme.md#conceal) for deployment
137
153
  and [`toa env`](/runtime/cli/readme.md#env)
138
154
  for local environment.
139
155
  `endpoint` parameter is optional.
140
156
 
141
- `s3://us-east-1/my-bucket?endpoint=http://s3.my-instance.com:4566`
142
-
143
157
  ### Filesystem
144
158
 
145
- Annotation value format is `file:///{path}`.
159
+ Annotation format is:
146
160
 
147
- `file:///var/my-storage`
161
+ ```yaml
162
+ storages:
163
+ photos@dev:
164
+ provider: fs
165
+ path: /var/my-storage
166
+ ```
148
167
 
149
168
  ### Temporary
150
169
 
151
170
  Filesystem using OS temporary directory.
152
171
 
153
- Annotation value format is `tmp:///{path}`.
172
+ Annotation format is:
154
173
 
155
- `tmp:///my-storage`
174
+ ```yaml
175
+ storages:
176
+ photos@dev:
177
+ provider: tmp
178
+ directory: my-app-tmp
179
+ ```
180
+
181
+ ### Memory
182
+
183
+ In-memory non-persistent storage.
184
+
185
+ Annotation value format is:
186
+
187
+ ```yaml
188
+ storages:
189
+ photos@dev:
190
+ provider: mem
191
+ ```
156
192
 
157
193
  ## Deduplication
158
194
 
@@ -199,8 +235,16 @@ provider-specific URLs as values.
199
235
 
200
236
  ```yaml
201
237
  storages:
202
- photos: s3://us-east-1/my-bucket
203
- photos@dev: file:///var/my-storage
238
+ photos:
239
+ provider: s3
240
+ bucket: my-bucket
241
+ photos@dev:
242
+ provider: fs
243
+ path: /var/my-storage
244
+ tmp:
245
+ provider: s3
246
+ endpoint: http://localhost:9000
247
+ bucket: my-bucket
204
248
  ```
205
249
 
206
250
  ## Secrets
@@ -0,0 +1,10 @@
1
+ type: object
2
+ patternProperties:
3
+ '^[a-z0-9_]{1,32}$':
4
+ oneOf:
5
+ - $ref: s3
6
+ - $ref: fs
7
+ - $ref: tmp
8
+ - $ref: mem
9
+ - $ref: test
10
+ additionalProperties: false
@@ -0,0 +1,9 @@
1
+ type: object
2
+ properties:
3
+ provider:
4
+ const: fs
5
+ path:
6
+ type: string
7
+ maxLength: 4096
8
+ required: [provider, path]
9
+ additionalProperties: false
@@ -0,0 +1,6 @@
1
+ type: object
2
+ properties:
3
+ provider:
4
+ const: mem
5
+ required: [provider]
6
+ additionalProperties: false
@@ -0,0 +1,16 @@
1
+ type: object
2
+ properties:
3
+ provider:
4
+ const: s3
5
+ bucket:
6
+ type: string
7
+ minLength: 3
8
+ maxLength: 63
9
+ region:
10
+ type: string
11
+ prefix:
12
+ type: string
13
+ endpoint:
14
+ type: string
15
+ format: uri
16
+ required: [provider, bucket]
@@ -0,0 +1,8 @@
1
+ type: object
2
+ properties:
3
+ provider:
4
+ const: test
5
+ directory:
6
+ type: string
7
+ required: [provider, directory]
8
+ additionalProperties: false
@@ -0,0 +1,9 @@
1
+ type: object
2
+ properties:
3
+ provider:
4
+ type: string
5
+ const: tmp
6
+ directory:
7
+ type: string
8
+ required: [provider, directory]
9
+ additionalProperties: false
@@ -0,0 +1,39 @@
1
+ import assert from 'node:assert'
2
+ import { providers } from './providers'
3
+ import * as schemas from './schemas'
4
+ import type { Declaration } from './providers'
5
+ import type { Schema } from '@toa.io/schemas'
6
+
7
+ export type Annotation = Record<string, Declaration>
8
+
9
+ export function validateAnnotation (annotation: unknown): asserts annotation is Annotation {
10
+ try {
11
+ schemas.annotation.validate(annotation)
12
+ } catch (error) {
13
+ explain(annotation)
14
+
15
+ // if all declarations are valid, re-throw the error
16
+ throw error
17
+ }
18
+ }
19
+
20
+ /*
21
+ It is required because `oneOf` schema is used for the annotation validation.
22
+ */
23
+ function explain (annotation: unknown): void {
24
+ assert.ok(typeof annotation === 'object' && annotation !== null,
25
+ 'TOA_STORAGES is not an object')
26
+
27
+ 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]
36
+
37
+ schema.validate(declaration, `Storage '${declaration.provider}' annotation`)
38
+ }
39
+ }
package/source/Aspect.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import assert from 'node:assert'
1
2
  import { Connector, type extensions } from '@toa.io/core'
2
3
  import { type Storage, type Storages } from './Storage'
3
4
 
@@ -12,12 +13,13 @@ export class Aspect extends Connector implements extensions.Aspect {
12
13
  this.storages = storages
13
14
  }
14
15
 
15
- public invoke (name: string, method: keyof Storage, ...args: any[]): any {
16
- if (!(name in this.storages))
17
- throw new Error(`Storage '${name}' is not defined`)
16
+ public invoke (name: string, method: keyof Storage, ...args: unknown[]): unknown {
17
+ const storage = this.storages[name]
18
+
19
+ assert.ok(storage !== undefined, `Storage '${name}' is not defined`)
18
20
 
19
21
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
20
22
  // @ts-expect-error
21
- return this.storages[name][method](...args)
23
+ return storage[method](...args)
22
24
  }
23
25
  }
package/source/Factory.ts CHANGED
@@ -1,18 +1,25 @@
1
- import { decode } from 'msgpackr'
2
- import { type ProviderClass, providers } from './providers'
1
+ import assert from 'node:assert'
2
+ import { decode } from '@toa.io/generic'
3
+ import { providers } from './providers'
3
4
  import { Storage, type Storages } from './Storage'
4
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'
5
11
 
6
12
  export class Factory {
7
- private readonly declaration: Record<string, string>
13
+ private readonly annotation: Annotation
8
14
 
9
15
  public constructor () {
10
16
  const env = process.env.TOA_STORAGES
11
17
 
12
- if (env === undefined)
13
- throw new Error('TOA_STORAGES is not defined')
18
+ assert.ok(env !== undefined, 'TOA_STORAGES is not defined')
14
19
 
15
- this.declaration = decode(Buffer.from(env, 'base64')) as Record<string, string>
20
+ this.annotation = decode(env)
21
+
22
+ validateAnnotation(this.annotation)
16
23
  }
17
24
 
18
25
  public aspect (): Aspect {
@@ -21,43 +28,39 @@ export class Factory {
21
28
  return new Aspect(storages)
22
29
  }
23
30
 
24
- public createStorage (name: string, ref: string): Storage {
25
- const url = new URL(ref)
26
- const Provider = providers[url.protocol]
27
-
28
- if (Provider === undefined)
29
- throw new Error(`No provider found for '${url.protocol}'`)
30
-
31
- const secrets = this.resolveSecrets(name, Provider)
32
-
33
- const provider = new Provider(url, secrets)
34
-
35
- return new Storage(provider)
36
- }
37
-
38
31
  private createStorages (): Storages {
39
32
  const storages: Storages = {}
40
33
 
41
- for (const [name, ref] of Object.entries(this.declaration))
42
- storages[name] = this.createStorage(name, ref)
34
+ for (const [name, declaration] of Object.entries(this.annotation))
35
+ storages[name] = this.createStorage(name, declaration)
43
36
 
44
37
  return storages
45
38
  }
46
39
 
47
- private resolveSecrets (name: string, Class: ProviderClass): Record<string, string> {
40
+ private createStorage (name: string, declaration: Declaration): Storage {
41
+ const { provider: providerId, ...options } = declaration
42
+ const Provider: ProviderConstructor = providers[providerId]
43
+ const secrets = this.resolveSecrets(name, Provider)
44
+ const provider = new Provider(options, secrets)
45
+
46
+ return new Storage(provider)
47
+ }
48
+
49
+ private resolveSecrets (storageName: string,
50
+ Class: ProviderConstructor): ProviderSecrets {
48
51
  if (Class.SECRETS === undefined)
49
52
  return {}
50
53
 
51
- const secrets: Record<string, string> = {}
54
+ const secrets: Record<string, string | undefined> = {}
52
55
 
53
56
  for (const secret of Class.SECRETS) {
54
- const variable = `TOA_STORAGES_${name.toUpperCase()}_${secret.toUpperCase()}`
57
+ const variable = `${SERIALIZATION_PREFIX}_${storageName}_${secret.name}`.toUpperCase()
55
58
  const value = process.env[variable]
56
59
 
57
- if (value === undefined)
58
- throw new Error(`${variable} is not defined`)
60
+ assert.ok(secret.optional === true || value !== undefined,
61
+ `'${variable}' is not defined`)
59
62
 
60
- secrets[secret] = value
63
+ secrets[secret.name] = value
61
64
  }
62
65
 
63
66
  return secrets
@@ -1,8 +1,33 @@
1
1
  import { type Readable } from 'node:stream'
2
+ import * as assert from 'node:assert'
2
3
 
3
- export interface Provider {
4
- get: (path: string) => Promise<Readable | null>
5
- put: (path: string, filename: string, stream: Readable) => Promise<void>
6
- delete: (path: string) => Promise<void>
7
- move: (from: string, to: string) => Promise<void>
4
+ export type ProviderSecrets<K extends string = string> = Record<K | string, string | undefined>
5
+
6
+ export interface ProviderSecret {
7
+ readonly name: string
8
+ readonly optional?: boolean
9
+ }
10
+
11
+ export abstract class Provider<Options = void> {
12
+ public static readonly SECRETS: readonly ProviderSecret[] = []
13
+
14
+ public constructor (_: Options, secrets?: ProviderSecrets) {
15
+ for (const { name, optional = false } of new.target.SECRETS)
16
+ assert.ok(optional || secrets?.[name] !== undefined, `Missing secret '${name}'`)
17
+ }
18
+
19
+ public abstract get (path: string): Promise<Readable | null>
20
+
21
+ public abstract put (path: string, filename: string, stream: Readable): Promise<void>
22
+
23
+ public abstract delete (path: string): Promise<void>
24
+
25
+ public abstract move (from: string, to: string): Promise<void>
26
+ }
27
+
28
+ export interface ProviderConstructor {
29
+ readonly SECRETS: readonly ProviderSecret[]
30
+ prototype: Provider
31
+
32
+ new (options: any, secrets?: ProviderSecrets): Provider
8
33
  }
package/source/Scanner.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { PassThrough, type TransformCallback } from 'node:stream'
2
2
  import { createHash } from 'node:crypto'
3
- import { negotiate } from '@toa.io/http'
3
+ import { negotiate } from '@toa.io/agent'
4
4
  import { Err } from 'error-value'
5
5
 
6
6
  export class Scanner extends PassThrough {
@@ -15,7 +15,7 @@ export class Scanner extends PassThrough {
15
15
  private detected = false
16
16
  private readonly chunks: Buffer[] = []
17
17
 
18
- public constructor (control?: TypeControl) {
18
+ public constructor (control?: ScanOptions) {
19
19
  super()
20
20
 
21
21
  this.claim = control?.claim
@@ -121,7 +121,7 @@ const KNOWN_TYPES = new Set(SIGNATURES.map(({ type }) => type))
121
121
  const ERR_TYPE_MISMATCH = Err('TYPE_MISMATCH')
122
122
  const ERR_NOT_ACCEPTABLE = Err('NOT_ACCEPTABLE')
123
123
 
124
- export interface TypeControl {
124
+ export interface ScanOptions {
125
125
  claim?: string
126
126
  accept?: string
127
127
  }