@travetto/model-s3 7.1.4 → 8.0.0-alpha.1

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
@@ -37,7 +37,7 @@ export class Init {
37
37
  }
38
38
  ```
39
39
 
40
- where the [S3ModelConfig](https://github.com/travetto/travetto/tree/main/module/model-s3/src/config.ts#L12) is defined by:
40
+ where the [S3ModelConfig](https://github.com/travetto/travetto/tree/main/module/model-s3/src/config.ts#L13) is defined by:
41
41
 
42
42
  **Code: Structure of S3ModelConfig**
43
43
  ```typescript
@@ -74,7 +74,8 @@ export class S3ModelConfig {
74
74
  /**
75
75
  * Produces the s3 config from the provide details, post construction
76
76
  */
77
- async postConstruct(): Promise<void> {
77
+ @PostConstruct()
78
+ async finalizeConfig(): Promise<void> {
78
79
  if (!Runtime.production) {
79
80
  this.endpoint ??= 'http://localhost:4566'; // From docker
80
81
  this.bucket ??= 'app';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-s3",
3
- "version": "7.1.4",
3
+ "version": "8.0.0-alpha.1",
4
4
  "type": "module",
5
5
  "description": "S3 backing for the travetto model module.",
6
6
  "keywords": [
@@ -26,14 +26,14 @@
26
26
  "directory": "module/model-s3"
27
27
  },
28
28
  "dependencies": {
29
- "@aws-sdk/client-s3": "^3.966.0",
30
- "@aws-sdk/credential-provider-ini": "^3.966.0",
31
- "@aws-sdk/s3-request-presigner": "^3.966.0",
32
- "@travetto/config": "^7.1.4",
33
- "@travetto/model": "^7.1.4"
29
+ "@aws-sdk/client-s3": "^3.1004.0",
30
+ "@aws-sdk/credential-provider-ini": "^3.972.17",
31
+ "@aws-sdk/s3-request-presigner": "^3.1004.0",
32
+ "@travetto/config": "^8.0.0-alpha.1",
33
+ "@travetto/model": "^8.0.0-alpha.1"
34
34
  },
35
35
  "peerDependencies": {
36
- "@travetto/cli": "^7.1.4"
36
+ "@travetto/cli": "^8.0.0-alpha.1"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@travetto/cli": {
package/src/config.ts CHANGED
@@ -4,6 +4,7 @@ import type s3 from '@aws-sdk/client-s3';
4
4
  import { Config, EnvVar } from '@travetto/config';
5
5
  import { Required } from '@travetto/schema';
6
6
  import { Runtime } from '@travetto/runtime';
7
+ import { PostConstruct } from '@travetto/di';
7
8
 
8
9
  /**
9
10
  * S3 Support as an Asset Source
@@ -41,7 +42,8 @@ export class S3ModelConfig {
41
42
  /**
42
43
  * Produces the s3 config from the provide details, post construction
43
44
  */
44
- async postConstruct(): Promise<void> {
45
+ @PostConstruct()
46
+ async finalizeConfig(): Promise<void> {
45
47
  if (!Runtime.production) {
46
48
  this.endpoint ??= 'http://localhost:4566'; // From docker
47
49
  this.bucket ??= 'app';
package/src/service.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { Readable } from 'node:stream';
2
- import { text as toText, buffer as toBuffer } from 'node:stream/consumers';
3
1
  import { Agent } from 'node:https';
4
2
 
5
3
  import { S3, type CompletedPart, type CreateMultipartUploadRequest, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
@@ -9,12 +7,12 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
9
7
 
10
8
  import {
11
9
  type ModelCrudSupport, type ModelStorageSupport, type ModelType, ModelRegistryIndex, ExistsError, NotFoundError, type OptionalId,
12
- type ModelBlobSupport, type ModelExpirySupport, ModelBlobUtil, ModelCrudUtil, ModelExpiryUtil, ModelStorageUtil
10
+ type ModelBlobSupport, type ModelExpirySupport, ModelCrudUtil, ModelExpiryUtil, ModelStorageUtil
13
11
  } from '@travetto/model';
14
- import { Injectable } from '@travetto/di';
12
+ import { Injectable, PostConstruct } from '@travetto/di';
15
13
  import {
16
- type Class, AppError, castTo, asFull, type BlobMeta,
17
- type ByteRange, type BinaryInput, BinaryUtil, type TimeSpan, TimeUtil
14
+ type Class, RuntimeError, castTo, asFull, type BinaryMetadata, type ByteRange, type BinaryType,
15
+ BinaryUtil, type TimeSpan, TimeUtil, type BinaryArray, CodecUtil, BinaryMetadataUtil, TypedObject, JSONUtil
18
16
  } from '@travetto/runtime';
19
17
 
20
18
  import type { S3ModelConfig } from './config.ts';
@@ -27,7 +25,7 @@ function hasContentType<T>(value: T): value is T & { contenttype?: string } {
27
25
  return value !== undefined && value !== null && Object.hasOwn(value, 'contenttype');
28
26
  }
29
27
 
30
- type MetaBase = Pick<CreateMultipartUploadRequest,
28
+ type S3Metadata = Pick<CreateMultipartUploadRequest,
31
29
  'ContentType' | 'Metadata' | 'ContentEncoding' | 'ContentLanguage' | 'CacheControl' | 'ContentDisposition'
32
30
  >;
33
31
 
@@ -43,16 +41,16 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
43
41
 
44
42
  constructor(config: S3ModelConfig) { this.config = config; }
45
43
 
46
- #getMetaBase({ range: _, size, ...meta }: BlobMeta): MetaBase {
44
+ #getMetadata(metadata: BinaryMetadata): S3Metadata {
47
45
  return {
48
- ContentType: meta.contentType,
49
- ...(meta.contentEncoding ? { ContentEncoding: meta.contentEncoding } : {}),
50
- ...(meta.contentLanguage ? { ContentLanguage: meta.contentLanguage } : {}),
51
- ...(meta.cacheControl ? { CacheControl: meta.cacheControl } : {}),
52
- Metadata: {
53
- ...meta,
54
- ...(size ? { size: `${size}` } : {})
55
- }
46
+ ContentType: metadata.contentType,
47
+ ...(metadata.contentEncoding ? { ContentEncoding: metadata.contentEncoding } : {}),
48
+ ...(metadata.contentLanguage ? { ContentLanguage: metadata.contentLanguage } : {}),
49
+ ...(metadata.cacheControl ? { CacheControl: metadata.cacheControl } : {}),
50
+ Metadata: TypedObject.fromEntries(
51
+ TypedObject.entries(metadata)
52
+ .map(([key, value]) => [key, typeof value === 'string' ? value : JSONUtil.toUTF8(value)] as const)
53
+ )
56
54
  };
57
55
  }
58
56
 
@@ -117,17 +115,17 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
117
115
  /**
118
116
  * Write multipart file upload, in chunks
119
117
  */
120
- async #writeMultipart(id: string, input: Readable, meta: BlobMeta): Promise<void> {
121
- const { UploadId } = await this.client.createMultipartUpload(this.#queryBlob(id, this.#getMetaBase(meta)));
118
+ async #writeMultipart(id: string, input: BinaryType, metadata: BinaryMetadata): Promise<void> {
119
+ const { UploadId } = await this.client.createMultipartUpload(this.#queryBlob(id, this.#getMetadata(metadata)));
122
120
 
123
121
  const parts: CompletedPart[] = [];
124
- let buffers: Buffer[] = [];
122
+ let buffers: BinaryArray[] = [];
125
123
  let total = 0;
126
124
  let i = 1;
127
125
  const flush = async (): Promise<void> => {
128
126
  if (!total) { return; }
129
127
  const part = await this.client.uploadPart(this.#queryBlob(id, {
130
- Body: Buffer.concat(buffers),
128
+ Body: BinaryUtil.binaryArrayToUint8Array(BinaryUtil.combineBinaryArrays(buffers)),
131
129
  PartNumber: i,
132
130
  UploadId
133
131
  }));
@@ -137,10 +135,10 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
137
135
  total = 0;
138
136
  };
139
137
  try {
140
- for await (const chunk of input) {
141
- const chunked = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
138
+ for await (const chunk of BinaryUtil.toBinaryStream(input)) {
139
+ const chunked = CodecUtil.readUtf8Chunk(chunk);
142
140
  buffers.push(chunked);
143
- total += chunked.length;
141
+ total += chunked.byteLength;
144
142
  if (total > this.config.chunkSize) {
145
143
  await flush();
146
144
  }
@@ -180,7 +178,8 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
180
178
  }
181
179
  }
182
180
 
183
- async postConstruct(): Promise<void> {
181
+ @PostConstruct()
182
+ async initializeClient(): Promise<void> {
184
183
  this.client = new S3({
185
184
  ...this.config.config,
186
185
  ...('requestHandler' in this.config.config ? {
@@ -217,7 +216,7 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
217
216
  try {
218
217
  const result = await this.client.getObject(this.#query(cls, id));
219
218
  if (result.Body) {
220
- const body = await toText(castTo(result.Body));
219
+ const body = await BinaryUtil.toBinaryArray(result.Body);
221
220
  const output = await ModelCrudUtil.load(cls, body);
222
221
  if (output) {
223
222
  const { expiresAt } = ModelRegistryIndex.getConfig(cls);
@@ -247,11 +246,11 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
247
246
  if (preStore) {
248
247
  prepped = await ModelCrudUtil.preStore(cls, item, this);
249
248
  }
250
- const content = Buffer.from(JSON.stringify(prepped), 'utf8');
249
+ const content = JSONUtil.toBinaryArray(prepped);
251
250
  await this.client.putObject(this.#query(cls, prepped.id, {
252
- Body: content,
251
+ Body: BinaryUtil.binaryArrayToUint8Array(content),
253
252
  ContentType: 'application/json',
254
- ContentLength: content.length,
253
+ ContentLength: content.byteLength,
255
254
  ...this.#getExpiryConfig(cls, prepped)
256
255
  }));
257
256
  return prepped;
@@ -314,53 +313,52 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
314
313
  }
315
314
 
316
315
  // Blob support
317
- async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite = true): Promise<void> {
318
- if (!overwrite && await this.getBlobMeta(location).then(() => true, () => false)) {
316
+ async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
317
+ if (!overwrite && await this.getBlobMetadata(location).then(() => true, () => false)) {
319
318
  return;
320
319
  }
321
320
 
322
- const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
321
+ const resolved = await BinaryMetadataUtil.compute(input, metadata);
322
+
323
+ const length = BinaryMetadataUtil.readLength(resolved);
323
324
 
324
- if (blobMeta.size && blobMeta.size < this.config.chunkSize) { // If smaller than chunk size
325
+ if (length && length < this.config.chunkSize) { // If smaller than chunk size
326
+ const blob = this.#queryBlob(location, {
327
+ Body: BinaryUtil.toReadable(input),
328
+ ContentLength: length,
329
+ ...this.#getMetadata(resolved),
330
+ });
325
331
  // Upload to s3
326
- await this.client.putObject(this.#queryBlob(location, {
327
- Body: await toBuffer(stream),
328
- ContentLength: blobMeta.size,
329
- ...this.#getMetaBase(blobMeta),
330
- }));
332
+ await this.client.putObject(blob);
331
333
  } else {
332
- await this.#writeMultipart(location, stream, blobMeta);
334
+ await this.#writeMultipart(location, input, resolved);
333
335
  }
334
336
  }
335
337
 
336
- async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
338
+ async #getObject(location: string, range?: Required<ByteRange>): Promise<BinaryType> {
337
339
  // Read from s3
338
340
  const result = await this.client.getObject(this.#queryBlob(location, range ? {
339
341
  Range: `bytes=${range.start}-${range.end}`
340
342
  } : {}));
341
343
 
342
- if (!result.Body) {
343
- throw new AppError('Unable to read type: undefined');
344
- }
344
+ const body: BinaryType | string | undefined = castTo(result.Body);
345
345
 
346
- if (typeof result.Body === 'string') { // string
347
- return Readable.from(result.Body, { encoding: castTo<string>(result.Body).endsWith('=') ? 'base64' : 'utf8' });
348
- } else if (result.Body instanceof Buffer) { // Buffer
349
- return Readable.from(result.Body);
350
- } else if ('pipe' in result.Body) { // Stream
351
- return castTo<Readable>(result.Body);
346
+ switch (typeof body) {
347
+ case 'undefined': throw new RuntimeError('Unable to read type: undefined');
348
+ case 'string': return body.endsWith('=') ?
349
+ CodecUtil.fromBase64String(body) :
350
+ CodecUtil.fromUTF8String(body);
351
+ default: return body;
352
352
  }
353
- throw new AppError(`Unable to read type: ${typeof result.Body}`);
354
353
  }
355
354
 
356
355
  async getBlob(location: string, range?: ByteRange): Promise<Blob> {
357
- const meta = await this.getBlobMeta(location);
358
- const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
359
- const result = (): Promise<Readable> => this.#getObject(location, final);
360
- return BinaryUtil.readableBlob(result, { ...meta, range: final });
356
+ const metadata = await this.getBlobMetadata(location);
357
+ const final = range ? BinaryMetadataUtil.enforceRange(range, metadata) : undefined;
358
+ return BinaryMetadataUtil.makeBlob(() => this.#getObject(location, final), { ...metadata, range: final });
361
359
  }
362
360
 
363
- async headBlob(location: string): Promise<{ Metadata?: BlobMeta, ContentLength?: number }> {
361
+ async headBlob(location: string): Promise<{ Metadata?: BinaryMetadata, ContentLength?: number }> {
364
362
  const query = this.#queryBlob(location);
365
363
  try {
366
364
  return (await this.client.headObject(query));
@@ -374,20 +372,20 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
374
372
  }
375
373
  }
376
374
 
377
- async getBlobMeta(location: string): Promise<BlobMeta> {
375
+ async getBlobMetadata(location: string): Promise<BinaryMetadata> {
378
376
  const blob = await this.headBlob(location);
379
377
 
380
378
  if (blob) {
381
- const meta: BlobMeta = {
379
+ const metadata: BinaryMetadata = {
382
380
  contentType: '',
383
381
  ...blob.Metadata,
384
382
  size: blob.ContentLength!,
385
383
  };
386
- if (hasContentType(meta)) {
387
- meta['contentType'] = meta['contenttype']!;
388
- delete meta['contenttype'];
384
+ if (hasContentType(metadata)) {
385
+ metadata['contentType'] = metadata['contenttype']!;
386
+ delete metadata['contenttype'];
389
387
  }
390
- return meta;
388
+ return metadata;
391
389
  } else {
392
390
  throw new NotFoundError('Blob', location);
393
391
  }
@@ -397,38 +395,38 @@ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, Model
397
395
  await this.client.deleteObject(this.#queryBlob(location));
398
396
  }
399
397
 
400
- async updateBlobMeta(location: string, meta: BlobMeta): Promise<void> {
398
+ async updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void> {
401
399
  await this.client.copyObject({
402
400
  Bucket: this.config.bucket,
403
401
  Key: this.#basicKey(location),
404
402
  CopySource: `/${this.config.bucket}/${this.#basicKey(location)}`,
405
- ...this.#getMetaBase(meta),
403
+ ...this.#getMetadata(metadata),
406
404
  MetadataDirective: 'REPLACE'
407
405
  });
408
406
  }
409
407
 
410
408
  // Signed urls
411
- async getBlobReadUrl(location: string, exp: TimeSpan = '1h'): Promise<string> {
409
+ async getBlobReadUrl(location: string, expiresIn: TimeSpan = '1h'): Promise<string> {
412
410
  return await getSignedUrl(
413
411
  this.client,
414
412
  new GetObjectCommand(this.#queryBlob(location)),
415
- { expiresIn: TimeUtil.asSeconds(exp) }
413
+ { expiresIn: TimeUtil.duration(expiresIn, 's') }
416
414
  );
417
415
  }
418
416
 
419
- async getBlobWriteUrl(location: string, meta: BlobMeta, exp: TimeSpan = '1h'): Promise<string> {
420
- const base = this.#getMetaBase(meta);
417
+ async getBlobWriteUrl(location: string, metadata: BinaryMetadata, expiresIn: TimeSpan = '1h'): Promise<string> {
418
+ const base = this.#getMetadata(metadata);
421
419
  return await getSignedUrl(
422
420
  this.client,
423
421
  new PutObjectCommand({
424
422
  ...this.#queryBlob(location),
425
423
  ...base,
426
- ...(meta.size ? { ContentLength: meta.size } : {}),
427
- ...((meta.hash && meta.hash !== '-1') ? { ChecksumSHA256: meta.hash } : {}),
424
+ ...(metadata.size ? { ContentLength: metadata.size } : {}),
425
+ ...((metadata.hash && metadata.hash !== '-1') ? { ChecksumSHA256: metadata.hash } : {}),
428
426
  }),
429
427
  {
430
- expiresIn: TimeUtil.asSeconds(exp),
431
- ...(meta.contentType ? { signableHeaders: new Set(['content-type']) } : {})
428
+ expiresIn: TimeUtil.duration(expiresIn, 's'),
429
+ ...(metadata.contentType ? { signableHeaders: new Set(['content-type']) } : {})
432
430
  }
433
431
  );
434
432
  }