@toa.io/extensions.storages 0.24.0-alpha.21 → 0.24.0-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +6 -5
  2. package/readme.md +10 -12
  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/Factory.ts +26 -23
  11. package/source/Provider.ts +8 -6
  12. package/source/Scanner.ts +3 -3
  13. package/source/Storage.test.ts +4 -3
  14. package/source/Storage.ts +13 -6
  15. package/source/deployment.ts +12 -23
  16. package/source/providers/Declaration.ts +10 -0
  17. package/source/providers/FileSystem.ts +5 -5
  18. package/source/providers/S3.test.ts +4 -5
  19. package/source/providers/S3.ts +17 -12
  20. package/source/providers/Temporary.ts +6 -4
  21. package/source/providers/Test.ts +3 -3
  22. package/source/providers/index.test.ts +5 -5
  23. package/source/providers/index.ts +5 -3
  24. package/source/schemas.test.ts +58 -0
  25. package/source/schemas.ts +15 -0
  26. package/source/test/util.ts +14 -8
  27. package/transpiled/Annotation.d.ts +3 -0
  28. package/transpiled/Annotation.js +57 -0
  29. package/transpiled/Annotation.js.map +1 -0
  30. package/transpiled/Factory.d.ts +2 -3
  31. package/transpiled/Factory.js +16 -15
  32. package/transpiled/Factory.js.map +1 -1
  33. package/transpiled/Provider.d.ts +3 -5
  34. package/transpiled/Provider.js +2 -2
  35. package/transpiled/Provider.js.map +1 -1
  36. package/transpiled/Scanner.d.ts +2 -2
  37. package/transpiled/Scanner.js +2 -2
  38. package/transpiled/Scanner.js.map +1 -1
  39. package/transpiled/Storage.d.ts +6 -2
  40. package/transpiled/Storage.js +5 -5
  41. package/transpiled/Storage.js.map +1 -1
  42. package/transpiled/deployment.d.ts +1 -8
  43. package/transpiled/deployment.js +7 -14
  44. package/transpiled/deployment.js.map +1 -1
  45. package/transpiled/providers/Declaration.d.ts +14 -0
  46. package/transpiled/providers/Declaration.js +3 -0
  47. package/transpiled/providers/Declaration.js.map +1 -0
  48. package/transpiled/providers/FileSystem.d.ts +2 -1
  49. package/transpiled/providers/FileSystem.js +3 -5
  50. package/transpiled/providers/FileSystem.js.map +1 -1
  51. package/transpiled/providers/S3.d.ts +4 -2
  52. package/transpiled/providers/S3.js +13 -12
  53. package/transpiled/providers/S3.js.map +1 -1
  54. package/transpiled/providers/Temporary.d.ts +3 -2
  55. package/transpiled/providers/Temporary.js +3 -2
  56. package/transpiled/providers/Temporary.js.map +1 -1
  57. package/transpiled/providers/Test.d.ts +2 -2
  58. package/transpiled/providers/Test.js +2 -2
  59. package/transpiled/providers/Test.js.map +1 -1
  60. package/transpiled/providers/index.d.ts +3 -2
  61. package/transpiled/providers/index.js +3 -3
  62. package/transpiled/providers/index.js.map +1 -1
  63. package/transpiled/schemas.d.ts +9 -0
  64. package/transpiled/schemas.js +14 -0
  65. package/transpiled/schemas.js.map +1 -0
  66. package/transpiled/test/util.d.ts +28 -17
  67. package/transpiled/test/util.js +9 -5
  68. package/transpiled/test/util.js.map +1 -1
  69. package/transpiled/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.storages",
3
- "version": "0.24.0-alpha.21",
3
+ "version": "0.24.0-alpha.23",
4
4
  "description": "Toa Storages",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -25,18 +25,19 @@
25
25
  "transpile": "npx tsc"
26
26
  },
27
27
  "devDependencies": {
28
- "@toa.io/streams": "0.24.0-alpha.21",
28
+ "@toa.io/streams": "0.24.0-alpha.23",
29
29
  "dotenv": "16.3.1"
30
30
  },
