@travetto/model-s3 8.0.0-alpha.23 → 8.0.0-alpha.24

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
@@ -13,11 +13,11 @@ npm install @travetto/model-s3
13
13
  yarn add @travetto/model-s3
14
14
  ```
15
15
 
16
- This module provides an [s3](https://aws.amazon.com/documentation/s3/)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and stream against [s3](https://aws.amazon.com/documentation/s3/).
16
+ This module provides an [s3](https://aws.amazon.com/documentation/s3/)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and stream against [s3](https://aws.amazon.com/documentation/s3/).
17
17
 
18
18
  Supported features:
19
- * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
20
19
  * [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/types/blob.ts#L8)
20
+ * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L10)
21
21
  * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
22
22
 
23
23
  Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source or config, you can override and register it with the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module.
@@ -114,4 +114,4 @@ export class S3ModelConfig {
114
114
 
115
115
  Additionally, you can see that the class is registered with the [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L13) annotation, and so these values can be overridden using the standard [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support") resolution paths.
116
116
 
117
- **Note**: Do not commit your `accessKeyId` or `secretAccessKey` values to your source repository, especially if it is public facing. Not only is it a security risk, but Amazon will scan public repos, looking for keys, and if found will react swiftly.
117
+ **Note**: Do not commit your `accessKeyId` or `secretAccessKey` values to your source repository, especially if it is public facing. Not only is it a security risk, but Amazon will scan public repos, looking for keys, and if found will react swiftly.
package/__index__.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from './src/config.ts';
2
- export * from './src/service.ts';
2
+ export * from './src/service.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-s3",
3
- "version": "8.0.0-alpha.23",
3
+ "version": "8.0.0-alpha.24",
4
4
  "type": "module",
5
5
  "description": "S3 backing for the travetto model module.",
6
6
  "keywords": [
@@ -29,11 +29,11 @@
29
29
  "@aws-sdk/client-s3": "^3.1081.0",
30
30
  "@aws-sdk/credential-provider-ini": "^3.972.62",
31
31
  "@aws-sdk/s3-request-presigner": "^3.1081.0",
32
- "@travetto/config": "^8.0.0-alpha.21",
33
- "@travetto/model": "^8.0.0-alpha.22"
32
+ "@travetto/config": "^8.0.0-alpha.22",
33
+ "@travetto/model": "^8.0.0-alpha.23"
34
34
  },
35
35
  "peerDependencies": {
36
- "@travetto/cli": "^8.0.0-alpha.27"
36
+ "@travetto/cli": "^8.0.0-alpha.28"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@travetto/cli": {
package/src/config.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { fromIni } from '@aws-sdk/credential-provider-ini';
2
1
  import type s3 from '@aws-sdk/client-s3';
2
+ import { fromIni } from '@aws-sdk/credential-provider-ini';
3
3
 
4
4
  import { Config, EnvVar } from '@travetto/config';
5
- import { Required, Url } from '@travetto/schema';
6
- import { Runtime } from '@travetto/runtime';
7
5
  import { PostConstruct } from '@travetto/di';
6
+ import { Runtime } from '@travetto/runtime';
7
+ import { Required, Url } from '@travetto/schema';
8
8
 
9
9
  /**
10
10
  * S3 Support as an Asset Source
@@ -77,4 +77,4 @@ export class S3ModelConfig {
77
77
  this.config.forcePathStyle ??= true;
78
78
  }
79
79
  }
80
- }
80
+ }
package/src/service.ts CHANGED
@@ -1,19 +1,42 @@
1
1
  import { Agent } from 'node:https';
2
2
 
3
- import { S3, type CompletedPart, type CreateMultipartUploadRequest, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
3
+ import { type CompletedPart, type CreateMultipartUploadRequest, GetObjectCommand, PutObjectCommand, S3 } from '@aws-sdk/client-s3';
4
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
4
5
  import type { MetadataBearer } from '@aws-sdk/types';
5
6
  import { NodeHttpHandler } from '@smithy/node-http-handler';
6
- import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
7
7
 
8
+ import { Injectable, PostConstruct } from '@travetto/di';
8
9
  import {
9
- type ModelCrudSupport, type ModelStorageSupport, type ModelType, ModelRegistryIndex, ExistsError, NotFoundError, type OptionalId,
10
- type ModelBlobSupport, type ModelExpirySupport, ModelCrudUtil, ModelExpiryUtil, ModelStorageUtil,
11
- type ModelListOptions
10
+ ExistsError,
11
+ type ModelBlobSupport,
12
+ type ModelCrudSupport,
13
+ ModelCrudUtil,
14
+ type ModelExpirySupport,
15
+ ModelExpiryUtil,
16
+ type ModelListOptions,
17
+ ModelRegistryIndex,
18
+ type ModelStorageSupport,
19
+ ModelStorageUtil,
20
+ type ModelType,
21
+ NotFoundError,
22
+ type OptionalId
12
23
  } from '@travetto/model';
13
- import { Injectable, PostConstruct } from '@travetto/di';
14
24
  import {
15
- type Class, RuntimeError, castTo, asFull, type BinaryMetadata, type ByteRange, type BinaryType,
16
- BinaryUtil, type TimeSpan, TimeUtil, type BinaryArray, CodecUtil, BinaryMetadataUtil, TypedObject, JSONUtil
25
+ asFull,
26
+ type BinaryArray,
27
+ type BinaryMetadata,
28
+ BinaryMetadataUtil,
29
+ type BinaryType,
30
+ BinaryUtil,
31
+ type ByteRange,
32
+ type Class,
33
+ CodecUtil,
34
+ castTo,
35
+ JSONUtil,
36
+ RuntimeError,
37
+ type TimeSpan,
38
+ TimeUtil,
39
+ TypedObject
17
40
  } from '@travetto/runtime';
18
41
 
19
42
  import type { S3ModelConfig } from './config.ts';
@@ -26,7 +49,8 @@ function hasLowerContentType<T>(value: T): value is T & { contenttype?: string }
26
49
  return value !== undefined && value !== null && Object.hasOwn(value, 'contenttype');
27
50
  }
28
51
 
29
- type S3Metadata = Pick<CreateMultipartUploadRequest,
52
+ type S3Metadata = Pick<
53
+ CreateMultipartUploadRequest,
30
54
  'ContentType' | 'Metadata' | 'ContentEncoding' | 'ContentLanguage' | 'CacheControl' | 'ContentDisposition'
31
55
  >;
32
56
 
@@ -35,12 +59,13 @@ type S3Metadata = Pick<CreateMultipartUploadRequest,
35
59
  */
36
60
  @Injectable()
37
61
  export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, ModelStorageSupport, ModelExpirySupport {
38
-
39
62
  idSource = ModelCrudUtil.uuidSource();
40
63
  client: S3;
41
64
  config: S3ModelConfig;
42
65
 
43
- constructor(config: S3ModelConfig) { this.config = config; }
66
+ constructor(config: S3ModelConfig) {
67
+ this.config = config;
68
+ }
44
69
 
45
70
  #getMetadata(metadata: BinaryMetadata): S3Metadata {
46
71
  return {
@@ -49,8 +74,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
49
74
  ...(metadata.contentLanguage ? { ContentLanguage: metadata.contentLanguage } : {}),
50
75
  ...(metadata.cacheControl ? { CacheControl: metadata.cacheControl } : {}),
51
76
  Metadata: TypedObject.fromEntries(
52
- TypedObject.entries(metadata)
53
- .map(([key, value]) => [key, typeof value === 'string' ? value : JSONUtil.toUTF8(value)] as const)
77
+ TypedObject.entries(metadata).map(([key, value]) => [key, typeof value === 'string' ? value : JSONUtil.toUTF8(value)] as const)
54
78
  )
55
79
  };
56
80
  }
@@ -74,12 +98,12 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
74
98
  return this.#basicKey(key);
75
99
  }
76
100
 
77
- #query<U extends object>(cls: string | Class, id: string, extra: U = asFull({})): (U & { Key: string, Bucket: string }) {
101
+ #query<U extends object>(cls: string | Class, id: string, extra: U = asFull({})): U & { Key: string; Bucket: string } {
78
102
  const key = this.#resolveKey(cls, id);
79
103
  return { Key: key, Bucket: this.config.bucket, ...extra };
80
104
  }
81
105
 
82
- #queryBlob<U extends object>(id: string, extra: U = asFull({})): (U & { Key: string, Bucket: string }) {
106
+ #queryBlob<U extends object>(id: string, extra: U = asFull({})): U & { Key: string; Bucket: string } {
83
107
  const key = this.#basicKey(id);
84
108
  return { Key: key, Bucket: this.config.bucket, ...extra };
85
109
  }
@@ -94,12 +118,12 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
94
118
  return {};
95
119
  }
96
120
 
97
- async * #iterateBucket(cls?: string | Class, options?: ModelListOptions): AsyncIterable<{ Key: string, id: string }[]> {
121
+ async *#iterateBucket(cls?: string | Class, options?: ModelListOptions): AsyncIterable<{ Key: string; id: string }[]> {
98
122
  let Marker: string | undefined;
99
123
  const batchSize = options?.batchSizeHint ?? 100;
100
124
  const maxCount = options?.limit ?? Number.MAX_SAFE_INTEGER;
101
125
  let produced = 0;
102
- for (; !(options?.abort?.aborted) && produced < maxCount;) {
126
+ for (; !options?.abort?.aborted && produced < maxCount; ) {
103
127
  const items = await this.client.listObjects({
104
128
  Bucket: this.config.bucket,
105
129
  Prefix: cls ? this.#resolveKey(cls) : this.config.namespace,
@@ -134,12 +158,16 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
134
158
  let total = 0;
135
159
  let i = 1;
136
160
  const flush = async (): Promise<void> => {
137
- if (!total) { return; }
138
- const part = await this.client.uploadPart(this.#queryBlob(id, {
139
- Body: BinaryUtil.binaryArrayToUint8Array(BinaryUtil.combineBinaryArrays(buffers)),
140
- PartNumber: i,
141
- UploadId
142
- }));
161
+ if (!total) {
162
+ return;
163
+ }
164
+ const part = await this.client.uploadPart(
165
+ this.#queryBlob(id, {
166
+ Body: BinaryUtil.binaryArrayToUint8Array(BinaryUtil.combineBinaryArrays(buffers)),
167
+ PartNumber: i,
168
+ UploadId
169
+ })
170
+ );
143
171
  parts.push({ PartNumber: i, ETag: part.ETag });
144
172
  i += 1;
145
173
  buffers = [];
@@ -156,10 +184,12 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
156
184
  }
157
185
  await flush();
158
186
 
159
- await this.client.completeMultipartUpload(this.#queryBlob(id, {
160
- UploadId,
161
- MultipartUpload: { Parts: parts }
162
- }));
187
+ await this.client.completeMultipartUpload(
188
+ this.#queryBlob(id, {
189
+ UploadId,
190
+ MultipartUpload: { Parts: parts }
191
+ })
192
+ );
163
193
  } catch (error) {
164
194
  await this.client.abortMultipartUpload(this.#queryBlob(id, { UploadId }));
165
195
  throw error;
@@ -193,14 +223,18 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
193
223
  async initializeClient(): Promise<void> {
194
224
  this.client = new S3({
195
225
  ...this.config.config,
196
- ...('requestHandler' in this.config.config ? {
197
- requestHandler: new NodeHttpHandler({
198
- ...this.config.config.requestHandler,
199
- ...('httpsAgent' in this.config.config.requestHandler! ? {
200
- httpsAgent: new Agent({ ...this.config.config.requestHandler?.httpsAgent ?? {} }),
201
- } : {})
202
- }),
203
- } : {})
226
+ ...('requestHandler' in this.config.config
227
+ ? {
228
+ requestHandler: new NodeHttpHandler({
229
+ ...this.config.config.requestHandler,
230
+ ...('httpsAgent' in this.config.config.requestHandler!
231
+ ? {
232
+ httpsAgent: new Agent({ ...(this.config.config.requestHandler?.httpsAgent ?? {}) })
233
+ }
234
+ : {})
235
+ })
236
+ }
237
+ : {})
204
238
  });
205
239
  ModelStorageUtil.storageInitialization(this);
206
240
  }
@@ -243,10 +277,8 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
243
277
  }
244
278
  throw new NotFoundError(cls, id);
245
279
  } catch (error) {
246
- if (isMetadataBearer(error)) {
247
- if (error.$metadata.httpStatusCode === 404) {
248
- error = new NotFoundError(cls, id);
249
- }
280
+ if (isMetadataBearer(error) && error.$metadata.httpStatusCode === 404) {
281
+ throw new NotFoundError(cls, id);
250
282
  }
251
283
  throw error;
252
284
  }
@@ -258,12 +290,14 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
258
290
  prepped = await ModelCrudUtil.preStore(cls, item, this);
259
291
  }
260
292
  const content = JSONUtil.toBinaryArray(prepped);
261
- await this.client.putObject(this.#query(cls, prepped.id, {
262
- Body: BinaryUtil.binaryArrayToUint8Array(content),
263
- ContentType: 'application/json',
264
- ContentLength: content.byteLength,
265
- ...this.#getExpiryConfig(cls, prepped)
266
- }));
293
+ await this.client.putObject(
294
+ this.#query(cls, prepped.id, {
295
+ Body: BinaryUtil.binaryArrayToUint8Array(content),
296
+ ContentType: 'application/json',
297
+ ContentLength: content.byteLength,
298
+ ...this.#getExpiryConfig(cls, prepped)
299
+ })
300
+ );
267
301
  return prepped;
268
302
  }
269
303
 
@@ -304,7 +338,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
304
338
  await this.client.deleteObject(this.#query(cls, id));
305
339
  }
306
340
 
307
- async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
341
+ async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
308
342
  for await (const batch of this.#iterateBucket(cls, options)) {
309
343
  yield ModelCrudUtil.filterOutNotFound(batch.map(item => this.get(cls, item.id)));
310
344
  }
@@ -317,7 +351,13 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
317
351
 
318
352
  // Blob support
319
353
  async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
320
- if (!overwrite && await this.getBlobMetadata(location).then(() => true, () => false)) {
354
+ if (
355
+ !overwrite &&
356
+ (await this.getBlobMetadata(location).then(
357
+ () => true,
358
+ () => false
359
+ ))
360
+ ) {
321
361
  return;
322
362
  }
323
363
 
@@ -325,11 +365,12 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
325
365
 
326
366
  const length = BinaryMetadataUtil.readLength(resolved);
327
367
 
328
- if (length && length < this.config.chunkSize) { // If smaller than chunk size
368
+ if (length && length < this.config.chunkSize) {
369
+ // If smaller than chunk size
329
370
  const blob = this.#queryBlob(location, {
330
371
  Body: BinaryUtil.toReadable(input),
331
372
  ContentLength: length,
332
- ...this.#getMetadata(resolved),
373
+ ...this.#getMetadata(resolved)
333
374
  });
334
375
  // Upload to s3
335
376
  await this.client.putObject(blob);
@@ -340,18 +381,26 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
340
381
 
341
382
  async #getObject(location: string, range?: Required<ByteRange>): Promise<BinaryType> {
342
383
  // Read from s3
343
- const result = await this.client.getObject(this.#queryBlob(location, range ? {
344
- Range: `bytes=${range.start}-${range.end}`
345
- } : {}));
384
+ const result = await this.client.getObject(
385
+ this.#queryBlob(
386
+ location,
387
+ range
388
+ ? {
389
+ Range: `bytes=${range.start}-${range.end}`
390
+ }
391
+ : {}
392
+ )
393
+ );
346
394
 
347
395
  const body: BinaryType | string | undefined = castTo(result.Body);
348
396
 
349
397
  switch (typeof body) {
350
- case 'undefined': throw new RuntimeError('Unable to read type: undefined');
351
- case 'string': return body.endsWith('=') ?
352
- CodecUtil.fromBase64String(body) :
353
- CodecUtil.fromUTF8String(body);
354
- default: return body;
398
+ case 'undefined':
399
+ throw new RuntimeError('Unable to read type: undefined');
400
+ case 'string':
401
+ return body.endsWith('=') ? CodecUtil.fromBase64String(body) : CodecUtil.fromUTF8String(body);
402
+ default:
403
+ return body;
355
404
  }
356
405
  }
357
406
 
@@ -361,15 +410,13 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
361
410
  return BinaryMetadataUtil.makeBlob(() => this.#getObject(location, final), { ...metadata, range: final });
362
411
  }
363
412
 
364
- async headBlob(location: string): Promise<{ Metadata?: BinaryMetadata, ContentLength?: number, ContentType?: string }> {
413
+ async headBlob(location: string): Promise<{ Metadata?: BinaryMetadata; ContentLength?: number; ContentType?: string }> {
365
414
  const query = this.#queryBlob(location);
366
415
  try {
367
- return (await this.client.headObject(query));
416
+ return await this.client.headObject(query);
368
417
  } catch (error) {
369
- if (isMetadataBearer(error)) {
370
- if (error.$metadata.httpStatusCode === 404) {
371
- error = new NotFoundError('Blob', location);
372
- }
418
+ if (isMetadataBearer(error) && error.$metadata.httpStatusCode === 404) {
419
+ throw new NotFoundError('Blob', location);
373
420
  }
374
421
  throw error;
375
422
  }
@@ -382,11 +429,11 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
382
429
  const metadata: BinaryMetadata = {
383
430
  contentType: blob.ContentType ?? '',
384
431
  ...blob.Metadata,
385
- size: blob.ContentLength!,
432
+ size: blob.ContentLength!
386
433
  };
387
434
  if (hasLowerContentType(metadata)) {
388
- metadata['contentType'] = metadata['contenttype']!;
389
- delete metadata['contenttype'];
435
+ metadata.contentType = metadata.contenttype!;
436
+ delete metadata.contenttype;
390
437
  }
391
438
  return metadata;
392
439
  } else {
@@ -419,11 +466,9 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
419
466
  return `${baseUrl}/${key}`;
420
467
  }
421
468
  }
422
- return await getSignedUrl(
423
- this.client,
424
- new GetObjectCommand(this.#queryBlob(location)),
425
- { expiresIn: TimeUtil.duration(expiresIn, 's') }
426
- );
469
+ return await getSignedUrl(this.client, new GetObjectCommand(this.#queryBlob(location)), {
470
+ expiresIn: TimeUtil.duration(expiresIn, 's')
471
+ });
427
472
  }
428
473
 
429
474
  async getBlobWriteUrl(location: string, metadata: BinaryMetadata, expiresIn: TimeSpan = '1h'): Promise<string> {
@@ -434,7 +479,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
434
479
  ...this.#queryBlob(location),
435
480
  ...base,
436
481
  ...(metadata.size ? { ContentLength: metadata.size } : {}),
437
- ...((metadata.hash && metadata.hash !== '-1') ? { ChecksumSHA256: metadata.hash } : {}),
482
+ ...(metadata.hash && metadata.hash !== '-1' ? { ChecksumSHA256: metadata.hash } : {})
438
483
  }),
439
484
  {
440
485
  expiresIn: TimeUtil.duration(expiresIn, 's'),
@@ -467,4 +512,4 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
467
512
  await this.client.deleteBucket({ Bucket: this.config.bucket });
468
513
  }
469
514
  }
470
- }
515
+ }
@@ -11,4 +11,4 @@ export const service: ServiceDescriptor = {
11
11
  env: {
12
12
  COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS: 'app'
13
13
  }
14
- };
14
+ };