@travetto/model-s3 2.0.2 → 2.1.0

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
@@ -39,7 +39,7 @@ import * as S3 from '@aws-sdk/client-s3';
39
39
 
40
40
  import { EnvUtil } from '@travetto/boot';
41
41
  import { Config } from '@travetto/config';
42
- import { Field } from '@travetto/schema';
42
+ import { Field, Required } from '@travetto/schema';
43
43
 
44
44
  /**
45
45
  * S3 Support as an Asset Source
@@ -55,7 +55,8 @@ export class S3ModelConfig {
55
55
  secretAccessKey = EnvUtil.get('AWS_SECRET_ACCESS_KEY', '');
56
56
 
57
57
  @Field(Object)
58
- config?: S3.S3ClientConfig; // Additional s3 config
58
+ @Required(false)
59
+ config: S3.S3ClientConfig; // Additional s3 config
59
60
 
60
61
  chunkSize = 5 * 2 ** 20; // Chunk size in bytes
61
62
 
@@ -80,6 +81,7 @@ export class S3ModelConfig {
80
81
 
81
82
  this.config = {
82
83
  ...(this.config ?? {}),
84
+ region: this.region,
83
85
  endpoint: this.endpoint,
84
86
  credentials: {
85
87
  accessKeyId: this.accessKeyId,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@travetto/model-s3",
3
3
  "displayName": "S3 Model Support",
4
- "version": "2.0.2",
4
+ "version": "2.1.0",
5
5
  "description": "S3 backing for the travetto model module.",
6
6
  "keywords": [
7
7
  "s3",
@@ -25,10 +25,10 @@
25
25
  "directory": "module/model-s3"
26
26
  },
27
27
  "dependencies": {
28
- "@aws-sdk/client-s3": "^3.18.0",
29
- "@aws-sdk/credential-provider-ini": "^3.18.0",
30
- "@travetto/config": "^2.0.2",
31
- "@travetto/model": "^2.0.2"
28
+ "@aws-sdk/client-s3": "^3.49.0",
29
+ "@aws-sdk/credential-provider-ini": "^3.49.0",
30
+ "@travetto/config": "^2.1.0",
31
+ "@travetto/model": "^2.1.0"
32
32
  },
33
33
  "publishConfig": {
34
34
  "access": "public"
package/src/config.ts CHANGED
@@ -3,7 +3,7 @@ import * as S3 from '@aws-sdk/client-s3';
3
3
 
4
4
  import { EnvUtil } from '@travetto/boot';
5
5
  import { Config } from '@travetto/config';
6
- import { Field } from '@travetto/schema';
6
+ import { Field, Required } from '@travetto/schema';
7
7
 
8
8
  /**
9
9
  * S3 Support as an Asset Source
@@ -19,7 +19,8 @@ export class S3ModelConfig {
19
19
  secretAccessKey = EnvUtil.get('AWS_SECRET_ACCESS_KEY', '');
20
20
 
21
21
  @Field(Object)
22
- config?: S3.S3ClientConfig; // Additional s3 config
22
+ @Required(false)
23
+ config: S3.S3ClientConfig; // Additional s3 config
23
24
 
24
25
  chunkSize = 5 * 2 ** 20; // Chunk size in bytes
25
26
 
@@ -44,6 +45,7 @@ export class S3ModelConfig {
44
45
 
45
46
  this.config = {
46
47
  ...(this.config ?? {}),
48
+ region: this.region,
47
49
  endpoint: this.endpoint,
48
50
  credentials: {
49
51
  accessKeyId: this.accessKeyId,
package/src/service.ts CHANGED
@@ -4,7 +4,7 @@ import type { MetadataBearer } from '@aws-sdk/types';
4
4
  import { StreamUtil } from '@travetto/boot';
5
5
  import {
6
6
  ModelCrudSupport, ModelStreamSupport, ModelStorageSupport, StreamMeta,
7
- ModelType, ModelRegistry, ExistsError, NotFoundError, SubTypeNotSupportedError, OptionalId
7
+ ModelType, ModelRegistry, ExistsError, NotFoundError, OptionalId
8
8
  } from '@travetto/model';
9
9
  import { Injectable } from '@travetto/di';
10
10
  import { Class, AppError, Util } from '@travetto/base';
@@ -23,6 +23,8 @@ function hasContenttype<T>(o: T): o is T & { contenttype?: string } {
23
23
  return o && 'contenttype' in o;
24
24
  }
25
25
 
26
+ const STREAM_SPACE = '@trv:stream';
27
+
26
28
  /**
27
29
  * Asset source backed by S3
28
30
  */
@@ -34,9 +36,15 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
34
36
  constructor(public readonly config: S3ModelConfig) { }
35
37
 
36
38
  #resolveKey(cls: Class | string, id?: string) {
