@travetto/model-s3 5.0.0-rc.1 → 5.0.0-rc.11

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
@@ -17,7 +17,7 @@ This module provides an [s3](https://aws.amazon.com/documentation/s3/)-based imp
17
17
 
18
18
  Supported features:
19
19
  * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11)
20
- * [Streaming](https://github.com/travetto/travetto/tree/main/module/model/src/service/stream.ts#L3)
20
+ * [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/service/blob.ts#L8)
21
21
  * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/service/expiry.ts#L11)
22
22
  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.
23
23
 
@@ -45,7 +45,7 @@ import type s3 from '@aws-sdk/client-s3';
45
45
 
46
46
  import { Config, EnvVar } from '@travetto/config';
47
47
  import { Field, Required } from '@travetto/schema';
48
- import { Env } from '@travetto/base';
48
+ import { Runtime } from '@travetto/runtime';
49
49
 
50
50
  /**
51
51
  * S3 Support as an Asset Source
@@ -100,7 +100,7 @@ export class S3ModelConfig {
100
100
  };
101
101
 
102
102
  // We are in localhost and not in prod, turn on forcePathStyle
103
- if (!Env.production && this.endpoint.includes('localhost')) {
103
+ if (!Runtime.production && this.endpoint.includes('localhost')) {
104
104
  this.config.forcePathStyle ??= true;
105
105
  }
106
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-s3",
3
- "version": "5.0.0-rc.1",
3
+ "version": "5.0.0-rc.11",
4
4
  "description": "S3 backing for the travetto model module.",
5
5
  "keywords": [
6
6
  "s3",
@@ -25,13 +25,13 @@
25
25
  "directory": "module/model-s3"
26
26
  },
27
27
  "dependencies": {
28
- "@aws-sdk/client-s3": "^3.609.0",
28
+ "@aws-sdk/client-s3": "^3.631.0",
29
29
  "@aws-sdk/credential-provider-ini": "^3.609.0",
30
- "@travetto/config": "^5.0.0-rc.1",
31
- "@travetto/model": "^5.0.0-rc.1"
30
+ "@travetto/config": "^5.0.0-rc.11",
31
+ "@travetto/model": "^5.0.0-rc.11"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/command": "^5.0.0-rc.1"
34
+ "@travetto/command": "^5.0.0-rc.10"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@travetto/command": {
package/src/config.ts CHANGED
@@ -3,7 +3,7 @@ import type s3 from '@aws-sdk/client-s3';
3
3
 
4
4
  import { Config, EnvVar } from '@travetto/config';
5
5
  import { Field, Required } from '@travetto/schema';
6
- import { Env } from '@travetto/base';
6
+ import { Runtime } from '@travetto/runtime';
7
7
 
8
8
  /**
9
9
  * S3 Support as an Asset Source
@@ -58,7 +58,7 @@ export class S3ModelConfig {
58
58
  };
59
59
 
60
60
  // We are in localhost and not in prod, turn on forcePathStyle
61
- if (!Env.production && this.endpoint.includes('localhost')) {
61
+ if (!Runtime.production && this.endpoint.includes('localhost')) {
62
62
  this.config.forcePathStyle ??= true;
63
63
  }
64
64
  }
package/src/service.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Readable } from 'node:stream';
2
- import { buffer as toBuffer } from 'node:stream/consumers';
2
+ import { text as toText } from 'node:stream/consumers';
3
3
  import { Agent } from 'node:https';
4
4
 
5
5
  import { S3, CompletedPart, type CreateMultipartUploadRequest } from '@aws-sdk/client-s3';
@@ -7,20 +7,19 @@ import type { MetadataBearer } from '@aws-sdk/types';
7
7
  import { NodeHttpHandler } from '@smithy/node-http-handler';
8
8
 
9
9
  import {
10
- ModelCrudSupport, ModelStreamSupport, ModelStorageSupport, StreamMeta,
11
- ModelType, ModelRegistry, ExistsError, NotFoundError, OptionalId,
12
- StreamRange
10
+ ModelCrudSupport, ModelStorageSupport, ModelType, ModelRegistry, ExistsError, NotFoundError, OptionalId,
11
+ ModelBlobSupport, ModelBlobUtil,
13
12
  } from '@travetto/model';
14
13
  import { Injectable } from '@travetto/di';
15
- import { Class, AppError } from '@travetto/base';
14
+ import { Class, AppError, castTo, asFull, BlobMeta, ByteRange, BinaryInput, BinaryUtil } from '@travetto/runtime';
16
15
 
16
+ import { MODEL_BLOB, ModelBlobNamespace } from '@travetto/model/src/internal/service/blob';
17
17
  import { ModelCrudUtil } from '@travetto/model/src/internal/service/crud';
18
18
  import { ModelExpirySupport } from '@travetto/model/src/service/expiry';
19
19
  import { ModelExpiryUtil } from '@travetto/model/src/internal/service/expiry';
20
20
  import { ModelStorageUtil } from '@travetto/model/src/internal/service/storage';
21
21
 
22
22
  import { S3ModelConfig } from './config';
23
- import { enforceRange } from '@travetto/model/src/internal/service/stream';
24
23
 
25
24
  function isMetadataBearer(o: unknown): o is MetadataBearer {
26
25
  return !!o && typeof o === 'object' && '$metadata' in o;
@@ -30,8 +29,6 @@ function hasContentType<T>(o: T): o is T & { contenttype?: string } {
30
29
  return o !== undefined && o !== null && Object.hasOwn(o, 'contenttype');
31
30
  }
32
31
 
33
- const STREAM_SPACE = '@travetto/model-s3:stream';
34
-
35
32
  type MetaBase = Pick<CreateMultipartUploadRequest,
36
33
  'ContentType' | 'Metadata' | 'ContentEncoding' | 'ContentLanguage' | 'CacheControl' | 'ContentDisposition'
37
34
  >;
@@ -40,14 +37,14 @@ type MetaBase = Pick<CreateMultipartUploadRequest,
40
37
  * Asset source backed by S3
41
38
  */
42
39
  @Injectable()
43
- export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, ModelStorageSupport, ModelExpirySupport {
40
+ export class S3ModelService implements ModelCrudSupport, ModelBlobSupport, ModelStorageSupport, ModelExpirySupport {
44
41
 
45
42
  idSource = ModelCrudUtil.uuidSource();
46
43
  client: S3;
47
44
 
48
45
  constructor(public readonly config: S3ModelConfig) { }
49
46
 
50
- #getMetaBase(meta: StreamMeta): MetaBase {
47
+ #getMetaBase({ range, ...meta }: BlobMeta): MetaBase {
51
48
  return {
52
49
  ContentType: meta.contentType,
53
50
  ...(meta.contentEncoding ? { ContentEncoding: meta.contentEncoding } : {}),
@@ -62,7 +59,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
62
59
 
63
60
  #resolveKey(cls: Class | string, id?: string): string {
64
61
  let key: string;
65
- if (cls === STREAM_SPACE) { // If we are streaming, treat as primary use case
62
+ if (cls === MODEL_BLOB) { // If we are streaming, treat as primary use case
66
63
  key = id!; // Store it directly at root
67
64
  } else {
68
65
  key = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
@@ -77,8 +74,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
77
74
  return key;
78
75
  }
79
76
 
80
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
81
- #q<U extends object>(cls: string | Class, id: string, extra: U = {} as U): (U & { Key: string, Bucket: string }) {
77
+ #q<U extends object>(cls: string | Class, id: string, extra: U = asFull({})): (U & { Key: string, Bucket: string }) {
82
78
  const key = this.#resolveKey(cls, id);
83
79
  return { Key: key, Bucket: this.config.bucket, ...extra };
84
80
  }
@@ -111,8 +107,8 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
111
107
  /**
112
108
  * Write multipart file upload, in chunks
113
109
  */
114
- async #writeMultipart(id: string, input: Readable, meta: StreamMeta): Promise<void> {
115
- const { UploadId } = await this.client.createMultipartUpload(this.#q(STREAM_SPACE, id, this.#getMetaBase(meta)));
110
+ async #writeMultipart(id: string, input: Readable, meta: BlobMeta): Promise<void> {
111
+ const { UploadId } = await this.client.createMultipartUpload(this.#q(MODEL_BLOB, id, this.#getMetaBase(meta)));
116
112
 
117
113
  const parts: CompletedPart[] = [];
118
114
  let buffers: Buffer[] = [];
@@ -120,7 +116,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
120
116
  let n = 1;
121
117
  const flush = async (): Promise<void> => {
122
118
  if (!total) { return; }
123
- const part = await this.client.uploadPart(this.#q(STREAM_SPACE, id, {
119
+ const part = await this.client.uploadPart(this.#q(MODEL_BLOB, id, {
124
120
  Body: Buffer.concat(buffers),
125
121
  PartNumber: n,
126
122
  UploadId
@@ -140,12 +136,12 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
140
136
  }
141
137
  await flush();
142
138
 
143
- await this.client.completeMultipartUpload(this.#q(STREAM_SPACE, id, {
139
+ await this.client.completeMultipartUpload(this.#q(MODEL_BLOB, id, {
144
140
  UploadId,
145
141
  MultipartUpload: { Parts: parts }
146
142
  }));
147
143
  } catch (err) {
148
- await this.client.abortMultipartUpload(this.#q(STREAM_SPACE, id, { UploadId }));
144
+ await this.client.abortMultipartUpload(this.#q(MODEL_BLOB, id, { UploadId }));
149
145
  throw err;
150
146
  }
151
147
  }
@@ -203,8 +199,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
203
199
  try {
204
200
  const result = await this.client.getObject(this.#q(cls, id));
205
201
  if (result.Body) {
206
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
207
- const body = (await toBuffer(result.Body as Readable)).toString('utf8');
202
+ const body = await toText(castTo(result.Body));
208
203
  const output = await ModelCrudUtil.load(cls, body);
209
204
  if (output) {
210
205
  const { expiresAt } = ModelRegistry.get(cls);
@@ -230,8 +225,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
230
225
  }
231
226
 
232
227
  async store<T extends ModelType>(cls: Class<T>, item: OptionalId<T>, preStore = true): Promise<T> {
233
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
234
- let prepped: T = item as T;
228
+ let prepped: T = castTo(item);
235
229
  if (preStore) {
236
230
  prepped = await ModelCrudUtil.preStore(cls, item, this);
237
231
  }
@@ -299,22 +293,33 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
299
293
  return -1;
300
294
  }
301
295
 
302
- async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
303
- if (meta.size < this.config.chunkSize) { // If smaller than chunk size
296
+ // Blob support
297
+ async insertBlob(location: string, input: BinaryInput, meta?: BlobMeta, errorIfExisting = false): Promise<void> {
298
+ await this.describeBlob(location);
299
+ if (errorIfExisting) {
300
+ throw new ExistsError(ModelBlobNamespace, location);
301
+ }
302
+ return this.upsertBlob(location, input, meta);
303
+ }
304
+
305
+ async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta): Promise<void> {
306
+ const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
307
+
308
+ if (blobMeta.size && blobMeta.size < this.config.chunkSize) { // If smaller than chunk size
304
309
  // Upload to s3
305
- await this.client.putObject(this.#q(STREAM_SPACE, location, {
306
- Body: await toBuffer(input),
307
- ContentLength: meta.size,
308
- ...this.#getMetaBase(meta),
310
+ await this.client.putObject(this.#q(MODEL_BLOB, location, {
311
+ Body: stream,
312
+ ContentLength: blobMeta.size,
313
+ ...this.#getMetaBase(blobMeta),
309
314
  }));
310
315
  } else {
311
- await this.#writeMultipart(location, input, meta);
316
+ await this.#writeMultipart(location, stream, blobMeta);
312
317
  }
313
318
  }
314
319
 
315
- async #getObject(location: string, range: Required<StreamRange>): Promise<Readable> {
320
+ async #getObject(location: string, range?: Required<ByteRange>): Promise<Readable> {
316
321
  // Read from s3
317
- const res = await this.client.getObject(this.#q(STREAM_SPACE, location, range ? {
322
+ const res = await this.client.getObject(this.#q(MODEL_BLOB, location, range ? {
318
323
  Range: `bytes=${range.start}-${range.end}`
319
324
  } : {}));
320
325
 
@@ -323,49 +328,43 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
323
328
  }
324
329
 
325
330
  if (typeof res.Body === 'string') { // string
326
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
327
- return Readable.from(res.Body, { encoding: (res.Body as string).endsWith('=') ? 'base64' : 'utf8' });
331
+ return Readable.from(res.Body, { encoding: castTo<string>(res.Body).endsWith('=') ? 'base64' : 'utf8' });
328
332
  } else if (res.Body instanceof Buffer) { // Buffer
329
333
  return Readable.from(res.Body);
330
334
  } else if ('pipe' in res.Body) { // Stream
331
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
332
- return res.Body as Readable;
335
+ return castTo<Readable>(res.Body);
333
336
  }
334
337
  throw new AppError(`Unable to read type: ${typeof res.Body}`);
335
338
  }
336
339
 
337
- async getStream(location: string, range?: StreamRange): Promise<Readable> {
338
- if (range) {
339
- const meta = await this.describeStream(location);
340
- range = enforceRange(range, meta.size);
341
- }
342
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
343
- return this.#getObject(location, range as Required<StreamRange>);
340
+ async getBlob(location: string, range?: ByteRange): Promise<Blob> {
341
+ const meta = await this.describeBlob(location);
342
+ const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
343
+ const res = (): Promise<Readable> => this.#getObject(location, final);
344
+ return BinaryUtil.readableBlob(res, { ...meta, range: final });
344
345
  }
345
346
 
346
- async headStream(location: string): Promise<{ Metadata?: Partial<StreamMeta>, ContentLength?: number }> {
347
- const query = this.#q(STREAM_SPACE, location);
347
+ async headBlob(location: string): Promise<{ Metadata?: BlobMeta, ContentLength?: number }> {
348
+ const query = this.#q(MODEL_BLOB, location);
348
349
  try {
349
350
  return (await this.client.headObject(query));
350
351
  } catch (err) {
351
352
  if (isMetadataBearer(err)) {
352
353
  if (err.$metadata.httpStatusCode === 404) {
353
- err = new NotFoundError(STREAM_SPACE, location);
354
+ err = new NotFoundError(MODEL_BLOB, location);
354
355
  }
355
356
  }
356
357
  throw err;
357
358
  }
358
359
  }
359
360
 
360
- async describeStream(location: string): Promise<StreamMeta> {
361
- const obj = await this.headStream(location);
361
+ async describeBlob(location: string): Promise<BlobMeta> {
362
+ const obj = await this.headBlob(location);
362
363
 
363
364
  if (obj) {
364
- const ret: StreamMeta = {
365
- // @ts-expect-error
365
+ const ret: BlobMeta = {
366
366
  contentType: '',
367
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
368
- ...obj.Metadata as StreamMeta,
367
+ ...obj.Metadata,
369
368
  size: obj.ContentLength!,
370
369
  };
371
370
  if (hasContentType(ret)) {
@@ -374,20 +373,21 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
374
373
  }
375
374
  return ret;
376
375
  } else {
377
- throw new NotFoundError(STREAM_SPACE, location);
376
+ throw new NotFoundError(MODEL_BLOB, location);
378
377
  }
379
378
  }
380
379
 
380
+ async deleteBlob(location: string): Promise<void> {
381
+ await this.client.deleteObject(this.#q(MODEL_BLOB, location));
382
+ }
383
+
384
+ // Storage
381
385
  async truncateModel<T extends ModelType>(model: Class<T>): Promise<void> {
382
386
  for await (const items of this.#iterateBucket(model)) {
383
387
  await this.#deleteKeys(items);
384
388
  }
385
389
  }
386
390
 
387
- async deleteStream(location: string): Promise<void> {
388
- await this.client.deleteObject(this.#q(STREAM_SPACE, location));
389
- }
390
-
391
391
  async createStorage(): Promise<void> {
392
392
  try {
393
393
  await this.client.headBucket({ Bucket: this.config.bucket });