@travetto/model-s3 2.1.3 → 2.2.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 +4 -4
- package/package.json +5 -5
- package/src/config.ts +4 -4
- package/src/service.ts +62 -57
package/README.md
CHANGED
|
@@ -51,8 +51,8 @@ export class S3ModelConfig {
|
|
|
51
51
|
bucket = ''; // S3 bucket
|
|
52
52
|
endpoint = ''; // Endpoint url
|
|
53
53
|
|
|
54
|
-
accessKeyId = EnvUtil.get('AWS_ACCESS_KEY_ID', '');
|
|
55
|
-
secretAccessKey = EnvUtil.get('AWS_SECRET_ACCESS_KEY', '');
|
|
54
|
+
accessKeyId: string = EnvUtil.get('AWS_ACCESS_KEY_ID', '');
|
|
55
|
+
secretAccessKey: string = EnvUtil.get('AWS_SECRET_ACCESS_KEY', '');
|
|
56
56
|
|
|
57
57
|
@Field(Object)
|
|
58
58
|
@Required(false)
|
|
@@ -65,14 +65,14 @@ export class S3ModelConfig {
|
|
|
65
65
|
/**
|
|
66
66
|
* Provide host to bucket
|
|
67
67
|
*/
|
|
68
|
-
get hostName() {
|
|
68
|
+
get hostName(): string {
|
|
69
69
|
return `${this.bucket}.s3.amazonaws.com`;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Produces the s3 config from the provide details, post construction
|
|
74
74
|
*/
|
|
75
|
-
async postConstruct() {
|
|
75
|
+
async postConstruct(): Promise<void> {
|
|
76
76
|
if (!this.accessKeyId && !this.secretAccessKey) {
|
|
77
77
|
const creds = await fromIni({ profile: EnvUtil.get('AWS_PROFILE') })();
|
|
78
78
|
this.accessKeyId = creds.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.
|
|
4
|
+
"version": "2.2.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.
|
|
29
|
-
"@aws-sdk/credential-provider-ini": "^3.
|
|
30
|
-
"@travetto/config": "^2.
|
|
31
|
-
"@travetto/model": "^2.
|
|
28
|
+
"@aws-sdk/client-s3": "^3.131.0",
|
|
29
|
+
"@aws-sdk/credential-provider-ini": "^3.131.0",
|
|
30
|
+
"@travetto/config": "^2.2.0",
|
|
31
|
+
"@travetto/model": "^2.2.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
package/src/config.ts
CHANGED
|
@@ -15,8 +15,8 @@ export class S3ModelConfig {
|
|
|
15
15
|
bucket = ''; // S3 bucket
|
|
16
16
|
endpoint = ''; // Endpoint url
|
|
17
17
|
|
|
18
|
-
accessKeyId = EnvUtil.get('AWS_ACCESS_KEY_ID', '');
|
|
19
|
-
secretAccessKey = EnvUtil.get('AWS_SECRET_ACCESS_KEY', '');
|
|
18
|
+
accessKeyId: string = EnvUtil.get('AWS_ACCESS_KEY_ID', '');
|
|
19
|
+
secretAccessKey: string = EnvUtil.get('AWS_SECRET_ACCESS_KEY', '');
|
|
20
20
|
|
|
21
21
|
@Field(Object)
|
|
22
22
|
@Required(false)
|
|
@@ -29,14 +29,14 @@ export class S3ModelConfig {
|
|
|
29
29
|
/**
|
|
30
30
|
* Provide host to bucket
|
|
31
31
|
*/
|
|
32
|
-
get hostName() {
|
|
32
|
+
get hostName(): string {
|
|
33
33
|
return `${this.bucket}.s3.amazonaws.com`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Produces the s3 config from the provide details, post construction
|
|
38
38
|
*/
|
|
39
|
-
async postConstruct() {
|
|
39
|
+
async postConstruct(): Promise<void> {
|
|
40
40
|
if (!this.accessKeyId && !this.secretAccessKey) {
|
|
41
41
|
const creds = await fromIni({ profile: EnvUtil.get('AWS_PROFILE') })();
|
|
42
42
|
this.accessKeyId = creds.accessKeyId;
|
package/src/service.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Readable } from 'stream';
|
|
2
|
+
|
|
1
3
|
import * as s3 from '@aws-sdk/client-s3';
|
|
2
4
|
import type { MetadataBearer } from '@aws-sdk/types';
|
|
3
5
|
|
|
@@ -16,10 +18,10 @@ import { ModelExpiryUtil } from '@travetto/model/src/internal/service/expiry';
|
|
|
16
18
|
import { S3ModelConfig } from './config';
|
|
17
19
|
|
|
18
20
|
function isMetadataBearer(o: unknown): o is MetadataBearer {
|
|
19
|
-
return !!o && '$metadata' in
|
|
21
|
+
return !!o && typeof o === 'object' && '$metadata' in o;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
function
|
|
24
|
+
function hasContentType<T>(o: T): o is T & { contenttype?: string } {
|
|
23
25
|
return o && 'contenttype' in o;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -35,7 +37,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
35
37
|
|
|
36
38
|
constructor(public readonly config: S3ModelConfig) { }
|
|
37
39
|
|
|
38
|
-
#resolveKey(cls: Class | string, id?: string) {
|
|
40
|
+
#resolveKey(cls: Class | string, id?: string): string {
|
|
39
41
|
let key: string;
|
|
40
42
|
if (cls === STREAM_SPACE) { // If we are streaming, treat as primary use case
|
|
41
43
|
key = id!; // Store it directly at root
|
|
@@ -52,14 +54,15 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
52
54
|
return key;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
58
|
+
#q<U extends object>(cls: string | Class, id: string, extra: U = {} as U): (U & { Key: string, Bucket: string }) {
|
|
56
59
|
const key = this.#resolveKey(cls, id);
|
|
57
|
-
return { Key: key, Bucket: this.config.bucket, ...extra }
|
|
60
|
+
return { Key: key, Bucket: this.config.bucket, ...extra };
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
#getExpiryConfig<T extends ModelType>(cls: Class<T>, item: T) {
|
|
63
|
+
#getExpiryConfig<T extends ModelType>(cls: Class<T>, item: T): { Expires?: Date } {
|
|
61
64
|
if (ModelRegistry.get(cls).expiresAt) {
|
|
62
|
-
const { expiresAt } = ModelExpiryUtil.getExpiryState(cls, item
|
|
65
|
+
const { expiresAt } = ModelExpiryUtil.getExpiryState(cls, item);
|
|
63
66
|
if (expiresAt) {
|
|
64
67
|
return { Expires: expiresAt };
|
|
65
68
|
}
|
|
@@ -67,7 +70,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
67
70
|
return {};
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
async * #iterateBucket(cls?: string | Class) {
|
|
73
|
+
async * #iterateBucket(cls?: string | Class): AsyncIterable<{ Key: string, id: string }[]> {
|
|
71
74
|
let Marker: string | undefined;
|
|
72
75
|
for (; ;) {
|
|
73
76
|
const obs = await this.client.listObjects({ Bucket: this.config.bucket, Prefix: cls ? this.#resolveKey(cls) : undefined, Marker });
|
|
@@ -85,7 +88,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
85
88
|
/**
|
|
86
89
|
* Write multipart file upload, in chunks
|
|
87
90
|
*/
|
|
88
|
-
async #writeMultipart(id: string, input:
|
|
91
|
+
async #writeMultipart(id: string, input: Readable, meta: StreamMeta): Promise<void> {
|
|
89
92
|
const { UploadId } = await this.client.createMultipartUpload(this.#q(STREAM_SPACE, id, {
|
|
90
93
|
ContentType: meta.contentType,
|
|
91
94
|
ContentLength: meta.size,
|
|
@@ -95,7 +98,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
95
98
|
let buffers: Buffer[] = [];
|
|
96
99
|
let total = 0;
|
|
97
100
|
let n = 1;
|
|
98
|
-
const flush = async () => {
|
|
101
|
+
const flush = async (): Promise<void> => {
|
|
99
102
|
if (!total) { return; }
|
|
100
103
|
const part = await this.client.uploadPart(this.#q(STREAM_SPACE, id, {
|
|
101
104
|
Body: Buffer.concat(buffers),
|
|
@@ -121,13 +124,13 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
121
124
|
UploadId,
|
|
122
125
|
MultipartUpload: { Parts: parts }
|
|
123
126
|
}));
|
|
124
|
-
} catch (
|
|
127
|
+
} catch (err) {
|
|
125
128
|
await this.client.abortMultipartUpload(this.#q(STREAM_SPACE, id, { UploadId }));
|
|
126
|
-
throw
|
|
129
|
+
throw err;
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
async #deleteKeys(items: { Key: string }[]) {
|
|
133
|
+
async #deleteKeys(items: { Key: string }[]): Promise<void> {
|
|
131
134
|
if (this.config.endpoint.includes('localhost')) {
|
|
132
135
|
await Promise.all(items.map(item => this.client.deleteObject({
|
|
133
136
|
Bucket: this.config.bucket,
|
|
@@ -143,15 +146,15 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
uuid() {
|
|
149
|
+
uuid(): string {
|
|
147
150
|
return Util.uuid(32);
|
|
148
151
|
}
|
|
149
152
|
|
|
150
|
-
async postConstruct() {
|
|
153
|
+
async postConstruct(): Promise<void> {
|
|
151
154
|
this.client = new s3.S3(this.config.config);
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
async head<T extends ModelType>(cls: Class<T>, id: string) {
|
|
157
|
+
async head<T extends ModelType>(cls: Class<T>, id: string): Promise<boolean> {
|
|
155
158
|
try {
|
|
156
159
|
const res = await this.client.headObject(this.#q(cls, id));
|
|
157
160
|
const { expiresAt } = ModelRegistry.get(cls);
|
|
@@ -159,17 +162,17 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
159
162
|
return false;
|
|
160
163
|
}
|
|
161
164
|
return true;
|
|
162
|
-
} catch (
|
|
163
|
-
if (isMetadataBearer(
|
|
164
|
-
if (
|
|
165
|
+
} catch (err) {
|
|
166
|
+
if (isMetadataBearer(err)) {
|
|
167
|
+
if (err.$metadata.httpStatusCode === 404) {
|
|
165
168
|
return false;
|
|
166
169
|
}
|
|
167
170
|
}
|
|
168
|
-
throw
|
|
171
|
+
throw err;
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
async get<T extends ModelType>(cls: Class<T>, id: string) {
|
|
175
|
+
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
173
176
|
try {
|
|
174
177
|
const result = await this.client.getObject(this.#q(cls, id));
|
|
175
178
|
if (result.Body) {
|
|
@@ -188,17 +191,18 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
188
191
|
}
|
|
189
192
|
}
|
|
190
193
|
throw new NotFoundError(cls, id);
|
|
191
|
-
} catch (
|
|
192
|
-
if (isMetadataBearer(
|
|
193
|
-
if (
|
|
194
|
-
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (isMetadataBearer(err)) {
|
|
196
|
+
if (err.$metadata.httpStatusCode === 404) {
|
|
197
|
+
err = new NotFoundError(cls, id);
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
|
-
throw
|
|
200
|
+
throw err;
|
|
198
201
|
}
|
|
199
202
|
}
|
|
200
203
|
|
|
201
|
-
async store<T extends ModelType>(cls: Class<T>, item: OptionalId<T>, preStore = true) {
|
|
204
|
+
async store<T extends ModelType>(cls: Class<T>, item: OptionalId<T>, preStore = true): Promise<T> {
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
202
206
|
let prepped: T = item as T;
|
|
203
207
|
if (preStore) {
|
|
204
208
|
prepped = await ModelCrudUtil.preStore(cls, item, this);
|
|
@@ -211,7 +215,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
211
215
|
return prepped;
|
|
212
216
|
}
|
|
213
217
|
|
|
214
|
-
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
|
|
218
|
+
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
215
219
|
if (item.id) {
|
|
216
220
|
if (await this.head(cls, item.id)) {
|
|
217
221
|
throw new ExistsError(cls, item.id);
|
|
@@ -220,7 +224,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
220
224
|
return this.store(cls, item);
|
|
221
225
|
}
|
|
222
226
|
|
|
223
|
-
async update<T extends ModelType>(cls: Class<T>, item: T) {
|
|
227
|
+
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
224
228
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
225
229
|
if (!(await this.head(cls, item.id))) {
|
|
226
230
|
throw new NotFoundError(cls, item.id);
|
|
@@ -228,19 +232,19 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
228
232
|
return this.store(cls, item);
|
|
229
233
|
}
|
|
230
234
|
|
|
231
|
-
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
|
|
235
|
+
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
232
236
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
233
237
|
return this.store(cls, item);
|
|
234
238
|
}
|
|
235
239
|
|
|
236
|
-
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string) {
|
|
240
|
+
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
|
|
237
241
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
238
242
|
const id = item.id;
|
|
239
|
-
|
|
240
|
-
return this.store<T>(cls,
|
|
243
|
+
const prepped = await ModelCrudUtil.naivePartialUpdate(cls, item, view, (): Promise<T> => this.get(cls, id));
|
|
244
|
+
return this.store<T>(cls, prepped, false);
|
|
241
245
|
}
|
|
242
246
|
|
|
243
|
-
async delete<T extends ModelType>(cls: Class<T>, id: string) {
|
|
247
|
+
async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
244
248
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
245
249
|
if (!(await this.head(cls, id))) {
|
|
246
250
|
throw new NotFoundError(cls, id);
|
|
@@ -248,14 +252,14 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
248
252
|
await this.client.deleteObject(this.#q(cls, id));
|
|
249
253
|
}
|
|
250
254
|
|
|
251
|
-
async * list<T extends ModelType>(cls: Class<T>) {
|
|
255
|
+
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
252
256
|
for await (const batch of this.#iterateBucket(cls)) {
|
|
253
257
|
for (const { id } of batch) {
|
|
254
258
|
try {
|
|
255
259
|
yield await this.get(cls, id);
|
|
256
|
-
} catch (
|
|
257
|
-
if (!(
|
|
258
|
-
throw
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (!(err instanceof NotFoundError)) {
|
|
262
|
+
throw err;
|
|
259
263
|
}
|
|
260
264
|
}
|
|
261
265
|
}
|
|
@@ -267,7 +271,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
267
271
|
return -1;
|
|
268
272
|
}
|
|
269
273
|
|
|
270
|
-
async upsertStream(location: string, input:
|
|
274
|
+
async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
|
|
271
275
|
if (meta.size < this.config.chunkSize) { // If bigger than 5 mb
|
|
272
276
|
// Upload to s3
|
|
273
277
|
await this.client.putObject(this.#q(STREAM_SPACE, location, {
|
|
@@ -284,7 +288,7 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
284
288
|
}
|
|
285
289
|
}
|
|
286
290
|
|
|
287
|
-
async getStream(location: string) {
|
|
291
|
+
async getStream(location: string): Promise<Readable> {
|
|
288
292
|
// Read from s3
|
|
289
293
|
const res = await this.client.getObject(this.#q(STREAM_SPACE, location));
|
|
290
294
|
if (res.Body instanceof Buffer || // Buffer
|
|
@@ -296,29 +300,30 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
296
300
|
throw new AppError(`Unable to read type: ${typeof res.Body}`);
|
|
297
301
|
}
|
|
298
302
|
|
|
299
|
-
async headStream(location: string) {
|
|
303
|
+
async headStream(location: string): Promise<{ Metadata?: Partial<StreamMeta>, ContentLength?: number }> {
|
|
300
304
|
const query = this.#q(STREAM_SPACE, location);
|
|
301
305
|
try {
|
|
302
|
-
return await this.client.headObject(query);
|
|
303
|
-
} catch (
|
|
304
|
-
if (isMetadataBearer(
|
|
305
|
-
if (
|
|
306
|
-
|
|
306
|
+
return (await this.client.headObject(query));
|
|
307
|
+
} catch (err) {
|
|
308
|
+
if (isMetadataBearer(err)) {
|
|
309
|
+
if (err.$metadata.httpStatusCode === 404) {
|
|
310
|
+
err = new NotFoundError(STREAM_SPACE, location);
|
|
307
311
|
}
|
|
308
312
|
}
|
|
309
|
-
throw
|
|
313
|
+
throw err;
|
|
310
314
|
}
|
|
311
315
|
}
|
|
312
316
|
|
|
313
|
-
async describeStream(location: string) {
|
|
317
|
+
async describeStream(location: string): Promise<StreamMeta> {
|
|
314
318
|
const obj = await this.headStream(location);
|
|
315
319
|
|
|
316
320
|
if (obj) {
|
|
317
|
-
const ret = {
|
|
318
|
-
|
|
321
|
+
const ret: StreamMeta = {
|
|
322
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
323
|
+
...obj.Metadata as StreamMeta,
|
|
319
324
|
size: obj.ContentLength!,
|
|
320
|
-
}
|
|
321
|
-
if (
|
|
325
|
+
};
|
|
326
|
+
if (hasContentType(ret)) {
|
|
322
327
|
ret['contentType'] = ret['contenttype']!;
|
|
323
328
|
delete ret['contenttype'];
|
|
324
329
|
}
|
|
@@ -328,25 +333,25 @@ export class S3ModelService implements ModelCrudSupport, ModelStreamSupport, Mod
|
|
|
328
333
|
}
|
|
329
334
|
}
|
|
330
335
|
|
|
331
|
-
async truncateModel<T extends ModelType>(model: Class<T>) {
|
|
336
|
+
async truncateModel<T extends ModelType>(model: Class<T>): Promise<void> {
|
|
332
337
|
for await (const items of this.#iterateBucket(model)) {
|
|
333
338
|
await this.#deleteKeys(items);
|
|
334
339
|
}
|
|
335
340
|
}
|
|
336
341
|
|
|
337
|
-
async deleteStream(location: string) {
|
|
342
|
+
async deleteStream(location: string): Promise<void> {
|
|
338
343
|
await this.client.deleteObject(this.#q(STREAM_SPACE, location));
|
|
339
344
|
}
|
|
340
345
|
|
|
341
|
-
async createStorage() {
|
|
346
|
+
async createStorage(): Promise<void> {
|
|
342
347
|
try {
|
|
343
348
|
await this.client.headBucket({ Bucket: this.config.bucket });
|
|
344
|
-
} catch
|
|
349
|
+
} catch {
|
|
345
350
|
await this.client.createBucket({ Bucket: this.config.bucket });
|
|
346
351
|
}
|
|
347
352
|
}
|
|
348
353
|
|
|
349
|
-
async deleteStorage() {
|
|
354
|
+
async deleteStorage(): Promise<void> {
|
|
350
355
|
if (this.config.namespace) {
|
|
351
356
|
for await (const items of this.#iterateBucket('')) {
|
|
352
357
|
await this.#deleteKeys(items);
|