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.
Files changed (52) hide show
  1. package/.github/workflows/pipeline.yml +16 -0
  2. package/README.md +742 -0
  3. package/build/cache/avro.serializer.js +16 -0
  4. package/build/cache/json.serializer.js +7 -0
  5. package/build/cache/s3-cache.class.js +157 -0
  6. package/build/cache/s3-resource-cache.class.js +77 -0
  7. package/build/cache/serializers.type.js +8 -0
  8. package/build/errors.js +64 -0
  9. package/build/index.js +9 -0
  10. package/build/metadata.interface.js +2 -0
  11. package/build/plugin.interface.js +2 -0
  12. package/build/resource.class.js +485 -0
  13. package/build/resource.interface.js +2 -0
  14. package/build/s3-client.class.js +274 -0
  15. package/build/s3db-config.interface.js +2 -0
  16. package/build/s3db.class.js +185 -0
  17. package/build/stream/resource-ids-read-stream.class.js +100 -0
  18. package/build/stream/resource-ids-transformer.class.js +40 -0
  19. package/build/stream/resource-write-stream.class.js +76 -0
  20. package/build/validator.js +37 -0
  21. package/examples/1-bulk-insert.js +64 -0
  22. package/examples/2-read-stream.js +61 -0
  23. package/examples/3-read-stream-to-csv.js +57 -0
  24. package/examples/4-read-stream-to-zip.js +56 -0
  25. package/examples/5-write-stream.js +98 -0
  26. package/examples/6-jwt-tokens.js +124 -0
  27. package/examples/concerns/index.js +64 -0
  28. package/jest.config.ts +10 -0
  29. package/package.json +51 -0
  30. package/src/cache/avro.serializer.ts +12 -0
  31. package/src/cache/json.serializer.ts +4 -0
  32. package/src/cache/s3-cache.class.ts +155 -0
  33. package/src/cache/s3-resource-cache.class.ts +75 -0
  34. package/src/cache/serializers.type.ts +8 -0
  35. package/src/errors.ts +96 -0
  36. package/src/index.ts +4 -0
  37. package/src/metadata.interface.ts +4 -0
  38. package/src/plugin.interface.ts +4 -0
  39. package/src/resource.class.ts +531 -0
  40. package/src/resource.interface.ts +21 -0
  41. package/src/s3-client.class.ts +297 -0
  42. package/src/s3db-config.interface.ts +9 -0
  43. package/src/s3db.class.ts +215 -0
  44. package/src/stream/resource-ids-read-stream.class.ts +90 -0
  45. package/src/stream/resource-ids-transformer.class.ts +38 -0
  46. package/src/stream/resource-write-stream.class.ts +78 -0
  47. package/src/validator.ts +39 -0
  48. package/tests/cache.spec.ts +187 -0
  49. package/tests/concerns/index.ts +16 -0
  50. package/tests/config.spec.ts +29 -0
  51. package/tests/resources.spec.ts +197 -0
  52. package/tsconfig.json +111 -0