37
- let key = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
38
- if (id) {
39
- key = `${key}:${id}`;
39
+ let key: string;
40
+ if (cls === STREAM_SPACE) { // If we are streaming, treat as primary use case
41
+ key = id!; // Store it directly at root
42
+ } else {
43
+ key = typeof cls === 'string' ? cls : ModelRegistry.getStore(cls);
44
+ if (id) {
45
+ key = `${key}:${id}`;
46
+ }
47
+ key = `_data/${key}`; // Separate data
40
48
  }
41
49
  if (this.config.namespace) {
42
50
  key = `${this.config.namespace}/${key}`;
@@ -78,7 +86,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
78
86
  * Write multipart file upload, in chunks
79
87
  */
80
88
  async #writeMultipart(id: string, input: NodeJS.ReadableStream, meta: StreamMeta): Promise<void> {
81
- const { UploadId } = await this.client.createMultipartUpload(this.#q('_stream', id, {
89
+ const { UploadId } = await this.client.createMultipartUpload(this.#q(STREAM_SPACE, id, {
82
90
  ContentType: meta.contentType,
83
91
  ContentLength: meta.size,
84
92
  }));
@@ -89,7 +97,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
89
97
  let n = 1;
90
98
  const flush = async () => {
91
99
  if (!total) { return; }
92
- const part = await this.client.uploadPart(this.#q('_stream', id, {
100
+ const part = await this.client.uploadPart(this.#q(STREAM_SPACE, id, {
93
101
  Body: Buffer.concat(buffers),
94
102
  PartNumber: n,
95
103
  UploadId
@@ -109,12 +117,12 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
109
117
  }
110
118
  await flush();
111
119
 
112
- await this.client.completeMultipartUpload(this.#q('_stream', id, {
120
+ await this.client.completeMultipartUpload(this.#q(STREAM_SPACE, id, {
113
121
  UploadId,
114
122
  MultipartUpload: { Parts: parts }
115
123
  }));
116
124
  } catch (e) {
117
- await this.client.abortMultipartUpload(this.#q('_stream', id, { UploadId }));
125
+ await this.client.abortMultipartUpload(this.#q(STREAM_SPACE, id, { UploadId }));
118
126
  throw e;
119
127
  }
120
128
  }
@@ -213,9 +221,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
213
221
  }
214
222
 
215
223
  async update<T extends ModelType>(cls: Class<T>, item: T) {
216
- if (ModelRegistry.get(cls).subType) {
217
- throw new SubTypeNotSupportedError(cls);
218
- }
224
+ ModelCrudUtil.ensureNotSubType(cls);
219
225
  if (!(await this.head(cls, item.id))) {
220
226
  throw new NotFoundError(cls, item.id);
221
227
  }
@@ -223,25 +229,19 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
223
229
  }
224
230
 
225
231
  async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
226
- if (ModelRegistry.get(cls).subType) {
227
- throw new SubTypeNotSupportedError(cls);
228
- }
232
+ ModelCrudUtil.ensureNotSubType(cls);
229
233
  return this.store(cls, item);
230
234
  }
231
235
 
232
236
  async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string) {
233
- if (ModelRegistry.get(cls).subType) {
234
- throw new SubTypeNotSupportedError(cls);
235
- }
237
+ ModelCrudUtil.ensureNotSubType(cls);
236
238
  const id = item.id;
237
239
  item = await ModelCrudUtil.naivePartialUpdate(cls, item, view, () => this.get(cls, id)) as T;
238
240
  return this.store<T>(cls, item as T, false);
239
241
  }
240
242
 
241
243
  async delete<T extends ModelType>(cls: Class<T>, id: string) {
242
- if (ModelRegistry.get(cls).subType) {
243
- throw new SubTypeNotSupportedError(cls);
244
- }
244
+ ModelCrudUtil.ensureNotSubType(cls);
245
245
  if (!(await this.head(cls, id))) {
246
246
  throw new NotFoundError(cls, id);
247
247
  }
@@ -270,7 +270,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
270
270
  async upsertStream(location: string, input: NodeJS.ReadableStream, meta: StreamMeta) {
271
271
  if (meta.size < this.config.chunkSize) { // If bigger than 5 mb
272
272
  // Upload to s3
273
- await this.client.putObject(this.#q('_stream', location, {
273
+ await this.client.putObject(this.#q(STREAM_SPACE, location, {
274
274
  Body: await StreamUtil.toBuffer(input),
275
275
  ContentType: meta.contentType,
276
276
  ContentLength: meta.size,
@@ -286,7 +286,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
286
286
 
287
287
  async getStream(location: string) {
288
288
  // Read from s3
289
- const res = await this.client.getObject(this.#q('_stream', location));
289
+ const res = await this.client.getObject(this.#q(STREAM_SPACE, location));
290
290
  if (res.Body instanceof Buffer || // Buffer
291
291
  typeof res.Body === 'string' || // string
292
292
  res.Body && ('pipe' in res.Body) // Stream
@@ -297,13 +297,13 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
297
297
  }
298
298
 
299
299
  async headStream(location: string) {
300
- const query = this.#q('_stream', location);
300
+ const query = this.#q(STREAM_SPACE, location);
301
301
  try {
302
302
  return await this.client.headObject(query);
303
303
  } catch (e) {
304
304
  if (isMetadataBearer(e)) {
305
305
  if (e.$metadata.httpStatusCode === 404) {
306
- e = new NotFoundError('_stream', location);
306
+ e = new NotFoundError(STREAM_SPACE, location);
307
307
  }
308
308
  }
309
309
  throw e;
@@ -324,7 +324,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
324
324
  }
325
325
  return ret;
326
326
  } else {
327
- throw new NotFoundError('_stream', location);
327
+ throw new NotFoundError(STREAM_SPACE, location);
328
328
  }
329
329
  }
330
330
 
@@ -335,7 +335,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
335
335
  }
336
336
 
337
337
  async deleteStream(location: string) {
338
- await this.client.deleteObject(this.#q('_stream', location));
338
+ await this.client.deleteObject(this.#q(STREAM_SPACE, location));
339
339
  }
340
340
 
341
341
  async createStorage() {