s3db.js 10.0.18 → 11.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/dist/s3db.cjs.js +606 -206
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +198 -2
- package/dist/s3db.es.js +606 -206
- package/dist/s3db.es.js.map +1 -1
- package/package.json +4 -2
- package/src/concerns/plugin-storage.js +443 -0
- package/src/database.class.js +48 -15
- package/src/plugins/audit.plugin.js +1 -1
- package/src/plugins/backup.plugin.js +1 -1
- package/src/plugins/cache.plugin.js +2 -6
- package/src/plugins/eventual-consistency/analytics.js +16 -4
- package/src/plugins/eventual-consistency/consolidation.js +18 -1
- package/src/plugins/eventual-consistency/index.js +4 -4
- package/src/plugins/eventual-consistency/{setup.js → install.js} +7 -6
- package/src/plugins/fulltext.plugin.js +3 -4
- package/src/plugins/metrics.plugin.js +10 -11
- package/src/plugins/plugin.class.js +79 -9
- package/src/plugins/queue-consumer.plugin.js +4 -3
- package/src/plugins/replicator.plugin.js +11 -13
- package/src/plugins/s3-queue.plugin.js +1 -1
- package/src/plugins/scheduler.plugin.js +8 -9
- package/src/plugins/state-machine.plugin.js +3 -4
- package/src/s3db.d.ts +198 -2
- package/src/plugins/eventual-consistency.plugin.js +0 -2559
package/dist/s3db.es.js
CHANGED
|
@@ -656,18 +656,496 @@ var id = /*#__PURE__*/Object.freeze({
|
|
|
656
656
|
passwordGenerator: passwordGenerator
|
|
657
657
|
});
|
|
658
658
|
|
|
659
|
+
function analyzeString(str) {
|
|
660
|
+
if (!str || typeof str !== "string") {
|
|
661
|
+
return { type: "none", safe: true };
|
|
662
|
+
}
|
|
663
|
+
let hasLatin1 = false;
|
|
664
|
+
let hasMultibyte = false;
|
|
665
|
+
let asciiCount = 0;
|
|
666
|
+
let latin1Count = 0;
|
|
667
|
+
let multibyteCount = 0;
|
|
668
|
+
for (let i = 0; i < str.length; i++) {
|
|
669
|
+
const code = str.charCodeAt(i);
|
|
670
|
+
if (code >= 32 && code <= 126) {
|
|
671
|
+
asciiCount++;
|
|
672
|
+
} else if (code < 32 || code === 127) {
|
|
673
|
+
hasMultibyte = true;
|
|
674
|
+
multibyteCount++;
|
|
675
|
+
} else if (code >= 128 && code <= 255) {
|
|
676
|
+
hasLatin1 = true;
|
|
677
|
+
latin1Count++;
|
|
678
|
+
} else {
|
|
679
|
+
hasMultibyte = true;
|
|
680
|
+
multibyteCount++;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (!hasLatin1 && !hasMultibyte) {
|
|
684
|
+
return {
|
|
685
|
+
type: "ascii",
|
|
686
|
+
safe: true,
|
|
687
|
+
stats: { ascii: asciiCount, latin1: 0, multibyte: 0 }
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
if (hasMultibyte) {
|
|
691
|
+
const multibyteRatio = multibyteCount / str.length;
|
|
692
|
+
if (multibyteRatio > 0.3) {
|
|
693
|
+
return {
|
|
694
|
+
type: "base64",
|
|
695
|
+
safe: false,
|
|
696
|
+
reason: "high multibyte content",
|
|
697
|
+
stats: { ascii: asciiCount, latin1: latin1Count, multibyte: multibyteCount }
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
type: "url",
|
|
702
|
+
safe: false,
|
|
703
|
+
reason: "contains multibyte characters",
|
|
704
|
+
stats: { ascii: asciiCount, latin1: latin1Count, multibyte: multibyteCount }
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
const latin1Ratio = latin1Count / str.length;
|
|
708
|
+
if (latin1Ratio > 0.5) {
|
|
709
|
+
return {
|
|
710
|
+
type: "base64",
|
|
711
|
+
safe: false,
|
|
712
|
+
reason: "high Latin-1 content",
|
|
713
|
+
stats: { ascii: asciiCount, latin1: latin1Count, multibyte: 0 }
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
type: "url",
|
|
718
|
+
safe: false,
|
|
719
|
+
reason: "contains Latin-1 extended characters",
|
|
720
|
+
stats: { ascii: asciiCount, latin1: latin1Count, multibyte: 0 }
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function metadataEncode(value) {
|
|
724
|
+
if (value === null) {
|
|
725
|
+
return { encoded: "null", encoding: "special" };
|
|
726
|
+
}
|
|
727
|
+
if (value === void 0) {
|
|
728
|
+
return { encoded: "undefined", encoding: "special" };
|
|
729
|
+
}
|
|
730
|
+
const stringValue = String(value);
|
|
731
|
+
const analysis = analyzeString(stringValue);
|
|
732
|
+
switch (analysis.type) {
|
|
733
|
+
case "none":
|
|
734
|
+
case "ascii":
|
|
735
|
+
return {
|
|
736
|
+
encoded: stringValue,
|
|
737
|
+
encoding: "none",
|
|
738
|
+
analysis
|
|
739
|
+
};
|
|
740
|
+
case "url":
|
|
741
|
+
return {
|
|
742
|
+
encoded: "u:" + encodeURIComponent(stringValue),
|
|
743
|
+
encoding: "url",
|
|
744
|
+
analysis
|
|
745
|
+
};
|
|
746
|
+
case "base64":
|
|
747
|
+
return {
|
|
748
|
+
encoded: "b:" + Buffer.from(stringValue, "utf8").toString("base64"),
|
|
749
|
+
encoding: "base64",
|
|
750
|
+
analysis
|
|
751
|
+
};
|
|
752
|
+
default:
|
|
753
|
+
return {
|
|
754
|
+
encoded: "b:" + Buffer.from(stringValue, "utf8").toString("base64"),
|
|
755
|
+
encoding: "base64",
|
|
756
|
+
analysis
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function metadataDecode(value) {
|
|
761
|
+
if (value === "null") {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
if (value === "undefined") {
|
|
765
|
+
return void 0;
|
|
766
|
+
}
|
|
767
|
+
if (value === null || value === void 0 || typeof value !== "string") {
|
|
768
|
+
return value;
|
|
769
|
+
}
|
|
770
|
+
if (value.startsWith("u:")) {
|
|
771
|
+
if (value.length === 2) return value;
|
|
772
|
+
try {
|
|
773
|
+
return decodeURIComponent(value.substring(2));
|
|
774
|
+
} catch (err) {
|
|
775
|
+
return value;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (value.startsWith("b:")) {
|
|
779
|
+
if (value.length === 2) return value;
|
|
780
|
+
try {
|
|
781
|
+
const decoded = Buffer.from(value.substring(2), "base64").toString("utf8");
|
|
782
|
+
return decoded;
|
|
783
|
+
} catch (err) {
|
|
784
|
+
return value;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (value.length > 0 && /^[A-Za-z0-9+/]+=*$/.test(value)) {
|
|
788
|
+
try {
|
|
789
|
+
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
790
|
+
if (/[^\x00-\x7F]/.test(decoded) && Buffer.from(decoded, "utf8").toString("base64") === value) {
|
|
791
|
+
return decoded;
|
|
792
|
+
}
|
|
793
|
+
} catch {
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const S3_METADATA_LIMIT = 2047;
|
|
800
|
+
class PluginStorage {
|
|
801
|
+
/**
|
|
802
|
+
* @param {Object} client - S3db Client instance
|
|
803
|
+
* @param {string} pluginSlug - Plugin identifier (kebab-case)
|
|
804
|
+
*/
|
|
805
|
+
constructor(client, pluginSlug) {
|
|
806
|
+
if (!client) {
|
|
807
|
+
throw new Error("PluginStorage requires a client instance");
|
|
808
|
+
}
|
|
809
|
+
if (!pluginSlug) {
|
|
810
|
+
throw new Error("PluginStorage requires a pluginSlug");
|
|
811
|
+
}
|
|
812
|
+
this.client = client;
|
|
813
|
+
this.pluginSlug = pluginSlug;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Generate hierarchical plugin-scoped key
|
|
817
|
+
*
|
|
818
|
+
* @param {string} resourceName - Resource name (optional, for resource-scoped data)
|
|
819
|
+
* @param {...string} parts - Additional path parts
|
|
820
|
+
* @returns {string} S3 key
|
|
821
|
+
*
|
|
822
|
+
* @example
|
|
823
|
+
* // Resource-scoped: resource=wallets/plugin=eventual-consistency/balance/transactions/id=txn1
|
|
824
|
+
* getPluginKey('wallets', 'balance', 'transactions', 'id=txn1')
|
|
825
|
+
*
|
|
826
|
+
* // Global plugin data: plugin=eventual-consistency/config
|
|
827
|
+
* getPluginKey(null, 'config')
|
|
828
|
+
*/
|
|
829
|
+
getPluginKey(resourceName, ...parts) {
|
|
830
|
+
if (resourceName) {
|
|
831
|
+
return `resource=${resourceName}/plugin=${this.pluginSlug}/${parts.join("/")}`;
|
|
832
|
+
}
|
|
833
|
+
return `plugin=${this.pluginSlug}/${parts.join("/")}`;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Save data with metadata encoding and behavior support
|
|
837
|
+
*
|
|
838
|
+
* @param {string} key - S3 key
|
|
839
|
+
* @param {Object} data - Data to save
|
|
840
|
+
* @param {Object} options - Options
|
|
841
|
+
* @param {string} options.behavior - 'body-overflow' | 'body-only' | 'enforce-limits'
|
|
842
|
+
* @param {string} options.contentType - Content type (default: application/json)
|
|
843
|
+
* @returns {Promise<void>}
|
|
844
|
+
*/
|
|
845
|
+
async put(key, data, options = {}) {
|
|
846
|
+
const { behavior = "body-overflow", contentType = "application/json" } = options;
|
|
847
|
+
const { metadata, body } = this._applyBehavior(data, behavior);
|
|
848
|
+
const putParams = {
|
|
849
|
+
key,
|
|
850
|
+
metadata,
|
|
851
|
+
contentType
|
|
852
|
+
};
|
|
853
|
+
if (body !== null) {
|
|
854
|
+
putParams.body = JSON.stringify(body);
|
|
855
|
+
}
|
|
856
|
+
const [ok, err] = await tryFn(() => this.client.putObject(putParams));
|
|
857
|
+
if (!ok) {
|
|
858
|
+
throw new Error(`PluginStorage.put failed for key ${key}: ${err.message}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Get data with automatic metadata decoding
|
|
863
|
+
*
|
|
864
|
+
* @param {string} key - S3 key
|
|
865
|
+
* @returns {Promise<Object|null>} Data or null if not found
|
|
866
|
+
*/
|
|
867
|
+
async get(key) {
|
|
868
|
+
const [ok, err, response] = await tryFn(() => this.client.getObject(key));
|
|
869
|
+
if (!ok) {
|
|
870
|
+
if (err.name === "NoSuchKey" || err.Code === "NoSuchKey") {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
throw new Error(`PluginStorage.get failed for key ${key}: ${err.message}`);
|
|
874
|
+
}
|
|
875
|
+
const metadata = response.Metadata || {};
|
|
876
|
+
const parsedMetadata = this._parseMetadataValues(metadata);
|
|
877
|
+
if (response.Body) {
|
|
878
|
+
try {
|
|
879
|
+
const bodyContent = await response.Body.transformToString();
|
|
880
|
+
if (bodyContent && bodyContent.trim()) {
|
|
881
|
+
const body = JSON.parse(bodyContent);
|
|
882
|
+
return { ...parsedMetadata, ...body };
|
|
883
|
+
}
|
|
884
|
+
} catch (parseErr) {
|
|
885
|
+
throw new Error(`PluginStorage.get failed to parse body for key ${key}: ${parseErr.message}`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return parsedMetadata;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Parse metadata values back to their original types
|
|
892
|
+
* @private
|
|
893
|
+
*/
|
|
894
|
+
_parseMetadataValues(metadata) {
|
|
895
|
+
const parsed = {};
|
|
896
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
897
|
+
if (typeof value === "string") {
|
|
898
|
+
if (value.startsWith("{") && value.endsWith("}") || value.startsWith("[") && value.endsWith("]")) {
|
|
899
|
+
try {
|
|
900
|
+
parsed[key] = JSON.parse(value);
|
|
901
|
+
continue;
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (!isNaN(value) && value.trim() !== "") {
|
|
906
|
+
parsed[key] = Number(value);
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (value === "true") {
|
|
910
|
+
parsed[key] = true;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (value === "false") {
|
|
914
|
+
parsed[key] = false;
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
parsed[key] = value;
|
|
919
|
+
}
|
|
920
|
+
return parsed;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* List all keys with plugin prefix
|
|
924
|
+
*
|
|
925
|
+
* @param {string} prefix - Additional prefix (optional)
|
|
926
|
+
* @param {Object} options - List options
|
|
927
|
+
* @param {number} options.limit - Max number of results
|
|
928
|
+
* @returns {Promise<Array<string>>} List of keys
|
|
929
|
+
*/
|
|
930
|
+
async list(prefix = "", options = {}) {
|
|
931
|
+
const { limit } = options;
|
|
932
|
+
const fullPrefix = prefix ? `plugin=${this.pluginSlug}/${prefix}` : `plugin=${this.pluginSlug}/`;
|
|
933
|
+
const [ok, err, result] = await tryFn(
|
|
934
|
+
() => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
|
|
935
|
+
);
|
|
936
|
+
if (!ok) {
|
|
937
|
+
throw new Error(`PluginStorage.list failed: ${err.message}`);
|
|
938
|
+
}
|
|
939
|
+
const keys = result.Contents?.map((item) => item.Key) || [];
|
|
940
|
+
return this._removeKeyPrefix(keys);
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* List keys for a specific resource
|
|
944
|
+
*
|
|
945
|
+
* @param {string} resourceName - Resource name
|
|
946
|
+
* @param {string} subPrefix - Additional prefix within resource (optional)
|
|
947
|
+
* @param {Object} options - List options
|
|
948
|
+
* @returns {Promise<Array<string>>} List of keys
|
|
949
|
+
*/
|
|
950
|
+
async listForResource(resourceName, subPrefix = "", options = {}) {
|
|
951
|
+
const { limit } = options;
|
|
952
|
+
const fullPrefix = subPrefix ? `resource=${resourceName}/plugin=${this.pluginSlug}/${subPrefix}` : `resource=${resourceName}/plugin=${this.pluginSlug}/`;
|
|
953
|
+
const [ok, err, result] = await tryFn(
|
|
954
|
+
() => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
|
|
955
|
+
);
|
|
956
|
+
if (!ok) {
|
|
957
|
+
throw new Error(`PluginStorage.listForResource failed: ${err.message}`);
|
|
958
|
+
}
|
|
959
|
+
const keys = result.Contents?.map((item) => item.Key) || [];
|
|
960
|
+
return this._removeKeyPrefix(keys);
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Remove client keyPrefix from keys
|
|
964
|
+
* @private
|
|
965
|
+
*/
|
|
966
|
+
_removeKeyPrefix(keys) {
|
|
967
|
+
const keyPrefix = this.client.config.keyPrefix;
|
|
968
|
+
if (!keyPrefix) return keys;
|
|
969
|
+
return keys.map((key) => key.replace(keyPrefix, "")).map((key) => key.startsWith("/") ? key.replace("/", "") : key);
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Delete a single object
|
|
973
|
+
*
|
|
974
|
+
* @param {string} key - S3 key
|
|
975
|
+
* @returns {Promise<void>}
|
|
976
|
+
*/
|
|
977
|
+
async delete(key) {
|
|
978
|
+
const [ok, err] = await tryFn(() => this.client.deleteObject(key));
|
|
979
|
+
if (!ok) {
|
|
980
|
+
throw new Error(`PluginStorage.delete failed for key ${key}: ${err.message}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Delete all plugin data (for uninstall)
|
|
985
|
+
*
|
|
986
|
+
* @param {string} resourceName - Resource name (optional, if null deletes all plugin data)
|
|
987
|
+
* @returns {Promise<number>} Number of objects deleted
|
|
988
|
+
*/
|
|
989
|
+
async deleteAll(resourceName = null) {
|
|
990
|
+
let deleted = 0;
|
|
991
|
+
if (resourceName) {
|
|
992
|
+
const keys = await this.listForResource(resourceName);
|
|
993
|
+
for (const key of keys) {
|
|
994
|
+
await this.delete(key);
|
|
995
|
+
deleted++;
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
const allKeys = await this.client.getAllKeys({});
|
|
999
|
+
const pluginKeys = allKeys.filter(
|
|
1000
|
+
(key) => key.includes(`plugin=${this.pluginSlug}/`)
|
|
1001
|
+
);
|
|
1002
|
+
for (const key of pluginKeys) {
|
|
1003
|
+
await this.delete(key);
|
|
1004
|
+
deleted++;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return deleted;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Batch put operations
|
|
1011
|
+
*
|
|
1012
|
+
* @param {Array<{key: string, data: Object, options?: Object}>} items - Items to save
|
|
1013
|
+
* @returns {Promise<Array<{key: string, ok: boolean, error?: Error}>>} Results
|
|
1014
|
+
*/
|
|
1015
|
+
async batchPut(items) {
|
|
1016
|
+
const results = [];
|
|
1017
|
+
for (const item of items) {
|
|
1018
|
+
const [ok, err] = await tryFn(
|
|
1019
|
+
() => this.put(item.key, item.data, item.options)
|
|
1020
|
+
);
|
|
1021
|
+
results.push({
|
|
1022
|
+
key: item.key,
|
|
1023
|
+
ok,
|
|
1024
|
+
error: err
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
return results;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Batch get operations
|
|
1031
|
+
*
|
|
1032
|
+
* @param {Array<string>} keys - Keys to fetch
|
|
1033
|
+
* @returns {Promise<Array<{key: string, ok: boolean, data?: Object, error?: Error}>>} Results
|
|
1034
|
+
*/
|
|
1035
|
+
async batchGet(keys) {
|
|
1036
|
+
const results = [];
|
|
1037
|
+
for (const key of keys) {
|
|
1038
|
+
const [ok, err, data] = await tryFn(() => this.get(key));
|
|
1039
|
+
results.push({
|
|
1040
|
+
key,
|
|
1041
|
+
ok,
|
|
1042
|
+
data,
|
|
1043
|
+
error: err
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
return results;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Apply behavior to split data between metadata and body
|
|
1050
|
+
*
|
|
1051
|
+
* @private
|
|
1052
|
+
* @param {Object} data - Data to split
|
|
1053
|
+
* @param {string} behavior - Behavior strategy
|
|
1054
|
+
* @returns {{metadata: Object, body: Object|null}}
|
|
1055
|
+
*/
|
|
1056
|
+
_applyBehavior(data, behavior) {
|
|
1057
|
+
const effectiveLimit = calculateEffectiveLimit({ s3Limit: S3_METADATA_LIMIT });
|
|
1058
|
+
let metadata = {};
|
|
1059
|
+
let body = null;
|
|
1060
|
+
switch (behavior) {
|
|
1061
|
+
case "body-overflow": {
|
|
1062
|
+
const entries = Object.entries(data);
|
|
1063
|
+
const sorted = entries.map(([key, value]) => {
|
|
1064
|
+
const jsonValue = typeof value === "object" ? JSON.stringify(value) : value;
|
|
1065
|
+
const { encoded } = metadataEncode(jsonValue);
|
|
1066
|
+
const keySize = calculateUTF8Bytes(key);
|
|
1067
|
+
const valueSize = calculateUTF8Bytes(encoded);
|
|
1068
|
+
return { key, value, jsonValue, encoded, size: keySize + valueSize };
|
|
1069
|
+
}).sort((a, b) => a.size - b.size);
|
|
1070
|
+
let currentSize = 0;
|
|
1071
|
+
for (const item of sorted) {
|
|
1072
|
+
if (currentSize + item.size <= effectiveLimit) {
|
|
1073
|
+
metadata[item.key] = item.jsonValue;
|
|
1074
|
+
currentSize += item.size;
|
|
1075
|
+
} else {
|
|
1076
|
+
if (body === null) body = {};
|
|
1077
|
+
body[item.key] = item.value;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
case "body-only": {
|
|
1083
|
+
body = data;
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
case "enforce-limits": {
|
|
1087
|
+
let currentSize = 0;
|
|
1088
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1089
|
+
const jsonValue = typeof value === "object" ? JSON.stringify(value) : value;
|
|
1090
|
+
const { encoded } = metadataEncode(jsonValue);
|
|
1091
|
+
const keySize = calculateUTF8Bytes(key);
|
|
1092
|
+
const valueSize = calculateUTF8Bytes(encoded);
|
|
1093
|
+
currentSize += keySize + valueSize;
|
|
1094
|
+
if (currentSize > effectiveLimit) {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`Data exceeds metadata limit (${currentSize} > ${effectiveLimit} bytes). Use 'body-overflow' or 'body-only' behavior.`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
metadata[key] = jsonValue;
|
|
1100
|
+
}
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
default:
|
|
1104
|
+
throw new Error(`Unknown behavior: ${behavior}. Use 'body-overflow', 'body-only', or 'enforce-limits'.`);
|
|
1105
|
+
}
|
|
1106
|
+
return { metadata, body };
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
659
1110
|
class Plugin extends EventEmitter {
|
|
660
1111
|
constructor(options = {}) {
|
|
661
1112
|
super();
|
|
662
1113
|
this.name = this.constructor.name;
|
|
663
1114
|
this.options = options;
|
|
664
1115
|
this.hooks = /* @__PURE__ */ new Map();
|
|
1116
|
+
this.slug = options.slug || this._generateSlug();
|
|
1117
|
+
this._storage = null;
|
|
665
1118
|
}
|
|
666
|
-
|
|
1119
|
+
/**
|
|
1120
|
+
* Generate kebab-case slug from class name
|
|
1121
|
+
* @private
|
|
1122
|
+
* @returns {string}
|
|
1123
|
+
*/
|
|
1124
|
+
_generateSlug() {
|
|
1125
|
+
return this.name.replace(/Plugin$/, "").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Get PluginStorage instance (lazy-loaded)
|
|
1129
|
+
* @returns {PluginStorage}
|
|
1130
|
+
*/
|
|
1131
|
+
getStorage() {
|
|
1132
|
+
if (!this._storage) {
|
|
1133
|
+
if (!this.database || !this.database.client) {
|
|
1134
|
+
throw new Error("Plugin must be installed before accessing storage");
|
|
1135
|
+
}
|
|
1136
|
+
this._storage = new PluginStorage(this.database.client, this.slug);
|
|
1137
|
+
}
|
|
1138
|
+
return this._storage;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Install plugin
|
|
1142
|
+
* @param {Database} database - Database instance
|
|
1143
|
+
*/
|
|
1144
|
+
async install(database) {
|
|
667
1145
|
this.database = database;
|
|
668
|
-
this.
|
|
669
|
-
await this.
|
|
670
|
-
this.
|
|
1146
|
+
this.beforeInstall();
|
|
1147
|
+
await this.onInstall();
|
|
1148
|
+
this.afterInstall();
|
|
671
1149
|
}
|
|
672
1150
|
async start() {
|
|
673
1151
|
this.beforeStart();
|
|
@@ -679,13 +1157,30 @@ class Plugin extends EventEmitter {
|
|
|
679
1157
|
await this.onStop();
|
|
680
1158
|
this.afterStop();
|
|
681
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Uninstall plugin and cleanup all data
|
|
1162
|
+
* @param {Object} options - Uninstall options
|
|
1163
|
+
* @param {boolean} options.purgeData - Delete all plugin data from S3 (default: false)
|
|
1164
|
+
*/
|
|
1165
|
+
async uninstall(options = {}) {
|
|
1166
|
+
const { purgeData = false } = options;
|
|
1167
|
+
this.beforeUninstall();
|
|
1168
|
+
await this.onUninstall(options);
|
|
1169
|
+
if (purgeData && this._storage) {
|
|
1170
|
+
const deleted = await this._storage.deleteAll();
|
|
1171
|
+
this.emit("plugin.dataPurged", { deleted });
|
|
1172
|
+
}
|
|
1173
|
+
this.afterUninstall();
|
|
1174
|
+
}
|
|
682
1175
|
// Override these methods in subclasses
|
|
683
|
-
async
|
|
1176
|
+
async onInstall() {
|
|
684
1177
|
}
|
|
685
1178
|
async onStart() {
|
|
686
1179
|
}
|
|
687
1180
|
async onStop() {
|
|
688
1181
|
}
|
|
1182
|
+
async onUninstall(options) {
|
|
1183
|
+
}
|
|
689
1184
|
// Hook management methods
|
|
690
1185
|
addHook(resource, event, handler) {
|
|
691
1186
|
if (!this.hooks.has(resource)) {
|
|
@@ -797,11 +1292,11 @@ class Plugin extends EventEmitter {
|
|
|
797
1292
|
return value ?? null;
|
|
798
1293
|
}
|
|
799
1294
|
// Event emission methods
|
|
800
|
-
|
|
801
|
-
this.emit("plugin.
|
|
1295
|
+
beforeInstall() {
|
|
1296
|
+
this.emit("plugin.beforeInstall", /* @__PURE__ */ new Date());
|
|
802
1297
|
}
|
|
803
|
-
|
|
804
|
-
this.emit("plugin.
|
|
1298
|
+
afterInstall() {
|
|
1299
|
+
this.emit("plugin.afterInstall", /* @__PURE__ */ new Date());
|
|
805
1300
|
}
|
|
806
1301
|
beforeStart() {
|
|
807
1302
|
this.emit("plugin.beforeStart", /* @__PURE__ */ new Date());
|
|
@@ -815,6 +1310,12 @@ class Plugin extends EventEmitter {
|
|
|
815
1310
|
afterStop() {
|
|
816
1311
|
this.emit("plugin.afterStop", /* @__PURE__ */ new Date());
|
|
817
1312
|
}
|
|
1313
|
+
beforeUninstall() {
|
|
1314
|
+
this.emit("plugin.beforeUninstall", /* @__PURE__ */ new Date());
|
|
1315
|
+
}
|
|
1316
|
+
afterUninstall() {
|
|
1317
|
+
this.emit("plugin.afterUninstall", /* @__PURE__ */ new Date());
|
|
1318
|
+
}
|
|
818
1319
|
}
|
|
819
1320
|
|
|
820
1321
|
const PluginObject = {
|
|
@@ -837,7 +1338,7 @@ class AuditPlugin extends Plugin {
|
|
|
837
1338
|
...options
|
|
838
1339
|
};
|
|
839
1340
|
}
|
|
840
|
-
async
|
|
1341
|
+
async onInstall() {
|
|
841
1342
|
const [ok, err, auditResource] = await tryFn(() => this.database.createResource({
|
|
842
1343
|
name: "plg_audits",
|
|
843
1344
|
attributes: {
|
|
@@ -1928,7 +2429,7 @@ class BackupPlugin extends Plugin {
|
|
|
1928
2429
|
throw new Error("BackupPlugin: Invalid compression type. Use: none, gzip, brotli, deflate");
|
|
1929
2430
|
}
|
|
1930
2431
|
}
|
|
1931
|
-
async
|
|
2432
|
+
async onInstall() {
|
|
1932
2433
|
this.driver = createBackupDriver(this.config.driver, this.config.driverConfig);
|
|
1933
2434
|
await this.driver.setup(this.database);
|
|
1934
2435
|
await mkdir(this.config.tempDir, { recursive: true });
|
|
@@ -3813,10 +4314,7 @@ class CachePlugin extends Plugin {
|
|
|
3813
4314
|
verbose: options.verbose || false
|
|
3814
4315
|
};
|
|
3815
4316
|
}
|
|
3816
|
-
async
|
|
3817
|
-
await super.setup(database);
|
|
3818
|
-
}
|
|
3819
|
-
async onSetup() {
|
|
4317
|
+
async onInstall() {
|
|
3820
4318
|
if (this.config.driver && typeof this.config.driver === "object") {
|
|
3821
4319
|
this.driver = this.config.driver;
|
|
3822
4320
|
} else if (this.config.driver === "memory") {
|
|
@@ -4936,7 +5434,20 @@ async function consolidateRecord(originalId, transactionResource, targetResource
|
|
|
4936
5434
|
console.warn(`[EventualConsistency] ${errors.length} transactions failed to mark as applied`);
|
|
4937
5435
|
}
|
|
4938
5436
|
if (config.enableAnalytics && transactionsToUpdate.length > 0 && updateAnalyticsFn) {
|
|
4939
|
-
await
|
|
5437
|
+
const [analyticsOk, analyticsErr] = await tryFn(
|
|
5438
|
+
() => updateAnalyticsFn(transactionsToUpdate)
|
|
5439
|
+
);
|
|
5440
|
+
if (!analyticsOk) {
|
|
5441
|
+
console.error(
|
|
5442
|
+
`[EventualConsistency] ${config.resource}.${config.field} - CRITICAL: Analytics update failed for ${originalId}, but consolidation succeeded:`,
|
|
5443
|
+
{
|
|
5444
|
+
error: analyticsErr?.message || analyticsErr,
|
|
5445
|
+
stack: analyticsErr?.stack,
|
|
5446
|
+
originalId,
|
|
5447
|
+
transactionCount: transactionsToUpdate.length
|
|
5448
|
+
}
|
|
5449
|
+
);
|
|
5450
|
+
}
|
|
4940
5451
|
}
|
|
4941
5452
|
if (targetResource && targetResource.cache && typeof targetResource.cache.delete === "function") {
|
|
4942
5453
|
try {
|
|
@@ -5232,9 +5743,18 @@ async function updateAnalytics(transactions, analyticsResource, config) {
|
|
|
5232
5743
|
);
|
|
5233
5744
|
}
|
|
5234
5745
|
} catch (error) {
|
|
5235
|
-
console.
|
|
5236
|
-
`[EventualConsistency] ${config.resource}.${config.field} - Analytics update
|
|
5237
|
-
|
|
5746
|
+
console.error(
|
|
5747
|
+
`[EventualConsistency] CRITICAL: ${config.resource}.${config.field} - Analytics update failed:`,
|
|
5748
|
+
{
|
|
5749
|
+
error: error.message,
|
|
5750
|
+
stack: error.stack,
|
|
5751
|
+
field: config.field,
|
|
5752
|
+
resource: config.resource,
|
|
5753
|
+
transactionCount: transactions.length
|
|
5754
|
+
}
|
|
5755
|
+
);
|
|
5756
|
+
throw new Error(
|
|
5757
|
+
`Analytics update failed for ${config.resource}.${config.field}: ${error.message}`
|
|
5238
5758
|
);
|
|
5239
5759
|
}
|
|
5240
5760
|
}
|
|
@@ -5286,6 +5806,7 @@ async function upsertAnalytics(period, cohort, transactions, analyticsResource,
|
|
|
5286
5806
|
await tryFn(
|
|
5287
5807
|
() => analyticsResource.insert({
|
|
5288
5808
|
id,
|
|
5809
|
+
field: config.field,
|
|
5289
5810
|
period,
|
|
5290
5811
|
cohort,
|
|
5291
5812
|
transactionCount,
|
|
@@ -5317,8 +5838,8 @@ function calculateOperationBreakdown(transactions) {
|
|
|
5317
5838
|
async function rollupAnalytics(cohortHour, analyticsResource, config) {
|
|
5318
5839
|
const cohortDate = cohortHour.substring(0, 10);
|
|
5319
5840
|
const cohortMonth = cohortHour.substring(0, 7);
|
|
5320
|
-
await rollupPeriod("day", cohortDate, cohortDate, analyticsResource);
|
|
5321
|
-
await rollupPeriod("month", cohortMonth, cohortMonth, analyticsResource);
|
|
5841
|
+
await rollupPeriod("day", cohortDate, cohortDate, analyticsResource, config);
|
|
5842
|
+
await rollupPeriod("month", cohortMonth, cohortMonth, analyticsResource, config);
|
|
5322
5843
|
}
|
|
5323
5844
|
async function rollupPeriod(period, cohort, sourcePrefix, analyticsResource, config) {
|
|
5324
5845
|
const sourcePeriod = period === "day" ? "hour" : "day";
|
|
@@ -5368,6 +5889,7 @@ async function rollupPeriod(period, cohort, sourcePrefix, analyticsResource, con
|
|
|
5368
5889
|
await tryFn(
|
|
5369
5890
|
() => analyticsResource.insert({
|
|
5370
5891
|
id,
|
|
5892
|
+
field: config.field,
|
|
5371
5893
|
period,
|
|
5372
5894
|
cohort,
|
|
5373
5895
|
transactionCount,
|
|
@@ -5727,7 +6249,7 @@ function addHelperMethods(resource, plugin, config) {
|
|
|
5727
6249
|
};
|
|
5728
6250
|
}
|
|
5729
6251
|
|
|
5730
|
-
async function
|
|
6252
|
+
async function onInstall(database, fieldHandlers, completeFieldSetupFn, watchForResourceFn) {
|
|
5731
6253
|
for (const [resourceName, resourceHandlers] of fieldHandlers) {
|
|
5732
6254
|
const targetResource = database.resources[resourceName];
|
|
5733
6255
|
if (!targetResource) {
|
|
@@ -5827,6 +6349,7 @@ async function createAnalyticsResource(handler, database, resourceName, fieldNam
|
|
|
5827
6349
|
name: analyticsResourceName,
|
|
5828
6350
|
attributes: {
|
|
5829
6351
|
id: "string|required",
|
|
6352
|
+
field: "string|required",
|
|
5830
6353
|
period: "string|required",
|
|
5831
6354
|
cohort: "string|required",
|
|
5832
6355
|
transactionCount: "number|required",
|
|
@@ -5924,10 +6447,10 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
5924
6447
|
logInitialization(this.config, this.fieldHandlers, timezoneAutoDetected);
|
|
5925
6448
|
}
|
|
5926
6449
|
/**
|
|
5927
|
-
*
|
|
6450
|
+
* Install hook - create resources and register helpers
|
|
5928
6451
|
*/
|
|
5929
|
-
async
|
|
5930
|
-
await
|
|
6452
|
+
async onInstall() {
|
|
6453
|
+
await onInstall(
|
|
5931
6454
|
this.database,
|
|
5932
6455
|
this.fieldHandlers,
|
|
5933
6456
|
(handler) => completeFieldSetup(handler, this.database, this.config, this),
|
|
@@ -6292,9 +6815,8 @@ class FullTextPlugin extends Plugin {
|
|
|
6292
6815
|
};
|
|
6293
6816
|
this.indexes = /* @__PURE__ */ new Map();
|
|
6294
6817
|
}
|
|
6295
|
-
async
|
|
6296
|
-
|
|
6297
|
-
const [ok, err, indexResource] = await tryFn(() => database.createResource({
|
|
6818
|
+
async onInstall() {
|
|
6819
|
+
const [ok, err, indexResource] = await tryFn(() => this.database.createResource({
|
|
6298
6820
|
name: "plg_fulltext_indexes",
|
|
6299
6821
|
attributes: {
|
|
6300
6822
|
id: "string|required",
|
|
@@ -6307,7 +6829,7 @@ class FullTextPlugin extends Plugin {
|
|
|
6307
6829
|
lastUpdated: "string|required"
|
|
6308
6830
|
}
|
|
6309
6831
|
}));
|
|
6310
|
-
this.indexResource = ok ? indexResource : database.resources.fulltext_indexes;
|
|
6832
|
+
this.indexResource = ok ? indexResource : this.database.resources.fulltext_indexes;
|
|
6311
6833
|
await this.loadIndexes();
|
|
6312
6834
|
this.installDatabaseHooks();
|
|
6313
6835
|
this.installIndexingHooks();
|
|
@@ -6673,11 +7195,10 @@ class MetricsPlugin extends Plugin {
|
|
|
6673
7195
|
};
|
|
6674
7196
|
this.flushTimer = null;
|
|
6675
7197
|
}
|
|
6676
|
-
async
|
|
6677
|
-
this.database = database;
|
|
7198
|
+
async onInstall() {
|
|
6678
7199
|
if (typeof process !== "undefined" && process.env.NODE_ENV === "test") return;
|
|
6679
7200
|
const [ok, err] = await tryFn(async () => {
|
|
6680
|
-
const [ok1, err1, metricsResource] = await tryFn(() => database.createResource({
|
|
7201
|
+
const [ok1, err1, metricsResource] = await tryFn(() => this.database.createResource({
|
|
6681
7202
|
name: "plg_metrics",
|
|
6682
7203
|
attributes: {
|
|
6683
7204
|
id: "string|required",
|
|
@@ -6693,8 +7214,8 @@ class MetricsPlugin extends Plugin {
|
|
|
6693
7214
|
metadata: "json"
|
|
6694
7215
|
}
|
|
6695
7216
|
}));
|
|
6696
|
-
this.metricsResource = ok1 ? metricsResource : database.resources.plg_metrics;
|
|
6697
|
-
const [ok2, err2, errorsResource] = await tryFn(() => database.createResource({
|
|
7217
|
+
this.metricsResource = ok1 ? metricsResource : this.database.resources.plg_metrics;
|
|
7218
|
+
const [ok2, err2, errorsResource] = await tryFn(() => this.database.createResource({
|
|
6698
7219
|
name: "plg_error_logs",
|
|
6699
7220
|
attributes: {
|
|
6700
7221
|
id: "string|required",
|
|
@@ -6705,8 +7226,8 @@ class MetricsPlugin extends Plugin {
|
|
|
6705
7226
|
metadata: "json"
|
|
6706
7227
|
}
|
|
6707
7228
|
}));
|
|
6708
|
-
this.errorsResource = ok2 ? errorsResource : database.resources.plg_error_logs;
|
|
6709
|
-
const [ok3, err3, performanceResource] = await tryFn(() => database.createResource({
|
|
7229
|
+
this.errorsResource = ok2 ? errorsResource : this.database.resources.plg_error_logs;
|
|
7230
|
+
const [ok3, err3, performanceResource] = await tryFn(() => this.database.createResource({
|
|
6710
7231
|
name: "plg_performance_logs",
|
|
6711
7232
|
attributes: {
|
|
6712
7233
|
id: "string|required",
|
|
@@ -6717,12 +7238,12 @@ class MetricsPlugin extends Plugin {
|
|
|
6717
7238
|
metadata: "json"
|
|
6718
7239
|
}
|
|
6719
7240
|
}));
|
|
6720
|
-
this.performanceResource = ok3 ? performanceResource : database.resources.plg_performance_logs;
|
|
7241
|
+
this.performanceResource = ok3 ? performanceResource : this.database.resources.plg_performance_logs;
|
|
6721
7242
|
});
|
|
6722
7243
|
if (!ok) {
|
|
6723
|
-
this.metricsResource = database.resources.plg_metrics;
|
|
6724
|
-
this.errorsResource = database.resources.plg_error_logs;
|
|
6725
|
-
this.performanceResource = database.resources.plg_performance_logs;
|
|
7244
|
+
this.metricsResource = this.database.resources.plg_metrics;
|
|
7245
|
+
this.errorsResource = this.database.resources.plg_error_logs;
|
|
7246
|
+
this.performanceResource = this.database.resources.plg_performance_logs;
|
|
6726
7247
|
}
|
|
6727
7248
|
this.installDatabaseHooks();
|
|
6728
7249
|
this.installMetricsHooks();
|
|
@@ -7300,14 +7821,14 @@ function createConsumer(driver, config) {
|
|
|
7300
7821
|
return new ConsumerClass(config);
|
|
7301
7822
|
}
|
|
7302
7823
|
|
|
7303
|
-
class QueueConsumerPlugin {
|
|
7824
|
+
class QueueConsumerPlugin extends Plugin {
|
|
7304
7825
|
constructor(options = {}) {
|
|
7826
|
+
super(options);
|
|
7305
7827
|
this.options = options;
|
|
7306
7828
|
this.driversConfig = Array.isArray(options.consumers) ? options.consumers : [];
|
|
7307
7829
|
this.consumers = [];
|
|
7308
7830
|
}
|
|
7309
|
-
async
|
|
7310
|
-
this.database = database;
|
|
7831
|
+
async onInstall() {
|
|
7311
7832
|
for (const driverDef of this.driversConfig) {
|
|
7312
7833
|
const { driver, config: driverConfig = {}, consumers: consumerDefs = [] } = driverDef;
|
|
7313
7834
|
if (consumerDefs.length === 0 && driverDef.resources) {
|
|
@@ -8092,146 +8613,6 @@ class PostgresReplicator extends BaseReplicator {
|
|
|
8092
8613
|
}
|
|
8093
8614
|
}
|
|
8094
8615
|
|
|
8095
|
-
function analyzeString(str) {
|
|
8096
|
-
if (!str || typeof str !== "string") {
|
|
8097
|
-
return { type: "none", safe: true };
|
|
8098
|
-
}
|
|
8099
|
-
let hasLatin1 = false;
|
|
8100
|
-
let hasMultibyte = false;
|
|
8101
|
-
let asciiCount = 0;
|
|
8102
|
-
let latin1Count = 0;
|
|
8103
|
-
let multibyteCount = 0;
|
|
8104
|
-
for (let i = 0; i < str.length; i++) {
|
|
8105
|
-
const code = str.charCodeAt(i);
|
|
8106
|
-
if (code >= 32 && code <= 126) {
|
|
8107
|
-
asciiCount++;
|
|
8108
|
-
} else if (code < 32 || code === 127) {
|
|
8109
|
-
hasMultibyte = true;
|
|
8110
|
-
multibyteCount++;
|
|
8111
|
-
} else if (code >= 128 && code <= 255) {
|
|
8112
|
-
hasLatin1 = true;
|
|
8113
|
-
latin1Count++;
|
|
8114
|
-
} else {
|
|
8115
|
-
hasMultibyte = true;
|
|
8116
|
-
multibyteCount++;
|
|
8117
|
-
}
|
|
8118
|
-
}
|
|
8119
|
-
if (!hasLatin1 && !hasMultibyte) {
|
|
8120
|
-
return {
|
|
8121
|
-
type: "ascii",
|
|
8122
|
-
safe: true,
|
|
8123
|
-
stats: { ascii: asciiCount, latin1: 0, multibyte: 0 }
|
|
8124
|
-
};
|
|
8125
|
-
}
|
|
8126
|
-
if (hasMultibyte) {
|
|
8127
|
-
const multibyteRatio = multibyteCount / str.length;
|
|
8128
|
-
if (multibyteRatio > 0.3) {
|
|
8129
|
-
return {
|
|
8130
|
-
type: "base64",
|
|
8131
|
-
safe: false,
|
|
8132
|
-
reason: "high multibyte content",
|
|
8133
|
-
stats: { ascii: asciiCount, latin1: latin1Count, multibyte: multibyteCount }
|
|
8134
|
-
};
|
|
8135
|
-
}
|
|
8136
|
-
return {
|
|
8137
|
-
type: "url",
|
|
8138
|
-
safe: false,
|
|
8139
|
-
reason: "contains multibyte characters",
|
|
8140
|
-
stats: { ascii: asciiCount, latin1: latin1Count, multibyte: multibyteCount }
|
|
8141
|
-
};
|
|
8142
|
-
}
|
|
8143
|
-
const latin1Ratio = latin1Count / str.length;
|
|
8144
|
-
if (latin1Ratio > 0.5) {
|
|
8145
|
-
return {
|
|
8146
|
-
type: "base64",
|
|
8147
|
-
safe: false,
|
|
8148
|
-
reason: "high Latin-1 content",
|
|
8149
|
-
stats: { ascii: asciiCount, latin1: latin1Count, multibyte: 0 }
|
|
8150
|
-
};
|
|
8151
|
-
}
|
|
8152
|
-
return {
|
|
8153
|
-
type: "url",
|
|
8154
|
-
safe: false,
|
|
8155
|
-
reason: "contains Latin-1 extended characters",
|
|
8156
|
-
stats: { ascii: asciiCount, latin1: latin1Count, multibyte: 0 }
|
|
8157
|
-
};
|
|
8158
|
-
}
|
|
8159
|
-
function metadataEncode(value) {
|
|
8160
|
-
if (value === null) {
|
|
8161
|
-
return { encoded: "null", encoding: "special" };
|
|
8162
|
-
}
|
|
8163
|
-
if (value === void 0) {
|
|
8164
|
-
return { encoded: "undefined", encoding: "special" };
|
|
8165
|
-
}
|
|
8166
|
-
const stringValue = String(value);
|
|
8167
|
-
const analysis = analyzeString(stringValue);
|
|
8168
|
-
switch (analysis.type) {
|
|
8169
|
-
case "none":
|
|
8170
|
-
case "ascii":
|
|
8171
|
-
return {
|
|
8172
|
-
encoded: stringValue,
|
|
8173
|
-
encoding: "none",
|
|
8174
|
-
analysis
|
|
8175
|
-
};
|
|
8176
|
-
case "url":
|
|
8177
|
-
return {
|
|
8178
|
-
encoded: "u:" + encodeURIComponent(stringValue),
|
|
8179
|
-
encoding: "url",
|
|
8180
|
-
analysis
|
|
8181
|
-
};
|
|
8182
|
-
case "base64":
|
|
8183
|
-
return {
|
|
8184
|
-
encoded: "b:" + Buffer.from(stringValue, "utf8").toString("base64"),
|
|
8185
|
-
encoding: "base64",
|
|
8186
|
-
analysis
|
|
8187
|
-
};
|
|
8188
|
-
default:
|
|
8189
|
-
return {
|
|
8190
|
-
encoded: "b:" + Buffer.from(stringValue, "utf8").toString("base64"),
|
|
8191
|
-
encoding: "base64",
|
|
8192
|
-
analysis
|
|
8193
|
-
};
|
|
8194
|
-
}
|
|
8195
|
-
}
|
|
8196
|
-
function metadataDecode(value) {
|
|
8197
|
-
if (value === "null") {
|
|
8198
|
-
return null;
|
|
8199
|
-
}
|
|
8200
|
-
if (value === "undefined") {
|
|
8201
|
-
return void 0;
|
|
8202
|
-
}
|
|
8203
|
-
if (value === null || value === void 0 || typeof value !== "string") {
|
|
8204
|
-
return value;
|
|
8205
|
-
}
|
|
8206
|
-
if (value.startsWith("u:")) {
|
|
8207
|
-
if (value.length === 2) return value;
|
|
8208
|
-
try {
|
|
8209
|
-
return decodeURIComponent(value.substring(2));
|
|
8210
|
-
} catch (err) {
|
|
8211
|
-
return value;
|
|
8212
|
-
}
|
|
8213
|
-
}
|
|
8214
|
-
if (value.startsWith("b:")) {
|
|
8215
|
-
if (value.length === 2) return value;
|
|
8216
|
-
try {
|
|
8217
|
-
const decoded = Buffer.from(value.substring(2), "base64").toString("utf8");
|
|
8218
|
-
return decoded;
|
|
8219
|
-
} catch (err) {
|
|
8220
|
-
return value;
|
|
8221
|
-
}
|
|
8222
|
-
}
|
|
8223
|
-
if (value.length > 0 && /^[A-Za-z0-9+/]+=*$/.test(value)) {
|
|
8224
|
-
try {
|
|
8225
|
-
const decoded = Buffer.from(value, "base64").toString("utf8");
|
|
8226
|
-
if (/[^\x00-\x7F]/.test(decoded) && Buffer.from(decoded, "utf8").toString("base64") === value) {
|
|
8227
|
-
return decoded;
|
|
8228
|
-
}
|
|
8229
|
-
} catch {
|
|
8230
|
-
}
|
|
8231
|
-
}
|
|
8232
|
-
return value;
|
|
8233
|
-
}
|
|
8234
|
-
|
|
8235
8616
|
const S3_DEFAULT_REGION = "us-east-1";
|
|
8236
8617
|
const S3_DEFAULT_ENDPOINT = "https://s3.us-east-1.amazonaws.com";
|
|
8237
8618
|
class ConnectionString {
|
|
@@ -12348,7 +12729,7 @@ class Database extends EventEmitter {
|
|
|
12348
12729
|
this.id = idGenerator(7);
|
|
12349
12730
|
this.version = "1";
|
|
12350
12731
|
this.s3dbVersion = (() => {
|
|
12351
|
-
const [ok, err, version] = tryFn(() => true ? "
|
|
12732
|
+
const [ok, err, version] = tryFn(() => true ? "11.0.0" : "latest");
|
|
12352
12733
|
return ok ? version : "latest";
|
|
12353
12734
|
})();
|
|
12354
12735
|
this.resources = {};
|
|
@@ -12648,18 +13029,14 @@ class Database extends EventEmitter {
|
|
|
12648
13029
|
const db = this;
|
|
12649
13030
|
if (!isEmpty(this.pluginList)) {
|
|
12650
13031
|
const plugins = this.pluginList.map((p) => isFunction(p) ? new p(this) : p);
|
|
12651
|
-
const
|
|
12652
|
-
|
|
12653
|
-
await plugin.setup(db);
|
|
12654
|
-
if (plugin.afterSetup) await plugin.afterSetup();
|
|
13032
|
+
const installProms = plugins.map(async (plugin) => {
|
|
13033
|
+
await plugin.install(db);
|
|
12655
13034
|
const pluginName = this._getPluginName(plugin);
|
|
12656
13035
|
this.pluginRegistry[pluginName] = plugin;
|
|
12657
13036
|
});
|
|
12658
|
-
await Promise.all(
|
|
13037
|
+
await Promise.all(installProms);
|
|
12659
13038
|
const startProms = plugins.map(async (plugin) => {
|
|
12660
|
-
if (plugin.beforeStart) await plugin.beforeStart();
|
|
12661
13039
|
await plugin.start();
|
|
12662
|
-
if (plugin.afterStart) await plugin.afterStart();
|
|
12663
13040
|
});
|
|
12664
13041
|
await Promise.all(startProms);
|
|
12665
13042
|
}
|
|
@@ -12680,11 +13057,37 @@ class Database extends EventEmitter {
|
|
|
12680
13057
|
const pluginName = this._getPluginName(plugin, name);
|
|
12681
13058
|
this.plugins[pluginName] = plugin;
|
|
12682
13059
|
if (this.isConnected()) {
|
|
12683
|
-
await plugin.
|
|
13060
|
+
await plugin.install(this);
|
|
12684
13061
|
await plugin.start();
|
|
12685
13062
|
}
|
|
12686
13063
|
return plugin;
|
|
12687
13064
|
}
|
|
13065
|
+
/**
|
|
13066
|
+
* Uninstall a plugin and optionally purge its data
|
|
13067
|
+
* @param {string} name - Plugin name
|
|
13068
|
+
* @param {Object} options - Uninstall options
|
|
13069
|
+
* @param {boolean} options.purgeData - Delete all plugin data from S3 (default: false)
|
|
13070
|
+
*/
|
|
13071
|
+
async uninstallPlugin(name, options = {}) {
|
|
13072
|
+
const pluginName = name.toLowerCase().replace("plugin", "");
|
|
13073
|
+
const plugin = this.plugins[pluginName] || this.pluginRegistry[pluginName];
|
|
13074
|
+
if (!plugin) {
|
|
13075
|
+
throw new Error(`Plugin '${name}' not found`);
|
|
13076
|
+
}
|
|
13077
|
+
if (plugin.stop) {
|
|
13078
|
+
await plugin.stop();
|
|
13079
|
+
}
|
|
13080
|
+
if (plugin.uninstall) {
|
|
13081
|
+
await plugin.uninstall(options);
|
|
13082
|
+
}
|
|
13083
|
+
delete this.plugins[pluginName];
|
|
13084
|
+
delete this.pluginRegistry[pluginName];
|
|
13085
|
+
const index = this.pluginList.indexOf(plugin);
|
|
13086
|
+
if (index > -1) {
|
|
13087
|
+
this.pluginList.splice(index, 1);
|
|
13088
|
+
}
|
|
13089
|
+
this.emit("plugin.uninstalled", { name: pluginName, plugin });
|
|
13090
|
+
}
|
|
12688
13091
|
async uploadMetadataFile() {
|
|
12689
13092
|
const metadata = {
|
|
12690
13093
|
version: this.version,
|
|
@@ -14151,10 +14554,9 @@ class ReplicatorPlugin extends Plugin {
|
|
|
14151
14554
|
resource.on("delete", deleteHandler);
|
|
14152
14555
|
this.eventListenersInstalled.add(resource.name);
|
|
14153
14556
|
}
|
|
14154
|
-
async
|
|
14155
|
-
this.database = database;
|
|
14557
|
+
async onInstall() {
|
|
14156
14558
|
if (this.config.persistReplicatorLog) {
|
|
14157
|
-
const [ok, err, logResource] = await tryFn(() => database.createResource({
|
|
14559
|
+
const [ok, err, logResource] = await tryFn(() => this.database.createResource({
|
|
14158
14560
|
name: this.config.replicatorLogResource || "plg_replicator_logs",
|
|
14159
14561
|
attributes: {
|
|
14160
14562
|
id: "string|required",
|
|
@@ -14169,14 +14571,14 @@ class ReplicatorPlugin extends Plugin {
|
|
|
14169
14571
|
if (ok) {
|
|
14170
14572
|
this.replicatorLogResource = logResource;
|
|
14171
14573
|
} else {
|
|
14172
|
-
this.replicatorLogResource = database.resources[this.config.replicatorLogResource || "plg_replicator_logs"];
|
|
14574
|
+
this.replicatorLogResource = this.database.resources[this.config.replicatorLogResource || "plg_replicator_logs"];
|
|
14173
14575
|
}
|
|
14174
14576
|
}
|
|
14175
|
-
await this.initializeReplicators(database);
|
|
14577
|
+
await this.initializeReplicators(this.database);
|
|
14176
14578
|
this.installDatabaseHooks();
|
|
14177
|
-
for (const resource of Object.values(database.resources)) {
|
|
14579
|
+
for (const resource of Object.values(this.database.resources)) {
|
|
14178
14580
|
if (resource.name !== (this.config.replicatorLogResource || "plg_replicator_logs")) {
|
|
14179
|
-
this.installEventListeners(resource, database, this);
|
|
14581
|
+
this.installEventListeners(resource, this.database, this);
|
|
14180
14582
|
}
|
|
14181
14583
|
}
|
|
14182
14584
|
}
|
|
@@ -14220,8 +14622,8 @@ class ReplicatorPlugin extends Plugin {
|
|
|
14220
14622
|
}
|
|
14221
14623
|
}
|
|
14222
14624
|
async uploadMetadataFile(database) {
|
|
14223
|
-
if (typeof database.uploadMetadataFile === "function") {
|
|
14224
|
-
await database.uploadMetadataFile();
|
|
14625
|
+
if (typeof this.database.uploadMetadataFile === "function") {
|
|
14626
|
+
await this.database.uploadMetadataFile();
|
|
14225
14627
|
}
|
|
14226
14628
|
}
|
|
14227
14629
|
async retryWithBackoff(operation, maxRetries = 3) {
|
|
@@ -14595,7 +14997,7 @@ class S3QueuePlugin extends Plugin {
|
|
|
14595
14997
|
this.cacheCleanupInterval = null;
|
|
14596
14998
|
this.lockCleanupInterval = null;
|
|
14597
14999
|
}
|
|
14598
|
-
async
|
|
15000
|
+
async onInstall() {
|
|
14599
15001
|
this.targetResource = this.database.resources[this.config.resource];
|
|
14600
15002
|
if (!this.targetResource) {
|
|
14601
15003
|
throw new Error(`S3QueuePlugin: resource '${this.config.resource}' not found`);
|
|
@@ -15156,8 +15558,7 @@ class SchedulerPlugin extends Plugin {
|
|
|
15156
15558
|
if (parts.length !== 5) return false;
|
|
15157
15559
|
return true;
|
|
15158
15560
|
}
|
|
15159
|
-
async
|
|
15160
|
-
this.database = database;
|
|
15561
|
+
async onInstall() {
|
|
15161
15562
|
await this._createLockResource();
|
|
15162
15563
|
if (this.config.persistJobs) {
|
|
15163
15564
|
await this._createJobHistoryResource();
|
|
@@ -15715,8 +16116,7 @@ class StateMachinePlugin extends Plugin {
|
|
|
15715
16116
|
}
|
|
15716
16117
|
}
|
|
15717
16118
|
}
|
|
15718
|
-
async
|
|
15719
|
-
this.database = database;
|
|
16119
|
+
async onInstall() {
|
|
15720
16120
|
if (this.config.persistTransitions) {
|
|
15721
16121
|
await this._createStateResources();
|
|
15722
16122
|
}
|