@@ -0,0 +1,531 @@
1
+ import * as path from "path";
2
+ import { nanoid } from "nanoid";
3
+ import CryptoJS from "crypto-js";
4
+ import EventEmitter from "events";
5
+ import { flatten, unflatten } from "flat";
6
+ import { sortBy, chunk, isArray, merge } from "lodash";
7
+ import { PromisePool } from "@supercharge/promise-pool";
8
+
9
+ import S3db from "./s3db.class";
10
+ import S3Client from "./s3-client.class";
11
+ import { S3dbInvalidResource } from "./errors";
12
+ import S3ResourceCache from "./cache/s3-resource-cache.class";
13
+ import ResourceWriteStream from "./stream/resource-write-stream.class";
14
+ import ResourceIdsReadStream from "./stream/resource-ids-read-stream.class";
15
+ import ResourceIdsToDataTransformer from "./stream/resource-ids-transformer.class";
16
+
17
+ import {
18
+ ResourceInterface,
19
+ ResourceConfigInterface,
20
+ } from "./resource.interface";
21
+
22
+ export default class Resource
23
+ extends EventEmitter
24
+ implements ResourceInterface
25
+ {
26
+ name: any;
27
+ schema: any;
28
+ mapObj: any;
29
+ options: any;
30
+ validator: any;
31
+ reversedMapObj: any;
32
+
33
+ s3db: S3db;
34
+ s3Client: S3Client;
35
+ s3Cache: S3ResourceCache | undefined;
36
+
37
+ /**
38
+ * Constructor
39
+ */
40
+ constructor(params: ResourceConfigInterface) {
41
+ super();
42
+
43
+ this.s3db = params.s3db;
44
+ this.name = params.name;
45
+ this.schema = params.schema;
46
+ this.options = params.options;
47
+ this.s3Client = params.s3Client;
48
+
49
+ this.validator = params.validatorInstance.compile(this.schema);
50
+
51
+ const { mapObj, reversedMapObj } = this.getMappersFromSchema(this.schema);
52
+ this.mapObj = mapObj;
53
+ this.reversedMapObj = reversedMapObj;
54
+
55
+ this.studyOptions();
56
+
57
+ if (this.options.cache === true) {
58
+ this.s3Cache = new S3ResourceCache({
59
+ resource: this,
60
+ compressData: true,
61
+ serializer: "json",
62
+ });
63
+ }
64
+ }
65
+
66
+ getMappersFromSchema(schema: any) {
67
+ let i = 0;
68
+
69
+ const mapObj = sortBy(Object.entries(schema), ["0"]).reduce(
70
+ (acc: any, [key, value]) => {
71
+ acc[key] = String(i++);
72
+ return acc;
73
+ },
74
+ {}
75
+ );
76
+
77
+ const reversedMapObj = Object.entries(mapObj).reduce(
78
+ (acc: any, [key, value]) => {
79
+ acc[String(value)] = key;
80
+ return acc;
81
+ },
82
+ {}
83
+ );
84
+
85
+ return {
86
+ mapObj,
87
+ reversedMapObj,
88
+ };
89
+ }
90
+
91
+ export() {
92
+ const data = {
93
+ name: this.name,
94
+ schema: { ...this.schema },
95
+ mapper: this.mapObj,
96
+ options: this.options,
97
+ };
98
+
99
+ for (const [name, definition] of Object.entries(this.schema)) {
100
+ data.schema[name] = JSON.stringify(definition as any);
101
+ }
102
+
103
+ return data;
104
+ }
105
+
106
+ studyOptions() {
107
+ if (!this.options.afterUnmap) this.options.beforeMap = {};
108
+ if (!this.options.afterUnmap) this.options.afterUnmap = {};
109
+
110
+ const schema: any = flatten(this.schema, { safe: true });
111
+
112
+ const addRule = (arr: string, attribute: string, action: string) => {
113
+ if (!this.options[arr][attribute]) this.options[arr][attribute] = [];
114
+
115
+ this.options[arr][attribute] = [
116
+ ...new Set([...this.options[arr][attribute], action]),
117
+ ];
118
+ };
119
+
120
+ for (const [name, definition] of Object.entries(schema)) {
121
+ if ((definition as string).includes("secret")) {
122
+ if (this.options.autoDecrypt === true) {
123
+ addRule("afterUnmap", name, "decrypt");
124
+ }
125
+ }
126
+ if ((definition as string).includes("array")) {
127
+ addRule("beforeMap", name, "fromArray");
128
+ addRule("afterUnmap", name, "toArray");
129
+ }
130
+ if ((definition as string).includes("number")) {
131
+ addRule("beforeMap", name, "toString");
132
+ addRule("afterUnmap", name, "toNumber");
133
+ }
134
+ if ((definition as string).includes("boolean")) {
135
+ addRule("beforeMap", name, "toJson");
136
+ addRule("afterUnmap", name, "fromJson");
137
+ }
138
+ }
139
+ }
140
+
141
+ private check(data: any) {
142
+ const result = {
143
+ original: { ...data },
144
+ isValid: false,
145
+ errors: [],
146
+ };
147
+
148
+ const check = this.validator(data);
149
+
150
+ if (check === true) {
151
+ result.isValid = true;
152
+ } else {
153
+ result.errors = check;
154
+ }
155
+
156
+ return {
157
+ ...result,
158
+ data,
159
+ };
160
+ }
161
+
162
+ validate(data: any) {
163
+ return this.check(flatten(data, { safe: true }));
164
+ }
165
+
166
+ map(data: any) {
167
+ let obj: any = { ...data };
168
+
169
+ for (const [attribute, actions] of Object.entries(this.options.beforeMap)) {
170
+ for (const action of actions as string[]) {
171
+ if (action === "fromArray") {
172
+ obj[attribute] = (obj[attribute] || []).join("|");
173
+ } else if (action === "toString") {
174
+ obj[attribute] = String(obj[attribute]);
175
+ } else if (action === "toJson") {
176
+ obj[attribute] = JSON.stringify(obj[attribute]);
177
+ }
178
+ }
179
+ }
180
+
181
+ obj = Object.entries(obj).reduce((acc: any, [key, value]) => {
182
+ acc[this.mapObj[key]] = isArray(value) ? value.join("|") : value;
183
+ return acc;
184
+ }, {});
185
+
186
+ return obj;
187
+ }
188
+
189
+ unmap(data: any) {
190
+ const obj = Object.entries(data).reduce((acc: any, [key, value]) => {
191
+ acc[this.reversedMapObj[key]] = value;
192
+ return acc;
193
+ }, {});
194
+
195
+ for (const [attribute, actions] of Object.entries(
196
+ this.options.afterUnmap
197
+ )) {
198
+ for (const action of actions as string[]) {
199
+ if (action === "decrypt") {
200
+ let content = obj[attribute];
201
+ content = CryptoJS.AES.decrypt(content, this.s3db.passphrase);
202
+ content = content.toString(CryptoJS.enc.Utf8);
203
+ obj[attribute] = content;
204
+ } else if (action === "toArray") {
205
+ obj[attribute] = (obj[attribute] || "").split("|");
206
+ } else if (action === "toNumber") {
207
+ obj[attribute] = Number(obj[attribute] || "");
208
+ } else if (action === "fromJson") {
209
+ obj[attribute] = JSON.parse(obj[attribute]);
210
+ }
211
+ }
212
+ }
213
+
214
+ return obj;
215
+ }
216
+
217
+ /**
218
+ * Inserts a new object into the resource list.
219
+ * @param {Object} param
220
+ * @returns
221
+ */
222
+ async insert(attributes: any) {
223
+ let { id, ...attrs }: { id: any; attrs: any } = flatten(attributes, {
224
+ safe: true,
225
+ });
226
+
227
+ // validate
228
+ const { isValid, errors, data: validated } = this.check(attrs);
229
+
230
+ if (!isValid) {
231
+ return Promise.reject(
232
+ new S3dbInvalidResource({
233
+ bucket: this.s3Client.bucket,
234
+ resourceName: this.name,
235
+ attributes,
236
+ validation: errors,
237
+ })
238
+ );
239
+ }
240
+
241
+ if (!id && id !== 0) id = nanoid();
242
+
243
+ // save
244
+ await this.s3Client.putObject({
245
+ key: path.join(`resource=${this.name}`, `id=${id}`),
246
+ body: "",
247
+ metadata: this.map(validated),
248
+ });
249
+
250
+ const final = {
251
+ id,
252
+ ...(unflatten(validated) as object),
253
+ };
254
+
255
+ this.emit("inserted", final);
256
+ this.s3db.emit("inserted", this.name, final);
257
+
258
+ if (this.s3Cache) {
259
+ await this.s3Cache?.purge();
260
+ }
261
+
262
+ return final;
263
+ }
264
+
265
+ /**
266
+ * Get a resource by id
267
+ * @param {Object} param
268
+ * @returns
269
+ */
270
+ async getById(id: any) {
271
+ const request = await this.s3Client.headObject({
272
+ key: path.join(`resource=${this.name}`, `id=${id}`),
273
+ });
274
+
275
+ let data: any = this.unmap(request.Metadata);
276
+ data = unflatten(data);
277
+
278
+ data.id = id;
279
+ data._length = request.ContentLength;
280
+ data._createdAt = request.LastModified;
281
+
282
+ if (request.Expiration) data._expiresAt = request.Expiration;
283
+
284
+ this.emit("got", data);
285
+ this.s3db.emit("got", this.name, data);
286
+
287
+ return data;
288
+ }
289
+
290
+ /**
291
+ * Update a resource by id
292
+ * @param {Object} param
293
+ * @returns
294
+ */
295
+ async updateById(id: any, attributes: any) {
296
+ const obj = await this.getById(id);
297
+
298
+ let attrs1 = flatten(attributes, { safe: true });
299
+ let attrs2 = flatten(obj, { safe: true });
300
+
301
+ const attrs = merge(attrs2, attrs1) as any;
302
+ delete attrs.id;
303
+
304
+ const { isValid, errors, data: validated } = this.check(attrs);
305
+
306
+ if (!isValid) {
307
+ return Promise.reject(
308
+ new S3dbInvalidResource({
309
+ bucket: this.s3Client.bucket,
310
+ resourceName: this.name,
311
+ attributes,
312
+ validation: errors,
313
+ })
314
+ );
315
+ }
316
+
317
+ if (!id && id !== 0) id = nanoid();
318
+
319
+ // save
320
+ await this.s3Client.putObject({
321
+ key: path.join(`resource=${this.name}`, `id=${id}`),
322
+ body: "",
323
+ metadata: this.map(validated),
324
+ });
325
+
326
+ const final = {
327
+ id,
328
+ ...(unflatten(validated) as object),
329
+ };
330
+
331
+ this.emit("updated", final);
332
+ this.s3db.emit("updated", this.name, final);
333
+
334
+ if (this.s3Cache) {
335
+ await this.s3Cache?.purge();
336
+ }
337
+
338
+ return final;
339
+ }
340
+
341
+ /**
342
+ * Delete a resource by id
343
+ * @param {Object} param
344
+ * @returns
345
+ */
346
+ async deleteById(id: any) {
347
+ const key = path.join(`resource=${this.name}`, `id=${id}`);
348
+ const response = await this.s3Client.deleteObject(key);
349
+
350
+ this.emit("deleted", id);
351
+ this.s3db.emit("deleted", this.name, id);
352
+
353
+ if (this.s3Cache) {
354
+ await this.s3Cache?.purge();
355
+ }
356
+
357
+ return response;
358
+ }
359
+
360
+ /**
361
+ *
362
+ */
363
+ async bulkInsert(objects: any[]) {
364
+ const { results } = await PromisePool.for(objects)
365
+ .withConcurrency(this.s3db.parallelism)
366
+ .handleError(async (error, content) => {
367
+ this.emit("error", error, content);
368
+ this.s3db.emit("error", this.name, error, content);
369
+ })
370
+ .process(async (attributes: any) => {
371
+ const result = await this.insert(attributes);
372
+ return result;
373
+ });
374
+
375
+ return results;
376
+ }
377
+
378
+ /**
379
+ *
380
+ * @returns number
381
+ */
382
+ async count() {
383
+ if (this.s3Cache) {
384
+ const cached = await this.s3Cache.get({ action: "count" });
385
+ if (cached) return cached;
386
+ }
387
+
388
+ const count = await this.s3Client.count({
389
+ prefix: `resource=${this.name}`,
390
+ });
391
+
392
+ if (this.s3Cache) {
393
+ await this.s3Cache.put({ action: "count", data: count });
394
+ }
395
+
396
+ return count;
397
+ }
398
+
399
+ /**
400
+ * Delete resources by a list of ids
401
+ * @param {Object} param
402
+ * @returns
403
+ */
404
+ async bulkDelete(ids: any[]): Promise<any[]> {
405
+ let packages = chunk(
406
+ ids.map((x) => path.join(`resource=${this.name}`, `id=${x}`)),
407
+ 1000
408
+ );
409
+
410
+ const { results } = await PromisePool.for(packages)
411
+ .withConcurrency(this.s3db.parallelism)
412
+ .handleError(async (error, content) => {
413
+ this.emit("error", error, content);
414
+ this.s3db.emit("error", this.name, error, content);
415
+ })
416
+ .process(async (keys: string[]) => {
417
+ const response = await this.s3Client.deleteObjects(keys);
418
+
419
+ keys.forEach((key) => {
420
+ const id = key.split("=").pop();
421
+ this.emit("deleted", id);
422
+ this.s3db.emit("deleted", this.name, id);
423
+ });
424
+
425
+ return response;
426
+ });
427
+
428
+ if (this.s3Cache) {
429
+ await this.s3Cache?.purge();
430
+ }
431
+
432
+ return results;
433
+ }
434
+
435
+ async getAllIds() {
436
+ if (this.s3Cache) {
437
+ const cached = await this.s3Cache.get({ action: "getAllIds" });
438
+ if (cached) return cached;
439
+ }
440
+
441
+ const keys = await this.s3Client.getAllKeys({
442
+ prefix: `resource=${this.name}`,
443
+ });
444
+
445
+ const ids = keys.map((x) => x.replace(`resource=${this.name}/id=`, ""));
446
+
447
+ if (this.s3Cache) {
448
+ await this.s3Cache.put({ action: "getAllIds", data: ids });
449
+ const x = await this.s3Cache.get({ action: "getAllIds" });
450
+ }
451
+
452
+ return ids;
453
+ }
454
+
455
+ async deleteAll() {
456
+ const ids = await this.getAllIds();
457
+ await this.bulkDelete(ids);
458
+ }
459
+
460
+ async getByIdList(ids: string[]) {
461
+ if (this.s3Cache) {
462
+ const cached = await this.s3Cache.get({ action: "getAll" });
463
+ if (cached) return cached;
464
+ }
465
+
466
+ const { results } = await PromisePool.for(ids)
467
+ .withConcurrency(this.s3Client.parallelism)
468
+ .process(async (id: string) => {
469
+ this.emit("id", id);
470
+ const data = await this.getById(id);
471
+ this.emit("data", data);
472
+ return data;
473
+ });
474
+
475
+ if (this.s3Cache) {
476
+ await this.s3Cache.put({ action: "getAll", data: results });
477
+ }
478
+
479
+ return results;
480
+ }
481
+
482
+ async getAll() {
483
+ if (this.s3Cache) {
484
+ const cached = await this.s3Cache.get({ action: "getAll" });
485
+ if (cached) return cached;
486
+ }
487
+
488
+ let ids: string[] = [];
489
+ let gotFromCache = false;
490
+
491
+ if (this.s3Cache) {
492
+ const cached = await this.s3Cache.get({ action: "getAllIds" });
493
+ if (cached) {
494
+ ids = cached;
495
+ gotFromCache = true;
496
+ }
497
+ }
498
+
499
+ if (!gotFromCache) ids = await this.getAllIds();
500
+
501
+ if (ids.length === 0) return [];
502
+
503
+ const { results } = await PromisePool.for(ids)
504
+ .withConcurrency(this.s3Client.parallelism)
505
+ .process(async (id: string) => {
506
+ this.emit("id", id);
507
+ const data = await this.getById(id);
508
+ this.emit("data", data);
509
+ return data;
510
+ });
511
+
512
+ if (this.s3Cache && results.length > 0) {
513
+ await this.s3Cache.put({ action: "getAll", data: results });
514
+ const x = await this.s3Cache.get({ action: "getAll" });
515
+ }
516
+
517
+ return results;
518
+ }
519
+
520
+ readable() {
521
+ const stream = new ResourceIdsReadStream({ resource: this });
522
+ const transformer = new ResourceIdsToDataTransformer({ resource: this });
523
+
524
+ return stream.pipe(transformer);
525
+ }
526
+
527
+ writable() {
528
+ const stream = new ResourceWriteStream({ resource: this });
529
+ return stream;
530
+ }
531
+ }
@@ -0,0 +1,21 @@
1
+ import S3db from "./s3db.class";
2
+ import S3Client from "./s3-client.class";
3
+
4
+ export interface MetadataResourceInterface {
5
+ schema: any;
6
+ }
7
+
8
+ export interface ResourceInterface {
9
+ schema: any;
10
+ validator: any;
11
+ }
12
+
13
+ export interface ResourceConfigInterface {
14
+ s3db: S3db;
15
+ name: string;
16
+ schema: any;
17
+ options?: any;
18
+ cache?: boolean
19
+ s3Client: S3Client;
20
+ validatorInstance: any;
21
+ }