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,155 @@
|
|
|
1
|
+
import zlib from "zlib";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { isString } from "lodash";
|
|
4
|
+
|
|
5
|
+
import S3Client from "../s3-client.class";
|
|
6
|
+
import Serializers from "./serializers.type";
|
|
7
|
+
import { JsonSerializer } from "./json.serializer";
|
|
8
|
+
import { AvroSerializer } from "./avro.serializer";
|
|
9
|
+
|
|
10
|
+
export default class S3Cache {
|
|
11
|
+
serializers: any;
|
|
12
|
+
s3Client: S3Client;
|
|
13
|
+
compressData: boolean;
|
|
14
|
+
serializer: Serializers;
|
|
15
|
+
|
|
16
|
+
constructor({
|
|
17
|
+
s3Client,
|
|
18
|
+
compressData = true,
|
|
19
|
+
serializer = Serializers.json,
|
|
20
|
+
}: {
|
|
21
|
+
s3Client: S3Client;
|
|
22
|
+
compressData?: boolean;
|
|
23
|
+
serializer?: Serializers;
|
|
24
|
+
}) {
|
|
25
|
+
this.s3Client = s3Client;
|
|
26
|
+
this.serializer = serializer;
|
|
27
|
+
this.compressData = compressData;
|
|
28
|
+
|
|
29
|
+
this.serializers = {
|
|
30
|
+
[Serializers.json]: JsonSerializer,
|
|
31
|
+
[Serializers.avro]: AvroSerializer,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getKey({
|
|
36
|
+
params,
|
|
37
|
+
hashed = true,
|
|
38
|
+
additionalPrefix = "",
|
|
39
|
+
}: {
|
|
40
|
+
params?: any;
|
|
41
|
+
hashed?: boolean;
|
|
42
|
+
additionalPrefix?: string;
|
|
43
|
+
}) {
|
|
44
|
+
let filename: any = Object.keys(params || {})
|
|
45
|
+
.sort()
|
|
46
|
+
.map((x) => `${x}:${params[x]}`)
|
|
47
|
+
.join("|");
|
|
48
|
+
|
|
49
|
+
if (filename.length === 0) filename = `empty`;
|
|
50
|
+
|
|
51
|
+
if (additionalPrefix.length > 0) {
|
|
52
|
+
filename = additionalPrefix + `|` + filename;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (hashed) {
|
|
56
|
+
filename = Buffer.from(filename)
|
|
57
|
+
.toString("base64")
|
|
58
|
+
.split("")
|
|
59
|
+
.reverse()
|
|
60
|
+
.join("");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
filename = filename + "." + this.serializer;
|
|
64
|
+
if (this.compressData) filename += ".gz";
|
|
65
|
+
|
|
66
|
+
return path.join("cache", filename);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async _put({ key, data }: { key: string; data: any }) {
|
|
70
|
+
const lengthRaw = isString(data)
|
|
71
|
+
? data.length
|
|
72
|
+
: JSON.stringify(data).length;
|
|
73
|
+
|
|
74
|
+
let body: string | Uint8Array = this.serialize({ data });
|
|
75
|
+
const lengthSerialized = body.length;
|
|
76
|
+
|
|
77
|
+
if (this.compressData) {
|
|
78
|
+
body = zlib.gzipSync(body);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const metadata = {
|
|
82
|
+
compressor: "zlib",
|
|
83
|
+
"client-id": this.s3Client.id,
|
|
84
|
+
serializer: String(this.serializer),
|
|
85
|
+
compressed: String(this.compressData),
|
|
86
|
+
"length-raw": String(lengthRaw),
|
|
87
|
+
"length-serialized": String(lengthSerialized),
|
|
88
|
+
"length-compressed": String(body.length),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return this.s3Client.putObject({
|
|
92
|
+
key,
|
|
93
|
+
body,
|
|
94
|
+
metadata,
|
|
95
|
+
contentEncoding: this.compressData ? "gzip" : null,
|
|
96
|
+
contentType: this.compressData
|
|
97
|
+
? "application/gzip"
|
|
98
|
+
: `application/${this.serializer}`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async _get({ key }: { key: string }) {
|
|
103
|
+
try {
|
|
104
|
+
const res = await this.s3Client.getObject({ key });
|
|
105
|
+
|
|
106
|
+
if (!res.Body) return "";
|
|
107
|
+
let content = res.Body;
|
|
108
|
+
|
|
109
|
+
if (res.Metadata) {
|
|
110
|
+
const { serializer, compressor, compressed } = res.Metadata;
|
|
111
|
+
|
|
112
|
+
if (["true", true].includes(compressed)) {
|
|
113
|
+
if (compressor === `zlib`) {
|
|
114
|
+
content = zlib.unzipSync(content as Buffer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { data } = this.serializers[serializer].unserialize(content);
|
|
119
|
+
return data;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return this.unserialize(content);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof Error) {
|
|
125
|
+
if (error.name !== "ClientNoSuchKey") {
|
|
126
|
+
return Promise.reject(error);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async _delete({ key }: { key: string }) {
|
|
135
|
+
try {
|
|
136
|
+
await this.s3Client.deleteObject(key);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error instanceof Error) {
|
|
139
|
+
if (error.name !== "ClientNoSuchKey") {
|
|
140
|
+
return Promise.reject(error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
serialize(data: any) {
|
|
149
|
+
return this.serializers[this.serializer].serialize(data);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
unserialize(data: any) {
|
|
153
|
+
return this.serializers[this.serializer].unserialize(data);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import S3Cache from "./s3-cache.class";
|
|
2
|
+
import Resource from "../resource.class";
|
|
3
|
+
import Serializers from "./serializers.type";
|
|
4
|
+
|
|
5
|
+
export default class S3ResourceCache extends S3Cache {
|
|
6
|
+
resource: Resource;
|
|
7
|
+
|
|
8
|
+
constructor({
|
|
9
|
+
resource,
|
|
10
|
+
compressData = true,
|
|
11
|
+
serializer = Serializers.json,
|
|
12
|
+
}: {
|
|
13
|
+
resource: Resource;
|
|
14
|
+
compressData?: boolean;
|
|
15
|
+
serializer?: Serializers;
|
|
16
|
+
}) {
|
|
17
|
+
super({
|
|
18
|
+
s3Client: resource.s3Client,
|
|
19
|
+
compressData: compressData,
|
|
20
|
+
serializer: serializer,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
this.resource = resource;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getKey({ action = "list", params }: { action?: string; params?: any }) {
|
|
27
|
+
return super.getKey({
|
|
28
|
+
params,
|
|
29
|
+
additionalPrefix: `${this.resource.name}|${action}`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async put({
|
|
34
|
+
action = "list",
|
|
35
|
+
params,
|
|
36
|
+
data,
|
|
37
|
+
}: {
|
|
38
|
+
action?: string;
|
|
39
|
+
params?: any;
|
|
40
|
+
data: any;
|
|
41
|
+
}) {
|
|
42
|
+
return super._put({
|
|
43
|
+
data,
|
|
44
|
+
key: this.getKey({ action, params }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async get({ action = "list", params }: { action?: string; params?: any }) {
|
|
49
|
+
return super._get({
|
|
50
|
+
key: this.getKey({ action, params }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async delete({ action = "list", params }: { action?: string; params: any }) {
|
|
55
|
+
const key = this.getKey({ action, params });
|
|
56
|
+
|
|
57
|
+
return super._delete({
|
|
58
|
+
key: this.getKey({ action, params }),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async purge() {
|
|
63
|
+
let keys = await this.s3Client.getAllKeys({ prefix: "cache" });
|
|
64
|
+
|
|
65
|
+
const key = Buffer.from(this.resource.name)
|
|
66
|
+
.toString("base64")
|
|
67
|
+
.split("")
|
|
68
|
+
.reverse()
|
|
69
|
+
.join("");
|
|
70
|
+
|
|
71
|
+
keys = keys.filter((k) => k.includes(key));
|
|
72
|
+
|
|
73
|
+
await this.s3Client.deleteObjects(keys);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Errors interfaces
|
|
2
|
+
export interface S3Error {
|
|
3
|
+
name: string;
|
|
4
|
+
message: string;
|
|
5
|
+
cause?: Error;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface S3dbError {
|
|
9
|
+
name: string;
|
|
10
|
+
message: string;
|
|
11
|
+
cause?: Error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class BaseError extends Error {
|
|
15
|
+
bucket: any;
|
|
16
|
+
thrownAt: Date;
|
|
17
|
+
cause: Error | undefined;
|
|
18
|
+
|
|
19
|
+
constructor({ bucket, message, cause }: { bucket: string; message: string, cause?: Error }) {
|
|
20
|
+
super(message);
|
|
21
|
+
|
|
22
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
23
|
+
Error.captureStackTrace(this, this.constructor);
|
|
24
|
+
} else {
|
|
25
|
+
this.stack = (new Error(message)).stack;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
super.name = this.constructor.name;
|
|
29
|
+
this.name = this.constructor.name;
|
|
30
|
+
this.cause = cause
|
|
31
|
+
this.thrownAt = new Date();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toJson() {
|
|
35
|
+
return { ...this };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toString() {
|
|
39
|
+
return `${this.name} | ${this.message}`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// AWS S3 errors
|
|
44
|
+
export abstract class BaseS3Error extends BaseError implements S3Error {
|
|
45
|
+
constructor({ bucket, message }: { bucket: string; message: string }) {
|
|
46
|
+
super({ bucket, message });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class ClientNoSuchKey extends BaseS3Error {
|
|
51
|
+
key: string;
|
|
52
|
+
constructor({ bucket, key }: { bucket: string; key: string }) {
|
|
53
|
+
super({ bucket, message: `Key does not exists [s3://${bucket}/${key}]` });
|
|
54
|
+
this.key = key;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Our errors
|
|
59
|
+
export abstract class BaseS3dbError extends BaseError implements S3dbError {
|
|
60
|
+
constructor({ bucket, message, cause }: { bucket: string; message: string, cause?: Error }) {
|
|
61
|
+
super({ bucket, message, cause });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class S3dbMissingMetadata extends BaseS3dbError {
|
|
66
|
+
constructor({ bucket, cause }: { bucket: string, cause?: Error }) {
|
|
67
|
+
super({ bucket, cause, message: `Missing metadata for bucket [s3://${bucket}]` });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class S3dbInvalidResource extends BaseS3dbError {
|
|
72
|
+
resourceName: any;
|
|
73
|
+
attributes: any;
|
|
74
|
+
validation: any;
|
|
75
|
+
|
|
76
|
+
constructor({
|
|
77
|
+
bucket,
|
|
78
|
+
resourceName,
|
|
79
|
+
attributes,
|
|
80
|
+
validation,
|
|
81
|
+
}: {
|
|
82
|
+
bucket: string;
|
|
83
|
+
resourceName: string;
|
|
84
|
+
attributes: string;
|
|
85
|
+
validation: any[];
|
|
86
|
+
}) {
|
|
87
|
+
super({
|
|
88
|
+
bucket,
|
|
89
|
+
message: `Resource is not valid. Name=${resourceName} [s3://${bucket}].\n${JSON.stringify(validation, null, 2)}`,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.resourceName = resourceName;
|
|
93
|
+
this.attributes = attributes;
|
|
94
|
+
this.validation = validation;
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/index.ts
ADDED