@storecraft/storage-s3-compatible 1.0.11 → 1.0.13

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/README.md CHANGED
@@ -28,10 +28,10 @@ npm i @storecraft/storage-s3-compatible
28
28
  import { R2, S3, DigitalOceanSpaces, S3CompatibleStorage } from '@storecraft/storage-s3-compatible'
29
29
 
30
30
  const storage = new R2({
31
- accessKeyId: process.env.R2_ACCESS_KEY_ID,
32
- account_id: process.env.R2_ACCOUNT_ID,
33
- bucket: process.env.R2_BUCKET,
34
- secretAccessKey: process.env.R2_SECRET_ACCESS_KEY
31
+ accessKeyId: process.env.S3_ACCESS_KEY_ID,
32
+ account_id: process.env.CF_ACCOUNT_ID,
33
+ bucket: process.env.S3_BUCKET,
34
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY
35
35
  });
36
36
 
37
37
  // write
@@ -69,12 +69,7 @@ const app = new App(
69
69
  .withPlatform(new NodePlatform())
70
70
  .withDatabase(new MongoDB())
71
71
  .withStorage(
72
- new R2(
73
- process.env.R2_BUCKET,
74
- process.env.R2_ACCOUNT_ID,
75
- process.env.R2_ACCESS_KEY_ID,
76
- process.env.R2_SECRET_ACCESS_KEY
77
- )
72
+ new R2() // config will be inferred by env variables
78
73
  );
79
74
 
80
75
  await app.init();
package/adapter.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * @import { AwsS3Config, Config, R2Config } from './types.public.js'
3
+ * @import { ENV } from '@storecraft/core';
3
4
  * @import { storage_driver, StorageFeatures } from '@storecraft/core/storage'
4
5
  */
5
6
  import { App } from '@storecraft/core'
@@ -27,7 +28,6 @@ const infer_content_type = (name) => {
27
28
  return type ?? 'application/octet-stream';
28
29
  }
29
30
 
30
-
31
31
  /**
32
32
  * @description The base S3 compatible class
33
33
  *
@@ -35,24 +35,16 @@ const infer_content_type = (name) => {
35
35
  */
36
36
  export class S3CompatibleStorage {
37
37
 
38
+ /** @satisfies {ENV<Config>} */
39
+ static EnvConfig = /** @type{const} */ ({
40
+ accessKeyId: 'S3_ACCESS_KEY_ID',
41
+ secretAccessKey: 'S3_SECRET_ACCESS_KEY',
42
+ bucket: 'S3_BUCKET',
43
+ region: 'S3_REGION',
44
+ });
45
+
38
46
  /** @type {AwsClient} */ #_client;
39
47
  /** @type {Config} */ #_config;
40
- /** @type {string} */ #_url;
41
-
42
- /**
43
- *
44
- * @param {Config} options
45
- */
46
- #compute_url(options) {
47
- const url = new URL(options.endpoint);
48
- if(options.forcePathStyle) {
49
- url.pathname = options.bucket;
50
- } else {
51
- url.host = `${options.bucket}.${url.host}`;
52
- }
53
- return url.toString();
54
- }
55
-
56
48
 
57
49
  /**
58
50
  *
@@ -60,17 +52,20 @@ export class S3CompatibleStorage {
60
52
  */
61
53
  constructor(config) {
62
54
  this.#_config = config;
63
- this.#_client = new AwsClient({
64
- accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey,
65
- region: config.region ?? 'auto', service: 's3'
66
- });
67
-
68
- this.#_url = this.#compute_url(config);
69
55
  }
70
56
 
