s3db.js 1.0.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/.github/workflows/pipeline.yml +16 -0
- package/README.md +742 -0
- package/build/cache/avro.serializer.js +16 -0
- package/build/cache/json.serializer.js +7 -0
- package/build/cache/s3-cache.class.js +157 -0
- package/build/cache/s3-resource-cache.class.js +77 -0
- package/build/cache/serializers.type.js +8 -0
- package/build/errors.js +64 -0
- package/build/index.js +9 -0
- package/build/metadata.interface.js +2 -0
- package/build/plugin.interface.js +2 -0
- package/build/resource.class.js +485 -0
- package/build/resource.interface.js +2 -0
- package/build/s3-client.class.js +274 -0
- package/build/s3db-config.interface.js +2 -0
- package/build/s3db.class.js +185 -0
- package/build/stream/resource-ids-read-stream.class.js +100 -0
- package/build/stream/resource-ids-transformer.class.js +40 -0
- package/build/stream/resource-write-stream.class.js +76 -0
- package/build/validator.js +37 -0
- package/examples/1-bulk-insert.js +64 -0
- package/examples/2-read-stream.js +61 -0
- package/examples/3-read-stream-to-csv.js +57 -0
- package/examples/4-read-stream-to-zip.js +56 -0
- package/examples/5-write-stream.js +98 -0
- package/examples/6-jwt-tokens.js +124 -0
- package/examples/concerns/index.js +64 -0
- package/jest.config.ts +10 -0
- package/package.json +51 -0
- package/src/cache/avro.serializer.ts +12 -0
- package/src/cache/json.serializer.ts +4 -0
- package/src/cache/s3-cache.class.ts +155 -0
- package/src/cache/s3-resource-cache.class.ts +75 -0
- package/src/cache/serializers.type.ts +8 -0
- package/src/errors.ts +96 -0
- package/src/index.ts +4 -0
- package/src/metadata.interface.ts +4 -0
- package/src/plugin.interface.ts +4 -0
- package/src/resource.class.ts +531 -0
- package/src/resource.interface.ts +21 -0
- package/src/s3-client.class.ts +297 -0
- package/src/s3db-config.interface.ts +9 -0
- package/src/s3db.class.ts +215 -0
- package/src/stream/resource-ids-read-stream.class.ts +90 -0
- package/src/stream/resource-ids-transformer.class.ts +38 -0
- package/src/stream/resource-write-stream.class.ts +78 -0
- package/src/validator.ts +39 -0
- package/tests/cache.spec.ts +187 -0
- package/tests/concerns/index.ts +16 -0
- package/tests/config.spec.ts +29 -0
- package/tests/resources.spec.ts +197 -0
- package/tsconfig.json +111 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { chunk } from "lodash";
|
|
3
|
+
import { nanoid } from "nanoid";
|
|
4
|
+
import { Stream } from "stream";
|
|
5
|
+
import EventEmitter from "events";
|
|
6
|
+
import { S3, Credentials } from "aws-sdk";
|
|
7
|
+
import PromisePool from "@supercharge/promise-pool";
|
|
8
|
+
|
|
9
|
+
import { ClientNoSuchKey } from "./errors";
|
|
10
|
+
|
|
11
|
+
export default class S3Client extends EventEmitter {
|
|
12
|
+
id: string;
|
|
13
|
+
client: S3;
|
|
14
|
+
bucket: string;
|
|
15
|
+
keyPrefix: string;
|
|
16
|
+
parallelism: number;
|
|
17
|
+
|
|
18
|
+
constructor({
|
|
19
|
+
connectionString,
|
|
20
|
+
parallelism = 10,
|
|
21
|
+
AwsS3,
|
|
22
|
+
}: {
|
|
23
|
+
connectionString: string;
|
|
24
|
+
parallelism?: number;
|
|
25
|
+
AwsS3?: S3;
|
|
26
|
+
}) {
|
|
27
|
+
super();
|
|
28
|
+
this.id = nanoid(7);
|
|
29
|
+
|
|
30
|
+
const uri = new URL(connectionString);
|
|
31
|
+
this.bucket = uri.hostname;
|
|
32
|
+
this.parallelism = parallelism;
|
|
33
|
+
|
|
34
|
+
let [, ...subpath] = uri.pathname.split("/");
|
|
35
|
+
this.keyPrefix = [...(subpath || [])].join("/");
|
|
36
|
+
|
|
37
|
+
this.client =
|
|
38
|
+
AwsS3 ||
|
|
39
|
+
new S3({
|
|
40
|
+
credentials: new Credentials({
|
|
41
|
+
accessKeyId: uri.username,
|
|
42
|
+
secretAccessKey: uri.password,
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
*
|
|
49
|
+
* @param param0
|
|
50
|
+
* @returns
|
|
51
|
+
*/
|
|
52
|
+
async getObject({ key }: { key: string }) {
|
|
53
|
+
try {
|
|
54
|
+
const options = {
|
|
55
|
+
Bucket: this.bucket,
|
|
56
|
+
Key: path.join(this.keyPrefix, key),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const response = await this.client.getObject(options).promise();
|
|
60
|
+
this.emit("request", "getObject", options);
|
|
61
|
+
|
|
62
|
+
return response;
|
|
63
|
+
} catch (error: unknown) {
|
|
64
|
+
if (error instanceof Error) {
|
|
65
|
+
if (error.name === "NoSuchKey") {
|
|
66
|
+
return Promise.reject(
|
|
67
|
+
new ClientNoSuchKey({ bucket: this.bucket, key })
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Promise.reject(error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
*
|
|
78
|
+
* @param param0
|
|
79
|
+
* @returns
|
|
80
|
+
*/
|
|
81
|
+
async putObject({
|
|
82
|
+
key,
|
|
83
|
+
metadata,
|
|
84
|
+
contentType,
|
|
85
|
+
body,
|
|
86
|
+
contentEncoding,
|
|
87
|
+
}: {
|
|
88
|
+
key: string;
|
|
89
|
+
metadata?: object;
|
|
90
|
+
contentType?: string;
|
|
91
|
+
body: string | Stream | Uint8Array;
|
|
92
|
+
contentEncoding?: string | null | undefined;
|
|
93
|
+
}) {
|
|
94
|
+
try {
|
|
95
|
+
const options: any = {
|
|
96
|
+
Bucket: this.bucket,
|
|
97
|
+
Key: path.join(this.keyPrefix, key),
|
|
98
|
+
Metadata: { ...metadata },
|
|
99
|
+
Body: body,
|
|
100
|
+
ContentType: contentType,
|
|
101
|
+
ContentEncoding: contentEncoding,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const response = await this.client.putObject(options).promise();
|
|
105
|
+
this.emit("request", "putObject", options);
|
|
106
|
+
|
|
107
|
+
return response;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
this.emit("error", error);
|
|
110
|
+
return Promise.reject(error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Proxy to AWS S3's headObject
|
|
116
|
+
* @param {Object} param
|
|
117
|
+
* @param {string} param.key
|
|
118
|
+
* @returns
|
|
119
|
+
*/
|
|
120
|
+
async headObject({ key }: { key: string }) {
|
|
121
|
+
try {
|
|
122
|
+
const options: any = {
|
|
123
|
+
Bucket: this.bucket,
|
|
124
|
+
Key: path.join(this.keyPrefix, key),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const response = await this.client.headObject(options).promise();
|
|
128
|
+
this.emit("request", "headObject", options);
|
|
129
|
+
|
|
130
|
+
return response;
|
|
131
|
+
} catch (error: unknown) {
|
|
132
|
+
if (error instanceof Error) {
|
|
133
|
+
if (error.name === "NoSuchKey" || error.name === "NotFound") {
|
|
134
|
+
return Promise.reject(
|
|
135
|
+
new ClientNoSuchKey({ bucket: this.bucket, key })
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.emit("error", error);
|
|
141
|
+
return Promise.reject(error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Proxy to AWS S3's deleteObject
|
|
147
|
+
* @param {Object} param
|
|
148
|
+
* @param {string} param.key
|
|
149
|
+
* @returns
|
|
150
|
+
*/
|
|
151
|
+
async deleteObject(key: string) {
|
|
152
|
+
try {
|
|
153
|
+
const options: any = {
|
|
154
|
+
Bucket: this.bucket,
|
|
155
|
+
Key: path.join(this.keyPrefix, key),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const response = await this.client.deleteObject(options).promise();
|
|
159
|
+
this.emit("request", "deleteObject", options);
|
|
160
|
+
|
|
161
|
+
return response;
|
|
162
|
+
} catch (error: unknown) {
|
|
163
|
+
this.emit("error", error);
|
|
164
|
+
|
|
165
|
+
if (error instanceof Error) {
|
|
166
|
+
if (error.name === "NoSuchKey") {
|
|
167
|
+
return Promise.reject(
|
|
168
|
+
new ClientNoSuchKey({ bucket: this.bucket, key })
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return Promise.reject(error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Proxy to AWS S3's deleteObjects
|
|
179
|
+
* @param {Object} param
|
|
180
|
+
* @param {string} param.keys
|
|
181
|
+
* @returns
|
|
182
|
+
*/
|
|
183
|
+
async deleteObjects(keys: string[]) {
|
|
184
|
+
const packages = chunk(keys, 1000);
|
|
185
|
+
|
|
186
|
+
const { results, errors } = await PromisePool.for(packages)
|
|
187
|
+
.withConcurrency(this.parallelism)
|
|
188
|
+
.process(async (keys: string[]) => {
|
|
189
|
+
try {
|
|
190
|
+
const options = {
|
|
191
|
+
Bucket: this.bucket,
|
|
192
|
+
Delete: {
|
|
193
|
+
Objects: keys.map((key) => ({
|
|
194
|
+
Key: path.join(this.keyPrefix, key),
|
|
195
|
+
})),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const response = await this.client.deleteObjects(options).promise();
|
|
200
|
+
this.emit("request", "deleteObjects", options);
|
|
201
|
+
|
|
202
|
+
return response;
|
|
203
|
+
} catch (error: unknown) {
|
|
204
|
+
this.emit("error", error);
|
|
205
|
+
return Promise.reject(error);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
deleted: results,
|
|
211
|
+
notFound: errors,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
*
|
|
217
|
+
* @param param0
|
|
218
|
+
* @returns
|
|
219
|
+
*/
|
|
220
|
+
async listObjects({
|
|
221
|
+
prefix,
|
|
222
|
+
maxKeys = 1000,
|
|
223
|
+
continuationToken,
|
|
224
|
+
}: {
|
|
225
|
+
prefix?: string;
|
|
226
|
+
maxKeys?: number;
|
|
227
|
+
continuationToken: any;
|
|
228
|
+
}): Promise<S3.ListObjectsV2Output> {
|
|
229
|
+
try {
|
|
230
|
+
const options = {
|
|
231
|
+
Bucket: this.bucket,
|
|
232
|
+
MaxKeys: maxKeys,
|
|
233
|
+
ContinuationToken: continuationToken,
|
|
234
|
+
Prefix: path.join(this.keyPrefix, prefix || ""),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const response = await this.client.listObjectsV2(options).promise();
|
|
238
|
+
this.emit("request", "listObjectsV2", options);
|
|
239
|
+
|
|
240
|
+
return response;
|
|
241
|
+
} catch (error: unknown) {
|
|
242
|
+
this.emit("error", error);
|
|
243
|
+
return Promise.reject(error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async count({ prefix }: { prefix?: string } = {}) {
|
|
248
|
+
this.emit("request", "count", { prefix });
|
|
249
|
+
|
|
250
|
+
let count = 0;
|
|
251
|
+
let truncated = true;
|
|
252
|
+
let continuationToken;
|
|
253
|
+
|
|
254
|
+
while (truncated) {
|
|
255
|
+
const options = {
|
|
256
|
+
prefix,
|
|
257
|
+
continuationToken,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const res: S3.ListObjectsV2Output = await this.listObjects(options);
|
|
261
|
+
|
|
262
|
+
count += res.KeyCount || 0;
|
|
263
|
+
truncated = res.IsTruncated || false;
|
|
264
|
+
continuationToken = res.NextContinuationToken;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return count;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async getAllKeys({ prefix }: { prefix?: string } = {}) {
|
|
271
|
+
this.emit("request", "getAllKeys", { prefix });
|
|
272
|
+
|
|
273
|
+
let keys: any[] = [];
|
|
274
|
+
let truncated = true;
|
|
275
|
+
let continuationToken;
|
|
276
|
+
|
|
277
|
+
while (truncated) {
|
|
278
|
+
const options: any = {
|
|
279
|
+
prefix,
|
|
280
|
+
continuationToken,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const res = await this.listObjects(options);
|
|
284
|
+
|
|
285
|
+
if (res.Contents) {
|
|
286
|
+
keys = keys.concat(res.Contents.map((x) => x.Key));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
truncated = res.IsTruncated || false;
|
|
290
|
+
continuationToken = res.NextContinuationToken;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return keys
|
|
294
|
+
.map((x) => x.replace(this.keyPrefix, ""))
|
|
295
|
+
.map((x) => (x.startsWith("/") ? x.replace(`/`, "") : x));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { flatten } from "flat";
|
|
2
|
+
import { isEmpty } from "lodash";
|
|
3
|
+
import EventEmitter from "events";
|
|
4
|
+
|
|
5
|
+
import Resource from "./resource.class";
|
|
6
|
+
import S3Client from "./s3-client.class";
|
|
7
|
+
import { ValidatorFactory } from "./validator";
|
|
8
|
+
import PluginInterface from "./plugin.interface";
|
|
9
|
+
import S3dbConfigInterface from "./s3db-config.interface";
|
|
10
|
+
import MetadataInterface from "./metadata.interface";
|
|
11
|
+
import { S3dbMissingMetadata, ClientNoSuchKey } from "./errors";
|
|
12
|
+
import { MetadataResourceInterface } from "./resource.interface";
|
|
13
|
+
|
|
14
|
+
export default class S3db extends EventEmitter {
|
|
15
|
+
options: S3dbConfigInterface;
|
|
16
|
+
client: S3Client;
|
|
17
|
+
keyPrefix: string = "";
|
|
18
|
+
bucket: string = "s3db";
|
|
19
|
+
version: string;
|
|
20
|
+
validatorInstance: any;
|
|
21
|
+
parallelism: number;
|
|
22
|
+
resources: any;
|
|
23
|
+
passphrase: string;
|
|
24
|
+
plugins: PluginInterface[];
|
|
25
|
+
cache: boolean | undefined = false;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Constructor
|
|
29
|
+
*/
|
|
30
|
+
constructor(options: S3dbConfigInterface) {
|
|
31
|
+
super();
|
|
32
|
+
|
|
33
|
+
this.version = "1";
|
|
34
|
+
this.resources = {};
|
|
35
|
+
this.options = options;
|
|
36
|
+
this.parallelism = parseInt(options.parallelism + "") || 10;
|
|
37
|
+
this.plugins = options.plugins || [];
|
|
38
|
+
this.cache = options.cache;
|
|
39
|
+
this.passphrase = options.passphrase || ""
|
|
40
|
+
|
|
41
|
+
this.validatorInstance = ValidatorFactory({
|
|
42
|
+
passphrase: options?.passphrase,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.client = new S3Client({
|
|
46
|
+
connectionString: options.uri,
|
|
47
|
+
parallelism: this.parallelism,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.bucket = this.client.bucket;
|
|
51
|
+
this.keyPrefix = this.client.keyPrefix;
|
|
52
|
+
|
|
53
|
+
this.startPlugins();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Remotely setups s3db file.
|
|
58
|
+
*/
|
|
59
|
+
async connect(): Promise<void> {
|
|
60
|
+
let metadata = null;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
metadata = await this.getMetadataFile();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof S3dbMissingMetadata) {
|
|
66
|
+
metadata = this.blankMetadataStructure();
|
|
67
|
+
await this.uploadMetadataFile();
|
|
68
|
+
} else {
|
|
69
|
+
this.emit("error", error);
|
|
70
|
+
return Promise.reject(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const resource of Object.entries(metadata.resources)) {
|
|
75
|
+
const [name, definition]: [string, any] = resource;
|
|
76
|
+
|
|
77
|
+
this.resources[name] = new Resource({
|
|
78
|
+
name,
|
|
79
|
+
s3db: this,
|
|
80
|
+
s3Client: this.client,
|
|
81
|
+
schema: definition.schema,
|
|
82
|
+
options: definition.options,
|
|
83
|
+
validatorInstance: this.validatorInstance,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.emit("connected", new Date());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async startPlugins() {
|
|
91
|
+
if (this.plugins && !isEmpty(this.plugins)) {
|
|
92
|
+
const startProms = this.plugins.map((plugin) => plugin.setup(this));
|
|
93
|
+
await Promise.all(startProms);
|
|
94
|
+
this.plugins.map((plugin) => plugin.start());
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Downloads current metadata.
|
|
100
|
+
* If there isnt any file, creates an empty metadata.
|
|
101
|
+
* @returns MetadataInterface
|
|
102
|
+
*/
|
|
103
|
+
private async getMetadataFile() {
|
|
104
|
+
try {
|
|
105
|
+
const request = await this.client.getObject({ key: `s3db.json` });
|
|
106
|
+
const metadata = JSON.parse(String(request?.Body));
|
|
107
|
+
return this.unserializeMetadata(metadata);
|
|
108
|
+
} catch (error: unknown) {
|
|
109
|
+
if (error instanceof ClientNoSuchKey) {
|
|
110
|
+
return Promise.reject(
|
|
111
|
+
new S3dbMissingMetadata({ bucket: this.bucket, cause: error })
|
|
112
|
+
);
|
|
113
|
+
} else {
|
|
114
|
+
return Promise.reject(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private unserializeMetadata(metadata: any) {
|
|
120
|
+
const file = { ...metadata };
|
|
121
|
+
if (isEmpty(file.resources)) return file;
|
|
122
|
+
|
|
123
|
+
for (const [name, structure] of Object.entries(
|
|
124
|
+
file.resources as MetadataResourceInterface[]
|
|
125
|
+
)) {
|
|
126
|
+
for (const [attr, value] of Object.entries(structure.schema)) {
|
|
127
|
+
file.resources[name].schema[attr] = JSON.parse(value as any);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return file;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async uploadMetadataFile() {
|
|
135
|
+
const file = {
|
|
136
|
+
version: this.version,
|
|
137
|
+
resources: Object.entries(this.resources).reduce(
|
|
138
|
+
(acc: any, definition) => {
|
|
139
|
+
const [name, resource] = definition;
|
|
140
|
+
acc[name] = (resource as Resource).export();
|
|
141
|
+
return acc;
|
|
142
|
+
},
|
|
143
|
+
{}
|
|
144
|
+
),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
await this.client.putObject({
|
|
148
|
+
key: `s3db.json`,
|
|
149
|
+
body: JSON.stringify(file, null, 2),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Generates empty metadata structure.
|
|
155
|
+
* @returns MetadataInterface
|
|
156
|
+
*/
|
|
157
|
+
private blankMetadataStructure(): MetadataInterface {
|
|
158
|
+
return {
|
|
159
|
+
version: `1`,
|
|
160
|
+
resources: {},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Generates a new resorce with its translators and validatos.
|
|
166
|
+
* @param {Object} param
|
|
167
|
+
* @param {string} param.resourceName
|
|
168
|
+
* @param {Object} param.attributes
|
|
169
|
+
* @param {Object} param.options
|
|
170
|
+
*/
|
|
171
|
+
async createResource({
|
|
172
|
+
resourceName,
|
|
173
|
+
attributes,
|
|
174
|
+
options = {},
|
|
175
|
+
}: {
|
|
176
|
+
resourceName: string;
|
|
177
|
+
attributes: any;
|
|
178
|
+
options?: any;
|
|
179
|
+
}) {
|
|
180
|
+
const schema: any = flatten(attributes, { safe: true });
|
|
181
|
+
|
|
182
|
+
const resource = new Resource({
|
|
183
|
+
schema,
|
|
184
|
+
s3db: this,
|
|
185
|
+
name: resourceName,
|
|
186
|
+
s3Client: this.client,
|
|
187
|
+
validatorInstance: this.validatorInstance,
|
|
188
|
+
|
|
189
|
+
options: {
|
|
190
|
+
autoDecrypt: true,
|
|
191
|
+
cache: this.cache,
|
|
192
|
+
...options,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.resources[resourceName] = resource;
|
|
197
|
+
|
|
198
|
+
await this.uploadMetadataFile();
|
|
199
|
+
|
|
200
|
+
return resource;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Looper
|
|
205
|
+
* @param {string} resourceName
|
|
206
|
+
* @returns
|
|
207
|
+
*/
|
|
208
|
+
resource(resourceName: string): Resource | any {
|
|
209
|
+
if (!this.resources[resourceName]) {
|
|
210
|
+
return Promise.reject(`resource ${resourceName} does not exist`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return this.resources[resourceName];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { S3 } from "aws-sdk";
|
|
3
|
+
import { chunk } from "lodash";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { PromisePool } from "@supercharge/promise-pool";
|
|
6
|
+
|
|
7
|
+
import Resource from "../resource.class";
|
|
8
|
+
|
|
9
|
+
export default class ResourceIdsReadStream extends Readable {
|
|
10
|
+
resource: Resource;
|
|
11
|
+
finishedReadingResource: boolean;
|
|
12
|
+
content: any[];
|
|
13
|
+
loading: Promise<void> | null;
|
|
14
|
+
pagesCount: number;
|
|
15
|
+
|
|
16
|
+
constructor({ resource }: { resource: Resource }) {
|
|
17
|
+
super({
|
|
18
|
+
objectMode: true,
|
|
19
|
+
highWaterMark: resource.s3Client.parallelism * 3,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
this.resource = resource;
|
|
23
|
+
this.pagesCount = 0;
|
|
24
|
+
this.content = [];
|
|
25
|
+
this.finishedReadingResource = false;
|
|
26
|
+
this.loading = this.getItems();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async _read(size: number): Promise<void> {
|
|
30
|
+
if (this.content.length === 0) {
|
|
31
|
+
if (this.loading) {
|
|
32
|
+
await this.loading;
|
|
33
|
+
} else if (this.finishedReadingResource) {
|
|
34
|
+
this.push(null);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = this.content.shift();
|
|
40
|
+
this.push(data);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getItems({
|
|
44
|
+
continuationToken = null,
|
|
45
|
+
}: {
|
|
46
|
+
continuationToken?: string | null;
|
|
47
|
+
} = {}) {
|
|
48
|
+
this.emit("page", this.pagesCount++);
|
|
49
|
+
|
|
50
|
+
const res: S3.ListObjectsV2Output = await this.resource.s3Client.listObjects({
|
|
51
|
+
prefix: `resource=${this.resource.name}`,
|
|
52
|
+
continuationToken,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (res.Contents) {
|
|
56
|
+
const contents = chunk(res.Contents, this.resource.s3Client.parallelism);
|
|
57
|
+
|
|
58
|
+
await PromisePool.for(contents)
|
|
59
|
+
.withConcurrency(5)
|
|
60
|
+
.handleError(async (error, content) => {
|
|
61
|
+
this.emit("error", error, content);
|
|
62
|
+
})
|
|
63
|
+
.process((pkg: any[]) => {
|
|
64
|
+
const ids = pkg.map((obj) => {
|
|
65
|
+
return (obj.Key || "").replace(
|
|
66
|
+
path.join(
|
|
67
|
+
this.resource.s3Client.keyPrefix,
|
|
68
|
+
`resource=${this.resource.name}`,
|
|
69
|
+
"id="
|
|
70
|
+
),
|
|
71
|
+
""
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.content.push(ids);
|
|
76
|
+
ids.forEach((id: string) => this.emit("id", this.resource.name, id));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.finishedReadingResource = !res.IsTruncated;
|
|
81
|
+
|
|
82
|
+
if (res.NextContinuationToken) {
|
|
83
|
+
this.loading = this.getItems({
|
|
84
|
+
continuationToken: res.NextContinuationToken,
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
this.loading = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isArray } from "lodash";
|
|
2
|
+
import { PromisePool } from "@supercharge/promise-pool";
|
|
3
|
+
import { Transform, TransformCallback } from "node:stream";
|
|
4
|
+
|
|
5
|
+
import Resource from "../resource.class";
|
|
6
|
+
|
|
7
|
+
export default class ResourceIdsToDataTransformer extends Transform {
|
|
8
|
+
resource: Resource;
|
|
9
|
+
|
|
10
|
+
constructor({ resource }: { resource: Resource }) {
|
|
11
|
+
super({ objectMode: true, highWaterMark: resource.s3Client.parallelism * 2 });
|
|
12
|
+
|
|
13
|
+
this.resource = resource;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async _transform(
|
|
17
|
+
chunk: any,
|
|
18
|
+
encoding: BufferEncoding,
|
|
19
|
+
callback: TransformCallback
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
if (!isArray(chunk)) this.push(null);
|
|
22
|
+
this.emit("page", chunk);
|
|
23
|
+
|
|
24
|
+
await PromisePool.for(chunk)
|
|
25
|
+
.withConcurrency(this.resource.s3Client.parallelism)
|
|
26
|
+
.handleError(async (error, content) => {
|
|
27
|
+
this.emit("error", error, content);
|
|
28
|
+
})
|
|
29
|
+
.process(async (id: any) => {
|
|
30
|
+
this.emit("id", id);
|
|
31
|
+
const data = await this.resource.getById(id);
|
|
32
|
+
this.push(data);
|
|
33
|
+
return data;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
callback(null);
|
|
37
|
+
}
|
|
38
|
+
}
|