@storecraft/storage-s3-compatible 1.0.10 → 1.0.12

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,3 +1,8 @@
1
+ /**
2
+ * @import { AwsS3Config, Config, R2Config } from './types.public.js'
3
+ * @import { ENV } from '@storecraft/core';
4
+ * @import { storage_driver, StorageFeatures } from '@storecraft/core/storage'
5
+ */
1
6
  import { App } from '@storecraft/core'
2
7
  import { AwsClient } from './aws4fetch.js';
3
8
 
@@ -23,38 +28,23 @@ const infer_content_type = (name) => {
23
28
  return type ?? 'application/octet-stream';
24
29
  }
25
30
 
26
-
27
- /**
28
- * @typedef {import('./types.public.d.ts').Config} Config
29
- */
30
-
31
31
  /**
32
32
  * @description The base S3 compatible class
33
33
  *
34
- * @typedef {import('@storecraft/core/storage').storage_driver} storage
35
- *
36
- * @implements {storage}
34
+ * @implements {storage_driver}
37
35
  */
38
36
  export class S3CompatibleStorage {
39
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
+
40
46
  /** @type {AwsClient} */ #_client;
41
47
  /** @type {Config} */ #_config;
42
- /** @type {string} */ #_url;
43
-
44
- /**
45
- *
46
- * @param {Config} options
47
- */
48
- #compute_url(options) {
49
- const url = new URL(options.endpoint);
50
- if(options.forcePathStyle) {
51
- url.pathname = options.bucket;
52
- } else {
53
- url.host = `${options.bucket}.${url.host}`;
54
- }
55
- return url.toString();
56
- }
57
-
58
48
 
59
49
  /**
60
50
  *
@@ -62,20 +52,23 @@ export class S3CompatibleStorage {
62
52
  */
63
53
  constructor(config) {
64
54
  this.#_config = config;
65
- this.#_client = new AwsClient({
66
- accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey,
67
- region: config.region ?? 'auto', service: 's3'
68
- });
69
-
70
- this.#_url = this.#compute_url(config);
71
55
  }
72
56
 
