s3db.js 6.1.0 → 7.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 (60) hide show
  1. package/PLUGINS.md +2724 -0
  2. package/README.md +377 -492
  3. package/UNLICENSE +24 -0
  4. package/dist/s3db.cjs.js +30054 -18189
  5. package/dist/s3db.cjs.min.js +1 -1
  6. package/dist/s3db.d.ts +373 -72
  7. package/dist/s3db.es.js +30040 -18186
  8. package/dist/s3db.es.min.js +1 -1
  9. package/dist/s3db.iife.js +29727 -17863
  10. package/dist/s3db.iife.min.js +1 -1
  11. package/package.json +44 -69
  12. package/src/behaviors/body-only.js +110 -0
  13. package/src/behaviors/body-overflow.js +153 -0
  14. package/src/behaviors/enforce-limits.js +195 -0
  15. package/src/behaviors/index.js +39 -0
  16. package/src/behaviors/truncate-data.js +204 -0
  17. package/src/behaviors/user-managed.js +147 -0
  18. package/src/client.class.js +515 -0
  19. package/src/concerns/base62.js +61 -0
  20. package/src/concerns/calculator.js +204 -0
  21. package/src/concerns/crypto.js +142 -0
  22. package/src/concerns/id.js +8 -0
  23. package/src/concerns/index.js +5 -0
  24. package/src/concerns/try-fn.js +151 -0
  25. package/src/connection-string.class.js +75 -0
  26. package/src/database.class.js +599 -0
  27. package/src/errors.js +261 -0
  28. package/src/index.js +17 -0
  29. package/src/plugins/audit.plugin.js +442 -0
  30. package/src/plugins/cache/cache.class.js +53 -0
  31. package/src/plugins/cache/index.js +6 -0
  32. package/src/plugins/cache/memory-cache.class.js +164 -0
  33. package/src/plugins/cache/s3-cache.class.js +189 -0
  34. package/src/plugins/cache.plugin.js +275 -0
  35. package/src/plugins/consumers/index.js +24 -0
  36. package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
  37. package/src/plugins/consumers/sqs-consumer.js +102 -0
  38. package/src/plugins/costs.plugin.js +81 -0
  39. package/src/plugins/fulltext.plugin.js +473 -0
  40. package/src/plugins/index.js +12 -0
  41. package/src/plugins/metrics.plugin.js +603 -0
  42. package/src/plugins/plugin.class.js +210 -0
  43. package/src/plugins/plugin.obj.js +13 -0
  44. package/src/plugins/queue-consumer.plugin.js +134 -0
  45. package/src/plugins/replicator.plugin.js +769 -0
  46. package/src/plugins/replicators/base-replicator.class.js +85 -0
  47. package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
  48. package/src/plugins/replicators/index.js +44 -0
  49. package/src/plugins/replicators/postgres-replicator.class.js +427 -0
  50. package/src/plugins/replicators/s3db-replicator.class.js +352 -0
  51. package/src/plugins/replicators/sqs-replicator.class.js +427 -0
  52. package/src/resource.class.js +2626 -0
  53. package/src/s3db.d.ts +1263 -0
  54. package/src/schema.class.js +706 -0
  55. package/src/stream/index.js +16 -0
  56. package/src/stream/resource-ids-page-reader.class.js +10 -0
  57. package/src/stream/resource-ids-reader.class.js +63 -0
  58. package/src/stream/resource-reader.class.js +81 -0
  59. package/src/stream/resource-writer.class.js +92 -0
  60. package/src/validator.class.js +97 -0