71
- get url() { return this.#_url; }
72
- get client() { return this.#_client; }
73
57
  get config() { return this.#_config; }
58
+ get client() {
59
+ this.#_client = this.#_client ?? new AwsClient(
60
+ {
61
+ accessKeyId: this.config.accessKeyId,
62
+ secretAccessKey: this.config.secretAccessKey,
63
+ region: this.config.region ?? 'auto',
64
+ service: 's3'
65
+ }
66
+ );
67
+ return this.#_client;
68
+ }
74
69
 
75
70
  features() {
76
71
  /** @type {StorageFeatures} */
@@ -85,7 +80,14 @@ export class S3CompatibleStorage {
85
80
  *
86
81
  * @type {storage_driver["init"]}
87
82
  */
88
- async init(app) { return this; }
83
+ async init(app) {
84
+ this.config.accessKeyId ??= app.platform.env[S3CompatibleStorage.EnvConfig.accessKeyId];
85
+ this.config.secretAccessKey ??= app.platform.env[S3CompatibleStorage.EnvConfig.secretAccessKey];
86
+ this.config.bucket ??= app.platform.env[S3CompatibleStorage.EnvConfig.bucket];
87
+ // @ts-ignore
88
+ this.config.region ??= app.platform.env[S3CompatibleStorage.EnvConfig.region];
89
+ return this;
90
+ }
89
91
 
90
92
  // puts
91
93
 
@@ -170,9 +172,19 @@ export class S3CompatibleStorage {
170
172
 
171
173
  // gets
172
174
 
175
+ get url() {
176
+ const url = new URL(this.config.endpoint);
177
+ if(this.config.forcePathStyle) {
178
+ url.pathname = this.config.bucket;
179
+ } else {
180
+ url.host = `${this.config.bucket}.${url.host}`;
181
+ }
182
+ return url.toString();
183
+ }
184
+
173
185
  /** @param {string} key */
174
186
  get_file_url(key) {
175
- return `${this.#_url}/${key}`;
187
+ return `${this.url}/${key}`;
176
188
  }
177
189
 
178
190
  /** @param {string} key */
@@ -269,58 +281,117 @@ export class S3CompatibleStorage {
269
281
  */
270
282
  export class R2 extends S3CompatibleStorage {
271
283
 
284
+ /** @satisfies {ENV<R2Config>} */
285
+ static R2EnvConfig = /** @type{const} */ ({
286
+ accessKeyId: 'S3_ACCESS_KEY_ID',
287
+ secretAccessKey: 'S3_SECRET_ACCESS_KEY',
288
+ bucket: 'S3_BUCKET',
289
+ account_id: 'CF_ACCOUNT_ID',
290
+ });
291
+
272
292
  /**
273
293
  * @param {R2Config} config
274
294
  */
275
- constructor({bucket, account_id, accessKeyId, secretAccessKey}) {
295
+ constructor(config={}) {
276
296
  super(
277
297
  {
278
- endpoint: `https://${account_id}.r2.cloudflarestorage.com`,
279
- accessKeyId, secretAccessKey, bucket,
280
- forcePathStyle: true, region: 'auto'
298
+ endpoint: config.account_id ?
299
+ `https://${config.account_id}.r2.cloudflarestorage.com` :
300
+ undefined,
301
+ accessKeyId: config.accessKeyId,
302
+ secretAccessKey:config.secretAccessKey,
303
+ bucket: config.bucket,
304
+ forcePathStyle: true,
305
+ region: 'auto'
281
306
  }
282
- )
307
+ );
308
+ this.r2_config = config;
283
309
  }
284
310
 
311
+ /** @type {S3CompatibleStorage["init"]} */
312
+ init = async (app) => {
313
+ await super.init(app);
314
+ this.r2_config.account_id ??= app.platform.env[R2.R2EnvConfig.account_id];
315
+ this.config.endpoint ??= `https://${this.r2_config.account_id}.r2.cloudflarestorage.com`;
316
+ return this;
317
+ }
318
+
285
319
  }
286
320
 
321
+
287
322
  /**
288
323
  * Amazon S3
289
324
  */
290
325
  export class S3 extends S3CompatibleStorage {
291
326
 
327
+ /** @satisfies {ENV<AwsS3Config>} */
328
+ static AWSS3EnvConfig = /** @type{const} */ ({
329
+ ...S3CompatibleStorage.EnvConfig
330
+ });
331
+
292
332
  /**
293
- * @param {AwsS3Config} config
333
+ * @param {Partial<AwsS3Config>} config
294
334
  */
295
- constructor({bucket, region, accessKeyId, secretAccessKey, forcePathStyle=false}) {
335
+ constructor(config = {}) {
296
336
  super(
297
337
  {
298
- endpoint: `https://s3${region ? ('.'+region) : ''}.amazonaws.com`,
299
- accessKeyId, secretAccessKey,
300
- bucket, forcePathStyle, region
338
+ endpoint: config.region ? `https://s3.${config.region}.amazonaws.com` : undefined,
339
+ accessKeyId: config.accessKeyId,
340
+ secretAccessKey: config.secretAccessKey,
341
+ bucket: config.bucket,
342
+ forcePathStyle: config.forcePathStyle ?? false,
343
+ region: config.region
301
344
  }
302
345
  )
303
346
  }
304
347
 
348
+ /** @type {S3CompatibleStorage["init"]} */
349
+ init = async (app) => {
350
+ await super.init(app);
351
+ this.config.endpoint = `https://s3${this.config.region ?
352
+ ('.'+this.config.region) : ''}.amazonaws.com`;
353
+ return this;
354
+ }
355
+
305
356
  }
306
357
 
358
+
307
359
  /**
308
360
  * Digital Ocean spaces
309
361
  */
310
362
  export class DigitalOceanSpaces extends S3CompatibleStorage {
311
363
 
364
+ /** @satisfies {ENV<AwsS3Config>} */
365
+ static DOEnvConfig = /** @type{const} */ ({
366
+ ...S3CompatibleStorage.EnvConfig
367
+ });
368
+
312
369
  /**
313
- * @param {Omit<Config, 'endpoint' | 'forcePathStyle'>} config
370
+ * @param {Partial<Omit<Config, 'endpoint' | 'forcePathStyle'>>} config
314
371
  */
315
- constructor({bucket, region, accessKeyId, secretAccessKey}) {
372
+ constructor(config = {}) {
316
373
  super(
317
374
  {
318
- endpoint: `https://${region}.digitaloceanspaces.com`,
319
- accessKeyId, secretAccessKey,
320
- bucket, forcePathStyle: false, region: 'auto'
375
+ endpoint: config.region ?
376
+ `https://${config.region}.digitaloceanspaces.com` : undefined,
377
+ accessKeyId: config.accessKeyId,
378
+ secretAccessKey: config.secretAccessKey,
379
+ bucket: config.bucket,
380
+ forcePathStyle: false, region: 'auto'
321
381
  }
322
382
  )
323
383
  }
324
384
 
385
+ /** @type {S3CompatibleStorage["init"]} */
386
+ init = async (app) => {
387
+ await super.init(app);
388
+ this.config.endpoint = this.config.region ?
389
+ `https://${this.config.region}.digitaloceanspaces.com` :
390
+ undefined;
391
+
392
+ return this;
393
+ }
394
+
395
+
325
396
  }
326
397
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storecraft/storage-s3-compatible",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Official S3-Compatible Storage adapter for storecraft",
5
5
  "license": "MIT",
6
6
  "author": "Tomer Shalev (https://github.com/store-craft)",
@@ -16,6 +16,7 @@ const storage = new S3({
16
16
  accessKeyId: process.env.S3_ACCESS_KEY_ID,
17
17
  bucket: process.env.S3_BUCKET,
18
18
  forcePathStyle: FORCE_PATH_STYLE,
19
+ // @ts-ignore
19
20
  region: process.env.S3_REGION,
20
21
  secretAccessKey: process.env.S3_SECRET_KEY
21
22
  });
@@ -12,10 +12,10 @@ const areBlobsEqual = async (blob1, blob2) => {
12
12
  };
13
13
 
14
14
  const storage = new R2({
15
- accessKeyId: process.env.R2_ACCESS_KEY_ID,
16
- account_id: process.env.R2_ACCOUNT_ID,
17
- bucket: process.env.R2_BUCKET,
18
- secretAccessKey: process.env.R2_SECRET_ACCESS_KEY
15
+ accessKeyId: process.env.S3_ACCESS_KEY_ID,
16
+ account_id: process.env.CF_ACCOUNT_ID,
17
+ bucket: process.env.S3_BUCKET,
18
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY
19
19
  });
20
20
 
21
21
  const suite = storage_test_runner.create(storage);
@@ -6,12 +6,13 @@ const FORCE_PATH_STYLE = true;
6
6
 
7
7
  const storage = new S3CompatibleStorage(
8
8
  {
9
- accessKeyId: process.env.ACCESS_KEY_ID,
10
- secretAccessKey: process.env.SECRET_ACCESS_KEY,
11
- bucket: process.env.BUCKET,
9
+ accessKeyId: process.env.S3_ACCESS_KEY_ID,
10
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
11
+ bucket: process.env.S3_BUCKET,
12
12
  endpoint: process.env.ENDPOINT,
13
13
  forcePathStyle: FORCE_PATH_STYLE,
14
- region: process.env.REGION
14
+ // @ts-ignore
15
+ region: process.env.S3_REGION
15
16
  }
16
17
  );
17
18
 
package/types.public.d.ts CHANGED
@@ -1,21 +1,58 @@
1
1
  export { DigitalOceanSpaces, R2, S3, S3CompatibleStorage } from './adapter.js';
2
2
 
3
3
  export type Config = {
4
- endpoint: string;
5
- bucket: string;
6
- accessKeyId: string;
7
- secretAccessKey: string;
8
- region: string;
9
- forcePathStyle: boolean;
4
+ /** Optional, complete endpoint if you know it in advance */
5
+ endpoint?: string;
6
+ /** If missing, will be inferred by env variable `S3_BUCKET` */
7
+ bucket?: string;
8
+ /** If missing, will be inferred by env variable `S3_ACCESS_KEY_ID` */
9
+ accessKeyId?: string;
10
+ /** If missing, will be inferred by env variable `S3_SECRET_ACCESS_KEY` */
11
+ secretAccessKey?: string;
12
+ /** If missing, will be inferred by env variable `S3_REGION` */
13
+ region?: 'auto' | AWSRegion;
14
+ forcePathStyle?: boolean;
10
15
  }
11
16
 
12
17
  export type R2Config = Omit<Config, 'region' | 'forcePathStyle' | 'endpoint'> & {
13
18
  /**
14
- * @description cloudflare account id
19
+ * @description cloudflare account id. If missing will be inferred by env variable `CF_ACCOUNT_ID`
15
20
  */
16
- account_id: string;
21
+ account_id?: string;
17
22
  };
18
23
 
19
24
 
20
25
  export type AwsS3Config = Omit<Config, 'endpoint'>;
21
26
 
27
+ type AWSRegion =
28
+ | "us-east-1"
29
+ | "us-east-2"
30
+ | "us-west-1"
31
+ | "us-west-2"
32
+ | "af-south-1"
33
+ | "ap-east-1"
34
+ | "ap-south-1"
35
+ | "ap-south-2"
36
+ | "ap-southeast-1"
37
+ | "ap-southeast-2"
38
+ | "ap-southeast-3"
39
+ | "ap-northeast-1"
40
+ | "ap-northeast-2"
41
+ | "ap-northeast-3"
42
+ | "ca-central-1"
43
+ | "ca-west-1"
44
+ | "eu-central-1"
45
+ | "eu-central-2"
46
+ | "eu-west-1"
47
+ | "eu-west-2"
48
+ | "eu-west-3"
49
+ | "eu-north-1"
50
+ | "eu-south-1"
51
+ | "eu-south-2"
52
+ | "me-central-1"
53
+ | "me-south-1"
54
+ | "sa-east-1"
55
+ | "us-gov-east-1"
56
+ | "us-gov-west-1"
57
+ | "cn-north-1"
58
+ | "cn-northwest-1";