73
- get url() { return this.#_url; }
74
- get client() { return this.#_client; }
75
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
+ }
76
69
 
77
70
  features() {
78
- /** @type {import('@storecraft/core/storage').StorageFeatures} */
71
+ /** @type {StorageFeatures} */
79
72
  const f = {
80
73
  supports_signed_urls: true
81
74
  }
@@ -85,9 +78,16 @@ export class S3CompatibleStorage {
85
78
 
86
79
  /**
87
80
  *
88
- * @type {storage["init"]}
81
+ * @type {storage_driver["init"]}
89
82
  */
90
- 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
+ }
91
91
 
92
92
  // puts
93
93
 
@@ -118,7 +118,7 @@ export class S3CompatibleStorage {
118
118
 
119
119
  /**
120
120
  *
121
- * @type {storage["putBlob"]}
121
+ * @type {storage_driver["putBlob"]}
122
122
  */
123
123
  async putBlob(key, blob) {
124
124
  return this.#put_internal(key, blob);
@@ -126,7 +126,7 @@ export class S3CompatibleStorage {
126
126
 
127
127
  /**
128
128
  *
129
- * @type {storage["putArraybuffer"]}
129
+ * @type {storage_driver["putArraybuffer"]}
130
130
  */
131
131
  async putArraybuffer(key, buffer) {
132
132
  return this.#put_internal(key, buffer);
@@ -134,7 +134,7 @@ export class S3CompatibleStorage {
134
134
 
135
135
  /**
136
136
  *
137
- * @type {storage["putStream"]}
137
+ * @type {storage_driver["putStream"]}
138
138
  */
139
139
  async putStream(key, stream, meta={}, bytesLength=0) {
140
140
  const extra_headers = {};
@@ -150,7 +150,7 @@ export class S3CompatibleStorage {
150
150
 
151
151
  /**
152
152
  *
153
- * @type {storage["putSigned"]}
153
+ * @type {storage_driver["putSigned"]}
154
154
  */
155
155
  async putSigned(key) {
156
156
  const url = new URL(this.get_file_url(key));
@@ -172,9 +172,19 @@ export class S3CompatibleStorage {
172
172
 
173
173
  // gets
174
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
+
175
185
  /** @param {string} key */
176
186
  get_file_url(key) {
177
- return `${this.#_url}/${key}`;
187
+ return `${this.url}/${key}`;
178
188
  }
179
189
 
180
190
  /** @param {string} key */
@@ -183,7 +193,7 @@ export class S3CompatibleStorage {
183
193
  }
184
194
 
185
195
  /**
186
- * @type {storage["getArraybuffer"]}
196
+ * @type {storage_driver["getArraybuffer"]}
187
197
  */
188
198
  async getArraybuffer(key) {
189
199
  const r = await this.#get_request(key);
@@ -199,7 +209,7 @@ export class S3CompatibleStorage {
199
209
 
200
210
  /**
201
211
  *
202
- * @type {storage["getBlob"]}
212
+ * @type {storage_driver["getBlob"]}
203
213
  */
204
214
  async getBlob(key) {
205
215
  const r = await this.#get_request(key);
@@ -214,7 +224,7 @@ export class S3CompatibleStorage {
214
224
  }
215
225
 
216
226
  /**
217
- * @type {storage["getStream"]}
227
+ * @type {storage_driver["getStream"]}
218
228
  */
219
229
  async getStream(key) {
220
230
  const r = await this.#get_request(key);
@@ -231,7 +241,7 @@ export class S3CompatibleStorage {
231
241
 
232
242
  /**
233
243
  *
234
- * @type {storage["getSigned"]}
244
+ * @type {storage_driver["getSigned"]}
235
245
  */
236
246
  async getSigned(key) {
237
247
  const url = new URL(this.get_file_url(key));
@@ -256,7 +266,7 @@ export class S3CompatibleStorage {
256
266
 
257
267
  /**
258
268
  *
259
- * @type {storage["remove"]}
269
+ * @type {storage_driver["remove"]}
260
270
  */
261
271
  async remove(key) {
262
272
  const r = await this.client.fetch(
@@ -271,58 +281,117 @@ export class S3CompatibleStorage {
271
281
  */
272
282
  export class R2 extends S3CompatibleStorage {
273
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
+
274
292
  /**
275
- * @param {import('./types.public.d.ts').R2Config} config
293
+ * @param {R2Config} config
276
294
  */
277
- constructor({bucket, account_id, accessKeyId, secretAccessKey}) {
295
+ constructor(config={}) {
278
296
  super(
279
297
  {
280
- endpoint: `https://${account_id}.r2.cloudflarestorage.com`,
281
- accessKeyId, secretAccessKey, bucket,
282
- 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'
283
306
  }
284
- )
307
+ );
308
+ this.r2_config = config;
285
309
  }
286
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
+
287
319
  }
288
320
 
321
+
289
322
  /**
290
323
  * Amazon S3
291
324
  */
292
325
  export class S3 extends S3CompatibleStorage {
293
326
 
327
+ /** @satisfies {ENV<AwsS3Config>} */
328
+ static AWSS3EnvConfig = /** @type{const} */ ({
329
+ ...S3CompatibleStorage.EnvConfig
330
+ });
331
+
294
332
  /**
295
- * @param {import('./types.public.d.ts').AwsS3Config} config
333
+ * @param {Partial<AwsS3Config>} config
296
334
  */
297
- constructor({bucket, region, accessKeyId, secretAccessKey, forcePathStyle=false}) {
335
+ constructor(config = {}) {
298
336
  super(
299
337
  {
300
- endpoint: `https://s3${region ? ('.'+region) : ''}.amazonaws.com`,
301
- accessKeyId, secretAccessKey,
302
- 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
303
344
  }
304
345
  )
305
346
  }
306
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
+
307
356
  }
308
357
 
358
+
309
359
  /**
310
360
  * Digital Ocean spaces
311
361
  */
312
362
  export class DigitalOceanSpaces extends S3CompatibleStorage {
313
363
 
364
+ /** @satisfies {ENV<AwsS3Config>} */
365
+ static DOEnvConfig = /** @type{const} */ ({
366
+ ...S3CompatibleStorage.EnvConfig
367
+ });
368
+
314
369
  /**
315
- * @param {Omit<import('./types.public.d.ts').Config, 'endpoint' | 'forcePathStyle'>} config
370
+ * @param {Partial<Omit<Config, 'endpoint' | 'forcePathStyle'>>} config
316
371
  */
317
- constructor({bucket, region, accessKeyId, secretAccessKey}) {
372
+ constructor(config = {}) {
318
373
  super(
319
374
  {
320
- endpoint: `https://${region}.digitaloceanspaces.com`,
321
- accessKeyId, secretAccessKey,
322
- 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'
323
381
  }
324
382
  )
325
383
  }
326
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
+
327
396
  }
328
397
 
package/aws4fetch.js CHANGED
@@ -1,4 +1,4 @@
1
- // @ts-check
1
+ // @ts-nocheck
2
2
 
3
3
  /**
4
4
  * @license MIT <https://opensource.org/licenses/MIT>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storecraft/storage-s3-compatible",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
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";