@@ -0,0 +1,16 @@
1
+ export * from "./resource-reader.class.js"
2
+ export * from "./resource-writer.class.js"
3
+ export * from "./resource-ids-reader.class.js"
4
+ export * from "./resource-ids-page-reader.class.js"
5
+
6
+ export function streamToString(stream) {
7
+ return new Promise((resolve, reject) => {
8
+ if (!stream) {
9
+ return reject(new Error('streamToString: stream is undefined'));
10
+ }
11
+ const chunks = [];
12
+ stream.on('data', (chunk) => chunks.push(chunk));
13
+ stream.on('error', reject);
14
+ stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
15
+ });
16
+ }
@@ -0,0 +1,10 @@
1
+ import ResourceIdsReader from "./resource-ids-reader.class.js";
2
+
3
+ export class ResourceIdsPageReader extends ResourceIdsReader {
4
+ enqueue(ids) {
5
+ this.controller.enqueue(ids)
6
+ this.emit("page", ids);
7
+ }
8
+ }
9
+
10
+ export default ResourceIdsPageReader
@@ -0,0 +1,63 @@
1
+ import EventEmitter from "events";
2
+ import { ReadableStream } from "node:stream/web";
3
+
4
+ export class ResourceIdsReader extends EventEmitter {
5
+ constructor({ resource }) {
6
+ super()
7
+
8
+ this.resource = resource;
9
+ this.client = resource.client;
10
+
11
+ this.stream = new ReadableStream({
12
+ highWaterMark: this.client.parallelism * 3,
13
+ start: this._start.bind(this),
14
+ pull: this._pull.bind(this),
15
+ cancel: this._cancel.bind(this),
16
+ });
17
+ }
18
+
19
+ build () {
20
+ return this.stream.getReader();
21
+ }
22
+
23
+ async _start(controller) {
24
+ this.controller = controller;
25
+ this.continuationToken = null;
26
+ this.closeNextIteration = false;
27
+ }
28
+
29
+ async _pull(controller) {
30
+ if (this.closeNextIteration) {
31
+ controller.close();
32
+ return;
33
+ }
34
+
35
+ const response = await this.client.listObjects({
36
+ prefix: `resource=${this.resource.name}`,
37
+ continuationToken: this.continuationToken,
38
+ });
39
+
40
+ const keys = response?.Contents
41
+ .map((x) => x.Key)
42
+ .map((x) => x.replace(this.client.config.keyPrefix, ""))
43
+ .map((x) => (x.startsWith("/") ? x.replace(`/`, "") : x))
44
+ .map((x) => x.replace(`resource=${this.resource.name}/id=`, ""))
45
+
46
+ this.continuationToken = response.NextContinuationToken;
47
+ this.enqueue(keys);
48
+
49
+ if (!response.IsTruncated) this.closeNextIteration = true;
50
+ }
51
+
52
+ enqueue(ids) {
53
+ ids.forEach((key) => {
54
+ this.controller.enqueue(key)
55
+ this.emit("id", key);
56
+ });
57
+ }
58
+
59
+ _cancel(reason) {
60
+ }
61
+ }
62
+
63
+ export default ResourceIdsReader
@@ -0,0 +1,81 @@
1
+ import EventEmitter from "events";
2
+ import { Transform } from "stream";
3
+ import { PromisePool } from "@supercharge/promise-pool";
4
+
5
+ import { ResourceIdsPageReader } from "./resource-ids-page-reader.class.js"
6
+ import tryFn from "../concerns/try-fn.js";
7
+
8
+ export class ResourceReader extends EventEmitter {
9
+ constructor({ resource, batchSize = 10, concurrency = 5 }) {
10
+ super()
11
+
12
+ if (!resource) {
13
+ throw new Error("Resource is required for ResourceReader");
14
+ }
15
+
16
+ this.resource = resource;
17
+ this.client = resource.client;
18
+ this.batchSize = batchSize;
19
+ this.concurrency = concurrency;
20
+
21
+ this.input = new ResourceIdsPageReader({ resource: this.resource });
22
+
23
+ // Create a Node.js Transform stream instead of Web Stream
24
+ this.transform = new Transform({
25
+ objectMode: true,
26
+ transform: this._transform.bind(this)
27
+ });
28
+
29
+ // Set up event forwarding
30
+ this.input.on('data', (chunk) => {
31
+ this.transform.write(chunk);
32
+ });
33
+
34
+ this.input.on('end', () => {
35
+ this.transform.end();
36
+ });
37
+
38
+ this.input.on('error', (error) => {
39
+ this.emit('error', error);
40
+ });
41
+
42
+ // Forward transform events
43
+ this.transform.on('data', (data) => {
44
+ this.emit('data', data);
45
+ });
46
+
47
+ this.transform.on('end', () => {
48
+ this.emit('end');
49
+ });
50
+
51
+ this.transform.on('error', (error) => {
52
+ this.emit('error', error);
53
+ });
54
+ }
55
+
56
+ build() {
57
+ return this;
58
+ }
59
+
60
+ async _transform(chunk, encoding, callback) {
61
+ const [ok, err] = await tryFn(async () => {
62
+ await PromisePool.for(chunk)
63
+ .withConcurrency(this.concurrency)
64
+ .handleError(async (error, content) => {
65
+ this.emit("error", error, content);
66
+ })
67
+ .process(async (id) => {
68
+ const data = await this.resource.get(id);
69
+ this.push(data);
70
+ return data;
71
+ });
72
+ });
73
+ callback(err);
74
+ }
75
+
76
+ resume() {
77
+ this.input.resume();
78
+ }
79
+ }
80
+
81
+ export default ResourceReader;
@@ -0,0 +1,92 @@
1
+ import EventEmitter from "events";
2
+ import { Writable } from 'stream';
3
+ import { PromisePool } from '@supercharge/promise-pool';
4
+ import tryFn from "../concerns/try-fn.js";
5
+
6
+ export class ResourceWriter extends EventEmitter {
7
+ constructor({ resource, batchSize = 10, concurrency = 5 }) {
8
+ super()
9
+
10
+ this.resource = resource;
11
+ this.client = resource.client;
12
+ this.batchSize = batchSize;
13
+ this.concurrency = concurrency;
14
+ this.buffer = [];
15
+ this.writing = false;
16
+
17
+ // Create a Node.js Writable stream instead of Web Stream
18
+ this.writable = new Writable({
19
+ objectMode: true,
20
+ write: this._write.bind(this)
21
+ });
22
+
23
+ // Set up event forwarding
24
+ this.writable.on('finish', () => {
25
+ this.emit('finish');
26
+ });
27
+
28
+ this.writable.on('error', (error) => {
29
+ this.emit('error', error);
30
+ });
31
+ }
32
+
33
+ build() {
34
+ return this;
35
+ }
36
+
37
+ write(chunk) {
38
+ this.buffer.push(chunk);
39
+ this._maybeWrite().catch(error => {
40
+ this.emit('error', error);
41
+ });
42
+ return true;
43
+ }
44
+
45
+ end() {
46
+ this.ended = true;
47
+ this._maybeWrite().catch(error => {
48
+ this.emit('error', error);
49
+ });
50
+ }
51
+
52
+ async _maybeWrite() {
53
+ if (this.writing) return;
54
+ if (this.buffer.length === 0 && !this.ended) return;
55
+ this.writing = true;
56
+ while (this.buffer.length > 0) {
57
+ const batch = this.buffer.splice(0, this.batchSize);
58
+ const [ok, err] = await tryFn(async () => {
59
+ await PromisePool.for(batch)
60
+ .withConcurrency(this.concurrency)
61
+ .handleError(async (error, content) => {
62
+ this.emit("error", error, content);
63
+ })
64
+ .process(async (item) => {
65
+ const [ok, err, result] = await tryFn(async () => {
66
+ const res = await this.resource.insert(item);
67
+ return res;
68
+ });
69
+ if (!ok) {
70
+ this.emit('error', err, item);
71
+ return null;
72
+ }
73
+ return result;
74
+ });
75
+ });
76
+ if (!ok) {
77
+ this.emit('error', err);
78
+ }
79
+ }
80
+ this.writing = false;
81
+ if (this.ended) {
82
+ this.writable.emit('finish');
83
+ }
84
+ }
85
+
86
+ async _write(chunk, encoding, callback) {
87
+ // Not used, as we handle batching in write/end
88
+ callback();
89
+ }
90
+ }
91
+
92
+ export default ResourceWriter;
@@ -0,0 +1,97 @@
1
+ import { merge, isString } from "lodash-es";
2
+ import FastestValidator from "fastest-validator";
3
+
4
+ import { encrypt } from "./concerns/crypto.js";
5
+ import tryFn, { tryFnSync } from "./concerns/try-fn.js";
6
+ import { ValidationError } from "./errors.js";
7
+
8
+ async function secretHandler (actual, errors, schema) {
9
+ if (!this.passphrase) {
10
+ errors.push(new ValidationError("Missing configuration for secrets encryption.", {
11
+ actual,
12
+ type: "encryptionKeyMissing",
13
+ suggestion: "Provide a passphrase for secret encryption."
14
+ }));
15
+ return actual;
16
+ }
17
+
18
+ const [ok, err, res] = await tryFn(() => encrypt(String(actual), this.passphrase));
19
+ if (ok) return res;
20
+ errors.push(new ValidationError("Problem encrypting secret.", {
21
+ actual,
22
+ type: "encryptionProblem",
23
+ error: err,
24
+ suggestion: "Check the passphrase and input value."
25
+ }));
26
+ return actual;
27
+ }
28
+
29
+ async function jsonHandler (actual, errors, schema) {
30
+ if (isString(actual)) return actual;
31
+ const [ok, err, json] = tryFnSync(() => JSON.stringify(actual));
32
+ if (!ok) throw new ValidationError("Failed to stringify JSON", { original: err, input: actual });
33
+ return json;
34
+ }
35
+
36
+ export class Validator extends FastestValidator {
37
+ constructor({ options, passphrase, autoEncrypt = true } = {}) {
38
+ super(merge({}, {
39
+ useNewCustomCheckerFunction: true,
40
+
41
+ messages: {
42
+ encryptionKeyMissing: "Missing configuration for secrets encryption.",
43
+ encryptionProblem: "Problem encrypting secret. Actual: {actual}. Error: {error}",
44
+ },
45
+
46
+ defaults: {
47
+ string: {
48
+ trim: true,
49
+ },
50
+ object: {
51
+ strict: "remove",
52
+ },
53
+ number: {
54
+ convert: true,
55
+ }
56
+ },
57
+ }, options))
58
+
59
+ this.passphrase = passphrase;
60
+ this.autoEncrypt = autoEncrypt;
61
+
62
+ this.alias('secret', {
63
+ type: "string",
64
+ custom: this.autoEncrypt ? secretHandler : undefined,
65
+ messages: {
66
+ string: "The '{field}' field must be a string.",
67
+ stringMin: "This secret '{field}' field length must be at least {expected} long.",
68
+ },
69
+ })
70
+
71
+ this.alias('secretAny', {
72
+ type: "any" ,
73
+ custom: this.autoEncrypt ? secretHandler : undefined,
74
+ })
75
+
76
+ this.alias('secretNumber', {
77
+ type: "number",
78
+ custom: this.autoEncrypt ? secretHandler : undefined,
79
+ })
80
+
81
+ this.alias('json', {
82
+ type: "any",
83
+ custom: this.autoEncrypt ? jsonHandler : undefined,
84
+ })
85
+ }
86
+ }
87
+
88
+ export const ValidatorManager = new Proxy(Validator, {
89
+ instance: null,
90
+
91
+ construct(target, args) {
92
+ if (!this.instance) this.instance = new target(...args);
93
+ return this.instance;
94
+ }
95
+ })
96
+
97
+ export default Validator;