31
31
  "dependencies": {
32
32
  "@aws-sdk/client-s3": "3.481.0",
33
33
  "@aws-sdk/lib-storage": "3.481.0",
34
- "@toa.io/generic": "0.20.0-alpha.2",
35
- "@toa.io/http": "0.24.0-alpha.21",
34
+ "@toa.io/agent": "0.24.0-alpha.23",
35
+ "@toa.io/generic": "0.24.0-alpha.23",
36
+ "@toa.io/schemas": "0.24.0-alpha.23",
36
37
  "error-value": "0.3.0",
37
38
  "matchacho": "0.6.0",
38
39
  "msgpackr": "1.10.1",
39
40
  "smithy-node-native-fetch": "1.0.6"
40
41
  },
41
- "gitHead": "b30bbcf178be9339914e2e5203c61c0da2c3daf2"
42
+ "gitHead": "df9de3cbb530e8f985a660bca0bf65bd027dbb01"
42
43
  }
package/readme.md CHANGED
@@ -47,29 +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
65
  If the `type` argument is specified and the value of the `claim` does not match the detected BLOB
65
- type, then
66
- a `TYPE_MISMATCH` error is returned.
66
+ type, then a `TYPE_MISMATCH` error is returned.
67
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
69
- value is used.
68
+ types, then the given value is used.
70
69
  If the list of [acceptable types](https://datatracker.ietf.org/doc/html/rfc2616#section-14.1) is
71
- passed and the type of
72
- the BLOB does not match any of its values, then a `NOT_ACCEPTABLE` error is returned.
70
+ passed and the type of the BLOB does not match any of its values, then a `NOT_ACCEPTABLE` error is
71
+ returned.
73
72
 
74
73
  Known types
75
74
  are: `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/heic`, `image/jxl`, `image/avif`.
@@ -155,7 +154,6 @@ and [`toa env`](/runtime/cli/readme.md#env)
155
154
  for local environment.
156
155
  `endpoint` parameter is optional.
157
156
 
158
-
159
157
  ### Filesystem
160
158
 
161
159
  Annotation format is:
@@ -177,7 +175,7 @@ Annotation format is:
177
175
  storages:
178
176
  photos@dev:
179
177
  provider: tmp
180
- prefix: my-app-tmp
178
+ directory: my-app-tmp
181
179
  ```
182
180
 
183
181
  ### Memory
@@ -189,7 +187,7 @@ Annotation value format is:
189
187
  ```yaml
190
188
  storages:
191
189
  photos@dev:
192
- provider: memory
190
+ provider: mem
193
191
  ```
194
192
 
195
193
  ## Deduplication
@@ -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/Factory.ts CHANGED
@@ -3,18 +3,23 @@ import { decode } from '@toa.io/generic'
3
3
  import { providers } from './providers'
4
4
  import { Storage, type Storages } from './Storage'
5
5
  import { Aspect } from './Aspect'
6
- import { validateProviderId, SERIALIZATION_PREFIX } from './deployment'
7
- import type { ProviderConstructor } from './Provider'
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'
8
11
 
9
12
  export class Factory {
10
- private readonly declaration: Record<string, Record<string, unknown>>
13
+ private readonly annotation: Annotation
11
14
 
12
15
  public constructor () {
13
16
  const env = process.env.TOA_STORAGES
14
17
 
15
- if (env === undefined) throw new Error('TOA_STORAGES is not defined')
18
+ assert.ok(env !== undefined, 'TOA_STORAGES is not defined')
16
19
 
17
- this.declaration = decode(env)
20
+ this.annotation = decode(env)
21
+
22
+ validateAnnotation(this.annotation)
18
23
  }
19
24
 
20
25
  public aspect (): Aspect {
@@ -23,35 +28,33 @@ export class Factory {
23
28
  return new Aspect(storages)
24
29
  }
25
30
 
26
- public createStorage (componentName: string, { provider: providerId, ...props }: any): Storage {
27
- validateProviderId(providerId)
28
-
29
- const Provider = providers[providerId]
30
-
31
- const secrets = this.resolveSecrets(componentName, Provider)
32
-
33
- const provider = new Provider({ ...props, secrets })
34
-
35
- return new Storage(provider)
36
- }
37
-
38
31
  private createStorages (): Storages {
39
32
  const storages: Storages = {}
40
33
 
41
- for (const [componentName, props] of Object.entries(this.declaration))
42
- storages[componentName] = this.createStorage(componentName, props)
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 (componentName: string,
48
- Class: ProviderConstructor): Record<string, string | undefined> {
49
- if (Class.SECRETS === undefined) return {}
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 {
51
+ if (Class.SECRETS === undefined)
52
+ return {}
50
53
 
51
54
  const secrets: Record<string, string | undefined> = {}
52
55
 
53
56
  for (const secret of Class.SECRETS) {
54
- const variable = `${SERIALIZATION_PREFIX}_${componentName}_${secret.name}`.toUpperCase()
57
+ const variable = `${SERIALIZATION_PREFIX}_${storageName}_${secret.name}`.toUpperCase()
55
58
  const value = process.env[variable]
56
59
 
57
60
  assert.ok(secret.optional === true || value !== undefined,
@@ -1,9 +1,7 @@
1
1
  import { type Readable } from 'node:stream'
2
2
  import * as assert from 'node:assert'
3
3
 
4
- export interface ProviderSecrets {
5
- secrets?: Record<string, string>
6
- }
4
+ export type ProviderSecrets<K extends string = string> = Record<K | string, string | undefined>
7
5
 
8
6
  export interface ProviderSecret {
9
7
  readonly name: string
@@ -13,19 +11,23 @@ export interface ProviderSecret {
13
11
  export abstract class Provider<Options = void> {
14
12
  public static readonly SECRETS: readonly ProviderSecret[] = []
15
13
 
16
- public constructor (props: Options & ProviderSecrets) {
14
+ public constructor (_: Options, secrets?: ProviderSecrets) {
17
15
  for (const { name, optional = false } of new.target.SECRETS)
18
- assert.ok(optional || props.secrets?.[name] !== undefined, `Missing secret '${name}'`)
16
+ assert.ok(optional || secrets?.[name] !== undefined, `Missing secret '${name}'`)
19
17
  }
20
18
 
21
19
  public abstract get (path: string): Promise<Readable | null>
20
+
22
21
  public abstract put (path: string, filename: string, stream: Readable): Promise<void>
22
+
23
23
  public abstract delete (path: string): Promise<void>
24
+
24
25
  public abstract move (from: string, to: string): Promise<void>
25
26
  }
26
27
 
27
28
  export interface ProviderConstructor {
28
29
  readonly SECRETS: readonly ProviderSecret[]
29
30
  prototype: Provider
30
- new(props: any): Provider
31
+
32
+ new (options: any, secrets?: ProviderSecrets): Provider
31
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
  }
@@ -8,11 +8,12 @@ import { Storage } from './Storage'
8
8
  import { suites } from './test/util'
9
9
  import { type Entry } from './Entry'
10
10
  import { providers } from './providers'
11
+ import type { ProviderConstructor } from './Provider'
11
12
 
12
13
  let storage: Storage
13
14
  let dir: string
14
15
 
15
- const { run, provider: providerId, ...props } = suites[0]
16
+ const suite = suites[0]
16
17
 
17
18
  beforeAll(async () => {
18
19
  process.chdir(path.join(__dirname, 'test'))
@@ -21,8 +22,8 @@ beforeAll(async () => {
21
22
  beforeEach(() => {
22
23
  dir = '/' + randomUUID()
23
24
 
24
- const Provider = providers[providerId]
25
- const provider = new Provider(props)
25
+ const Provider: ProviderConstructor = providers[suite.provider]
26
+ const provider = new Provider(suite.options)
26
27
 
27
28
  storage = new Storage(provider)
28
29
  })
package/source/Storage.ts CHANGED
@@ -4,7 +4,7 @@ import { decode, encode } from 'msgpackr'
4
4
  import { buffer, newid } from '@toa.io/generic'
5
5
  import { Err } from 'error-value'
6
6
  import { Scanner } from './Scanner'
7
- import type { TypeControl } from './Scanner'
7
+ import type { ScanOptions } from './Scanner'
8
8
  import type { Provider } from './Provider'
9
9
  import type { Entry } from './Entry'
10
10
 
@@ -15,8 +15,8 @@ export class Storage {
15
15
  this.provider = provider
16
16
  }
17
17
 
18
- public async put (path: string, stream: Readable, type?: TypeControl): Maybe<Entry> {
19
- const scanner = new Scanner(type)
18
+ public async put (path: string, stream: Readable, options?: Options): Maybe<Entry> {
19
+ const scanner = new Scanner(options)
20
20
  const pipe = stream.pipe(scanner)
21
21
  const tempname = await this.transit(pipe)
22
22
 
@@ -27,7 +27,7 @@ export class Storage {
27
27
 
28
28
  await this.persist(tempname, id)
29
29
 
30
- return await this.create(path, id, scanner.size, scanner.type)
30
+ return await this.create(path, id, scanner.size, scanner.type, options?.meta)
31
31
  }
32
32
 
33
33
  public async get (path: string): Maybe<Entry> {
@@ -160,14 +160,15 @@ export class Storage {
160
160
  }
161
161
 
162
162
  // eslint-disable-next-line max-params
163
- private async create (path: string, id: string, size: number, type: string): Promise<Entry> {
163
+ private async create
164
+ (path: string, id: string, size: number, type: string, meta: Meta = {}): Promise<Entry> {
164
165
  const entry: Entry = {
165
166
  id,
166
167
  size,
167
168
  type,
168
169
  created: Date.now(),
169
170
  variants: [],
170
- meta: {}
171
+ meta
171
172
  }
172
173
 
173
174
  const metafile = posix.join(path, entry.id)
@@ -235,4 +236,10 @@ interface Path {
235
236
  variant: string | null
236
237
  }
237
238
 
239
+ type Meta = Record<string, string>
240
+
241
+ interface Options extends ScanOptions {
242
+ meta?: Meta
243
+ }
244
+
238
245
  export type Storages = Record<string, Storage>
@@ -1,12 +1,14 @@
1
1
  import * as assert from 'node:assert'
2
2
  import { encode } from '@toa.io/generic'
3
3
  import { providers } from './providers'
4
+ import { validateAnnotation } from './Annotation'
5
+ import type { Annotation } from './Annotation'
4
6
  import type { Dependency, Variable } from '@toa.io/operations'
5
7
  import type { context } from '@toa.io/norm'
6
8
 
7
9
  export const SERIALIZATION_PREFIX = 'TOA_STORAGES'
8
10
 
9
- export function deployment (instances: Instance[], annotation: Annotation): Dependency {
11
+ export function deployment (instances: Instance[], annotation: unknown): Dependency {
10
12
  validate(instances, annotation)
11
13
 
12
14
  const value = encode(annotation)
@@ -18,16 +20,11 @@ export function deployment (instances: Instance[], annotation: Annotation): Depe
18
20
  }
19
21
  }
20
22
 
21
- function validate (instances: Instance[],
22
- annotation: Annotation): asserts annotation is ValidatedAnnotation {
23
- assert.ok(annotation !== undefined,
24
- `Storages annotation is required by: '${instances
25
- .map((i) => i.component.locator.id)
26
- .join("', '")}'`)
23
+ function validate (instances: Instance[], annotation: unknown): asserts annotation is Annotation {
24
+ validateAnnotation(annotation)
27
25
 
28
- for (const instance of instances) contains(instance, annotation)
29
-
30
- for (const { provider } of Object.values(annotation)) validateProviderId(provider)
26
+ for (const instance of instances)
27
+ contains(instance, annotation)
31
28
  }
32
29
 
33
30
  function contains (instance: Instance, annotation: Annotation): void {
@@ -37,21 +34,17 @@ function contains (instance: Instance, annotation: Annotation): void {
37
34
  `declared in '${instance.component.locator.id}'`)
38
35
  }
39
36
 
40
- export function validateProviderId (id: string | undefined): asserts id is keyof typeof providers {
41
- assert.ok(typeof id === 'string' && id in providers, `Unknown storage provider '${id}'`)
42
- }
43
-
44
- function getSecrets (annotation: ValidatedAnnotation): Variable[] {
37
+ function getSecrets (annotation: Annotation): Variable[] {
45
38
  const secrets: Variable[] = []
46
39
 
47
- for (const [componentName, props] of Object.entries(annotation)) {
48
- const Provider = providers[props.provider]
40
+ for (const [name, declaration] of Object.entries(annotation)) {
41
+ const Provider = providers[declaration.provider]
49
42
 
50
43
  for (const secret of Provider.SECRETS)
51
44
  secrets.push({
52
- name: `${SERIALIZATION_PREFIX}_${componentName}_${secret.name}`.toUpperCase(),
45
+ name: `${SERIALIZATION_PREFIX}_${name}_${secret.name}`.toUpperCase(),
53
46
  secret: {
54
- name: `toa-storages-${componentName}`,
47
+ name: `toa-storages-${name}`,
55
48
  key: secret.name,
56
49
  optional: secret.optional
57
50
  }
@@ -61,8 +54,4 @@ function getSecrets (annotation: ValidatedAnnotation): Variable[] {
61
54
  return secrets
62
55
  }
63
56
 
64
- type Annotation = Record<string, { [k: string]: unknown, provider: string }>
65
- type ValidatedAnnotation = Readonly<
66
- Record<string, { [k: string]: unknown, provider: keyof typeof providers }>
67
- >
68
57
  export type Instance = context.Dependency<string[]>
@@ -0,0 +1,10 @@
1
+ import type { S3Options } from './S3'
2
+ import type { FileSystemOptions } from './FileSystem'
3
+ import type { TemporaryOptions } from './Temporary'
4
+
5
+ export type Declaration =
6
+ ({ provider: 's3' } & S3Options)
7
+ | ({ provider: 'fs' } & FileSystemOptions)
8
+ | ({ provider: 'tmp' } & TemporaryOptions)
9
+ | ({ provider: 'mem' })
10
+ | ({ provider: 'test' } & TemporaryOptions)
@@ -1,8 +1,8 @@
1
1
  import { type Readable } from 'node:stream'
2
2
  import { dirname, join } from 'node:path'
3
3
  import fs from 'node:fs/promises'
4
- import assert from 'node:assert'
5
4
  import { Provider } from '../Provider'
5
+ import type { ProviderSecrets } from '../Provider'
6
6
 
7
7
  export interface FileSystemOptions {
8
8
  path: string
@@ -11,10 +11,10 @@ export interface FileSystemOptions {
11
11
  export class FileSystem extends Provider<FileSystemOptions> {
12
12
  protected readonly path: string
13
13
 
14
- public constructor (props: FileSystemOptions) {
15
- super(props)
16
- assert.ok(props.path, 'Missing path')
17
- this.path = props.path
14
+ public constructor (options: FileSystemOptions, secrets?: ProviderSecrets) {
15
+ super(options, secrets)
16
+
17
+ this.path = options.path
18
18
  }
19
19
 
20
20
  public async get (path: string): Promise<Readable | null> {
@@ -24,11 +24,10 @@ describe('S3 storage provider', () => {
24
24
 
25
25
  provider = new S3({
26
26
  bucket: TEST_BUCKET_NAME,
27
- region: AWS_REGION,
28
- secrets: {
29
- ACCESS_KEY_ID: 'test-key',
30
- SECRET_ACCESS_KEY: 'test-key-secret'
31
- }
27
+ region: AWS_REGION
28
+ }, {
29
+ ACCESS_KEY_ID: 'test-key',
30
+ SECRET_ACCESS_KEY: 'test-key-secret'
32
31
  })
33
32
  })
34
33
 
@@ -22,11 +22,13 @@ import type { ReadableStream } from 'node:stream/web'
22
22
 
23
23
  export interface S3Options {
24
24
  bucket: string
25
- prefix?: string
26
25
  region?: string
26
+ prefix?: string
27
27
  endpoint?: string
28
28
  }
29
29
 
30
+ type S3Secrets = ProviderSecrets<'ACCESS_KEY_ID' | 'SECRET_ACCESS_KEY'>
31
+
30
32
  export class S3 extends Provider<S3Options> {
31
33
  public static override readonly SECRETS: readonly ProviderSecret[] = [
32
34
  { name: 'ACCESS_KEY_ID', optional: true },
@@ -36,29 +38,32 @@ export class S3 extends Provider<S3Options> {
36
38
  protected readonly bucket: string
37
39
  protected readonly client: S3Client
38
40
 
39
- public constructor (props: S3Options & ProviderSecrets) {
40
- super(props)
41
+ public constructor (options: S3Options, secrets?: S3Secrets) {
42
+ super(options)
41
43
 
42
- assert.ok(props.bucket, 'Missing bucket name')
43
- this.bucket = props.bucket
44
+ this.bucket = options.bucket
44
45
 
45
46
  const s3Config: S3ClientConfigType = {
46
47
  retryMode: 'adaptive',
47
48
  ...nodeNativeFetch
48
49
  }
49
50
 
50
- if (props.endpoint !== undefined) {
51
- s3Config.forcePathStyle = props.endpoint.startsWith('http://')
52
- s3Config.endpoint = props.endpoint
51
+ if (options.endpoint !== undefined) {
52
+ s3Config.forcePathStyle = options.endpoint.startsWith('http://')
53
+ s3Config.endpoint = options.endpoint
53
54
  }
54
55
 
55
- if (props.region !== undefined) s3Config.region = props.region
56
+ if (options.region !== undefined) s3Config.region = options.region
57
+
58
+ if (typeof secrets?.ACCESS_KEY_ID === 'string') {
59
+ assert.ok(secrets.SECRET_ACCESS_KEY !== undefined,
60
+ 'SECRET_ACCESS_KEY is required if ACCESS_KEY_ID is provided')
56
61
 
57
- if (typeof props.secrets?.ACCESS_KEY_ID === 'string')
58
62
  s3Config.credentials = {
59
- accessKeyId: props.secrets.ACCESS_KEY_ID,
60
- secretAccessKey: props.secrets.SECRET_ACCESS_KEY
63
+ accessKeyId: secrets.ACCESS_KEY_ID,
64
+ secretAccessKey: secrets.SECRET_ACCESS_KEY
61
65
  }
66
+ }
62
67
 
63
68
  this.client = new S3Client(s3Config)
64
69
 
@@ -1,14 +1,16 @@
1
1
  import { tmpdir } from 'node:os'
2
2
  import { join } from 'node:path'
3
-
4
3
  import { FileSystem } from './FileSystem'
4
+ import type { ProviderSecrets } from '../Provider'
5
5
 
6
6
  export interface TemporaryOptions {
7
- prefix?: string
7
+ directory: string
8
8
  }
9
9
 
10
10
  export class Temporary extends FileSystem {
11
- public constructor (props: TemporaryOptions) {
12
- super({ path: join(tmpdir(), props.prefix ?? '') })
11
+ public constructor (options: TemporaryOptions, secrets?: ProviderSecrets) {
12
+ const path = join(tmpdir(), options.directory)
13
+
14
+ super({ path }, secrets)
13
15
  }
14
16
  }
@@ -1,5 +1,5 @@
1
1
  import { Temporary, type TemporaryOptions } from './Temporary'
2
- import type { ProviderSecret } from '../Provider'
2
+ import type { ProviderSecret, ProviderSecrets } from '../Provider'
3
3
 
4
4
  export class Test extends Temporary {
5
5
  public static override readonly SECRETS: readonly ProviderSecret[] = [
@@ -7,7 +7,7 @@ export class Test extends Temporary {
7
7
  { name: 'PASSWORD' }
8
8
  ]
9
9
 
10
- public constructor (props: TemporaryOptions) {
11
- super(props)
10
+ public constructor (options: TemporaryOptions, secrets?: ProviderSecrets) {
11
+ super(options, secrets)
12
12
  }
13
13
  }