s3db.js 5.2.0 → 6.1.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 CHANGED
@@ -1,4 +1,3 @@
1
- /* istanbul ignore file */
2
1
  'use strict';
3
2
 
4
3
  Object.defineProperty(exports, '__esModule', { value: true });
@@ -943,15 +942,24 @@ class Client extends EventEmitter {
943
942
  if (errorClass) return new errorClass(data);
944
943
  return error;
945
944
  }
946
- async putObject({ key, metadata, contentType, body, contentEncoding }) {
945
+ async putObject({ key, metadata, contentType, body, contentEncoding, contentLength }) {
946
+ const keyPrefix = typeof this.config.keyPrefix === "string" ? this.config.keyPrefix : "";
947
+ const stringMetadata = {};
948
+ if (metadata) {
949
+ for (const [k, v] of Object.entries(metadata)) {
950
+ const validKey = String(k).replace(/[^a-zA-Z0-9\-_]/g, "_");
951
+ stringMetadata[validKey] = String(v);
952
+ }
953
+ }
947
954
  const options2 = {
948
955
  Bucket: this.config.bucket,
949
- Key: this.config.keyPrefix ? path.join(this.config.keyPrefix, key) : key,
950
- Metadata: { ...metadata },
956
+ Key: keyPrefix ? path.join(keyPrefix, key) : key,
957
+ Metadata: stringMetadata,
951
958
  Body: body || Buffer.alloc(0)
952
959
  };
953
960
  if (contentType !== void 0) options2.ContentType = contentType;
954
961
  if (contentEncoding !== void 0) options2.ContentEncoding = contentEncoding;
962
+ if (contentLength !== void 0) options2.ContentLength = contentLength;
955
963
  try {
956
964
  const response = await this.sendCommand(new clientS3.PutObjectCommand(options2));
957
965
  this.emit("putObject", response, options2);
@@ -964,9 +972,10 @@ class Client extends EventEmitter {
964
972
  }
965
973
  }
966
974
  async getObject(key) {
975
+ const keyPrefix = typeof this.config.keyPrefix === "string" ? this.config.keyPrefix : "";
967
976
  const options2 = {
968
977
  Bucket: this.config.bucket,
969
- Key: path.join(this.config.keyPrefix, key)
978
+ Key: keyPrefix ? path.join(keyPrefix, key) : key
970
979
  };
971
980
  try {
972
981
  const response = await this.sendCommand(new clientS3.GetObjectCommand(options2));
@@ -980,9 +989,10 @@ class Client extends EventEmitter {
980
989
  }
981
990
  }
982
991
  async headObject(key) {
992
+ const keyPrefix = typeof this.config.keyPrefix === "string" ? this.config.keyPrefix : "";
983
993
  const options2 = {
984
994
  Bucket: this.config.bucket,
985
- Key: this.config.keyPrefix ? path.join(this.config.keyPrefix, key) : key
995
+ Key: keyPrefix ? path.join(keyPrefix, key) : key
986
996
  };
987
997
  try {
988
998
  const response = await this.sendCommand(new clientS3.HeadObjectCommand(options2));
@@ -1024,9 +1034,10 @@ class Client extends EventEmitter {
1024
1034
  }
1025
1035
  }
1026
1036
  async deleteObject(key) {
1037
+ const keyPrefix = typeof this.config.keyPrefix === "string" ? this.config.keyPrefix : "";
1027
1038
  const options2 = {
1028
1039
  Bucket: this.config.bucket,
1029
- Key: this.config.keyPrefix ? path.join(this.config.keyPrefix, key) : key
1040
+ Key: keyPrefix ? path.join(keyPrefix, key) : key
1030
1041
  };
1031
1042
  try {
1032
1043
  const response = await this.sendCommand(new clientS3.DeleteObjectCommand(options2));
@@ -1040,13 +1051,14 @@ class Client extends EventEmitter {
1040
1051
  }
1041
1052
  }
1042
1053
  async deleteObjects(keys) {
1054
+ const keyPrefix = typeof this.config.keyPrefix === "string" ? this.config.keyPrefix : "";
1043
1055
  const packages = lodashEs.chunk(keys, 1e3);
1044
1056
  const { results, errors } = await promisePool.PromisePool.for(packages).withConcurrency(this.parallelism).process(async (keys2) => {
1045
1057
  const options2 = {
1046
1058
  Bucket: this.config.bucket,
1047
1059
  Delete: {
1048
1060
  Objects: keys2.map((key) => ({
1049
- Key: this.config.keyPrefix ? path.join(this.config.keyPrefix, key) : key
1061
+ Key: keyPrefix ? path.join(keyPrefix, key) : key
1050
1062
  }))
1051
1063
  }
1052
1064
  };
@@ -1074,12 +1086,13 @@ class Client extends EventEmitter {
1074
1086
  * @returns {Promise<number>} Number of objects deleted
1075
1087
  */
1076
1088
  async deleteAll({ prefix } = {}) {
1089
+ const keyPrefix = typeof this.config.keyPrefix === "string" ? this.config.keyPrefix : "";
1077
1090
  let continuationToken;
1078
1091
  let totalDeleted = 0;
1079
1092
  do {
1080
1093
  const listCommand = new clientS3.ListObjectsV2Command({
1081
1094
  Bucket: this.config.bucket,
1082
- Prefix: this.config.keyPrefix ? path.join(this.config.keyPrefix, prefix) : prefix,
1095
+ Prefix: keyPrefix ? path.join(keyPrefix, prefix) : prefix,
1083
1096
  ContinuationToken: continuationToken
1084
1097
  });
1085
1098
  const listResponse = await this.client.send(listCommand);
@@ -1221,6 +1234,10 @@ class Client extends EventEmitter {
1221
1234
  prefix,
1222
1235
  offset
1223
1236
  });
1237
+ if (!continuationToken) {
1238
+ this.emit("getKeysPage", [], params);
1239
+ return [];
1240
+ }
1224
1241
  }
1225
1242
  while (truncated) {
1226
1243
  const options2 = {
@@ -1233,8 +1250,8 @@ class Client extends EventEmitter {
1233
1250
  }
1234
1251
  truncated = res.IsTruncated || false;
1235
1252
  continuationToken = res.NextContinuationToken;
1236
- if (keys.length > amount) {
1237
- keys = keys.splice(0, amount);
1253
+ if (keys.length >= amount) {
1254
+ keys = keys.slice(0, amount);
1238
1255
  break;
1239
1256
  }
1240
1257
  }
@@ -6006,6 +6023,16 @@ if (typeof Object.create === 'function'){
6006
6023
  };
6007
6024
  }
6008
6025
 
6026
+ var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors ||
6027
+ function getOwnPropertyDescriptors(obj) {
6028
+ var keys = Object.keys(obj);
6029
+ var descriptors = {};
6030
+ for (var i = 0; i < keys.length; i++) {
6031
+ descriptors[keys[i]] = Object.getOwnPropertyDescriptor(obj, keys[i]);
6032
+ }
6033
+ return descriptors;
6034
+ };
6035
+
6009
6036
  var formatRegExp = /%[sdj%]/g;
6010
6037
  function format(f) {
6011
6038
  if (!isString(f)) {
@@ -6491,6 +6518,64 @@ function hasOwnProperty(obj, prop) {
6491
6518
  return Object.prototype.hasOwnProperty.call(obj, prop);
6492
6519
  }
6493
6520
 
6521
+ var kCustomPromisifiedSymbol = typeof Symbol !== 'undefined' ? Symbol('util.promisify.custom') : undefined;
6522
+
6523
+ function promisify(original) {
6524
+ if (typeof original !== 'function')
6525
+ throw new TypeError('The "original" argument must be of type Function');
6526
+
6527
+ if (kCustomPromisifiedSymbol && original[kCustomPromisifiedSymbol]) {
6528
+ var fn = original[kCustomPromisifiedSymbol];
6529
+ if (typeof fn !== 'function') {
6530
+ throw new TypeError('The "util.promisify.custom" argument must be of type Function');
6531
+ }
6532
+ Object.defineProperty(fn, kCustomPromisifiedSymbol, {
6533
+ value: fn, enumerable: false, writable: false, configurable: true
6534
+ });
6535
+ return fn;
6536
+ }
6537
+
6538
+ function fn() {
6539
+ var promiseResolve, promiseReject;
6540
+ var promise = new Promise(function (resolve, reject) {
6541
+ promiseResolve = resolve;
6542
+ promiseReject = reject;
6543
+ });
6544
+
6545
+ var args = [];
6546
+ for (var i = 0; i < arguments.length; i++) {
6547
+ args.push(arguments[i]);
6548
+ }
6549
+ args.push(function (err, value) {
6550
+ if (err) {
6551
+ promiseReject(err);
6552
+ } else {
6553
+ promiseResolve(value);
6554
+ }
6555
+ });
6556
+
6557
+ try {
6558
+ original.apply(this, args);
6559
+ } catch (err) {
6560
+ promiseReject(err);
6561
+ }
6562
+
6563
+ return promise;
6564
+ }
6565
+
6566
+ Object.setPrototypeOf(fn, Object.getPrototypeOf(original));
6567
+
6568
+ if (kCustomPromisifiedSymbol) Object.defineProperty(fn, kCustomPromisifiedSymbol, {
6569
+ value: fn, enumerable: false, writable: false, configurable: true
6570
+ });
6571
+ return Object.defineProperties(
6572
+ fn,
6573
+ getOwnPropertyDescriptors(original)
6574
+ );
6575
+ }
6576
+
6577
+ promisify.custom = kCustomPromisifiedSymbol;
6578
+
6494
6579
  function BufferList() {
6495
6580
  this.head = null;
6496
6581
  this.tail = null;
@@ -8720,41 +8805,41 @@ function getSizeBreakdown(mappedObject) {
8720
8805
  }
8721
8806
 
8722
8807
  const S3_METADATA_LIMIT_BYTES = 2048;
8723
- async function handleInsert$3({ resource, data, mappedData }) {
8808
+ async function handleInsert$4({ resource, data, mappedData }) {
8724
8809
  const totalSize = calculateTotalSize(mappedData);
8725
8810
  if (totalSize > S3_METADATA_LIMIT_BYTES) {
8726
8811
  throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
8727
8812
  }
8728
8813
  return { mappedData, body: "" };
8729
8814
  }
8730
- async function handleUpdate$3({ resource, id, data, mappedData }) {
8815
+ async function handleUpdate$4({ resource, id, data, mappedData }) {
8731
8816
  const totalSize = calculateTotalSize(mappedData);
8732
8817
  if (totalSize > S3_METADATA_LIMIT_BYTES) {
8733
8818
  throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
8734
8819
  }
8735
8820
  return { mappedData, body: "" };
8736
8821
  }
8737
- async function handleUpsert$3({ resource, id, data, mappedData }) {
8822
+ async function handleUpsert$4({ resource, id, data, mappedData }) {
8738
8823
  const totalSize = calculateTotalSize(mappedData);
8739
8824
  if (totalSize > S3_METADATA_LIMIT_BYTES) {
8740
8825
  throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
8741
8826
  }
8742
8827
  return { mappedData, body: "" };
8743
8828
  }
8744
- async function handleGet$3({ resource, metadata, body }) {
8829
+ async function handleGet$4({ resource, metadata, body }) {
8745
8830
  return { metadata, body };
8746
8831
  }
8747
8832
 
8748
8833
  var enforceLimits = /*#__PURE__*/Object.freeze({
8749
8834
  __proto__: null,
8750
8835
  S3_METADATA_LIMIT_BYTES: S3_METADATA_LIMIT_BYTES,
8751
- handleGet: handleGet$3,
8752
- handleInsert: handleInsert$3,
8753
- handleUpdate: handleUpdate$3,
8754
- handleUpsert: handleUpsert$3
8836
+ handleGet: handleGet$4,
8837
+ handleInsert: handleInsert$4,
8838
+ handleUpdate: handleUpdate$4,
8839
+ handleUpsert: handleUpsert$4
8755
8840
  });
8756
8841
 
8757
- async function handleInsert$2({ resource, data, mappedData }) {
8842
+ async function handleInsert$3({ resource, data, mappedData }) {
8758
8843
  const totalSize = calculateTotalSize(mappedData);
8759
8844
  if (totalSize > S3_METADATA_LIMIT_BYTES) {
8760
8845
  resource.emit("exceedsLimit", {
@@ -8767,7 +8852,7 @@ async function handleInsert$2({ resource, data, mappedData }) {
8767
8852
  }
8768
8853
  return { mappedData, body: "" };
8769
8854
  }
8770
- async function handleUpdate$2({ resource, id, data, mappedData }) {
8855
+ async function handleUpdate$3({ resource, id, data, mappedData }) {
8771
8856
  const totalSize = calculateTotalSize(mappedData);
8772
8857
  if (totalSize > S3_METADATA_LIMIT_BYTES) {
8773
8858
  resource.emit("exceedsLimit", {
@@ -8781,7 +8866,7 @@ async function handleUpdate$2({ resource, id, data, mappedData }) {
8781
8866
  }
8782
8867
  return { mappedData, body: "" };
8783
8868
  }
8784
- async function handleUpsert$2({ resource, id, data, mappedData }) {
8869
+ async function handleUpsert$3({ resource, id, data, mappedData }) {
8785
8870
  const totalSize = calculateTotalSize(mappedData);
8786
8871
  if (totalSize > S3_METADATA_LIMIT_BYTES) {
8787
8872
  resource.emit("exceedsLimit", {
@@ -8795,30 +8880,30 @@ async function handleUpsert$2({ resource, id, data, mappedData }) {
8795
8880
  }
8796
8881
  return { mappedData, body: "" };
8797
8882
  }
8798
- async function handleGet$2({ resource, metadata, body }) {
8883
+ async function handleGet$3({ resource, metadata, body }) {
8799
8884
  return { metadata, body };
8800
8885
  }
8801
8886
 
8802
- var userManagement = /*#__PURE__*/Object.freeze({
8887
+ var userManaged = /*#__PURE__*/Object.freeze({
8803
8888
  __proto__: null,
8804
- handleGet: handleGet$2,
8805
- handleInsert: handleInsert$2,
8806
- handleUpdate: handleUpdate$2,
8807
- handleUpsert: handleUpsert$2
8889
+ handleGet: handleGet$3,
8890
+ handleInsert: handleInsert$3,
8891
+ handleUpdate: handleUpdate$3,
8892
+ handleUpsert: handleUpsert$3
8808
8893
  });
8809
8894
 
8810
8895
  const TRUNCATE_SUFFIX = "...";
8811
8896
  const TRUNCATE_SUFFIX_BYTES = calculateUTF8Bytes(TRUNCATE_SUFFIX);
8812
- async function handleInsert$1({ resource, data, mappedData }) {
8897
+ async function handleInsert$2({ resource, data, mappedData }) {
8813
8898
  return handleTruncate({ resource, data, mappedData });
8814
8899
  }
8815
- async function handleUpdate$1({ resource, id, data, mappedData }) {
8900
+ async function handleUpdate$2({ resource, id, data, mappedData }) {
8816
8901
  return handleTruncate({ resource, data, mappedData });
8817
8902
  }
8818
- async function handleUpsert$1({ resource, id, data, mappedData }) {
8903
+ async function handleUpsert$2({ resource, id, data, mappedData }) {
8819
8904
  return handleTruncate({ resource, data, mappedData });
8820
8905
  }
8821
- async function handleGet$1({ resource, metadata, body }) {
8906
+ async function handleGet$2({ resource, metadata, body }) {
8822
8907
  return { metadata, body };
8823
8908
  }
8824
8909
  function handleTruncate({ resource, data, mappedData }) {
@@ -8858,25 +8943,25 @@ function handleTruncate({ resource, data, mappedData }) {
8858
8943
 
8859
8944
  var dataTruncate = /*#__PURE__*/Object.freeze({
8860
8945
  __proto__: null,
8861
- handleGet: handleGet$1,
8862
- handleInsert: handleInsert$1,
8863
- handleUpdate: handleUpdate$1,
8864
- handleUpsert: handleUpsert$1
8946
+ handleGet: handleGet$2,
8947
+ handleInsert: handleInsert$2,
8948
+ handleUpdate: handleUpdate$2,
8949
+ handleUpsert: handleUpsert$2
8865
8950
  });
8866
8951
 
8867
8952
  const OVERFLOW_FLAG = "$overflow";
8868
8953
  const OVERFLOW_FLAG_VALUE = "true";
8869
8954
  const OVERFLOW_FLAG_BYTES = calculateUTF8Bytes(OVERFLOW_FLAG) + calculateUTF8Bytes(OVERFLOW_FLAG_VALUE);
8870
- async function handleInsert({ resource, data, mappedData }) {
8955
+ async function handleInsert$1({ resource, data, mappedData }) {
8871
8956
  return handleOverflow({ resource, data, mappedData });
8872
8957
  }
8873
- async function handleUpdate({ resource, id, data, mappedData }) {
8958
+ async function handleUpdate$1({ resource, id, data, mappedData }) {
8874
8959
  return handleOverflow({ resource, data, mappedData });
8875
8960
  }
8876
- async function handleUpsert({ resource, id, data, mappedData }) {
8961
+ async function handleUpsert$1({ resource, id, data, mappedData }) {
8877
8962
  return handleOverflow({ resource, data, mappedData });
8878
8963
  }
8879
- async function handleGet({ resource, metadata, body }) {
8964
+ async function handleGet$1({ resource, metadata, body }) {
8880
8965
  if (metadata[OVERFLOW_FLAG] === OVERFLOW_FLAG_VALUE) {
8881
8966
  try {
8882
8967
  const bodyData = body ? JSON.parse(body) : {};
@@ -8918,6 +9003,51 @@ function handleOverflow({ resource, data, mappedData }) {
8918
9003
  }
8919
9004
 
8920
9005
  var bodyOverflow = /*#__PURE__*/Object.freeze({
9006
+ __proto__: null,
9007
+ handleGet: handleGet$1,
9008
+ handleInsert: handleInsert$1,
9009
+ handleUpdate: handleUpdate$1,
9010
+ handleUpsert: handleUpsert$1
9011
+ });
9012
+
9013
+ async function handleInsert({ resource, data, mappedData }) {
9014
+ const bodyContent = JSON.stringify(mappedData);
9015
+ return {
9016
+ mappedData: {},
9017
+ body: bodyContent
9018
+ };
9019
+ }
9020
+ async function handleUpdate({ resource, id, data, mappedData }) {
9021
+ const bodyContent = JSON.stringify(mappedData);
9022
+ return {
9023
+ mappedData: {},
9024
+ body: bodyContent
9025
+ };
9026
+ }
9027
+ async function handleUpsert({ resource, id, data, mappedData }) {
9028
+ const bodyContent = JSON.stringify(mappedData);
9029
+ return {
9030
+ mappedData: {},
9031
+ body: bodyContent
9032
+ };
9033
+ }
9034
+ async function handleGet({ resource, metadata, body }) {
9035
+ try {
9036
+ const bodyData = body ? JSON.parse(body) : {};
9037
+ return {
9038
+ metadata: bodyData,
9039
+ body: ""
9040
+ };
9041
+ } catch (error) {
9042
+ console.warn(`Failed to parse body-only content:`, error.message);
9043
+ return {
9044
+ metadata,
9045
+ body: ""
9046
+ };
9047
+ }
9048
+ }
9049
+
9050
+ var bodyOnly = /*#__PURE__*/Object.freeze({
8921
9051
  __proto__: null,
8922
9052
  handleGet: handleGet,
8923
9053
  handleInsert: handleInsert,
@@ -8926,10 +9056,11 @@ var bodyOverflow = /*#__PURE__*/Object.freeze({
8926
9056
  });
8927
9057
 
8928
9058
  const behaviors = {
8929
- "user-management": userManagement,
9059
+ "user-managed": userManaged,
8930
9060
  "enforce-limits": enforceLimits,
8931
9061
  "data-truncate": dataTruncate,
8932
- "body-overflow": bodyOverflow
9062
+ "body-overflow": bodyOverflow,
9063
+ "body-only": bodyOnly
8933
9064
  };
8934
9065
  function getBehavior(behaviorName) {
8935
9066
  const behavior = behaviors[behaviorName];
@@ -8939,8 +9070,11 @@ function getBehavior(behaviorName) {
8939
9070
  return behavior;
8940
9071
  }
8941
9072
  const AVAILABLE_BEHAVIORS = Object.keys(behaviors);
8942
- const DEFAULT_BEHAVIOR = "user-management";
9073
+ const DEFAULT_BEHAVIOR = "user-managed";
8943
9074
 
9075
+ function createIdGeneratorWithSize(size) {
9076
+ return nanoid.customAlphabet(nanoid.urlAlphabet, size);
9077
+ }
8944
9078
  class Resource extends EventEmitter {
8945
9079
  /**
8946
9080
  * Create a new Resource instance
@@ -8949,7 +9083,7 @@ class Resource extends EventEmitter {
8949
9083
  * @param {Object} config.client - S3 client instance
8950
9084
  * @param {string} [config.version='v0'] - Resource version
8951
9085
  * @param {Object} [config.attributes={}] - Resource attributes schema
8952
- * @param {string} [config.behavior='user-management'] - Resource behavior strategy
9086
+ * @param {string} [config.behavior='user-managed'] - Resource behavior strategy
8953
9087
  * @param {string} [config.passphrase='secret'] - Encryption passphrase
8954
9088
  * @param {number} [config.parallelism=10] - Parallelism for bulk operations
8955
9089
  * @param {Array} [config.observers=[]] - Observer instances
@@ -8961,6 +9095,9 @@ class Resource extends EventEmitter {
8961
9095
  * @param {boolean} [config.allNestedObjectsOptional=false] - Make nested objects optional
8962
9096
  * @param {Object} [config.hooks={}] - Custom hooks
8963
9097
  * @param {Object} [config.options={}] - Additional options
9098
+ * @param {Function} [config.idGenerator] - Custom ID generator function
9099
+ * @param {number} [config.idSize=22] - Size for auto-generated IDs
9100
+ * @param {boolean} [config.versioningEnabled=false] - Enable versioning for this resource
8964
9101
  * @example
8965
9102
  * const users = new Resource({
8966
9103
  * name: 'users',
@@ -8970,7 +9107,7 @@ class Resource extends EventEmitter {
8970
9107
  * email: 'string|required',
8971
9108
  * password: 'secret|required'
8972
9109
  * },
8973
- * behavior: 'user-management',
9110
+ * behavior: 'user-managed',
8974
9111
  * passphrase: 'my-secret-key',
8975
9112
  * timestamps: true,
8976
9113
  * partitions: {
@@ -8985,6 +9122,30 @@ class Resource extends EventEmitter {
8985
9122
  * }]
8986
9123
  * }
8987
9124
  * });
9125
+ *
9126
+ * // With custom ID size
9127
+ * const shortIdUsers = new Resource({
9128
+ * name: 'users',
9129
+ * client: s3Client,
9130
+ * attributes: { name: 'string|required' },
9131
+ * idSize: 8 // Generate 8-character IDs
9132
+ * });
9133
+ *
9134
+ * // With custom ID generator function
9135
+ * const customIdUsers = new Resource({
9136
+ * name: 'users',
9137
+ * client: s3Client,
9138
+ * attributes: { name: 'string|required' },
9139
+ * idGenerator: () => `user_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`
9140
+ * });
9141
+ *
9142
+ * // With custom ID generator using size parameter
9143
+ * const longIdUsers = new Resource({
9144
+ * name: 'users',
9145
+ * client: s3Client,
9146
+ * attributes: { name: 'string|required' },
9147
+ * idGenerator: 32 // Generate 32-character IDs (same as idSize: 32)
9148
+ * });
8988
9149
  */
8989
9150
  constructor(config) {
8990
9151
  super();
@@ -9008,7 +9169,10 @@ ${validation.errors.join("\n")}`);
9008
9169
  partitions = {},
9009
9170
  paranoid = true,
9010
9171
  allNestedObjectsOptional = true,
9011
- hooks = {}
9172
+ hooks = {},
9173
+ idGenerator: customIdGenerator,
9174
+ idSize = 22,
9175
+ versioningEnabled = false
9012
9176
  } = config;
9013
9177
  this.name = name;
9014
9178
  this.client = client;
@@ -9017,6 +9181,8 @@ ${validation.errors.join("\n")}`);
9017
9181
  this.observers = observers;
9018
9182
  this.parallelism = parallelism;
9019
9183
  this.passphrase = passphrase ?? "secret";
9184
+ this.versioningEnabled = versioningEnabled;
9185
+ this.idGenerator = this.configureIdGenerator(customIdGenerator, idSize);
9020
9186
  this.config = {
9021
9187
  cache,
9022
9188
  hooks,
@@ -9048,6 +9214,25 @@ ${validation.errors.join("\n")}`);
9048
9214
  }
9049
9215
  }
9050
9216
  }
9217
+ /**
9218
+ * Configure ID generator based on provided options
9219
+ * @param {Function|number} customIdGenerator - Custom ID generator function or size
9220
+ * @param {number} idSize - Size for auto-generated IDs
9221
+ * @returns {Function} Configured ID generator function
9222
+ * @private
9223
+ */
9224
+ configureIdGenerator(customIdGenerator, idSize) {
9225
+ if (typeof customIdGenerator === "function") {
9226
+ return customIdGenerator;
9227
+ }
9228
+ if (typeof customIdGenerator === "number" && customIdGenerator > 0) {
9229
+ return createIdGeneratorWithSize(customIdGenerator);
9230
+ }
9231
+ if (typeof idSize === "number" && idSize > 0) {
9232
+ return createIdGeneratorWithSize(idSize);
9233
+ }
9234
+ return idGenerator;
9235
+ }
9051
9236
  /**
9052
9237
  * Get resource options (for backward compatibility with tests)
9053
9238
  */
@@ -9104,6 +9289,15 @@ ${validation.errors.join("\n")}`);
9104
9289
  }
9105
9290
  }
9106
9291
  this.setupPartitionHooks();
9292
+ if (this.versioningEnabled) {
9293
+ if (!this.config.partitions.byVersion) {
9294
+ this.config.partitions.byVersion = {
9295
+ fields: {
9296
+ _v: "string"
9297
+ }
9298
+ };
9299
+ }
9300
+ }
9107
9301
  this.schema = new Schema({
9108
9302
  name: this.name,
9109
9303
  attributes: this.attributes,
@@ -9223,6 +9417,9 @@ ${validation.errors.join("\n")}`);
9223
9417
  * @returns {boolean} True if field exists
9224
9418
  */
9225
9419
  fieldExistsInAttributes(fieldName) {
9420
+ if (fieldName.startsWith("_")) {
9421
+ return true;
9422
+ }
9226
9423
  if (!fieldName.includes(".")) {
9227
9424
  return Object.keys(this.attributes || {}).includes(fieldName);
9228
9425
  }
@@ -9276,12 +9473,12 @@ ${validation.errors.join("\n")}`);
9276
9473
  return transformedValue;
9277
9474
  }
9278
9475
  /**
9279
- * Get the main resource key (always versioned path)
9476
+ * Get the main resource key (new format without version in path)
9280
9477
  * @param {string} id - Resource ID
9281
9478
  * @returns {string} The main S3 key path
9282
9479
  */
9283
9480
  getResourceKey(id) {
9284
- return join(`resource=${this.name}`, `v=${this.version}`, `id=${id}`);
9481
+ return join(`resource=${this.name}`, `data`, `id=${id}`);
9285
9482
  }
9286
9483
  /**
9287
9484
  * Generate partition key for a resource in a specific partition
@@ -9350,12 +9547,23 @@ ${validation.errors.join("\n")}`);
9350
9547
  }
9351
9548
  return currentLevel;
9352
9549
  }
9550
+ /**
9551
+ * Calculate estimated content length for body data
9552
+ * @param {string|Buffer} body - Body content
9553
+ * @returns {number} Estimated content length in bytes
9554
+ */
9555
+ calculateContentLength(body) {
9556
+ if (!body) return 0;
9557
+ if (Buffer.isBuffer(body)) return body.length;
9558
+ if (typeof body === "string") return Buffer.byteLength(body, "utf8");
9559
+ if (typeof body === "object") return Buffer.byteLength(JSON.stringify(body), "utf8");
9560
+ return Buffer.byteLength(String(body), "utf8");
9561
+ }
9353
9562
  /**
9354
9563
  * Insert a new resource object
9355
- * @param {Object} params - Insert parameters
9356
- * @param {string} [params.id] - Resource ID (auto-generated if not provided)
9357
- * @param {...Object} params - Resource attributes (any additional properties)
9358
- * @returns {Promise<Object>} The inserted resource object with all attributes and generated ID
9564
+ * @param {Object} attributes - Resource attributes
9565
+ * @param {string} [attributes.id] - Custom ID (optional, auto-generated if not provided)
9566
+ * @returns {Promise<Object>} The created resource object with all attributes
9359
9567
  * @example
9360
9568
  * // Insert with auto-generated ID
9361
9569
  * const user = await resource.insert({
@@ -9363,18 +9571,13 @@ ${validation.errors.join("\n")}`);
9363
9571
  * email: 'john@example.com',
9364
9572
  * age: 30
9365
9573
  * });
9574
+ * console.log(user.id); // Auto-generated ID
9366
9575
  *
9367
9576
  * // Insert with custom ID
9368
9577
  * const user = await resource.insert({
9369
- * id: 'custom-id-123',
9370
- * name: 'Jane Smith',
9371
- * email: 'jane@example.com'
9372
- * });
9373
- *
9374
- * // Insert with auto-generated password for secret field
9375
- * const user = await resource.insert({
9578
+ * id: 'user-123',
9376
9579
  * name: 'John Doe',
9377
- * email: 'john@example.com',
9580
+ * email: 'john@example.com'
9378
9581
  * });
9379
9582
  */
9380
9583
  async insert({ id, ...attributes }) {
@@ -9382,7 +9585,8 @@ ${validation.errors.join("\n")}`);
9382
9585
  attributes.createdAt = (/* @__PURE__ */ new Date()).toISOString();
9383
9586
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9384
9587
  }
9385
- const preProcessedData = await this.executeHooks("preInsert", attributes);
9588
+ const completeData = { id, ...attributes };
9589
+ const preProcessedData = await this.executeHooks("preInsert", completeData);
9386
9590
  const {
9387
9591
  errors,
9388
9592
  isValid,
@@ -9396,21 +9600,34 @@ ${validation.errors.join("\n")}`);
9396
9600
  validation: errors
9397
9601
  });
9398
9602
  }
9399
- if (!id && id !== 0) id = idGenerator();
9400
- const mappedData = await this.schema.mapper(validated);
9603
+ const { id: validatedId, ...validatedAttributes } = validated;
9604
+ const finalId = validatedId || id || this.idGenerator();
9605
+ const mappedData = await this.schema.mapper(validatedAttributes);
9606
+ mappedData._v = String(this.version);
9401
9607
  const behaviorImpl = getBehavior(this.behavior);
9402
9608
  const { mappedData: processedMetadata, body } = await behaviorImpl.handleInsert({
9403
9609
  resource: this,
9404
- data: validated,
9610
+ data: validatedAttributes,
9405
9611
  mappedData
9406
9612
  });
9407
- const key = this.getResourceKey(id);
9613
+ const finalMetadata = processedMetadata;
9614
+ const key = this.getResourceKey(finalId);
9615
+ let contentType = void 0;
9616
+ if (body && body !== "") {
9617
+ try {
9618
+ JSON.parse(body);
9619
+ contentType = "application/json";
9620
+ } catch {
9621
+ }
9622
+ }
9408
9623
  await this.client.putObject({
9409
- metadata: processedMetadata,
9624
+ metadata: finalMetadata,
9410
9625
  key,
9411
- body
9626
+ body,
9627
+ contentType,
9628
+ contentLength: this.calculateContentLength(body)
9412
9629
  });
9413
- const final = lodashEs.merge({ id }, validated);
9630
+ const final = lodashEs.merge({ id: finalId }, validatedAttributes);
9414
9631
  await this.executeHooks("afterInsert", final);
9415
9632
  this.emit("insert", final);
9416
9633
  return final;
@@ -9429,7 +9646,8 @@ ${validation.errors.join("\n")}`);
9429
9646
  const key = this.getResourceKey(id);
9430
9647
  try {
9431
9648
  const request = await this.client.headObject(key);
9432
- const objectVersion = this.extractVersionFromKey(key) || this.version;
9649
+ const objectVersionRaw = request.Metadata?._v || this.version;
9650
+ const objectVersion = typeof objectVersionRaw === "string" && objectVersionRaw.startsWith("v") ? objectVersionRaw.slice(1) : objectVersionRaw;
9433
9651
  const schema = await this.getSchemaForVersion(objectVersion);
9434
9652
  let metadata = await schema.unmapper(request.Metadata);
9435
9653
  const behaviorImpl = getBehavior(this.behavior);
@@ -9454,9 +9672,13 @@ ${validation.errors.join("\n")}`);
9454
9672
  data._lastModified = request.LastModified;
9455
9673
  data._hasContent = request.ContentLength > 0;
9456
9674
  data._mimeType = request.ContentType || null;
9675
+ data._v = objectVersion;
9457
9676
  if (request.VersionId) data._versionId = request.VersionId;
9458
9677
  if (request.Expiration) data._expiresAt = request.Expiration;
9459
9678
  data._definitionHash = this.getDefinitionHash();
9679
+ if (objectVersion !== this.version) {
9680
+ data = await this.applyVersionMapping(data, objectVersion, this.version);
9681
+ }
9460
9682
  this.emit("get", data);
9461
9683
  return data;
9462
9684
  } catch (error) {
@@ -9562,28 +9784,30 @@ ${validation.errors.join("\n")}`);
9562
9784
  attributes.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9563
9785
  }
9564
9786
  const preProcessedData = await this.executeHooks("preUpdate", attributes);
9565
- const attrs = lodashEs.merge(originalData, preProcessedData);
9566
- delete attrs.id;
9567
- const { isValid, errors, data: validated } = await this.validate(attrs);
9787
+ const completeData = { ...originalData, ...preProcessedData, id };
9788
+ const { isValid, errors, data: validated } = await this.validate(completeData);
9568
9789
  if (!isValid) {
9569
9790
  throw new InvalidResourceItem({
9570
- bucket: this.client.bucket,
9791
+ bucket: this.client.config.bucket,
9571
9792
  resourceName: this.name,
9572
9793
  attributes: preProcessedData,
9573
9794
  validation: errors
9574
9795
  });
9575
9796
  }
9797
+ const { id: validatedId, ...validatedAttributes } = validated;
9576
9798
  const oldData = { ...originalData, id };
9577
- const newData = { ...validated, id };
9799
+ const newData = { ...validatedAttributes, id };
9578
9800
  await this.handlePartitionReferenceUpdates(oldData, newData);
9579
- const mappedData = await this.schema.mapper(validated);
9801
+ const mappedData = await this.schema.mapper(validatedAttributes);
9802
+ mappedData._v = String(this.version);
9580
9803
  const behaviorImpl = getBehavior(this.behavior);
9581
9804
  const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
9582
9805
  resource: this,
9583
9806
  id,
9584
- data: validated,
9807
+ data: validatedAttributes,
9585
9808
  mappedData
9586
9809
  });
9810
+ const finalMetadata = processedMetadata;
9587
9811
  const key = this.getResourceKey(id);
9588
9812
  let existingContentType = void 0;
9589
9813
  let finalBody = body;
@@ -9603,16 +9827,27 @@ ${validation.errors.join("\n")}`);
9603
9827
  } catch (error) {
9604
9828
  }
9605
9829
  }
9830
+ let finalContentType = existingContentType;
9831
+ if (finalBody && finalBody !== "" && !finalContentType) {
9832
+ try {
9833
+ JSON.parse(finalBody);
9834
+ finalContentType = "application/json";
9835
+ } catch {
9836
+ }
9837
+ }
9838
+ if (this.versioningEnabled && originalData._v !== this.version) {
9839
+ await this.createHistoricalVersion(id, originalData);
9840
+ }
9606
9841
  await this.client.putObject({
9607
9842
  key,
9608
9843
  body: finalBody,
9609
- contentType: existingContentType,
9610
- metadata: processedMetadata
9844
+ contentType: finalContentType,
9845
+ metadata: finalMetadata
9611
9846
  });
9612
- validated.id = id;
9613
- await this.executeHooks("afterUpdate", validated);
9614
- this.emit("update", preProcessedData, validated);
9615
- return validated;
9847
+ validatedAttributes.id = id;
9848
+ await this.executeHooks("afterUpdate", validatedAttributes);
9849
+ this.emit("update", preProcessedData, validatedAttributes);
9850
+ return validatedAttributes;
9616
9851
  }
9617
9852
  /**
9618
9853
  * Delete a resource object by ID
@@ -9701,11 +9936,9 @@ ${validation.errors.join("\n")}`);
9701
9936
  prefix = `resource=${this.name}/partition=${partition}`;
9702
9937
  }
9703
9938
  } else {
9704
- prefix = `resource=${this.name}/v=${this.version}`;
9939
+ prefix = `resource=${this.name}/data`;
9705
9940
  }
9706
- const count = await this.client.count({
9707
- prefix
9708
- });
9941
+ const count = await this.client.count({ prefix });
9709
9942
  this.emit("count", count);
9710
9943
  return count;
9711
9944
  }
@@ -9772,7 +10005,7 @@ ${validation.errors.join("\n")}`);
9772
10005
  `deleteAll() is a dangerous operation and requires paranoid: false option. Current paranoid setting: ${this.config.paranoid}`
9773
10006
  );
9774
10007
  }
9775
- const prefix = `resource=${this.name}/v=${this.version}`;
10008
+ const prefix = `resource=${this.name}/data`;
9776
10009
  const deletedCount = await this.client.deleteAll({ prefix });
9777
10010
  this.emit("deleteAll", {
9778
10011
  version: this.version,
@@ -9850,7 +10083,7 @@ ${validation.errors.join("\n")}`);
9850
10083
  prefix = `resource=${this.name}/partition=${partition}`;
9851
10084
  }
9852
10085
  } else {
9853
- prefix = `resource=${this.name}/v=${this.version}`;
10086
+ prefix = `resource=${this.name}/data`;
9854
10087
  }
9855
10088
  const keys = await this.client.getKeysPage({
9856
10089
  prefix,
@@ -9867,200 +10100,221 @@ ${validation.errors.join("\n")}`);
9867
10100
  return ids;
9868
10101
  }
9869
10102
  /**
9870
- * List resource objects with optional partition filtering and pagination
10103
+ * List resources with optional partition filtering and pagination
9871
10104
  * @param {Object} [params] - List parameters
9872
10105
  * @param {string} [params.partition] - Partition name to list from
9873
10106
  * @param {Object} [params.partitionValues] - Partition field values to filter by
9874
- * @param {number} [params.limit] - Maximum number of results to return
9875
- * @param {number} [params.offset=0] - Offset for pagination
9876
- * @returns {Promise<Object[]>} Array of resource objects with all attributes
10107
+ * @param {number} [params.limit] - Maximum number of results
10108
+ * @param {number} [params.offset=0] - Number of results to skip
10109
+ * @returns {Promise<Object[]>} Array of resource objects
9877
10110
  * @example
9878
10111
  * // List all resources
9879
10112
  * const allUsers = await resource.list();
9880
10113
  *
9881
10114
  * // List with pagination
9882
- * const firstPage = await resource.list({ limit: 10, offset: 0 });
9883
- * const secondPage = await resource.list({ limit: 10, offset: 10 });
10115
+ * const first10 = await resource.list({ limit: 10, offset: 0 });
9884
10116
  *
9885
10117
  * // List from specific partition
9886
- * const googleUsers = await resource.list({
9887
- * partition: 'byUtmSource',
9888
- * partitionValues: { 'utm.source': 'google' }
9889
- * });
9890
- *
9891
- * // List from partition with pagination
9892
- * const googleUsersPage = await resource.list({
9893
- * partition: 'byUtmSource',
9894
- * partitionValues: { 'utm.source': 'google' },
9895
- * limit: 5,
9896
- * offset: 0
10118
+ * const usUsers = await resource.list({
10119
+ * partition: 'byCountry',
10120
+ * partitionValues: { 'profile.country': 'US' }
9897
10121
  * });
9898
10122
  */
9899
10123
  async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
9900
10124
  try {
9901
10125
  if (!partition) {
9902
- let ids2 = [];
9903
- try {
9904
- ids2 = await this.listIds({ partition, partitionValues });
9905
- } catch (listIdsError) {
9906
- console.warn(`Failed to get list IDs:`, listIdsError.message);
9907
- return [];
9908
- }
9909
- let filteredIds2 = ids2.slice(offset);
9910
- if (limit) {
9911
- filteredIds2 = filteredIds2.slice(0, limit);
9912
- }
9913
- const { results: results2, errors: errors2 } = await promisePool.PromisePool.for(filteredIds2).withConcurrency(this.parallelism).handleError(async (error, id) => {
9914
- console.warn(`Failed to get resource ${id}:`, error.message);
9915
- return null;
9916
- }).process(async (id) => {
9917
- try {
9918
- return await this.get(id);
9919
- } catch (error) {
9920
- if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9921
- console.warn(`Decryption failed for ${id}, returning basic info`);
9922
- return {
9923
- id,
9924
- _decryptionFailed: true,
9925
- _error: error.message
9926
- };
9927
- }
9928
- throw error;
9929
- }
9930
- });
9931
- const validResults2 = results2.filter((item) => item !== null);
9932
- this.emit("list", { partition, partitionValues, count: validResults2.length, errors: errors2.length });
9933
- return validResults2;
9934
- }
9935
- if (!this.config.partitions || !this.config.partitions[partition]) {
9936
- console.warn(`Partition '${partition}' not found in resource '${this.name}'`);
9937
- this.emit("list", { partition, partitionValues, count: 0, errors: 0 });
9938
- return [];
9939
- }
9940
- const partitionDef = this.config.partitions[partition];
9941
- const partitionSegments = [];
9942
- const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
9943
- for (const [fieldName, rule] of sortedFields) {
9944
- const value = partitionValues[fieldName];
9945
- if (value !== void 0 && value !== null) {
9946
- const transformedValue = this.applyPartitionRule(value, rule);
9947
- partitionSegments.push(`${fieldName}=${transformedValue}`);
9948
- }
9949
- }
9950
- let prefix;
9951
- if (partitionSegments.length > 0) {
9952
- prefix = `resource=${this.name}/partition=${partition}/${partitionSegments.join("/")}`;
9953
- } else {
9954
- prefix = `resource=${this.name}/partition=${partition}`;
9955
- }
9956
- let keys = [];
9957
- try {
9958
- keys = await this.client.getAllKeys({ prefix });
9959
- } catch (getKeysError) {
9960
- console.warn(`Failed to get partition keys:`, getKeysError.message);
9961
- return [];
10126
+ return await this.listMain({ limit, offset });
9962
10127
  }
9963
- const ids = keys.map((key) => {
9964
- const parts = key.split("/");
9965
- const idPart = parts.find((part) => part.startsWith("id="));
9966
- return idPart ? idPart.replace("id=", "") : null;
9967
- }).filter(Boolean);
9968
- let filteredIds = ids.slice(offset);
9969
- if (limit) {
9970
- filteredIds = filteredIds.slice(0, limit);
9971
- }
9972
- const { results, errors } = await promisePool.PromisePool.for(filteredIds).withConcurrency(this.parallelism).handleError(async (error, id) => {
9973
- console.warn(`Failed to get partition resource ${id}:`, error.message);
9974
- return null;
9975
- }).process(async (id) => {
9976
- try {
9977
- const keyForId = keys.find((key) => key.includes(`id=${id}`));
9978
- if (!keyForId) {
9979
- throw new Error(`Partition key not found for ID ${id}`);
9980
- }
9981
- const keyParts = keyForId.split("/");
9982
- const actualPartitionValues = {};
9983
- for (const [fieldName, rule] of sortedFields) {
9984
- const fieldPart = keyParts.find((part) => part.startsWith(`${fieldName}=`));
9985
- if (fieldPart) {
9986
- const value = fieldPart.replace(`${fieldName}=`, "");
9987
- actualPartitionValues[fieldName] = value;
9988
- }
9989
- }
9990
- return await this.getFromPartition({ id, partitionName: partition, partitionValues: actualPartitionValues });
9991
- } catch (error) {
9992
- if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
9993
- console.warn(`Decryption failed for partition resource ${id}, returning basic info`);
9994
- return {
9995
- id,
9996
- _partition: partition,
9997
- _partitionValues: partitionValues,
9998
- _decryptionFailed: true,
9999
- _error: error.message
10000
- };
10001
- }
10002
- throw error;
10003
- }
10004
- });
10005
- const validResults = results.filter((item) => item !== null);
10006
- this.emit("list", { partition, partitionValues, count: validResults.length, errors: errors.length });
10007
- return validResults;
10128
+ return await this.listPartition({ partition, partitionValues, limit, offset });
10008
10129
  } catch (error) {
10009
- if (error.message.includes("Partition '") && error.message.includes("' not found")) {
10010
- console.warn(`Partition error in list method:`, error.message);
10011
- this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
10012
- return [];
10013
- }
10014
- console.error(`Critical error in list method:`, error.message);
10015
- this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
10016
- return [];
10130
+ return this.handleListError(error, { partition, partitionValues });
10017
10131
  }
10018
10132
  }
10019
10133
  /**
10020
- * Get multiple resources by their IDs
10021
- * @param {string[]} ids - Array of resource IDs
10022
- * @returns {Promise<Object[]>} Array of resource objects
10023
- * @example
10024
- * const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
10025
- * users.forEach(user => console.log(user.name));
10134
+ * List resources from main resource (no partition)
10026
10135
  */
10027
- async getMany(ids) {
10028
- const { results, errors } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
10029
- console.warn(`Failed to get resource ${id}:`, error.message);
10030
- return {
10031
- id,
10032
- _error: error.message,
10033
- _decryptionFailed: error.message.includes("Cipher job failed") || error.message.includes("OperationError")
10034
- };
10035
- }).process(async (id) => {
10036
- this.emit("id", id);
10037
- try {
10038
- const data = await this.get(id);
10039
- this.emit("data", data);
10040
- return data;
10041
- } catch (error) {
10042
- if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
10043
- console.warn(`Decryption failed for ${id}, returning basic info`);
10044
- return {
10045
- id,
10046
- _decryptionFailed: true,
10047
- _error: error.message
10048
- };
10049
- }
10050
- throw error;
10051
- }
10052
- });
10053
- this.emit("getMany", ids.length);
10136
+ async listMain({ limit, offset = 0 }) {
10137
+ const ids = await this.listIds({ limit, offset });
10138
+ const results = await this.processListResults(ids, "main");
10139
+ this.emit("list", { count: results.length, errors: 0 });
10054
10140
  return results;
10055
10141
  }
10056
10142
  /**
10057
- * Get all resources (equivalent to list() without pagination)
10058
- * @returns {Promise<Object[]>} Array of all resource objects
10059
- * @example
10060
- * const allUsers = await resource.getAll();
10061
- * console.log(`Total users: ${allUsers.length}`);
10143
+ * List resources from specific partition
10062
10144
  */
10063
- async getAll() {
10145
+ async listPartition({ partition, partitionValues, limit, offset = 0 }) {
10146
+ if (!this.config.partitions?.[partition]) {
10147
+ console.warn(`Partition '${partition}' not found in resource '${this.name}'`);
10148
+ this.emit("list", { partition, partitionValues, count: 0, errors: 0 });
10149
+ return [];
10150
+ }
10151
+ const partitionDef = this.config.partitions[partition];
10152
+ const prefix = this.buildPartitionPrefix(partition, partitionDef, partitionValues);
10153
+ const keys = await this.client.getAllKeys({ prefix });
10154
+ const ids = this.extractIdsFromKeys(keys).slice(offset);
10155
+ const filteredIds = limit ? ids.slice(0, limit) : ids;
10156
+ const results = await this.processPartitionResults(filteredIds, partition, partitionDef, keys);
10157
+ this.emit("list", { partition, partitionValues, count: results.length, errors: 0 });
10158
+ return results;
10159
+ }
10160
+ /**
10161
+ * Build partition prefix from partition definition and values
10162
+ */
10163
+ buildPartitionPrefix(partition, partitionDef, partitionValues) {
10164
+ const partitionSegments = [];
10165
+ const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
10166
+ for (const [fieldName, rule] of sortedFields) {
10167
+ const value = partitionValues[fieldName];
10168
+ if (value !== void 0 && value !== null) {
10169
+ const transformedValue = this.applyPartitionRule(value, rule);
10170
+ partitionSegments.push(`${fieldName}=${transformedValue}`);
10171
+ }
10172
+ }
10173
+ if (partitionSegments.length > 0) {
10174
+ return `resource=${this.name}/partition=${partition}/${partitionSegments.join("/")}`;
10175
+ }
10176
+ return `resource=${this.name}/partition=${partition}`;
10177
+ }
10178
+ /**
10179
+ * Extract IDs from S3 keys
10180
+ */
10181
+ extractIdsFromKeys(keys) {
10182
+ return keys.map((key) => {
10183
+ const parts = key.split("/");
10184
+ const idPart = parts.find((part) => part.startsWith("id="));
10185
+ return idPart ? idPart.replace("id=", "") : null;
10186
+ }).filter(Boolean);
10187
+ }
10188
+ /**
10189
+ * Process list results with error handling
10190
+ */
10191
+ async processListResults(ids, context = "main") {
10192
+ const { results, errors } = await promisePool.PromisePool.for(ids).withConcurrency(this.parallelism).handleError(async (error, id) => {
10193
+ console.warn(`Failed to get ${context} resource ${id}:`, error.message);
10194
+ return null;
10195
+ }).process(async (id) => {
10196
+ try {
10197
+ return await this.get(id);
10198
+ } catch (error) {
10199
+ return this.handleResourceError(error, id, context);
10200
+ }
10201
+ });
10202
+ return results.filter((item) => item !== null);
10203
+ }
10204
+ /**
10205
+ * Process partition results with error handling
10206
+ */
10207
+ async processPartitionResults(ids, partition, partitionDef, keys) {
10208
+ const sortedFields = Object.entries(partitionDef.fields).sort(([a], [b]) => a.localeCompare(b));
10209
+ const { results, errors } = await promisePool.PromisePool.for(ids).withConcurrency(this.parallelism).handleError(async (error, id) => {
10210
+ console.warn(`Failed to get partition resource ${id}:`, error.message);
10211
+ return null;
10212
+ }).process(async (id) => {
10213
+ try {
10214
+ const actualPartitionValues = this.extractPartitionValuesFromKey(id, keys, sortedFields);
10215
+ return await this.getFromPartition({
10216
+ id,
10217
+ partitionName: partition,
10218
+ partitionValues: actualPartitionValues
10219
+ });
10220
+ } catch (error) {
10221
+ return this.handleResourceError(error, id, "partition");
10222
+ }
10223
+ });
10224
+ return results.filter((item) => item !== null);
10225
+ }
10226
+ /**
10227
+ * Extract partition values from S3 key for specific ID
10228
+ */
10229
+ extractPartitionValuesFromKey(id, keys, sortedFields) {
10230
+ const keyForId = keys.find((key) => key.includes(`id=${id}`));
10231
+ if (!keyForId) {
10232
+ throw new Error(`Partition key not found for ID ${id}`);
10233
+ }
10234
+ const keyParts = keyForId.split("/");
10235
+ const actualPartitionValues = {};
10236
+ for (const [fieldName] of sortedFields) {
10237
+ const fieldPart = keyParts.find((part) => part.startsWith(`${fieldName}=`));
10238
+ if (fieldPart) {
10239
+ const value = fieldPart.replace(`${fieldName}=`, "");
10240
+ actualPartitionValues[fieldName] = value;
10241
+ }
10242
+ }
10243
+ return actualPartitionValues;
10244
+ }
10245
+ /**
10246
+ * Handle resource-specific errors
10247
+ */
10248
+ handleResourceError(error, id, context) {
10249
+ if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
10250
+ console.warn(`Decryption failed for ${context} resource ${id}, returning basic info`);
10251
+ return {
10252
+ id,
10253
+ _decryptionFailed: true,
10254
+ _error: error.message,
10255
+ ...context === "partition" && { _partition: context }
10256
+ };
10257
+ }
10258
+ throw error;
10259
+ }
10260
+ /**
10261
+ * Handle list method errors
10262
+ */
10263
+ handleListError(error, { partition, partitionValues }) {
10264
+ if (error.message.includes("Partition '") && error.message.includes("' not found")) {
10265
+ console.warn(`Partition error in list method:`, error.message);
10266
+ this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
10267
+ return [];
10268
+ }
10269
+ console.error(`Critical error in list method:`, error.message);
10270
+ this.emit("list", { partition, partitionValues, count: 0, errors: 1 });
10271
+ return [];
10272
+ }
10273
+ /**
10274
+ * Get multiple resources by their IDs
10275
+ * @param {string[]} ids - Array of resource IDs
10276
+ * @returns {Promise<Object[]>} Array of resource objects
10277
+ * @example
10278
+ * const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
10279
+ * users.forEach(user => console.log(user.name));
10280
+ */
10281
+ async getMany(ids) {
10282
+ const { results, errors } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
10283
+ console.warn(`Failed to get resource ${id}:`, error.message);
10284
+ return {
10285
+ id,
10286
+ _error: error.message,
10287
+ _decryptionFailed: error.message.includes("Cipher job failed") || error.message.includes("OperationError")
10288
+ };
10289
+ }).process(async (id) => {
10290
+ this.emit("id", id);
10291
+ try {
10292
+ const data = await this.get(id);
10293
+ this.emit("data", data);
10294
+ return data;
10295
+ } catch (error) {
10296
+ if (error.message.includes("Cipher job failed") || error.message.includes("OperationError")) {
10297
+ console.warn(`Decryption failed for ${id}, returning basic info`);
10298
+ return {
10299
+ id,
10300
+ _decryptionFailed: true,
10301
+ _error: error.message
10302
+ };
10303
+ }
10304
+ throw error;
10305
+ }
10306
+ });
10307
+ this.emit("getMany", ids.length);
10308
+ return results;
10309
+ }
10310
+ /**
10311
+ * Get all resources (equivalent to list() without pagination)
10312
+ * @returns {Promise<Object[]>} Array of all resource objects
10313
+ * @example
10314
+ * const allUsers = await resource.getAll();
10315
+ * console.log(`Total users: ${allUsers.length}`);
10316
+ */
10317
+ async getAll() {
10064
10318
  let ids = await this.listIds();
10065
10319
  if (ids.length === 0) return [];
10066
10320
  const { results, errors } = await promisePool.PromisePool.for(ids).withConcurrency(this.client.parallelism).handleError(async (error, id) => {
@@ -10362,21 +10616,14 @@ ${validation.errors.join("\n")}`);
10362
10616
  for (const [partitionName, partition] of Object.entries(partitions)) {
10363
10617
  const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
10364
10618
  if (partitionKey) {
10365
- const mappedData = await this.schema.mapper(data);
10366
- const behaviorImpl = getBehavior(this.behavior);
10367
- const { mappedData: processedMetadata, body } = await behaviorImpl.handleInsert({
10368
- resource: this,
10369
- data,
10370
- mappedData
10371
- });
10372
10619
  const partitionMetadata = {
10373
- ...processedMetadata,
10374
- _version: this.version
10620
+ _v: String(this.version)
10375
10621
  };
10376
10622
  await this.client.putObject({
10377
10623
  key: partitionKey,
10378
10624
  metadata: partitionMetadata,
10379
- body
10625
+ body: "",
10626
+ contentType: void 0
10380
10627
  });
10381
10628
  }
10382
10629
  }
@@ -10533,25 +10780,14 @@ ${validation.errors.join("\n")}`);
10533
10780
  }
10534
10781
  if (newPartitionKey) {
10535
10782
  try {
10536
- const mappedData = await this.schema.mapper(newData);
10537
- if (mappedData.undefined !== void 0) delete mappedData.undefined;
10538
- const behaviorImpl = getBehavior(this.behavior);
10539
- const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10540
- resource: this,
10541
- id,
10542
- data: newData,
10543
- mappedData
10544
- });
10545
- if (processedMetadata.undefined !== void 0) delete processedMetadata.undefined;
10546
10783
  const partitionMetadata = {
10547
- ...processedMetadata,
10548
- _version: this.version
10784
+ _v: String(this.version)
10549
10785
  };
10550
- if (partitionMetadata.undefined !== void 0) delete partitionMetadata.undefined;
10551
10786
  await this.client.putObject({
10552
10787
  key: newPartitionKey,
10553
10788
  metadata: partitionMetadata,
10554
- body
10789
+ body: "",
10790
+ contentType: void 0
10555
10791
  });
10556
10792
  } catch (error) {
10557
10793
  console.warn(`New partition object could not be created for ${partitionName}:`, error.message);
@@ -10559,25 +10795,14 @@ ${validation.errors.join("\n")}`);
10559
10795
  }
10560
10796
  } else if (newPartitionKey) {
10561
10797
  try {
10562
- const mappedData = await this.schema.mapper(newData);
10563
- if (mappedData.undefined !== void 0) delete mappedData.undefined;
10564
- const behaviorImpl = getBehavior(this.behavior);
10565
- const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10566
- resource: this,
10567
- id,
10568
- data: newData,
10569
- mappedData
10570
- });
10571
- if (processedMetadata.undefined !== void 0) delete processedMetadata.undefined;
10572
10798
  const partitionMetadata = {
10573
- ...processedMetadata,
10574
- _version: this.version
10799
+ _v: String(this.version)
10575
10800
  };
10576
- if (partitionMetadata.undefined !== void 0) delete partitionMetadata.undefined;
10577
10801
  await this.client.putObject({
10578
10802
  key: newPartitionKey,
10579
10803
  metadata: partitionMetadata,
10580
- body
10804
+ body: "",
10805
+ contentType: void 0
10581
10806
  });
10582
10807
  } catch (error) {
10583
10808
  console.warn(`Partition object could not be updated for ${partitionName}:`, error.message);
@@ -10600,23 +10825,15 @@ ${validation.errors.join("\n")}`);
10600
10825
  }
10601
10826
  const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
10602
10827
  if (partitionKey) {
10603
- const mappedData = await this.schema.mapper(data);
10604
- const behaviorImpl = getBehavior(this.behavior);
10605
- const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
10606
- resource: this,
10607
- id: data.id,
10608
- data,
10609
- mappedData
10610
- });
10611
10828
  const partitionMetadata = {
10612
- ...processedMetadata,
10613
- _version: this.version
10829
+ _v: String(this.version)
10614
10830
  };
10615
10831
  try {
10616
10832
  await this.client.putObject({
10617
10833
  key: partitionKey,
10618
10834
  metadata: partitionMetadata,
10619
- body
10835
+ body: "",
10836
+ contentType: void 0
10620
10837
  });
10621
10838
  } catch (error) {
10622
10839
  console.warn(`Partition object could not be updated for ${partitionName}:`, error.message);
@@ -10666,40 +10883,75 @@ ${validation.errors.join("\n")}`);
10666
10883
  throw new Error(`No partition values provided for partition '${partitionName}'`);
10667
10884
  }
10668
10885
  const partitionKey = join(`resource=${this.name}`, `partition=${partitionName}`, ...partitionSegments, `id=${id}`);
10669
- const request = await this.client.headObject(partitionKey);
10670
- const objectVersion = request.Metadata?._version || this.version;
10671
- const schema = await this.getSchemaForVersion(objectVersion);
10672
- let metadata = await schema.unmapper(request.Metadata);
10673
- const behaviorImpl = getBehavior(this.behavior);
10674
- let body = "";
10675
- if (request.ContentLength > 0) {
10676
- try {
10677
- const fullObject = await this.client.getObject(partitionKey);
10678
- body = await streamToString(fullObject.Body);
10679
- } catch (error) {
10680
- body = "";
10681
- }
10886
+ try {
10887
+ await this.client.headObject(partitionKey);
10888
+ } catch (error) {
10889
+ throw new Error(`Resource with id '${id}' not found in partition '${partitionName}'`);
10682
10890
  }
10683
- const { metadata: processedMetadata } = await behaviorImpl.handleGet({
10684
- resource: this,
10685
- metadata,
10686
- body
10687
- });
10688
- let data = processedMetadata;
10689
- data.id = id;
10690
- data._contentLength = request.ContentLength;
10691
- data._lastModified = request.LastModified;
10692
- data._hasContent = request.ContentLength > 0;
10693
- data._mimeType = request.ContentType || null;
10891
+ const data = await this.get(id);
10694
10892
  data._partition = partitionName;
10695
10893
  data._partitionValues = partitionValues;
10696
- if (request.VersionId) data._versionId = request.VersionId;
10697
- if (request.Expiration) data._expiresAt = request.Expiration;
10698
- data._definitionHash = this.getDefinitionHash();
10699
- if (data.undefined !== void 0) delete data.undefined;
10700
10894
  this.emit("getFromPartition", data);
10701
10895
  return data;
10702
10896
  }
10897
+ /**
10898
+ * Create a historical version of an object
10899
+ * @param {string} id - Resource ID
10900
+ * @param {Object} data - Object data to store historically
10901
+ */
10902
+ async createHistoricalVersion(id, data) {
10903
+ const historicalKey = join(`resource=${this.name}`, `historical`, `id=${id}`);
10904
+ const historicalData = {
10905
+ ...data,
10906
+ _v: data._v || this.version,
10907
+ _historicalTimestamp: (/* @__PURE__ */ new Date()).toISOString()
10908
+ };
10909
+ const mappedData = await this.schema.mapper(historicalData);
10910
+ const behaviorImpl = getBehavior(this.behavior);
10911
+ const { mappedData: processedMetadata, body } = await behaviorImpl.handleInsert({
10912
+ resource: this,
10913
+ data: historicalData,
10914
+ mappedData
10915
+ });
10916
+ const finalMetadata = {
10917
+ ...processedMetadata,
10918
+ _v: data._v || this.version,
10919
+ _historicalTimestamp: historicalData._historicalTimestamp
10920
+ };
10921
+ let contentType = void 0;
10922
+ if (body && body !== "") {
10923
+ try {
10924
+ JSON.parse(body);
10925
+ contentType = "application/json";
10926
+ } catch {
10927
+ }
10928
+ }
10929
+ await this.client.putObject({
10930
+ key: historicalKey,
10931
+ metadata: finalMetadata,
10932
+ body,
10933
+ contentType
10934
+ });
10935
+ }
10936
+ /**
10937
+ * Apply version mapping to convert an object from one version to another
10938
+ * @param {Object} data - Object data to map
10939
+ * @param {string} fromVersion - Source version
10940
+ * @param {string} toVersion - Target version
10941
+ * @returns {Object} Mapped object data
10942
+ */
10943
+ async applyVersionMapping(data, fromVersion, toVersion) {
10944
+ if (fromVersion === toVersion) {
10945
+ return data;
10946
+ }
10947
+ const mappedData = {
10948
+ ...data,
10949
+ _v: toVersion,
10950
+ _originalVersion: fromVersion,
10951
+ _versionMapped: true
10952
+ };
10953
+ return mappedData;
10954
+ }
10703
10955
  }
10704
10956
  function validateResourceConfig(config) {
10705
10957
  const errors = [];
@@ -10745,6 +10997,20 @@ function validateResourceConfig(config) {
10745
10997
  errors.push(`Resource '${field}' must be a boolean`);
10746
10998
  }
10747
10999
  }
11000
+ if (config.idGenerator !== void 0) {
11001
+ if (typeof config.idGenerator !== "function" && typeof config.idGenerator !== "number") {
11002
+ errors.push("Resource 'idGenerator' must be a function or a number (size)");
11003
+ } else if (typeof config.idGenerator === "number" && config.idGenerator <= 0) {
11004
+ errors.push("Resource 'idGenerator' size must be greater than 0");
11005
+ }
11006
+ }
11007
+ if (config.idSize !== void 0) {
11008
+ if (typeof config.idSize !== "number" || !Number.isInteger(config.idSize)) {
11009
+ errors.push("Resource 'idSize' must be an integer");
11010
+ } else if (config.idSize <= 0) {
11011
+ errors.push("Resource 'idSize' must be greater than 0");
11012
+ }
11013
+ }
10748
11014
  if (config.partitions !== void 0) {
10749
11015
  if (typeof config.partitions !== "object" || Array.isArray(config.partitions)) {
10750
11016
  errors.push("Resource 'partitions' must be an object");
@@ -10800,7 +11066,7 @@ class Database extends EventEmitter {
10800
11066
  this.version = "1";
10801
11067
  this.s3dbVersion = (() => {
10802
11068
  try {
10803
- return true ? "5.0.0" : "latest";
11069
+ return true ? "6.0.0" : "latest";
10804
11070
  } catch (e) {
10805
11071
  return "latest";
10806
11072
  }
@@ -10811,8 +11077,10 @@ class Database extends EventEmitter {
10811
11077
  this.verbose = options.verbose || false;
10812
11078
  this.parallelism = parseInt(options.parallelism + "") || 10;
10813
11079
  this.plugins = options.plugins || [];
11080
+ this.pluginList = options.plugins || [];
10814
11081
  this.cache = options.cache;
10815
11082
  this.passphrase = options.passphrase || "secret";
11083
+ this.versioningEnabled = options.versioningEnabled || false;
10816
11084
  let connectionString = options.connectionString;
10817
11085
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
10818
11086
  const { bucket, region, accessKeyId, secretAccessKey, endpoint, forcePathStyle } = options;
@@ -10863,7 +11131,7 @@ class Database extends EventEmitter {
10863
11131
  client: this.client,
10864
11132
  version: currentVersion,
10865
11133
  attributes: versionData.attributes,
10866
- behavior: versionData.behavior || "user-management",
11134
+ behavior: versionData.behavior || "user-managed",
10867
11135
  parallelism: this.parallelism,
10868
11136
  passphrase: this.passphrase,
10869
11137
  observers: [this],
@@ -10873,7 +11141,8 @@ class Database extends EventEmitter {
10873
11141
  paranoid: versionData.paranoid !== void 0 ? versionData.paranoid : true,
10874
11142
  allNestedObjectsOptional: versionData.allNestedObjectsOptional !== void 0 ? versionData.allNestedObjectsOptional : true,
10875
11143
  autoDecrypt: versionData.autoDecrypt !== void 0 ? versionData.autoDecrypt : true,
10876
- hooks: versionData.hooks || {}
11144
+ hooks: versionData.hooks || {},
11145
+ versioningEnabled: this.versioningEnabled
10877
11146
  });
10878
11147
  }
10879
11148
  }
@@ -10948,7 +11217,7 @@ class Database extends EventEmitter {
10948
11217
  }
10949
11218
  const hashObj = {
10950
11219
  attributes: stableAttributes,
10951
- behavior: behavior || definition.behavior || "user-management"
11220
+ behavior: behavior || definition.behavior || "user-managed"
10952
11221
  };
10953
11222
  const stableString = jsonStableStringify(hashObj);
10954
11223
  return `sha256:${crypto.createHash("sha256").update(stableString).digest("hex")}`;
@@ -10965,8 +11234,8 @@ class Database extends EventEmitter {
10965
11234
  }
10966
11235
  async startPlugins() {
10967
11236
  const db = this;
10968
- if (!lodashEs.isEmpty(this.plugins)) {
10969
- const plugins = this.plugins.map((p) => lodashEs.isFunction(p) ? new p(this) : p);
11237
+ if (!lodashEs.isEmpty(this.pluginList)) {
11238
+ const plugins = this.pluginList.map((p) => lodashEs.isFunction(p) ? new p(this) : p);
10970
11239
  const setupProms = plugins.map(async (plugin) => {
10971
11240
  if (plugin.beforeSetup) await plugin.beforeSetup();
10972
11241
  await plugin.setup(db);
@@ -10981,6 +11250,20 @@ class Database extends EventEmitter {
10981
11250
  await Promise.all(startProms);
10982
11251
  }
10983
11252
  }
11253
+ /**
11254
+ * Register and setup a plugin
11255
+ * @param {Plugin} plugin - Plugin instance to register
11256
+ * @param {string} [name] - Optional name for the plugin (defaults to plugin.constructor.name)
11257
+ */
11258
+ async usePlugin(plugin, name = null) {
11259
+ const pluginName = name || plugin.constructor.name.replace("Plugin", "").toLowerCase();
11260
+ this.plugins[pluginName] = plugin;
11261
+ if (this.isConnected()) {
11262
+ await plugin.setup(this);
11263
+ await plugin.start();
11264
+ }
11265
+ return plugin;
11266
+ }
10984
11267
  async uploadMetadataFile() {
10985
11268
  const metadata = {
10986
11269
  version: this.version,
@@ -11011,7 +11294,7 @@ class Database extends EventEmitter {
11011
11294
  [version]: {
11012
11295
  hash: definitionHash,
11013
11296
  attributes: resourceDef.attributes,
11014
- behavior: resourceDef.behavior || "user-management",
11297
+ behavior: resourceDef.behavior || "user-managed",
11015
11298
  timestamps: resource.config.timestamps,
11016
11299
  partitions: resource.config.partitions,
11017
11300
  paranoid: resource.config.paranoid,
@@ -11060,7 +11343,7 @@ class Database extends EventEmitter {
11060
11343
  * @param {Object} [config.options] - Resource options (deprecated, use root level parameters)
11061
11344
  * @returns {Object} Result with exists and hash information
11062
11345
  */
11063
- resourceExistsWithSameHash({ name, attributes, behavior = "user-management", options = {} }) {
11346
+ resourceExistsWithSameHash({ name, attributes, behavior = "user-managed", options = {} }) {
11064
11347
  if (!this.resources[name]) {
11065
11348
  return { exists: false, sameHash: false, hash: null };
11066
11349
  }
@@ -11073,6 +11356,7 @@ class Database extends EventEmitter {
11073
11356
  client: this.client,
11074
11357
  version: existingResource.version,
11075
11358
  passphrase: this.passphrase,
11359
+ versioningEnabled: this.versioningEnabled,
11076
11360
  ...options
11077
11361
  });
11078
11362
  const newHash = this.generateDefinitionHash(mockResource.export());
@@ -11083,7 +11367,7 @@ class Database extends EventEmitter {
11083
11367
  existingHash
11084
11368
  };
11085
11369
  }
11086
- async createResource({ name, attributes, behavior = "user-management", hooks, ...config }) {
11370
+ async createResource({ name, attributes, behavior = "user-managed", hooks, ...config }) {
11087
11371
  if (this.resources[name]) {
11088
11372
  const existingResource = this.resources[name];
11089
11373
  Object.assign(existingResource.config, {
@@ -11093,6 +11377,7 @@ class Database extends EventEmitter {
11093
11377
  if (behavior) {
11094
11378
  existingResource.behavior = behavior;
11095
11379
  }
11380
+ existingResource.versioningEnabled = this.versioningEnabled;
11096
11381
  existingResource.updateAttributes(attributes);
11097
11382
  if (hooks) {
11098
11383
  for (const [event, hooksArr] of Object.entries(hooks)) {
@@ -11127,6 +11412,7 @@ class Database extends EventEmitter {
11127
11412
  passphrase: this.passphrase,
11128
11413
  cache: this.cache,
11129
11414
  hooks,
11415
+ versioningEnabled: this.versioningEnabled,
11130
11416
  ...config
11131
11417
  });
11132
11418
  this.resources[name] = resource;
@@ -17593,12 +17879,117 @@ class S3Cache extends Cache {
17593
17879
  }
17594
17880
 
17595
17881
  class Plugin extends EventEmitter {
17882
+ constructor(options = {}) {
17883
+ super();
17884
+ this.name = this.constructor.name;
17885
+ this.options = options;
17886
+ this.hooks = /* @__PURE__ */ new Map();
17887
+ }
17596
17888
  async setup(database) {
17889
+ this.database = database;
17890
+ this.beforeSetup();
17891
+ await this.onSetup();
17892
+ this.afterSetup();
17597
17893
  }
17598
17894
  async start() {
17895
+ this.beforeStart();
17896
+ await this.onStart();
17897
+ this.afterStart();
17599
17898
  }
17600
17899
  async stop() {
17900
+ this.beforeStop();
17901
+ await this.onStop();
17902
+ this.afterStop();
17903
+ }
17904
+ // Override these methods in subclasses
17905
+ async onSetup() {
17906
+ }
17907
+ async onStart() {
17908
+ }
17909
+ async onStop() {
17910
+ }
17911
+ // Hook management methods
17912
+ addHook(resource, event, handler) {
17913
+ if (!this.hooks.has(resource)) {
17914
+ this.hooks.set(resource, /* @__PURE__ */ new Map());
17915
+ }
17916
+ const resourceHooks = this.hooks.get(resource);
17917
+ if (!resourceHooks.has(event)) {
17918
+ resourceHooks.set(event, []);
17919
+ }
17920
+ resourceHooks.get(event).push(handler);
17921
+ }
17922
+ removeHook(resource, event, handler) {
17923
+ const resourceHooks = this.hooks.get(resource);
17924
+ if (resourceHooks && resourceHooks.has(event)) {
17925
+ const handlers = resourceHooks.get(event);
17926
+ const index = handlers.indexOf(handler);
17927
+ if (index > -1) {
17928
+ handlers.splice(index, 1);
17929
+ }
17930
+ }
17931
+ }
17932
+ // Enhanced resource method wrapping that supports multiple plugins
17933
+ wrapResourceMethod(resource, methodName, wrapper) {
17934
+ const originalMethod = resource[methodName];
17935
+ if (!resource._pluginWrappers) {
17936
+ resource._pluginWrappers = /* @__PURE__ */ new Map();
17937
+ }
17938
+ if (!resource._pluginWrappers.has(methodName)) {
17939
+ resource._pluginWrappers.set(methodName, []);
17940
+ }
17941
+ resource._pluginWrappers.get(methodName).push(wrapper);
17942
+ if (!resource[`_wrapped_${methodName}`]) {
17943
+ resource[`_wrapped_${methodName}`] = originalMethod;
17944
+ const isJestMock = originalMethod && originalMethod._isMockFunction;
17945
+ resource[methodName] = async function(...args) {
17946
+ let result = await resource[`_wrapped_${methodName}`](...args);
17947
+ for (const wrapper2 of resource._pluginWrappers.get(methodName)) {
17948
+ result = await wrapper2.call(this, result, args, methodName);
17949
+ }
17950
+ return result;
17951
+ };
17952
+ if (isJestMock) {
17953
+ Object.setPrototypeOf(resource[methodName], Object.getPrototypeOf(originalMethod));
17954
+ Object.assign(resource[methodName], originalMethod);
17955
+ }
17956
+ }
17957
+ }
17958
+ // Partition-aware helper methods
17959
+ getPartitionValues(data, resource) {
17960
+ if (!resource.config?.partitions) return {};
17961
+ const partitionValues = {};
17962
+ for (const [partitionName, partitionDef] of Object.entries(resource.config.partitions)) {
17963
+ if (partitionDef.fields) {
17964
+ partitionValues[partitionName] = {};
17965
+ for (const [fieldName, rule] of Object.entries(partitionDef.fields)) {
17966
+ const value = this.getNestedFieldValue(data, fieldName);
17967
+ if (value !== null && value !== void 0) {
17968
+ partitionValues[partitionName][fieldName] = resource.applyPartitionRule(value, rule);
17969
+ }
17970
+ }
17971
+ } else {
17972
+ partitionValues[partitionName] = {};
17973
+ }
17974
+ }
17975
+ return partitionValues;
17976
+ }
17977
+ getNestedFieldValue(data, fieldPath) {
17978
+ if (!fieldPath.includes(".")) {
17979
+ return data[fieldPath] ?? null;
17980
+ }
17981
+ const keys = fieldPath.split(".");
17982
+ let value = data;
17983
+ for (const key of keys) {
17984
+ if (value && typeof value === "object" && key in value) {
17985
+ value = value[key];
17986
+ } else {
17987
+ return null;
17988
+ }
17989
+ }
17990
+ return value ?? null;
17601
17991
  }
17992
+ // Event emission methods
17602
17993
  beforeSetup() {
17603
17994
  this.emit("plugin.beforeSetup", /* @__PURE__ */ new Date());
17604
17995
  }
@@ -17628,208 +18019,2871 @@ const PluginObject = {
17628
18019
  }
17629
18020
  };
17630
18021
 
17631
- const CostsPlugin = {
17632
- async setup(db) {
17633
- this.client = db.client;
17634
- this.map = {
17635
- PutObjectCommand: "put",
17636
- GetObjectCommand: "get",
17637
- HeadObjectCommand: "get",
17638
- DeleteObjectCommand: "delete",
17639
- DeleteObjectsCommand: "delete",
17640
- ListObjectsV2Command: "list"
18022
+ class AuditPlugin extends Plugin {
18023
+ constructor(options = {}) {
18024
+ super(options);
18025
+ this.auditResource = null;
18026
+ this.config = {
18027
+ enabled: options.enabled !== false,
18028
+ includeData: options.includeData !== false,
18029
+ includePartitions: options.includePartitions !== false,
18030
+ maxDataSize: options.maxDataSize || 1e4,
18031
+ // 10KB limit
18032
+ ...options
17641
18033
  };
17642
- this.costs = {
17643
- total: 0,
17644
- prices: {
17645
- put: 5e-3 / 1e3,
17646
- copy: 5e-3 / 1e3,
17647
- list: 5e-3 / 1e3,
17648
- post: 5e-3 / 1e3,
17649
- get: 4e-4 / 1e3,
17650
- select: 4e-4 / 1e3,
17651
- delete: 4e-4 / 1e3
17652
- },
17653
- requests: {
17654
- total: 0,
17655
- put: 0,
17656
- post: 0,
17657
- copy: 0,
17658
- list: 0,
17659
- get: 0,
17660
- select: 0,
17661
- delete: 0
17662
- },
17663
- events: {
17664
- total: 0,
17665
- PutObjectCommand: 0,
17666
- GetObjectCommand: 0,
17667
- HeadObjectCommand: 0,
17668
- DeleteObjectCommand: 0,
17669
- DeleteObjectsCommand: 0,
17670
- ListObjectsV2Command: 0
18034
+ }
18035
+ async onSetup() {
18036
+ if (!this.config.enabled) {
18037
+ this.auditResource = null;
18038
+ return;
18039
+ }
18040
+ try {
18041
+ this.auditResource = await this.database.createResource({
18042
+ name: "audits",
18043
+ attributes: {
18044
+ id: "string|required",
18045
+ resourceName: "string|required",
18046
+ operation: "string|required",
18047
+ recordId: "string|required",
18048
+ userId: "string|optional",
18049
+ timestamp: "string|required",
18050
+ oldData: "string|optional",
18051
+ newData: "string|optional",
18052
+ partition: "string|optional",
18053
+ partitionValues: "string|optional",
18054
+ metadata: "string|optional"
18055
+ }
18056
+ });
18057
+ } catch (error) {
18058
+ try {
18059
+ this.auditResource = this.database.resources.audits;
18060
+ } catch (innerError) {
18061
+ this.auditResource = null;
18062
+ return;
18063
+ }
18064
+ }
18065
+ this.installDatabaseProxy();
18066
+ this.installResourceHooks();
18067
+ }
18068
+ async onStart() {
18069
+ }
18070
+ async onStop() {
18071
+ }
18072
+ installDatabaseProxy() {
18073
+ if (this.database._auditProxyInstalled) {
18074
+ return;
18075
+ }
18076
+ const installResourceHooksForResource = this.installResourceHooksForResource.bind(this);
18077
+ this.database._originalCreateResource = this.database.createResource;
18078
+ this.database.createResource = async function(...args) {
18079
+ const resource = await this._originalCreateResource(...args);
18080
+ if (resource.name !== "audit_logs") {
18081
+ installResourceHooksForResource(resource);
17671
18082
  }
18083
+ return resource;
17672
18084
  };
17673
- this.client.costs = JSON.parse(JSON.stringify(this.costs));
17674
- },
17675
- async start() {
17676
- this.client.on("command.response", (name) => this.addRequest(name, this.map[name]));
17677
- },
17678
- addRequest(name, method) {
17679
- this.costs.events[name]++;
17680
- this.costs.events.total++;
17681
- this.costs.requests.total++;
17682
- this.costs.requests[method]++;
17683
- this.costs.total += this.costs.prices[method];
17684
- this.client.costs.events[name]++;
17685
- this.client.costs.events.total++;
17686
- this.client.costs.requests.total++;
17687
- this.client.costs.requests[method]++;
17688
- this.client.costs.total += this.client.costs.prices[method];
18085
+ this.database._auditProxyInstalled = true;
18086
+ }
18087
+ installResourceHooks() {
18088
+ for (const resource of Object.values(this.database.resources)) {
18089
+ if (resource.name === "audits") continue;
18090
+ this.installResourceHooksForResource(resource);
18091
+ }
18092
+ }
18093
+ installResourceHooksForResource(resource) {
18094
+ this.wrapResourceMethod(resource, "insert", async (result, args, methodName) => {
18095
+ const [data] = args;
18096
+ const recordId = data.id || result.id || "auto-generated";
18097
+ const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
18098
+ const auditRecord = {
18099
+ id: `audit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
18100
+ resourceName: resource.name,
18101
+ operation: "insert",
18102
+ recordId,
18103
+ userId: this.getCurrentUserId?.() || "system",
18104
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
18105
+ oldData: null,
18106
+ newData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(data)),
18107
+ partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
18108
+ partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
18109
+ metadata: JSON.stringify({
18110
+ source: "audit-plugin",
18111
+ version: "2.0"
18112
+ })
18113
+ };
18114
+ this.logAudit(auditRecord).catch(console.error);
18115
+ return result;
18116
+ });
18117
+ this.wrapResourceMethod(resource, "update", async (result, args, methodName) => {
18118
+ const [id, data] = args;
18119
+ let oldData = null;
18120
+ if (this.config.includeData) {
18121
+ try {
18122
+ oldData = await resource.get(id);
18123
+ } catch (error) {
18124
+ }
18125
+ }
18126
+ const partitionValues = this.config.includePartitions ? this.getPartitionValues(result, resource) : null;
18127
+ const auditRecord = {
18128
+ id: `audit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
18129
+ resourceName: resource.name,
18130
+ operation: "update",
18131
+ recordId: id,
18132
+ userId: this.getCurrentUserId?.() || "system",
18133
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
18134
+ oldData: oldData && this.config.includeData === false ? null : oldData ? JSON.stringify(this.truncateData(oldData)) : null,
18135
+ newData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(result)),
18136
+ partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
18137
+ partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
18138
+ metadata: JSON.stringify({
18139
+ source: "audit-plugin",
18140
+ version: "2.0"
18141
+ })
18142
+ };
18143
+ this.logAudit(auditRecord).catch(console.error);
18144
+ return result;
18145
+ });
18146
+ this.wrapResourceMethod(resource, "delete", async (result, args, methodName) => {
18147
+ const [id] = args;
18148
+ let oldData = null;
18149
+ if (this.config.includeData) {
18150
+ try {
18151
+ oldData = await resource.get(id);
18152
+ } catch (error) {
18153
+ }
18154
+ }
18155
+ const partitionValues = oldData && this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null;
18156
+ const auditRecord = {
18157
+ id: `audit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
18158
+ resourceName: resource.name,
18159
+ operation: "delete",
18160
+ recordId: id,
18161
+ userId: this.getCurrentUserId?.() || "system",
18162
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
18163
+ oldData: oldData && this.config.includeData === false ? null : oldData ? JSON.stringify(this.truncateData(oldData)) : null,
18164
+ newData: null,
18165
+ partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
18166
+ partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
18167
+ metadata: JSON.stringify({
18168
+ source: "audit-plugin",
18169
+ version: "2.0"
18170
+ })
18171
+ };
18172
+ this.logAudit(auditRecord).catch(console.error);
18173
+ return result;
18174
+ });
18175
+ this.wrapResourceMethod(resource, "deleteMany", async (result, args, methodName) => {
18176
+ const [ids] = args;
18177
+ const auditRecords = [];
18178
+ if (this.config.includeData) {
18179
+ for (const id of ids) {
18180
+ try {
18181
+ const oldData = await resource.get(id);
18182
+ const partitionValues = this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null;
18183
+ auditRecords.push({
18184
+ id: `audit-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
18185
+ resourceName: resource.name,
18186
+ operation: "delete",
18187
+ recordId: id,
18188
+ userId: this.getCurrentUserId?.() || "system",
18189
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
18190
+ oldData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(oldData)),
18191
+ newData: null,
18192
+ partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
18193
+ partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
18194
+ metadata: JSON.stringify({
18195
+ source: "audit-plugin",
18196
+ version: "2.0",
18197
+ batchOperation: true
18198
+ })
18199
+ });
18200
+ } catch (error) {
18201
+ }
18202
+ }
18203
+ }
18204
+ for (const auditRecord of auditRecords) {
18205
+ this.logAudit(auditRecord).catch(console.error);
18206
+ }
18207
+ return result;
18208
+ });
17689
18209
  }
17690
- };
18210
+ getPartitionValues(data, resource) {
18211
+ const partitions = resource.config?.partitions || {};
18212
+ const partitionValues = {};
18213
+ for (const [partitionName, partitionDef] of Object.entries(partitions)) {
18214
+ if (partitionDef.fields) {
18215
+ const partitionData = {};
18216
+ for (const [fieldName, fieldRule] of Object.entries(partitionDef.fields)) {
18217
+ const fieldValue = this.getNestedFieldValue(data, fieldName);
18218
+ if (fieldValue !== void 0 && fieldValue !== null) {
18219
+ partitionData[fieldName] = fieldValue;
18220
+ }
18221
+ }
18222
+ if (Object.keys(partitionData).length > 0) {
18223
+ partitionValues[partitionName] = partitionData;
18224
+ }
18225
+ }
18226
+ }
18227
+ return partitionValues;
18228
+ }
18229
+ getNestedFieldValue(data, fieldPath) {
18230
+ if (!fieldPath.includes(".")) {
18231
+ return data[fieldPath];
18232
+ }
18233
+ const keys = fieldPath.split(".");
18234
+ let currentLevel = data;
18235
+ for (const key of keys) {
18236
+ if (!currentLevel || typeof currentLevel !== "object" || !(key in currentLevel)) {
18237
+ return void 0;
18238
+ }
18239
+ currentLevel = currentLevel[key];
18240
+ }
18241
+ return currentLevel;
18242
+ }
18243
+ getPrimaryPartition(partitionValues) {
18244
+ if (!partitionValues) return null;
18245
+ const partitionNames = Object.keys(partitionValues);
18246
+ return partitionNames.length > 0 ? partitionNames[0] : null;
18247
+ }
18248
+ async logAudit(auditRecord) {
18249
+ if (!this.auditResource) return;
18250
+ try {
18251
+ await this.auditResource.insert(auditRecord);
18252
+ } catch (error) {
18253
+ console.error("Failed to log audit record:", error);
18254
+ if (error && error.stack) console.error(error.stack);
18255
+ }
18256
+ }
18257
+ truncateData(data) {
18258
+ if (!data) return data;
18259
+ const dataStr = JSON.stringify(data);
18260
+ if (dataStr.length <= this.config.maxDataSize) {
18261
+ return data;
18262
+ }
18263
+ return {
18264
+ ...data,
18265
+ _truncated: true,
18266
+ _originalSize: dataStr.length,
18267
+ _truncatedAt: (/* @__PURE__ */ new Date()).toISOString()
18268
+ };
18269
+ }
18270
+ // Utility methods for querying audit logs
18271
+ async getAuditLogs(options = {}) {
18272
+ if (!this.auditResource) return [];
18273
+ try {
18274
+ const {
18275
+ resourceName,
18276
+ operation,
18277
+ recordId,
18278
+ userId,
18279
+ partition,
18280
+ startDate,
18281
+ endDate,
18282
+ limit = 100,
18283
+ offset = 0
18284
+ } = options;
18285
+ const allAudits = await this.auditResource.getAll();
18286
+ let filtered = allAudits.filter((audit) => {
18287
+ if (resourceName && audit.resourceName !== resourceName) return false;
18288
+ if (operation && audit.operation !== operation) return false;
18289
+ if (recordId && audit.recordId !== recordId) return false;
18290
+ if (userId && audit.userId !== userId) return false;
18291
+ if (partition && audit.partition !== partition) return false;
18292
+ if (startDate && new Date(audit.timestamp) < new Date(startDate)) return false;
18293
+ if (endDate && new Date(audit.timestamp) > new Date(endDate)) return false;
18294
+ return true;
18295
+ });
18296
+ filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
18297
+ const deserialized = filtered.slice(offset, offset + limit).map((audit) => ({
18298
+ ...audit,
18299
+ oldData: audit.oldData ? JSON.parse(audit.oldData) : null,
18300
+ newData: audit.newData ? JSON.parse(audit.newData) : null,
18301
+ partitionValues: audit.partitionValues ? JSON.parse(audit.partitionValues) : null,
18302
+ metadata: audit.metadata ? JSON.parse(audit.metadata) : null
18303
+ }));
18304
+ return deserialized;
18305
+ } catch (error) {
18306
+ console.error("Failed to get audit logs:", error);
18307
+ if (error && error.stack) console.error(error.stack);
18308
+ return [];
18309
+ }
18310
+ }
18311
+ async getRecordHistory(resourceName, recordId) {
18312
+ return this.getAuditLogs({
18313
+ resourceName,
18314
+ recordId,
18315
+ limit: 1e3
18316
+ });
18317
+ }
18318
+ async getPartitionHistory(resourceName, partitionName, partitionValues) {
18319
+ return this.getAuditLogs({
18320
+ resourceName,
18321
+ partition: partitionName,
18322
+ limit: 1e3
18323
+ });
18324
+ }
18325
+ async getAuditStats(options = {}) {
18326
+ const {
18327
+ resourceName,
18328
+ startDate,
18329
+ endDate
18330
+ } = options;
18331
+ const allAudits = await this.getAuditLogs({
18332
+ resourceName,
18333
+ startDate,
18334
+ endDate,
18335
+ limit: 1e4
18336
+ });
18337
+ const stats = {
18338
+ total: allAudits.length,
18339
+ byOperation: {},
18340
+ byResource: {},
18341
+ byPartition: {},
18342
+ byUser: {},
18343
+ timeline: {}
18344
+ };
18345
+ for (const audit of allAudits) {
18346
+ stats.byOperation[audit.operation] = (stats.byOperation[audit.operation] || 0) + 1;
18347
+ stats.byResource[audit.resourceName] = (stats.byResource[audit.resourceName] || 0) + 1;
18348
+ if (audit.partition) {
18349
+ stats.byPartition[audit.partition] = (stats.byPartition[audit.partition] || 0) + 1;
18350
+ }
18351
+ stats.byUser[audit.userId] = (stats.byUser[audit.userId] || 0) + 1;
18352
+ const day = audit.timestamp.split("T")[0];
18353
+ stats.timeline[day] = (stats.timeline[day] || 0) + 1;
18354
+ }
18355
+ return stats;
18356
+ }
18357
+ }
17691
18358
 
17692
18359
  class CachePlugin extends Plugin {
17693
18360
  constructor(options = {}) {
17694
- super();
18361
+ super(options);
17695
18362
  this.driver = options.driver;
18363
+ this.config = {
18364
+ enabled: options.enabled !== false,
18365
+ includePartitions: options.includePartitions !== false,
18366
+ ...options
18367
+ };
17696
18368
  }
17697
18369
  async setup(database) {
17698
- this.database = database;
17699
- if (!this.driver) this.driver = new S3Cache({
17700
- keyPrefix: "cache",
17701
- client: database.client
17702
- });
17703
- this.installDatabaseProxy();
17704
- for (const resource of Object.values(database.resources)) {
17705
- this.installResourcesProxies(resource);
18370
+ if (!this.config.enabled) {
18371
+ return;
17706
18372
  }
18373
+ await super.setup(database);
17707
18374
  }
17708
- async start() {
18375
+ async onSetup() {
18376
+ if (this.config.driver) {
18377
+ this.driver = this.config.driver;
18378
+ } else if (this.config.driverType === "memory") {
18379
+ this.driver = new MemoryCache(this.config.memoryOptions || {});
18380
+ } else {
18381
+ this.driver = new S3Cache(this.config.s3Options || {});
18382
+ }
18383
+ this.installDatabaseProxy();
18384
+ this.installResourceHooks();
17709
18385
  }
17710
- async stop() {
18386
+ async onStart() {
18387
+ }
18388
+ async onStop() {
17711
18389
  }
17712
18390
  installDatabaseProxy() {
17713
- const installResourcesProxies = this.installResourcesProxies.bind(this);
17714
- this.database._createResource = this.database.createResource;
18391
+ if (this.database._cacheProxyInstalled) {
18392
+ return;
18393
+ }
18394
+ const installResourceHooks = this.installResourceHooks.bind(this);
18395
+ this.database._originalCreateResourceForCache = this.database.createResource;
17715
18396
  this.database.createResource = async function(...args) {
17716
- const resource = await this._createResource(...args);
17717
- installResourcesProxies(resource);
18397
+ const resource = await this._originalCreateResourceForCache(...args);
18398
+ installResourceHooks(resource);
17718
18399
  return resource;
17719
18400
  };
18401
+ this.database._cacheProxyInstalled = true;
18402
+ }
18403
+ installResourceHooks() {
18404
+ for (const resource of Object.values(this.database.resources)) {
18405
+ this.installResourceHooksForResource(resource);
18406
+ }
17720
18407
  }
17721
- installResourcesProxies(resource) {
18408
+ installResourceHooksForResource(resource) {
18409
+ if (!this.driver) return;
17722
18410
  resource.cache = this.driver;
17723
- let keyPrefix = `resource=${resource.name}`;
17724
- if (this.driver.keyPrefix) this.driver.keyPrefix = join(this.driver.keyPrefix, keyPrefix);
17725
- resource.cacheKeyFor = async function({
17726
- params = {},
17727
- action = "list"
17728
- }) {
17729
- let key = Object.keys(params).sort().map((x) => `${x}:${params[x]}`).join("|") || "empty";
17730
- key = await sha256(key);
17731
- key = join(keyPrefix, `action=${action}`, `${key}.json.gz`);
17732
- return key;
18411
+ resource.cacheKeyFor = async (options = {}) => {
18412
+ const { action, params = {}, partition, partitionValues } = options;
18413
+ return this.generateCacheKey(resource, action, params, partition, partitionValues);
17733
18414
  };
17734
- resource._count = resource.count;
17735
- resource._listIds = resource.listIds;
17736
- resource._getMany = resource.getMany;
17737
- resource._getAll = resource.getAll;
17738
- resource._page = resource.page;
17739
- resource.count = async function() {
17740
- const key = await this.cacheKeyFor({ action: "count" });
18415
+ resource._originalCount = resource.count;
18416
+ resource._originalListIds = resource.listIds;
18417
+ resource._originalGetMany = resource.getMany;
18418
+ resource._originalGetAll = resource.getAll;
18419
+ resource._originalPage = resource.page;
18420
+ resource._originalList = resource.list;
18421
+ resource.count = async function(options = {}) {
18422
+ const { partition, partitionValues } = options;
18423
+ const key = await resource.cacheKeyFor({
18424
+ action: "count",
18425
+ partition,
18426
+ partitionValues
18427
+ });
17741
18428
  try {
17742
- const cached = await this.cache.get(key);
17743
- if (cached) return cached;
18429
+ const cached = await resource.cache.get(key);
18430
+ if (cached !== null && cached !== void 0) return cached;
17744
18431
  } catch (err) {
17745
18432
  if (err.name !== "NoSuchKey") throw err;
17746
18433
  }
17747
- const data = await resource._count();
17748
- await this.cache.set(key, data);
17749
- return data;
18434
+ const result = await resource._originalCount(options);
18435
+ await resource.cache.set(key, result);
18436
+ return result;
17750
18437
  };
17751
- resource.listIds = async function() {
17752
- const key = await this.cacheKeyFor({ action: "listIds" });
18438
+ resource.listIds = async function(options = {}) {
18439
+ const { partition, partitionValues } = options;
18440
+ const key = await resource.cacheKeyFor({
18441
+ action: "listIds",
18442
+ partition,
18443
+ partitionValues
18444
+ });
17753
18445
  try {
17754
- const cached = await this.cache.get(key);
17755
- if (cached) return cached;
18446
+ const cached = await resource.cache.get(key);
18447
+ if (cached !== null && cached !== void 0) return cached;
17756
18448
  } catch (err) {
17757
18449
  if (err.name !== "NoSuchKey") throw err;
17758
18450
  }
17759
- const data = await resource._listIds();
17760
- await this.cache.set(key, data);
17761
- return data;
18451
+ const result = await resource._originalListIds(options);
18452
+ await resource.cache.set(key, result);
18453
+ return result;
17762
18454
  };
17763
18455
  resource.getMany = async function(ids) {
17764
- const key = await this.cacheKeyFor({
18456
+ const key = await resource.cacheKeyFor({
17765
18457
  action: "getMany",
17766
18458
  params: { ids }
17767
18459
  });
17768
18460
  try {
17769
- const cached = await this.cache.get(key);
17770
- if (cached) return cached;
18461
+ const cached = await resource.cache.get(key);
18462
+ if (cached !== null && cached !== void 0) return cached;
17771
18463
  } catch (err) {
17772
18464
  if (err.name !== "NoSuchKey") throw err;
17773
18465
  }
17774
- const data = await resource._getMany(ids);
17775
- await this.cache.set(key, data);
17776
- return data;
18466
+ const result = await resource._originalGetMany(ids);
18467
+ await resource.cache.set(key, result);
18468
+ return result;
17777
18469
  };
17778
18470
  resource.getAll = async function() {
17779
- const key = await this.cacheKeyFor({ action: "getAll" });
18471
+ const key = await resource.cacheKeyFor({ action: "getAll" });
17780
18472
  try {
17781
- const cached = await this.cache.get(key);
17782
- if (cached) return cached;
18473
+ const cached = await resource.cache.get(key);
18474
+ if (cached !== null && cached !== void 0) return cached;
17783
18475
  } catch (err) {
17784
18476
  if (err.name !== "NoSuchKey") throw err;
17785
18477
  }
17786
- const data = await resource._getAll();
17787
- await this.cache.set(key, data);
17788
- return data;
18478
+ const result = await resource._originalGetAll();
18479
+ await resource.cache.set(key, result);
18480
+ return result;
17789
18481
  };
17790
- resource.page = async function({ offset, size }) {
17791
- const key = await this.cacheKeyFor({
18482
+ resource.page = async function({ offset, size, partition, partitionValues } = {}) {
18483
+ const key = await resource.cacheKeyFor({
17792
18484
  action: "page",
17793
- params: { offset, size }
18485
+ params: { offset, size },
18486
+ partition,
18487
+ partitionValues
17794
18488
  });
17795
18489
  try {
17796
- const cached = await this.cache.get(key);
17797
- if (cached) return cached;
18490
+ const cached = await resource.cache.get(key);
18491
+ if (cached !== null && cached !== void 0) return cached;
17798
18492
  } catch (err) {
17799
18493
  if (err.name !== "NoSuchKey") throw err;
17800
18494
  }
17801
- const data = await resource._page({ offset, size });
17802
- await this.cache.set(key, data);
17803
- return data;
18495
+ const result = await resource._originalPage({ offset, size, partition, partitionValues });
18496
+ await resource.cache.set(key, result);
18497
+ return result;
17804
18498
  };
17805
- resource._insert = resource.insert;
17806
- resource._update = resource.update;
17807
- resource._delete = resource.delete;
17808
- resource._deleteMany = resource.deleteMany;
17809
- resource.insert = async function(...args) {
17810
- const data = await resource._insert(...args);
17811
- await this.cache.clear(keyPrefix);
17812
- return data;
18499
+ resource.list = async function(options = {}) {
18500
+ const { partition, partitionValues } = options;
18501
+ const key = await resource.cacheKeyFor({
18502
+ action: "list",
18503
+ partition,
18504
+ partitionValues
18505
+ });
18506
+ try {
18507
+ const cached = await resource.cache.get(key);
18508
+ if (cached !== null && cached !== void 0) return cached;
18509
+ } catch (err) {
18510
+ if (err.name !== "NoSuchKey") throw err;
18511
+ }
18512
+ const result = await resource._originalList(options);
18513
+ await resource.cache.set(key, result);
18514
+ return result;
17813
18515
  };
17814
- resource.update = async function(...args) {
17815
- const data = await resource._update(...args);
17816
- await this.cache.clear(keyPrefix);
17817
- return data;
18516
+ this.wrapResourceMethod(resource, "insert", async (result, args, methodName) => {
18517
+ const [data] = args;
18518
+ await this.clearCacheForResource(resource, data);
18519
+ return result;
18520
+ });
18521
+ this.wrapResourceMethod(resource, "update", async (result, args, methodName) => {
18522
+ const [id, data] = args;
18523
+ await this.clearCacheForResource(resource, { id, ...data });
18524
+ return result;
18525
+ });
18526
+ this.wrapResourceMethod(resource, "delete", async (result, args, methodName) => {
18527
+ const [id] = args;
18528
+ let data = { id };
18529
+ if (typeof resource.get === "function") {
18530
+ try {
18531
+ const full = await resource.get(id);
18532
+ if (full) data = full;
18533
+ } catch {
18534
+ }
18535
+ }
18536
+ await this.clearCacheForResource(resource, data);
18537
+ return result;
18538
+ });
18539
+ this.wrapResourceMethod(resource, "deleteMany", async (result, args, methodName) => {
18540
+ const [ids] = args;
18541
+ for (const id of ids) {
18542
+ let data = { id };
18543
+ if (typeof resource.get === "function") {
18544
+ try {
18545
+ const full = await resource.get(id);
18546
+ if (full) data = full;
18547
+ } catch {
18548
+ }
18549
+ }
18550
+ await this.clearCacheForResource(resource, data);
18551
+ }
18552
+ return result;
18553
+ });
18554
+ }
18555
+ async clearCacheForResource(resource, data) {
18556
+ if (!resource.cache) return;
18557
+ const keyPrefix = `resource=${resource.name}`;
18558
+ await resource.cache.clear(keyPrefix);
18559
+ if (this.config.includePartitions === true && resource.config?.partitions && Object.keys(resource.config.partitions).length > 0) {
18560
+ const partitionValues = this.getPartitionValues(data, resource);
18561
+ for (const [partitionName, values] of Object.entries(partitionValues)) {
18562
+ if (values && Object.keys(values).length > 0 && Object.values(values).some((v) => v !== null && v !== void 0)) {
18563
+ const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
18564
+ await resource.cache.clear(partitionKeyPrefix);
18565
+ }
18566
+ }
18567
+ }
18568
+ }
18569
+ async generateCacheKey(resource, action, params = {}, partition = null, partitionValues = null) {
18570
+ const keyParts = [
18571
+ `resource=${resource.name}`,
18572
+ `action=${action}`
18573
+ ];
18574
+ if (partition && partitionValues && Object.keys(partitionValues).length > 0) {
18575
+ keyParts.push(`partition:${partition}`);
18576
+ for (const [field, value] of Object.entries(partitionValues)) {
18577
+ if (value !== null && value !== void 0) {
18578
+ keyParts.push(`${field}:${value}`);
18579
+ }
18580
+ }
18581
+ }
18582
+ if (Object.keys(params).length > 0) {
18583
+ const paramsHash = await this.hashParams(params);
18584
+ keyParts.push(paramsHash);
18585
+ }
18586
+ return join(...keyParts) + ".json.gz";
18587
+ }
18588
+ async hashParams(params) {
18589
+ const sortedParams = Object.keys(params).sort().map((key) => `${key}:${params[key]}`).join("|") || "empty";
18590
+ return await sha256(sortedParams);
18591
+ }
18592
+ // Utility methods
18593
+ async getCacheStats() {
18594
+ if (!this.driver) return null;
18595
+ return {
18596
+ size: await this.driver.size(),
18597
+ keys: await this.driver.keys(),
18598
+ driver: this.driver.constructor.name
17818
18599
  };
17819
- resource.delete = async function(...args) {
17820
- const data = await resource._delete(...args);
17821
- await this.cache.clear(keyPrefix);
17822
- return data;
18600
+ }
18601
+ async clearAllCache() {
18602
+ if (!this.driver) return;
18603
+ for (const resource of Object.values(this.database.resources)) {
18604
+ if (resource.cache) {
18605
+ const keyPrefix = `resource=${resource.name}`;
18606
+ await resource.cache.clear(keyPrefix);
18607
+ }
18608
+ }
18609
+ }
18610
+ async warmCache(resourceName, options = {}) {
18611
+ const resource = this.database.resources[resourceName];
18612
+ if (!resource) {
18613
+ throw new Error(`Resource '${resourceName}' not found`);
18614
+ }
18615
+ const { includePartitions = true } = options;
18616
+ await resource.getAll();
18617
+ if (includePartitions && resource.config.partitions) {
18618
+ for (const [partitionName, partitionDef] of Object.entries(resource.config.partitions)) {
18619
+ if (partitionDef.fields) {
18620
+ const allRecords = await resource.getAll();
18621
+ const recordsArray = Array.isArray(allRecords) ? allRecords : [];
18622
+ const partitionValues = /* @__PURE__ */ new Set();
18623
+ for (const record of recordsArray.slice(0, 10)) {
18624
+ const values = this.getPartitionValues(record, resource);
18625
+ if (values[partitionName]) {
18626
+ partitionValues.add(JSON.stringify(values[partitionName]));
18627
+ }
18628
+ }
18629
+ for (const partitionValueStr of partitionValues) {
18630
+ const partitionValues2 = JSON.parse(partitionValueStr);
18631
+ await resource.list({ partition: partitionName, partitionValues: partitionValues2 });
18632
+ }
18633
+ }
18634
+ }
18635
+ }
18636
+ }
18637
+ }
18638
+
18639
+ const CostsPlugin = {
18640
+ async setup(db) {
18641
+ if (!db || !db.client) {
18642
+ return;
18643
+ }
18644
+ this.client = db.client;
18645
+ this.map = {
18646
+ PutObjectCommand: "put",
18647
+ GetObjectCommand: "get",
18648
+ HeadObjectCommand: "head",
18649
+ DeleteObjectCommand: "delete",
18650
+ DeleteObjectsCommand: "delete",
18651
+ ListObjectsV2Command: "list"
17823
18652
  };
17824
- resource.deleteMany = async function(...args) {
17825
- const data = await resource._deleteMany(...args);
17826
- await this.cache.clear(keyPrefix);
17827
- return data;
18653
+ this.costs = {
18654
+ total: 0,
18655
+ prices: {
18656
+ put: 5e-3 / 1e3,
18657
+ copy: 5e-3 / 1e3,
18658
+ list: 5e-3 / 1e3,
18659
+ post: 5e-3 / 1e3,
18660
+ get: 4e-4 / 1e3,
18661
+ select: 4e-4 / 1e3,
18662
+ delete: 4e-4 / 1e3,
18663
+ head: 4e-4 / 1e3
18664
+ },
18665
+ requests: {
18666
+ total: 0,
18667
+ put: 0,
18668
+ post: 0,
18669
+ copy: 0,
18670
+ list: 0,
18671
+ get: 0,
18672
+ select: 0,
18673
+ delete: 0,
18674
+ head: 0
18675
+ },
18676
+ events: {
18677
+ total: 0,
18678
+ PutObjectCommand: 0,
18679
+ GetObjectCommand: 0,
18680
+ HeadObjectCommand: 0,
18681
+ DeleteObjectCommand: 0,
18682
+ DeleteObjectsCommand: 0,
18683
+ ListObjectsV2Command: 0
18684
+ }
18685
+ };
18686
+ this.client.costs = JSON.parse(JSON.stringify(this.costs));
18687
+ },
18688
+ async start() {
18689
+ if (this.client) {
18690
+ this.client.on("command.response", (name) => this.addRequest(name, this.map[name]));
18691
+ this.client.on("command.error", (name) => this.addRequest(name, this.map[name]));
18692
+ }
18693
+ },
18694
+ addRequest(name, method) {
18695
+ if (!method) return;
18696
+ this.costs.events[name]++;
18697
+ this.costs.events.total++;
18698
+ this.costs.requests.total++;
18699
+ this.costs.requests[method]++;
18700
+ this.costs.total += this.costs.prices[method];
18701
+ if (this.client && this.client.costs) {
18702
+ this.client.costs.events[name]++;
18703
+ this.client.costs.events.total++;
18704
+ this.client.costs.requests.total++;
18705
+ this.client.costs.requests[method]++;
18706
+ this.client.costs.total += this.client.costs.prices[method];
18707
+ }
18708
+ }
18709
+ };
18710
+
18711
+ class FullTextPlugin extends Plugin {
18712
+ constructor(options = {}) {
18713
+ super();
18714
+ this.indexResource = null;
18715
+ this.config = {
18716
+ enabled: options.enabled !== false,
18717
+ minWordLength: options.minWordLength || 3,
18718
+ maxResults: options.maxResults || 100,
18719
+ ...options
17828
18720
  };
18721
+ this.indexes = /* @__PURE__ */ new Map();
18722
+ }
18723
+ async setup(database) {
18724
+ this.database = database;
18725
+ if (!this.config.enabled) return;
18726
+ try {
18727
+ this.indexResource = await database.createResource({
18728
+ name: "fulltext_indexes",
18729
+ attributes: {
18730
+ id: "string|required",
18731
+ resourceName: "string|required",
18732
+ fieldName: "string|required",
18733
+ word: "string|required",
18734
+ recordIds: "json|required",
18735
+ // Array of record IDs containing this word
18736
+ count: "number|required",
18737
+ lastUpdated: "string|required"
18738
+ }
18739
+ });
18740
+ } catch (error) {
18741
+ this.indexResource = database.resources.fulltext_indexes;
18742
+ }
18743
+ await this.loadIndexes();
18744
+ this.installIndexingHooks();
18745
+ }
18746
+ async start() {
18747
+ }
18748
+ async stop() {
18749
+ await this.saveIndexes();
18750
+ }
18751
+ async loadIndexes() {
18752
+ if (!this.indexResource) return;
18753
+ try {
18754
+ const allIndexes = await this.indexResource.getAll();
18755
+ for (const indexRecord of allIndexes) {
18756
+ const key = `${indexRecord.resourceName}:${indexRecord.fieldName}:${indexRecord.word}`;
18757
+ this.indexes.set(key, {
18758
+ recordIds: indexRecord.recordIds || [],
18759
+ count: indexRecord.count || 0
18760
+ });
18761
+ }
18762
+ } catch (error) {
18763
+ console.warn("Failed to load existing indexes:", error.message);
18764
+ }
18765
+ }
18766
+ async saveIndexes() {
18767
+ if (!this.indexResource) return;
18768
+ try {
18769
+ const existingIndexes = await this.indexResource.getAll();
18770
+ for (const index of existingIndexes) {
18771
+ await this.indexResource.delete(index.id);
18772
+ }
18773
+ for (const [key, data] of this.indexes.entries()) {
18774
+ const [resourceName, fieldName, word] = key.split(":");
18775
+ await this.indexResource.insert({
18776
+ id: `index-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
18777
+ resourceName,
18778
+ fieldName,
18779
+ word,
18780
+ recordIds: data.recordIds,
18781
+ count: data.count,
18782
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
18783
+ });
18784
+ }
18785
+ } catch (error) {
18786
+ console.error("Failed to save indexes:", error);
18787
+ }
18788
+ }
18789
+ installIndexingHooks() {
18790
+ if (!this.database.plugins) {
18791
+ this.database.plugins = {};
18792
+ }
18793
+ this.database.plugins.fulltext = this;
18794
+ for (const resource of Object.values(this.database.resources)) {
18795
+ if (resource.name === "fulltext_indexes") continue;
18796
+ this.installResourceHooks(resource);
18797
+ }
18798
+ if (!this.database._fulltextProxyInstalled) {
18799
+ this.database._previousCreateResourceForFullText = this.database.createResource;
18800
+ this.database.createResource = async function(...args) {
18801
+ const resource = await this._previousCreateResourceForFullText(...args);
18802
+ if (this.plugins?.fulltext && resource.name !== "fulltext_indexes") {
18803
+ this.plugins.fulltext.installResourceHooks(resource);
18804
+ }
18805
+ return resource;
18806
+ };
18807
+ this.database._fulltextProxyInstalled = true;
18808
+ }
18809
+ for (const resource of Object.values(this.database.resources)) {
18810
+ if (resource.name !== "fulltext_indexes") {
18811
+ this.installResourceHooks(resource);
18812
+ }
18813
+ }
18814
+ }
18815
+ installResourceHooks(resource) {
18816
+ resource._insert = resource.insert;
18817
+ resource._update = resource.update;
18818
+ resource._delete = resource.delete;
18819
+ resource._deleteMany = resource.deleteMany;
18820
+ this.wrapResourceMethod(resource, "insert", async (result, args, methodName) => {
18821
+ const [data] = args;
18822
+ this.indexRecord(resource.name, result.id, data).catch(console.error);
18823
+ return result;
18824
+ });
18825
+ this.wrapResourceMethod(resource, "update", async (result, args, methodName) => {
18826
+ const [id, data] = args;
18827
+ this.removeRecordFromIndex(resource.name, id).catch(console.error);
18828
+ this.indexRecord(resource.name, id, result).catch(console.error);
18829
+ return result;
18830
+ });
18831
+ this.wrapResourceMethod(resource, "delete", async (result, args, methodName) => {
18832
+ const [id] = args;
18833
+ this.removeRecordFromIndex(resource.name, id).catch(console.error);
18834
+ return result;
18835
+ });
18836
+ this.wrapResourceMethod(resource, "deleteMany", async (result, args, methodName) => {
18837
+ const [ids] = args;
18838
+ for (const id of ids) {
18839
+ this.removeRecordFromIndex(resource.name, id).catch(console.error);
18840
+ }
18841
+ return result;
18842
+ });
18843
+ }
18844
+ async indexRecord(resourceName, recordId, data) {
18845
+ const indexedFields = this.getIndexedFields(resourceName);
18846
+ if (!indexedFields || indexedFields.length === 0) return;
18847
+ for (const fieldName of indexedFields) {
18848
+ const fieldValue = this.getFieldValue(data, fieldName);
18849
+ if (!fieldValue) continue;
18850
+ const words = this.tokenize(fieldValue);
18851
+ for (const word of words) {
18852
+ if (word.length < this.config.minWordLength) continue;
18853
+ const key = `${resourceName}:${fieldName}:${word.toLowerCase()}`;
18854
+ const existing = this.indexes.get(key) || { recordIds: [], count: 0 };
18855
+ if (!existing.recordIds.includes(recordId)) {
18856
+ existing.recordIds.push(recordId);
18857
+ existing.count = existing.recordIds.length;
18858
+ }
18859
+ this.indexes.set(key, existing);
18860
+ }
18861
+ }
18862
+ }
18863
+ async removeRecordFromIndex(resourceName, recordId) {
18864
+ for (const [key, data] of this.indexes.entries()) {
18865
+ if (key.startsWith(`${resourceName}:`)) {
18866
+ const index = data.recordIds.indexOf(recordId);
18867
+ if (index > -1) {
18868
+ data.recordIds.splice(index, 1);
18869
+ data.count = data.recordIds.length;
18870
+ if (data.recordIds.length === 0) {
18871
+ this.indexes.delete(key);
18872
+ } else {
18873
+ this.indexes.set(key, data);
18874
+ }
18875
+ }
18876
+ }
18877
+ }
18878
+ }
18879
+ getFieldValue(data, fieldPath) {
18880
+ if (!fieldPath.includes(".")) {
18881
+ return data[fieldPath];
18882
+ }
18883
+ const keys = fieldPath.split(".");
18884
+ let value = data;
18885
+ for (const key of keys) {
18886
+ if (value && typeof value === "object" && key in value) {
18887
+ value = value[key];
18888
+ } else {
18889
+ return null;
18890
+ }
18891
+ }
18892
+ return value;
18893
+ }
18894
+ tokenize(text) {
18895
+ if (!text) return [];
18896
+ const str = String(text).toLowerCase();
18897
+ return str.replace(/[^\w\s\u00C0-\u017F]/g, " ").split(/\s+/).filter((word) => word.length > 0);
18898
+ }
18899
+ getIndexedFields(resourceName) {
18900
+ if (this.config.fields) {
18901
+ return this.config.fields;
18902
+ }
18903
+ const fieldMappings = {
18904
+ users: ["name", "email"],
18905
+ products: ["name", "description"],
18906
+ articles: ["title", "content"]
18907
+ // Add more mappings as needed
18908
+ };
18909
+ return fieldMappings[resourceName] || [];
18910
+ }
18911
+ // Main search method
18912
+ async search(resourceName, query, options = {}) {
18913
+ const {
18914
+ fields = null,
18915
+ // Specific fields to search in
18916
+ limit = this.config.maxResults,
18917
+ offset = 0,
18918
+ exactMatch = false
18919
+ } = options;
18920
+ if (!query || query.trim().length === 0) {
18921
+ return [];
18922
+ }
18923
+ const searchWords = this.tokenize(query);
18924
+ const results = /* @__PURE__ */ new Map();
18925
+ const searchFields = fields || this.getIndexedFields(resourceName);
18926
+ if (searchFields.length === 0) {
18927
+ return [];
18928
+ }
18929
+ for (const word of searchWords) {
18930
+ if (word.length < this.config.minWordLength) continue;
18931
+ for (const fieldName of searchFields) {
18932
+ if (exactMatch) {
18933
+ const key = `${resourceName}:${fieldName}:${word.toLowerCase()}`;
18934
+ const indexData = this.indexes.get(key);
18935
+ if (indexData) {
18936
+ for (const recordId of indexData.recordIds) {
18937
+ const currentScore = results.get(recordId) || 0;
18938
+ results.set(recordId, currentScore + 1);
18939
+ }
18940
+ }
18941
+ } else {
18942
+ for (const [key, indexData] of this.indexes.entries()) {
18943
+ if (key.startsWith(`${resourceName}:${fieldName}:${word.toLowerCase()}`)) {
18944
+ for (const recordId of indexData.recordIds) {
18945
+ const currentScore = results.get(recordId) || 0;
18946
+ results.set(recordId, currentScore + 1);
18947
+ }
18948
+ }
18949
+ }
18950
+ }
18951
+ }
18952
+ }
18953
+ const sortedResults = Array.from(results.entries()).map(([recordId, score]) => ({ recordId, score })).sort((a, b) => b.score - a.score).slice(offset, offset + limit);
18954
+ return sortedResults;
18955
+ }
18956
+ // Search and return full records
18957
+ async searchRecords(resourceName, query, options = {}) {
18958
+ const searchResults = await this.search(resourceName, query, options);
18959
+ if (searchResults.length === 0) {
18960
+ return [];
18961
+ }
18962
+ const resource = this.database.resources[resourceName];
18963
+ if (!resource) {
18964
+ throw new Error(`Resource '${resourceName}' not found`);
18965
+ }
18966
+ const recordIds = searchResults.map((result) => result.recordId);
18967
+ const records = await resource.getMany(recordIds);
18968
+ return records.map((record) => {
18969
+ const searchResult = searchResults.find((sr) => sr.recordId === record.id);
18970
+ return {
18971
+ ...record,
18972
+ _searchScore: searchResult ? searchResult.score : 0
18973
+ };
18974
+ }).sort((a, b) => b._searchScore - a._searchScore);
18975
+ }
18976
+ // Utility methods
18977
+ async rebuildIndex(resourceName) {
18978
+ const resource = this.database.resources[resourceName];
18979
+ if (!resource) {
18980
+ throw new Error(`Resource '${resourceName}' not found`);
18981
+ }
18982
+ for (const [key] of this.indexes.entries()) {
18983
+ if (key.startsWith(`${resourceName}:`)) {
18984
+ this.indexes.delete(key);
18985
+ }
18986
+ }
18987
+ const allRecords = await resource.getAll();
18988
+ const batchSize = 100;
18989
+ for (let i = 0; i < allRecords.length; i += batchSize) {
18990
+ const batch = allRecords.slice(i, i + batchSize);
18991
+ for (const record of batch) {
18992
+ await this.indexRecord(resourceName, record.id, record);
18993
+ }
18994
+ }
18995
+ await this.saveIndexes();
18996
+ }
18997
+ async getIndexStats() {
18998
+ const stats = {
18999
+ totalIndexes: this.indexes.size,
19000
+ resources: {},
19001
+ totalWords: 0
19002
+ };
19003
+ for (const [key, data] of this.indexes.entries()) {
19004
+ const [resourceName, fieldName] = key.split(":");
19005
+ if (!stats.resources[resourceName]) {
19006
+ stats.resources[resourceName] = {
19007
+ fields: {},
19008
+ totalRecords: /* @__PURE__ */ new Set(),
19009
+ totalWords: 0
19010
+ };
19011
+ }
19012
+ if (!stats.resources[resourceName].fields[fieldName]) {
19013
+ stats.resources[resourceName].fields[fieldName] = {
19014
+ words: 0,
19015
+ totalOccurrences: 0
19016
+ };
19017
+ }
19018
+ stats.resources[resourceName].fields[fieldName].words++;
19019
+ stats.resources[resourceName].fields[fieldName].totalOccurrences += data.count;
19020
+ stats.resources[resourceName].totalWords++;
19021
+ for (const recordId of data.recordIds) {
19022
+ stats.resources[resourceName].totalRecords.add(recordId);
19023
+ }
19024
+ stats.totalWords++;
19025
+ }
19026
+ for (const resourceName in stats.resources) {
19027
+ stats.resources[resourceName].totalRecords = stats.resources[resourceName].totalRecords.size;
19028
+ }
19029
+ return stats;
19030
+ }
19031
+ async rebuildAllIndexes({ timeout } = {}) {
19032
+ if (timeout) {
19033
+ return Promise.race([
19034
+ this._rebuildAllIndexesInternal(),
19035
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeout))
19036
+ ]);
19037
+ }
19038
+ return this._rebuildAllIndexesInternal();
19039
+ }
19040
+ async _rebuildAllIndexesInternal() {
19041
+ const resourceNames = Object.keys(this.database.resources).filter((name) => name !== "fulltext_indexes");
19042
+ for (const resourceName of resourceNames) {
19043
+ try {
19044
+ await this.rebuildIndex(resourceName);
19045
+ } catch (error) {
19046
+ console.warn(`Failed to rebuild index for resource ${resourceName}:`, error.message);
19047
+ }
19048
+ }
19049
+ }
19050
+ async clearIndex(resourceName) {
19051
+ for (const [key] of this.indexes.entries()) {
19052
+ if (key.startsWith(`${resourceName}:`)) {
19053
+ this.indexes.delete(key);
19054
+ }
19055
+ }
19056
+ await this.saveIndexes();
19057
+ }
19058
+ async clearAllIndexes() {
19059
+ this.indexes.clear();
19060
+ await this.saveIndexes();
19061
+ }
19062
+ }
19063
+
19064
+ class MetricsPlugin extends Plugin {
19065
+ constructor(options = {}) {
19066
+ super();
19067
+ this.config = {
19068
+ enabled: options.enabled !== false,
19069
+ collectPerformance: options.collectPerformance !== false,
19070
+ collectErrors: options.collectErrors !== false,
19071
+ collectUsage: options.collectUsage !== false,
19072
+ retentionDays: options.retentionDays || 30,
19073
+ flushInterval: options.flushInterval || 6e4,
19074
+ // 1 minute
19075
+ ...options
19076
+ };
19077
+ this.metrics = {
19078
+ operations: {
19079
+ insert: { count: 0, totalTime: 0, errors: 0 },
19080
+ update: { count: 0, totalTime: 0, errors: 0 },
19081
+ delete: { count: 0, totalTime: 0, errors: 0 },
19082
+ get: { count: 0, totalTime: 0, errors: 0 },
19083
+ list: { count: 0, totalTime: 0, errors: 0 },
19084
+ count: { count: 0, totalTime: 0, errors: 0 }
19085
+ },
19086
+ resources: {},
19087
+ errors: [],
19088
+ performance: [],
19089
+ startTime: (/* @__PURE__ */ new Date()).toISOString()
19090
+ };
19091
+ this.flushTimer = null;
19092
+ }
19093
+ async setup(database) {
19094
+ this.database = database;
19095
+ if (!this.config.enabled || process.env.NODE_ENV === "test") return;
19096
+ try {
19097
+ this.metricsResource = await database.createResource({
19098
+ name: "metrics",
19099
+ attributes: {
19100
+ id: "string|required",
19101
+ type: "string|required",
19102
+ // 'operation', 'error', 'performance'
19103
+ resourceName: "string",
19104
+ operation: "string",
19105
+ count: "number|required",
19106
+ totalTime: "number|required",
19107
+ errors: "number|required",
19108
+ avgTime: "number|required",
19109
+ timestamp: "string|required",
19110
+ metadata: "json"
19111
+ }
19112
+ });
19113
+ this.errorsResource = await database.createResource({
19114
+ name: "error_logs",
19115
+ attributes: {
19116
+ id: "string|required",
19117
+ resourceName: "string|required",
19118
+ operation: "string|required",
19119
+ error: "string|required",
19120
+ timestamp: "string|required",
19121
+ metadata: "json"
19122
+ }
19123
+ });
19124
+ this.performanceResource = await database.createResource({
19125
+ name: "performance_logs",
19126
+ attributes: {
19127
+ id: "string|required",
19128
+ resourceName: "string|required",
19129
+ operation: "string|required",
19130
+ duration: "number|required",
19131
+ timestamp: "string|required",
19132
+ metadata: "json"
19133
+ }
19134
+ });
19135
+ } catch (error) {
19136
+ this.metricsResource = database.resources.metrics;
19137
+ this.errorsResource = database.resources.error_logs;
19138
+ this.performanceResource = database.resources.performance_logs;
19139
+ }
19140
+ this.installMetricsHooks();
19141
+ if (process.env.NODE_ENV !== "test") {
19142
+ this.startFlushTimer();
19143
+ }
19144
+ }
19145
+ async start() {
19146
+ }
19147
+ async stop() {
19148
+ if (this.flushTimer) {
19149
+ clearInterval(this.flushTimer);
19150
+ this.flushTimer = null;
19151
+ }
19152
+ if (process.env.NODE_ENV !== "test") {
19153
+ await this.flushMetrics();
19154
+ }
19155
+ }
19156
+ installMetricsHooks() {
19157
+ for (const resource of Object.values(this.database.resources)) {
19158
+ if (["metrics", "error_logs", "performance_logs"].includes(resource.name)) {
19159
+ continue;
19160
+ }
19161
+ this.installResourceHooks(resource);
19162
+ }
19163
+ this.database._createResource = this.database.createResource;
19164
+ this.database.createResource = async function(...args) {
19165
+ const resource = await this._createResource(...args);
19166
+ if (this.plugins?.metrics && !["metrics", "error_logs", "performance_logs"].includes(resource.name)) {
19167
+ this.plugins.metrics.installResourceHooks(resource);
19168
+ }
19169
+ return resource;
19170
+ };
19171
+ }
19172
+ installResourceHooks(resource) {
19173
+ resource._insert = resource.insert;
19174
+ resource._update = resource.update;
19175
+ resource._delete = resource.delete;
19176
+ resource._deleteMany = resource.deleteMany;
19177
+ resource._get = resource.get;
19178
+ resource._getMany = resource.getMany;
19179
+ resource._getAll = resource.getAll;
19180
+ resource._list = resource.list;
19181
+ resource._listIds = resource.listIds;
19182
+ resource._count = resource.count;
19183
+ resource._page = resource.page;
19184
+ resource.insert = async function(...args) {
19185
+ const startTime = Date.now();
19186
+ try {
19187
+ const result = await resource._insert(...args);
19188
+ this.recordOperation(resource.name, "insert", Date.now() - startTime, false);
19189
+ return result;
19190
+ } catch (error) {
19191
+ this.recordOperation(resource.name, "insert", Date.now() - startTime, true);
19192
+ this.recordError(resource.name, "insert", error);
19193
+ throw error;
19194
+ }
19195
+ }.bind(this);
19196
+ resource.update = async function(...args) {
19197
+ const startTime = Date.now();
19198
+ try {
19199
+ const result = await resource._update(...args);
19200
+ this.recordOperation(resource.name, "update", Date.now() - startTime, false);
19201
+ return result;
19202
+ } catch (error) {
19203
+ this.recordOperation(resource.name, "update", Date.now() - startTime, true);
19204
+ this.recordError(resource.name, "update", error);
19205
+ throw error;
19206
+ }
19207
+ }.bind(this);
19208
+ resource.delete = async function(...args) {
19209
+ const startTime = Date.now();
19210
+ try {
19211
+ const result = await resource._delete(...args);
19212
+ this.recordOperation(resource.name, "delete", Date.now() - startTime, false);
19213
+ return result;
19214
+ } catch (error) {
19215
+ this.recordOperation(resource.name, "delete", Date.now() - startTime, true);
19216
+ this.recordError(resource.name, "delete", error);
19217
+ throw error;
19218
+ }
19219
+ }.bind(this);
19220
+ resource.deleteMany = async function(...args) {
19221
+ const startTime = Date.now();
19222
+ try {
19223
+ const result = await resource._deleteMany(...args);
19224
+ this.recordOperation(resource.name, "delete", Date.now() - startTime, false);
19225
+ return result;
19226
+ } catch (error) {
19227
+ this.recordOperation(resource.name, "delete", Date.now() - startTime, true);
19228
+ this.recordError(resource.name, "delete", error);
19229
+ throw error;
19230
+ }
19231
+ }.bind(this);
19232
+ resource.get = async function(...args) {
19233
+ const startTime = Date.now();
19234
+ try {
19235
+ const result = await resource._get(...args);
19236
+ this.recordOperation(resource.name, "get", Date.now() - startTime, false);
19237
+ return result;
19238
+ } catch (error) {
19239
+ this.recordOperation(resource.name, "get", Date.now() - startTime, true);
19240
+ this.recordError(resource.name, "get", error);
19241
+ throw error;
19242
+ }
19243
+ }.bind(this);
19244
+ resource.getMany = async function(...args) {
19245
+ const startTime = Date.now();
19246
+ try {
19247
+ const result = await resource._getMany(...args);
19248
+ this.recordOperation(resource.name, "get", Date.now() - startTime, false);
19249
+ return result;
19250
+ } catch (error) {
19251
+ this.recordOperation(resource.name, "get", Date.now() - startTime, true);
19252
+ this.recordError(resource.name, "get", error);
19253
+ throw error;
19254
+ }
19255
+ }.bind(this);
19256
+ resource.getAll = async function(...args) {
19257
+ const startTime = Date.now();
19258
+ try {
19259
+ const result = await resource._getAll(...args);
19260
+ this.recordOperation(resource.name, "list", Date.now() - startTime, false);
19261
+ return result;
19262
+ } catch (error) {
19263
+ this.recordOperation(resource.name, "list", Date.now() - startTime, true);
19264
+ this.recordError(resource.name, "list", error);
19265
+ throw error;
19266
+ }
19267
+ }.bind(this);
19268
+ resource.list = async function(...args) {
19269
+ const startTime = Date.now();
19270
+ try {
19271
+ const result = await resource._list(...args);
19272
+ this.recordOperation(resource.name, "list", Date.now() - startTime, false);
19273
+ return result;
19274
+ } catch (error) {
19275
+ this.recordOperation(resource.name, "list", Date.now() - startTime, true);
19276
+ this.recordError(resource.name, "list", error);
19277
+ throw error;
19278
+ }
19279
+ }.bind(this);
19280
+ resource.listIds = async function(...args) {
19281
+ const startTime = Date.now();
19282
+ try {
19283
+ const result = await resource._listIds(...args);
19284
+ this.recordOperation(resource.name, "list", Date.now() - startTime, false);
19285
+ return result;
19286
+ } catch (error) {
19287
+ this.recordOperation(resource.name, "list", Date.now() - startTime, true);
19288
+ this.recordError(resource.name, "list", error);
19289
+ throw error;
19290
+ }
19291
+ }.bind(this);
19292
+ resource.count = async function(...args) {
19293
+ const startTime = Date.now();
19294
+ try {
19295
+ const result = await resource._count(...args);
19296
+ this.recordOperation(resource.name, "count", Date.now() - startTime, false);
19297
+ return result;
19298
+ } catch (error) {
19299
+ this.recordOperation(resource.name, "count", Date.now() - startTime, true);
19300
+ this.recordError(resource.name, "count", error);
19301
+ throw error;
19302
+ }
19303
+ }.bind(this);
19304
+ resource.page = async function(...args) {
19305
+ const startTime = Date.now();
19306
+ try {
19307
+ const result = await resource._page(...args);
19308
+ this.recordOperation(resource.name, "list", Date.now() - startTime, false);
19309
+ return result;
19310
+ } catch (error) {
19311
+ this.recordOperation(resource.name, "list", Date.now() - startTime, true);
19312
+ this.recordError(resource.name, "list", error);
19313
+ throw error;
19314
+ }
19315
+ }.bind(this);
19316
+ }
19317
+ recordOperation(resourceName, operation, duration, isError) {
19318
+ if (this.metrics.operations[operation]) {
19319
+ this.metrics.operations[operation].count++;
19320
+ this.metrics.operations[operation].totalTime += duration;
19321
+ if (isError) {
19322
+ this.metrics.operations[operation].errors++;
19323
+ }
19324
+ }
19325
+ if (!this.metrics.resources[resourceName]) {
19326
+ this.metrics.resources[resourceName] = {
19327
+ insert: { count: 0, totalTime: 0, errors: 0 },
19328
+ update: { count: 0, totalTime: 0, errors: 0 },
19329
+ delete: { count: 0, totalTime: 0, errors: 0 },
19330
+ get: { count: 0, totalTime: 0, errors: 0 },
19331
+ list: { count: 0, totalTime: 0, errors: 0 },
19332
+ count: { count: 0, totalTime: 0, errors: 0 }
19333
+ };
19334
+ }
19335
+ if (this.metrics.resources[resourceName][operation]) {
19336
+ this.metrics.resources[resourceName][operation].count++;
19337
+ this.metrics.resources[resourceName][operation].totalTime += duration;
19338
+ if (isError) {
19339
+ this.metrics.resources[resourceName][operation].errors++;
19340
+ }
19341
+ }
19342
+ if (this.config.collectPerformance) {
19343
+ this.metrics.performance.push({
19344
+ resourceName,
19345
+ operation,
19346
+ duration,
19347
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19348
+ });
19349
+ }
19350
+ }
19351
+ recordError(resourceName, operation, error) {
19352
+ if (!this.config.collectErrors) return;
19353
+ this.metrics.errors.push({
19354
+ resourceName,
19355
+ operation,
19356
+ error: error.message,
19357
+ stack: error.stack,
19358
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
19359
+ });
19360
+ }
19361
+ startFlushTimer() {
19362
+ if (this.flushTimer) {
19363
+ clearInterval(this.flushTimer);
19364
+ }
19365
+ if (this.config.flushInterval > 0) {
19366
+ this.flushTimer = setInterval(() => {
19367
+ this.flushMetrics().catch(console.error);
19368
+ }, this.config.flushInterval);
19369
+ }
19370
+ }
19371
+ async flushMetrics() {
19372
+ if (!this.metricsResource) return;
19373
+ try {
19374
+ const metadata = process.env.NODE_ENV === "test" ? {} : { global: "true" };
19375
+ const perfMetadata = process.env.NODE_ENV === "test" ? {} : { perf: "true" };
19376
+ const errorMetadata = process.env.NODE_ENV === "test" ? {} : { error: "true" };
19377
+ const resourceMetadata = process.env.NODE_ENV === "test" ? {} : { resource: "true" };
19378
+ for (const [operation, data] of Object.entries(this.metrics.operations)) {
19379
+ if (data.count > 0) {
19380
+ await this.metricsResource.insert({
19381
+ id: `metrics-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
19382
+ type: "operation",
19383
+ resourceName: "global",
19384
+ operation,
19385
+ count: data.count,
19386
+ totalTime: data.totalTime,
19387
+ errors: data.errors,
19388
+ avgTime: data.count > 0 ? data.totalTime / data.count : 0,
19389
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
19390
+ metadata
19391
+ });
19392
+ }
19393
+ }
19394
+ for (const [resourceName, operations] of Object.entries(this.metrics.resources)) {
19395
+ for (const [operation, data] of Object.entries(operations)) {
19396
+ if (data.count > 0) {
19397
+ await this.metricsResource.insert({
19398
+ id: `metrics-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
19399
+ type: "operation",
19400
+ resourceName,
19401
+ operation,
19402
+ count: data.count,
19403
+ totalTime: data.totalTime,
19404
+ errors: data.errors,
19405
+ avgTime: data.count > 0 ? data.totalTime / data.count : 0,
19406
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
19407
+ metadata: resourceMetadata
19408
+ });
19409
+ }
19410
+ }
19411
+ }
19412
+ if (this.config.collectPerformance && this.metrics.performance.length > 0) {
19413
+ for (const perf of this.metrics.performance) {
19414
+ await this.performanceResource.insert({
19415
+ id: `perf-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
19416
+ resourceName: perf.resourceName,
19417
+ operation: perf.operation,
19418
+ duration: perf.duration,
19419
+ timestamp: perf.timestamp,
19420
+ metadata: perfMetadata
19421
+ });
19422
+ }
19423
+ }
19424
+ if (this.config.collectErrors && this.metrics.errors.length > 0) {
19425
+ for (const error of this.metrics.errors) {
19426
+ await this.errorsResource.insert({
19427
+ id: `error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
19428
+ resourceName: error.resourceName,
19429
+ operation: error.operation,
19430
+ error: error.error,
19431
+ stack: error.stack,
19432
+ timestamp: error.timestamp,
19433
+ metadata: errorMetadata
19434
+ });
19435
+ }
19436
+ }
19437
+ this.resetMetrics();
19438
+ } catch (error) {
19439
+ console.error("Failed to flush metrics:", error);
19440
+ }
19441
+ }
19442
+ resetMetrics() {
19443
+ for (const operation of Object.keys(this.metrics.operations)) {
19444
+ this.metrics.operations[operation] = { count: 0, totalTime: 0, errors: 0 };
19445
+ }
19446
+ for (const resourceName of Object.keys(this.metrics.resources)) {
19447
+ for (const operation of Object.keys(this.metrics.resources[resourceName])) {
19448
+ this.metrics.resources[resourceName][operation] = { count: 0, totalTime: 0, errors: 0 };
19449
+ }
19450
+ }
19451
+ this.metrics.performance = [];
19452
+ this.metrics.errors = [];
19453
+ }
19454
+ // Utility methods
19455
+ async getMetrics(options = {}) {
19456
+ const {
19457
+ type = "operation",
19458
+ resourceName,
19459
+ operation,
19460
+ startDate,
19461
+ endDate,
19462
+ limit = 100,
19463
+ offset = 0
19464
+ } = options;
19465
+ if (!this.metricsResource) return [];
19466
+ const allMetrics = await this.metricsResource.getAll();
19467
+ let filtered = allMetrics.filter((metric) => {
19468
+ if (type && metric.type !== type) return false;
19469
+ if (resourceName && metric.resourceName !== resourceName) return false;
19470
+ if (operation && metric.operation !== operation) return false;
19471
+ if (startDate && new Date(metric.timestamp) < new Date(startDate)) return false;
19472
+ if (endDate && new Date(metric.timestamp) > new Date(endDate)) return false;
19473
+ return true;
19474
+ });
19475
+ filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
19476
+ return filtered.slice(offset, offset + limit);
19477
+ }
19478
+ async getErrorLogs(options = {}) {
19479
+ if (!this.errorsResource) return [];
19480
+ const {
19481
+ resourceName,
19482
+ operation,
19483
+ startDate,
19484
+ endDate,
19485
+ limit = 100,
19486
+ offset = 0
19487
+ } = options;
19488
+ const allErrors = await this.errorsResource.getAll();
19489
+ let filtered = allErrors.filter((error) => {
19490
+ if (resourceName && error.resourceName !== resourceName) return false;
19491
+ if (operation && error.operation !== operation) return false;
19492
+ if (startDate && new Date(error.timestamp) < new Date(startDate)) return false;
19493
+ if (endDate && new Date(error.timestamp) > new Date(endDate)) return false;
19494
+ return true;
19495
+ });
19496
+ filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
19497
+ return filtered.slice(offset, offset + limit);
19498
+ }
19499
+ async getPerformanceLogs(options = {}) {
19500
+ if (!this.performanceResource) return [];
19501
+ const {
19502
+ resourceName,
19503
+ operation,
19504
+ startDate,
19505
+ endDate,
19506
+ limit = 100,
19507
+ offset = 0
19508
+ } = options;
19509
+ const allPerformance = await this.performanceResource.getAll();
19510
+ let filtered = allPerformance.filter((perf) => {
19511
+ if (resourceName && perf.resourceName !== resourceName) return false;
19512
+ if (operation && perf.operation !== operation) return false;
19513
+ if (startDate && new Date(perf.timestamp) < new Date(startDate)) return false;
19514
+ if (endDate && new Date(perf.timestamp) > new Date(endDate)) return false;
19515
+ return true;
19516
+ });
19517
+ filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
19518
+ return filtered.slice(offset, offset + limit);
19519
+ }
19520
+ async getStats() {
19521
+ const now = /* @__PURE__ */ new Date();
19522
+ const startDate = new Date(now.getTime() - 24 * 60 * 60 * 1e3);
19523
+ const [metrics, errors, performance] = await Promise.all([
19524
+ this.getMetrics({ startDate: startDate.toISOString() }),
19525
+ this.getErrorLogs({ startDate: startDate.toISOString() }),
19526
+ this.getPerformanceLogs({ startDate: startDate.toISOString() })
19527
+ ]);
19528
+ const stats = {
19529
+ period: "24h",
19530
+ totalOperations: 0,
19531
+ totalErrors: errors.length,
19532
+ avgResponseTime: 0,
19533
+ operationsByType: {},
19534
+ resources: {},
19535
+ uptime: {
19536
+ startTime: this.metrics.startTime,
19537
+ duration: now.getTime() - new Date(this.metrics.startTime).getTime()
19538
+ }
19539
+ };
19540
+ for (const metric of metrics) {
19541
+ if (metric.type === "operation") {
19542
+ stats.totalOperations += metric.count;
19543
+ if (!stats.operationsByType[metric.operation]) {
19544
+ stats.operationsByType[metric.operation] = {
19545
+ count: 0,
19546
+ errors: 0,
19547
+ avgTime: 0
19548
+ };
19549
+ }
19550
+ stats.operationsByType[metric.operation].count += metric.count;
19551
+ stats.operationsByType[metric.operation].errors += metric.errors;
19552
+ const current = stats.operationsByType[metric.operation];
19553
+ const totalCount2 = current.count;
19554
+ const newAvg = (current.avgTime * (totalCount2 - metric.count) + metric.totalTime) / totalCount2;
19555
+ current.avgTime = newAvg;
19556
+ }
19557
+ }
19558
+ const totalTime = metrics.reduce((sum, m) => sum + m.totalTime, 0);
19559
+ const totalCount = metrics.reduce((sum, m) => sum + m.count, 0);
19560
+ stats.avgResponseTime = totalCount > 0 ? totalTime / totalCount : 0;
19561
+ return stats;
19562
+ }
19563
+ async cleanupOldData() {
19564
+ const cutoffDate = /* @__PURE__ */ new Date();
19565
+ cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
19566
+ if (this.metricsResource) {
19567
+ const oldMetrics = await this.getMetrics({ endDate: cutoffDate.toISOString() });
19568
+ for (const metric of oldMetrics) {
19569
+ await this.metricsResource.delete(metric.id);
19570
+ }
19571
+ }
19572
+ if (this.errorsResource) {
19573
+ const oldErrors = await this.getErrorLogs({ endDate: cutoffDate.toISOString() });
19574
+ for (const error of oldErrors) {
19575
+ await this.errorsResource.delete(error.id);
19576
+ }
19577
+ }
19578
+ if (this.performanceResource) {
19579
+ const oldPerformance = await this.getPerformanceLogs({ endDate: cutoffDate.toISOString() });
19580
+ for (const perf of oldPerformance) {
19581
+ await this.performanceResource.delete(perf.id);
19582
+ }
19583
+ }
19584
+ console.log(`Cleaned up data older than ${this.config.retentionDays} days`);
19585
+ }
19586
+ }
19587
+
19588
+ class BaseReplicator extends EventEmitter {
19589
+ constructor(config = {}) {
19590
+ super();
19591
+ this.config = config;
19592
+ this.name = this.constructor.name;
19593
+ this.enabled = config.enabled !== false;
19594
+ }
19595
+ /**
19596
+ * Initialize the replicator
19597
+ * @param {Object} database - The s3db database instance
19598
+ * @returns {Promise<void>}
19599
+ */
19600
+ async initialize(database) {
19601
+ this.database = database;
19602
+ this.emit("initialized", { replicator: this.name });
19603
+ }
19604
+ /**
19605
+ * Replicate data to the target
19606
+ * @param {string} resourceName - Name of the resource being replicated
19607
+ * @param {string} operation - Operation type (insert, update, delete)
19608
+ * @param {Object} data - The data to replicate
19609
+ * @param {string} id - Record ID
19610
+ * @returns {Promise<Object>} Replication result
19611
+ */
19612
+ async replicate(resourceName, operation, data, id) {
19613
+ throw new Error(`replicate() method must be implemented by ${this.name}`);
19614
+ }
19615
+ /**
19616
+ * Replicate multiple records in batch
19617
+ * @param {string} resourceName - Name of the resource being replicated
19618
+ * @param {Array} records - Array of records to replicate
19619
+ * @returns {Promise<Object>} Batch replication result
19620
+ */
19621
+ async replicateBatch(resourceName, records) {
19622
+ throw new Error(`replicateBatch() method must be implemented by ${this.name}`);
19623
+ }
19624
+ /**
19625
+ * Test the connection to the target
19626
+ * @returns {Promise<boolean>} True if connection is successful
19627
+ */
19628
+ async testConnection() {
19629
+ throw new Error(`testConnection() method must be implemented by ${this.name}`);
19630
+ }
19631
+ /**
19632
+ * Get replicator status and statistics
19633
+ * @returns {Promise<Object>} Status information
19634
+ */
19635
+ async getStatus() {
19636
+ return {
19637
+ name: this.name,
19638
+ enabled: this.enabled,
19639
+ config: this.config,
19640
+ connected: false
19641
+ };
19642
+ }
19643
+ /**
19644
+ * Cleanup resources
19645
+ * @returns {Promise<void>}
19646
+ */
19647
+ async cleanup() {
19648
+ this.emit("cleanup", { replicator: this.name });
19649
+ }
19650
+ /**
19651
+ * Validate replicator configuration
19652
+ * @returns {Object} Validation result
19653
+ */
19654
+ validateConfig() {
19655
+ return { isValid: true, errors: [] };
19656
+ }
19657
+ }
19658
+
19659
+ class S3dbReplicator extends BaseReplicator {
19660
+ constructor(config = {}, resources = []) {
19661
+ super(config);
19662
+ this.resources = resources;
19663
+ this.connectionString = config.connectionString;
19664
+ this.region = config.region;
19665
+ this.bucket = config.bucket;
19666
+ this.keyPrefix = config.keyPrefix;
19667
+ }
19668
+ validateConfig() {
19669
+ const errors = [];
19670
+ if (!this.connectionString && !this.bucket) {
19671
+ errors.push("Either connectionString or bucket must be provided");
19672
+ }
19673
+ return {
19674
+ isValid: errors.length === 0,
19675
+ errors
19676
+ };
19677
+ }
19678
+ async initialize(database) {
19679
+ await super.initialize(database);
19680
+ const targetConfig = {
19681
+ connectionString: this.connectionString,
19682
+ region: this.region,
19683
+ bucket: this.bucket,
19684
+ keyPrefix: this.keyPrefix,
19685
+ verbose: this.config.verbose || false
19686
+ };
19687
+ this.targetDatabase = new S3db(targetConfig);
19688
+ await this.targetDatabase.connect();
19689
+ this.emit("connected", {
19690
+ replicator: this.name,
19691
+ target: this.connectionString || this.bucket
19692
+ });
19693
+ }
19694
+ async replicate(resourceName, operation, data, id) {
19695
+ if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
19696
+ return { skipped: true, reason: "resource_not_included" };
19697
+ }
19698
+ try {
19699
+ let result;
19700
+ switch (operation) {
19701
+ case "insert":
19702
+ result = await this.targetDatabase.resources[resourceName]?.insert(data);
19703
+ break;
19704
+ case "update":
19705
+ result = await this.targetDatabase.resources[resourceName]?.update(id, data);
19706
+ break;
19707
+ case "delete":
19708
+ result = await this.targetDatabase.resources[resourceName]?.delete(id);
19709
+ break;
19710
+ default:
19711
+ throw new Error(`Unsupported operation: ${operation}`);
19712
+ }
19713
+ this.emit("replicated", {
19714
+ replicator: this.name,
19715
+ resourceName,
19716
+ operation,
19717
+ id,
19718
+ success: true
19719
+ });
19720
+ return { success: true, result };
19721
+ } catch (error) {
19722
+ this.emit("replication_error", {
19723
+ replicator: this.name,
19724
+ resourceName,
19725
+ operation,
19726
+ id,
19727
+ error: error.message
19728
+ });
19729
+ return { success: false, error: error.message };
19730
+ }
19731
+ }
19732
+ async replicateBatch(resourceName, records) {
19733
+ if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
19734
+ return { skipped: true, reason: "resource_not_included" };
19735
+ }
19736
+ try {
19737
+ const results = [];
19738
+ const errors = [];
19739
+ for (const record of records) {
19740
+ try {
19741
+ const result = await this.replicate(
19742
+ resourceName,
19743
+ record.operation,
19744
+ record.data,
19745
+ record.id
19746
+ );
19747
+ results.push(result);
19748
+ } catch (error) {
19749
+ errors.push({ id: record.id, error: error.message });
19750
+ }
19751
+ }
19752
+ this.emit("batch_replicated", {
19753
+ replicator: this.name,
19754
+ resourceName,
19755
+ total: records.length,
19756
+ successful: results.filter((r) => r.success).length,
19757
+ errors: errors.length
19758
+ });
19759
+ return {
19760
+ success: errors.length === 0,
19761
+ results,
19762
+ errors,
19763
+ total: records.length
19764
+ };
19765
+ } catch (error) {
19766
+ this.emit("batch_replication_error", {
19767
+ replicator: this.name,
19768
+ resourceName,
19769
+ error: error.message
19770
+ });
19771
+ return { success: false, error: error.message };
19772
+ }
19773
+ }
19774
+ async testConnection() {
19775
+ try {
19776
+ if (!this.targetDatabase) {
19777
+ await this.initialize(this.database);
19778
+ }
19779
+ await this.targetDatabase.listResources();
19780
+ return true;
19781
+ } catch (error) {
19782
+ this.emit("connection_error", {
19783
+ replicator: this.name,
19784
+ error: error.message
19785
+ });
19786
+ return false;
19787
+ }
19788
+ }
19789
+ async getStatus() {
19790
+ const baseStatus = await super.getStatus();
19791
+ return {
19792
+ ...baseStatus,
19793
+ connected: !!this.targetDatabase,
19794
+ targetDatabase: this.connectionString || this.bucket,
19795
+ resources: this.resources,
19796
+ totalReplications: this.listenerCount("replicated"),
19797
+ totalErrors: this.listenerCount("replication_error")
19798
+ };
19799
+ }
19800
+ async cleanup() {
19801
+ if (this.targetDatabase) {
19802
+ this.targetDatabase.removeAllListeners();
19803
+ }
19804
+ await super.cleanup();
19805
+ }
19806
+ shouldReplicateResource(resourceName) {
19807
+ return this.resources.length === 0 || this.resources.includes(resourceName);
19808
+ }
19809
+ }
19810
+
19811
+ class SqsReplicator extends BaseReplicator {
19812
+ constructor(config = {}, resources = []) {
19813
+ super(config);
19814
+ this.resources = resources;
19815
+ this.queueUrl = config.queueUrl;
19816
+ this.queues = config.queues || {};
19817
+ this.defaultQueueUrl = config.defaultQueueUrl;
19818
+ this.region = config.region || "us-east-1";
19819
+ this.sqsClient = null;
19820
+ this.messageGroupId = config.messageGroupId;
19821
+ this.deduplicationId = config.deduplicationId;
19822
+ }
19823
+ validateConfig() {
19824
+ const errors = [];
19825
+ if (!this.queueUrl && Object.keys(this.queues).length === 0 && !this.defaultQueueUrl) {
19826
+ errors.push("Either queueUrl, queues object, or defaultQueueUrl must be provided");
19827
+ }
19828
+ return {
19829
+ isValid: errors.length === 0,
19830
+ errors
19831
+ };
19832
+ }
19833
+ /**
19834
+ * Get the appropriate queue URL for a resource
19835
+ */
19836
+ getQueueUrlForResource(resourceName) {
19837
+ if (this.queues[resourceName]) {
19838
+ return this.queues[resourceName];
19839
+ }
19840
+ if (this.queueUrl) {
19841
+ return this.queueUrl;
19842
+ }
19843
+ if (this.defaultQueueUrl) {
19844
+ return this.defaultQueueUrl;
19845
+ }
19846
+ throw new Error(`No queue URL found for resource '${resourceName}'`);
19847
+ }
19848
+ /**
19849
+ * Create standardized message structure
19850
+ */
19851
+ createMessage(resourceName, operation, data, id, beforeData = null) {
19852
+ const baseMessage = {
19853
+ resource: resourceName,
19854
+ action: operation,
19855
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
19856
+ source: "s3db-replication"
19857
+ };
19858
+ switch (operation) {
19859
+ case "insert":
19860
+ return {
19861
+ ...baseMessage,
19862
+ data
19863
+ };
19864
+ case "update":
19865
+ return {
19866
+ ...baseMessage,
19867
+ before: beforeData,
19868
+ data
19869
+ };
19870
+ case "delete":
19871
+ return {
19872
+ ...baseMessage,
19873
+ data
19874
+ };
19875
+ default:
19876
+ return {
19877
+ ...baseMessage,
19878
+ data
19879
+ };
19880
+ }
19881
+ }
19882
+ async initialize(database) {
19883
+ await super.initialize(database);
19884
+ try {
19885
+ const { SQSClient, SendMessageCommand, SendMessageBatchCommand } = await import('@aws-sdk/client-sqs');
19886
+ this.sqsClient = new SQSClient({
19887
+ region: this.region,
19888
+ credentials: this.config.credentials
19889
+ });
19890
+ this.emit("initialized", {
19891
+ replicator: this.name,
19892
+ queueUrl: this.queueUrl,
19893
+ queues: this.queues,
19894
+ defaultQueueUrl: this.defaultQueueUrl
19895
+ });
19896
+ } catch (error) {
19897
+ this.emit("initialization_error", {
19898
+ replicator: this.name,
19899
+ error: error.message
19900
+ });
19901
+ throw error;
19902
+ }
19903
+ }
19904
+ async replicate(resourceName, operation, data, id, beforeData = null) {
19905
+ if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
19906
+ return { skipped: true, reason: "resource_not_included" };
19907
+ }
19908
+ try {
19909
+ const { SendMessageCommand } = await import('@aws-sdk/client-sqs');
19910
+ const queueUrl = this.getQueueUrlForResource(resourceName);
19911
+ const message = this.createMessage(resourceName, operation, data, id, beforeData);
19912
+ const command = new SendMessageCommand({
19913
+ QueueUrl: queueUrl,
19914
+ MessageBody: JSON.stringify(message),
19915
+ MessageGroupId: this.messageGroupId,
19916
+ MessageDeduplicationId: this.deduplicationId ? `${resourceName}:${operation}:${id}` : void 0
19917
+ });
19918
+ const result = await this.sqsClient.send(command);
19919
+ this.emit("replicated", {
19920
+ replicator: this.name,
19921
+ resourceName,
19922
+ operation,
19923
+ id,
19924
+ queueUrl,
19925
+ messageId: result.MessageId,
19926
+ success: true
19927
+ });
19928
+ return { success: true, messageId: result.MessageId, queueUrl };
19929
+ } catch (error) {
19930
+ this.emit("replication_error", {
19931
+ replicator: this.name,
19932
+ resourceName,
19933
+ operation,
19934
+ id,
19935
+ error: error.message
19936
+ });
19937
+ return { success: false, error: error.message };
19938
+ }
19939
+ }
19940
+ async replicateBatch(resourceName, records) {
19941
+ if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
19942
+ return { skipped: true, reason: "resource_not_included" };
19943
+ }
19944
+ try {
19945
+ const { SendMessageBatchCommand } = await import('@aws-sdk/client-sqs');
19946
+ const queueUrl = this.getQueueUrlForResource(resourceName);
19947
+ const batchSize = 10;
19948
+ const batches = [];
19949
+ for (let i = 0; i < records.length; i += batchSize) {
19950
+ batches.push(records.slice(i, i + batchSize));
19951
+ }
19952
+ const results = [];
19953
+ const errors = [];
19954
+ for (const batch of batches) {
19955
+ try {
19956
+ const entries = batch.map((record, index) => ({
19957
+ Id: `${record.id}-${index}`,
19958
+ MessageBody: JSON.stringify(this.createMessage(
19959
+ resourceName,
19960
+ record.operation,
19961
+ record.data,
19962
+ record.id,
19963
+ record.beforeData
19964
+ )),
19965
+ MessageGroupId: this.messageGroupId,
19966
+ MessageDeduplicationId: this.deduplicationId ? `${resourceName}:${record.operation}:${record.id}` : void 0
19967
+ }));
19968
+ const command = new SendMessageBatchCommand({
19969
+ QueueUrl: queueUrl,
19970
+ Entries: entries
19971
+ });
19972
+ const result = await this.sqsClient.send(command);
19973
+ results.push(result);
19974
+ } catch (error) {
19975
+ errors.push({ batch: batch.length, error: error.message });
19976
+ }
19977
+ }
19978
+ this.emit("batch_replicated", {
19979
+ replicator: this.name,
19980
+ resourceName,
19981
+ queueUrl,
19982
+ total: records.length,
19983
+ successful: results.length,
19984
+ errors: errors.length
19985
+ });
19986
+ return {
19987
+ success: errors.length === 0,
19988
+ results,
19989
+ errors,
19990
+ total: records.length,
19991
+ queueUrl
19992
+ };
19993
+ } catch (error) {
19994
+ this.emit("batch_replication_error", {
19995
+ replicator: this.name,
19996
+ resourceName,
19997
+ error: error.message
19998
+ });
19999
+ return { success: false, error: error.message };
20000
+ }
20001
+ }
20002
+ async testConnection() {
20003
+ try {
20004
+ if (!this.sqsClient) {
20005
+ await this.initialize(this.database);
20006
+ }
20007
+ const { GetQueueAttributesCommand } = await import('@aws-sdk/client-sqs');
20008
+ const command = new GetQueueAttributesCommand({
20009
+ QueueUrl: this.queueUrl,
20010
+ AttributeNames: ["QueueArn"]
20011
+ });
20012
+ await this.sqsClient.send(command);
20013
+ return true;
20014
+ } catch (error) {
20015
+ this.emit("connection_error", {
20016
+ replicator: this.name,
20017
+ error: error.message
20018
+ });
20019
+ return false;
20020
+ }
20021
+ }
20022
+ async getStatus() {
20023
+ const baseStatus = await super.getStatus();
20024
+ return {
20025
+ ...baseStatus,
20026
+ connected: !!this.sqsClient,
20027
+ queueUrl: this.queueUrl,
20028
+ region: this.region,
20029
+ resources: this.resources,
20030
+ totalReplications: this.listenerCount("replicated"),
20031
+ totalErrors: this.listenerCount("replication_error")
20032
+ };
20033
+ }
20034
+ async cleanup() {
20035
+ if (this.sqsClient) {
20036
+ this.sqsClient.destroy();
20037
+ }
20038
+ await super.cleanup();
20039
+ }
20040
+ shouldReplicateResource(resourceName) {
20041
+ return this.resources.length === 0 || this.resources.includes(resourceName);
20042
+ }
20043
+ }
20044
+
20045
+ class BigqueryReplicator extends BaseReplicator {
20046
+ constructor(config = {}, resources = []) {
20047
+ super(config);
20048
+ this.resources = resources;
20049
+ this.projectId = config.projectId;
20050
+ this.datasetId = config.datasetId;
20051
+ this.tableId = config.tableId;
20052
+ this.tableMap = config.tableMap || {};
20053
+ this.bigqueryClient = null;
20054
+ this.credentials = config.credentials;
20055
+ this.location = config.location || "US";
20056
+ this.logOperations = config.logOperations !== false;
20057
+ }
20058
+ validateConfig() {
20059
+ const errors = [];
20060
+ if (!this.projectId) errors.push("projectId is required");
20061
+ if (!this.datasetId) errors.push("datasetId is required");
20062
+ if (!this.tableId) errors.push("tableId is required");
20063
+ return { isValid: errors.length === 0, errors };
20064
+ }
20065
+ async initialize(database) {
20066
+ await super.initialize(database);
20067
+ try {
20068
+ const { BigQuery } = await import('@google-cloud/bigquery');
20069
+ this.bigqueryClient = new BigQuery({
20070
+ projectId: this.projectId,
20071
+ credentials: this.credentials,
20072
+ location: this.location
20073
+ });
20074
+ this.emit("initialized", {
20075
+ replicator: this.name,
20076
+ projectId: this.projectId,
20077
+ datasetId: this.datasetId,
20078
+ tableId: this.tableId
20079
+ });
20080
+ } catch (error) {
20081
+ this.emit("initialization_error", { replicator: this.name, error: error.message });
20082
+ throw error;
20083
+ }
20084
+ }
20085
+ getTableForResource(resourceName) {
20086
+ return this.tableMap[resourceName] || this.tableId;
20087
+ }
20088
+ async replicate(resourceName, operation, data, id, beforeData = null) {
20089
+ if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
20090
+ return { skipped: true, reason: "resource_not_included" };
20091
+ }
20092
+ try {
20093
+ const dataset = this.bigqueryClient.dataset(this.datasetId);
20094
+ const tableId = this.getTableForResource(resourceName);
20095
+ const table = dataset.table(tableId);
20096
+ let job;
20097
+ if (operation === "insert") {
20098
+ const row = { ...data };
20099
+ job = await table.insert([row]);
20100
+ } else if (operation === "update") {
20101
+ const keys = Object.keys(data).filter((k) => k !== "id");
20102
+ const setClause = keys.map((k) => `
20103
+ ${k}=@${k}
20104
+ `).join(", ");
20105
+ const params = { id };
20106
+ keys.forEach((k) => {
20107
+ params[k] = data[k];
20108
+ });
20109
+ const query = `UPDATE \`${this.projectId}.${this.datasetId}.${tableId}\`
20110
+ SET ${setClause}
20111
+ WHERE id=@id`;
20112
+ const [updateJob] = await this.bigqueryClient.createQueryJob({
20113
+ query,
20114
+ params
20115
+ });
20116
+ await updateJob.getQueryResults();
20117
+ job = [updateJob];
20118
+ } else if (operation === "delete") {
20119
+ const query = `DELETE FROM \`${this.projectId}.${this.datasetId}.${tableId}\`
20120
+ WHERE id=@id`;
20121
+ const [deleteJob] = await this.bigqueryClient.createQueryJob({
20122
+ query,
20123
+ params: { id }
20124
+ });
20125
+ await deleteJob.getQueryResults();
20126
+ job = [deleteJob];
20127
+ } else {
20128
+ throw new Error(`Unsupported operation: ${operation}`);
20129
+ }
20130
+ if (this.logOperations) {
20131
+ const logTable = dataset.table(this.tableId);
20132
+ await logTable.insert([{
20133
+ resource_name: resourceName,
20134
+ operation,
20135
+ record_id: id,
20136
+ data: JSON.stringify(data),
20137
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
20138
+ source: "s3db-replication"
20139
+ }]);
20140
+ }
20141
+ this.emit("replicated", {
20142
+ replicator: this.name,
20143
+ resourceName,
20144
+ operation,
20145
+ id,
20146
+ jobId: job[0]?.id,
20147
+ success: true
20148
+ });
20149
+ return { success: true, jobId: job[0]?.id };
20150
+ } catch (error) {
20151
+ this.emit("replication_error", {
20152
+ replicator: this.name,
20153
+ resourceName,
20154
+ operation,
20155
+ id,
20156
+ error: error.message
20157
+ });
20158
+ return { success: false, error: error.message };
20159
+ }
20160
+ }
20161
+ async replicateBatch(resourceName, records) {
20162
+ const results = [];
20163
+ const errors = [];
20164
+ for (const record of records) {
20165
+ try {
20166
+ const res = await this.replicate(resourceName, record.operation, record.data, record.id, record.beforeData);
20167
+ results.push(res);
20168
+ } catch (err) {
20169
+ errors.push({ id: record.id, error: err.message });
20170
+ }
20171
+ }
20172
+ return { success: errors.length === 0, results, errors };
20173
+ }
20174
+ async testConnection() {
20175
+ try {
20176
+ if (!this.bigqueryClient) await this.initialize();
20177
+ const dataset = this.bigqueryClient.dataset(this.datasetId);
20178
+ await dataset.getMetadata();
20179
+ return true;
20180
+ } catch (error) {
20181
+ this.emit("connection_error", { replicator: this.name, error: error.message });
20182
+ return false;
20183
+ }
20184
+ }
20185
+ async cleanup() {
20186
+ }
20187
+ shouldReplicateResource(resourceName) {
20188
+ if (!this.resources || this.resources.length === 0) return true;
20189
+ return this.resources.includes(resourceName);
20190
+ }
20191
+ }
20192
+
20193
+ class PostgresReplicator extends BaseReplicator {
20194
+ constructor(config = {}, resources = []) {
20195
+ super(config);
20196
+ this.resources = resources;
20197
+ this.connectionString = config.connectionString;
20198
+ this.host = config.host;
20199
+ this.port = config.port || 5432;
20200
+ this.database = config.database;
20201
+ this.user = config.user;
20202
+ this.password = config.password;
20203
+ this.tableName = config.tableName || "s3db_replication";
20204
+ this.tableMap = config.tableMap || {};
20205
+ this.client = null;
20206
+ this.ssl = config.ssl;
20207
+ this.logOperations = config.logOperations !== false;
20208
+ }
20209
+ validateConfig() {
20210
+ const errors = [];
20211
+ if (!this.connectionString && (!this.host || !this.database)) {
20212
+ errors.push("Either connectionString or host+database must be provided");
20213
+ }
20214
+ return {
20215
+ isValid: errors.length === 0,
20216
+ errors
20217
+ };
20218
+ }
20219
+ async initialize(database) {
20220
+ await super.initialize(database);
20221
+ try {
20222
+ const { Client } = await import('pg');
20223
+ const config = this.connectionString ? {
20224
+ connectionString: this.connectionString,
20225
+ ssl: this.ssl
20226
+ } : {
20227
+ host: this.host,
20228
+ port: this.port,
20229
+ database: this.database,
20230
+ user: this.user,
20231
+ password: this.password,
20232
+ ssl: this.ssl
20233
+ };
20234
+ this.client = new Client(config);
20235
+ await this.client.connect();
20236
+ if (this.logOperations) await this.createTableIfNotExists();
20237
+ this.emit("initialized", {
20238
+ replicator: this.name,
20239
+ database: this.database || "postgres",
20240
+ table: this.tableName
20241
+ });
20242
+ } catch (error) {
20243
+ this.emit("initialization_error", {
20244
+ replicator: this.name,
20245
+ error: error.message
20246
+ });
20247
+ throw error;
20248
+ }
20249
+ }
20250
+ async createTableIfNotExists() {
20251
+ const createTableQuery = `
20252
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
20253
+ id SERIAL PRIMARY KEY,
20254
+ resource_name VARCHAR(255) NOT NULL,
20255
+ operation VARCHAR(50) NOT NULL,
20256
+ record_id VARCHAR(255) NOT NULL,
20257
+ data JSONB,
20258
+ timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
20259
+ source VARCHAR(100) DEFAULT 's3db-replication',
20260
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
20261
+ );
20262
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_resource_name ON ${this.tableName}(resource_name);
20263
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_operation ON ${this.tableName}(operation);
20264
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_record_id ON ${this.tableName}(record_id);
20265
+ CREATE INDEX IF NOT EXISTS idx_${this.tableName}_timestamp ON ${this.tableName}(timestamp);
20266
+ `;
20267
+ await this.client.query(createTableQuery);
20268
+ }
20269
+ getTableForResource(resourceName) {
20270
+ return this.tableMap[resourceName] || resourceName;
20271
+ }
20272
+ async replicate(resourceName, operation, data, id, beforeData = null) {
20273
+ if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
20274
+ return { skipped: true, reason: "resource_not_included" };
20275
+ }
20276
+ try {
20277
+ const table = this.getTableForResource(resourceName);
20278
+ let result;
20279
+ if (operation === "insert") {
20280
+ const keys = Object.keys(data);
20281
+ const values = keys.map((k) => data[k]);
20282
+ const columns = keys.map((k) => `"${k}"`).join(", ");
20283
+ const params = keys.map((_, i) => `$${i + 1}`).join(", ");
20284
+ const sql = `INSERT INTO ${table} (${columns}) VALUES (${params}) ON CONFLICT (id) DO NOTHING RETURNING *`;
20285
+ result = await this.client.query(sql, values);
20286
+ } else if (operation === "update") {
20287
+ const keys = Object.keys(data).filter((k) => k !== "id");
20288
+ const setClause = keys.map((k, i) => `"${k}"=$${i + 1}`).join(", ");
20289
+ const values = keys.map((k) => data[k]);
20290
+ values.push(id);
20291
+ const sql = `UPDATE ${table} SET ${setClause} WHERE id=$${keys.length + 1} RETURNING *`;
20292
+ result = await this.client.query(sql, values);
20293
+ } else if (operation === "delete") {
20294
+ const sql = `DELETE FROM ${table} WHERE id=$1 RETURNING *`;
20295
+ result = await this.client.query(sql, [id]);
20296
+ } else {
20297
+ throw new Error(`Unsupported operation: ${operation}`);
20298
+ }
20299
+ if (this.logOperations) {
20300
+ await this.client.query(
20301
+ `INSERT INTO ${this.tableName} (resource_name, operation, record_id, data, timestamp, source) VALUES ($1, $2, $3, $4, $5, $6)`,
20302
+ [resourceName, operation, id, JSON.stringify(data), (/* @__PURE__ */ new Date()).toISOString(), "s3db-replication"]
20303
+ );
20304
+ }
20305
+ this.emit("replicated", {
20306
+ replicator: this.name,
20307
+ resourceName,
20308
+ operation,
20309
+ id,
20310
+ result: result.rows,
20311
+ success: true
20312
+ });
20313
+ return { success: true, rows: result.rows };
20314
+ } catch (error) {
20315
+ this.emit("replication_error", {
20316
+ replicator: this.name,
20317
+ resourceName,
20318
+ operation,
20319
+ id,
20320
+ error: error.message
20321
+ });
20322
+ return { success: false, error: error.message };
20323
+ }
20324
+ }
20325
+ async replicateBatch(resourceName, records) {
20326
+ const results = [];
20327
+ const errors = [];
20328
+ for (const record of records) {
20329
+ try {
20330
+ const res = await this.replicate(resourceName, record.operation, record.data, record.id, record.beforeData);
20331
+ results.push(res);
20332
+ } catch (err) {
20333
+ errors.push({ id: record.id, error: err.message });
20334
+ }
20335
+ }
20336
+ return { success: errors.length === 0, results, errors };
20337
+ }
20338
+ async testConnection() {
20339
+ try {
20340
+ if (!this.client) await this.initialize();
20341
+ await this.client.query("SELECT 1");
20342
+ return true;
20343
+ } catch (error) {
20344
+ this.emit("connection_error", { replicator: this.name, error: error.message });
20345
+ return false;
20346
+ }
20347
+ }
20348
+ async cleanup() {
20349
+ if (this.client) await this.client.end();
20350
+ }
20351
+ shouldReplicateResource(resourceName) {
20352
+ if (!this.resources || this.resources.length === 0) return true;
20353
+ return this.resources.includes(resourceName);
20354
+ }
20355
+ }
20356
+
20357
+ const REPLICATOR_DRIVERS = {
20358
+ s3db: S3dbReplicator,
20359
+ sqs: SqsReplicator,
20360
+ bigquery: BigqueryReplicator,
20361
+ postgres: PostgresReplicator
20362
+ };
20363
+ function createReplicator(driver, config = {}, resources = []) {
20364
+ const ReplicatorClass = REPLICATOR_DRIVERS[driver];
20365
+ if (!ReplicatorClass) {
20366
+ throw new Error(`Unknown replicator driver: ${driver}. Available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(", ")}`);
20367
+ }
20368
+ return new ReplicatorClass(config, resources);
20369
+ }
20370
+ function validateReplicatorConfig(driver, config, resources = []) {
20371
+ const replicator = createReplicator(driver, config, resources);
20372
+ return replicator.validateConfig();
20373
+ }
20374
+
20375
+ const gzipAsync = promisify(gzip);
20376
+ const gunzipAsync = promisify(gunzip);
20377
+ class ReplicationPlugin extends Plugin {
20378
+ constructor(options = {}) {
20379
+ super();
20380
+ this.config = {
20381
+ enabled: options.enabled !== false,
20382
+ replicators: options.replicators || [],
20383
+ syncMode: options.syncMode || "async",
20384
+ // 'sync' or 'async'
20385
+ retryAttempts: options.retryAttempts || 3,
20386
+ retryDelay: options.retryDelay || 1e3,
20387
+ // ms
20388
+ batchSize: options.batchSize || 10,
20389
+ compression: options.compression || false,
20390
+ // Enable compression
20391
+ compressionLevel: options.compressionLevel || 6,
20392
+ // 0-9
20393
+ ...options
20394
+ };
20395
+ this.replicators = [];
20396
+ this.queue = [];
20397
+ this.isProcessing = false;
20398
+ this.stats = {
20399
+ totalOperations: 0,
20400
+ successfulOperations: 0,
20401
+ failedOperations: 0,
20402
+ lastSync: null
20403
+ };
20404
+ }
20405
+ /**
20406
+ * Process data according to replication mode
20407
+ */
20408
+ processDataForReplication(data, metadata = {}) {
20409
+ switch (this.config.replicationMode) {
20410
+ case "exact-copy":
20411
+ return {
20412
+ body: data,
20413
+ metadata
20414
+ };
20415
+ case "just-metadata":
20416
+ return {
20417
+ body: null,
20418
+ metadata
20419
+ };
20420
+ case "all-in-body":
20421
+ return {
20422
+ body: {
20423
+ data,
20424
+ metadata,
20425
+ replicationMode: this.config.replicationMode,
20426
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
20427
+ },
20428
+ metadata: {
20429
+ replicationMode: this.config.replicationMode,
20430
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
20431
+ }
20432
+ };
20433
+ default:
20434
+ return {
20435
+ body: data,
20436
+ metadata
20437
+ };
20438
+ }
20439
+ }
20440
+ /**
20441
+ * Compress data if compression is enabled
20442
+ */
20443
+ async compressData(data) {
20444
+ if (!this.config.compression || !data) {
20445
+ return data;
20446
+ }
20447
+ try {
20448
+ const jsonString = JSON.stringify(data);
20449
+ const compressed = await gzipAsync(jsonString, { level: this.config.compressionLevel });
20450
+ return compressed.toString("base64");
20451
+ } catch (error) {
20452
+ this.emit("replication.compression.failed", { error, data });
20453
+ return data;
20454
+ }
20455
+ }
20456
+ /**
20457
+ * Decompress data if it was compressed
20458
+ */
20459
+ async decompressData(data) {
20460
+ if (!this.config.compression || !data) {
20461
+ return data;
20462
+ }
20463
+ try {
20464
+ if (typeof data === "string" && data.startsWith("H4sI")) {
20465
+ const buffer = Buffer.from(data, "base64");
20466
+ const decompressed = await gunzipAsync(buffer);
20467
+ return JSON.parse(decompressed.toString());
20468
+ }
20469
+ return data;
20470
+ } catch (error) {
20471
+ this.emit("replication.decompression.failed", { error, data });
20472
+ return data;
20473
+ }
20474
+ }
20475
+ async setup(database) {
20476
+ this.database = database;
20477
+ if (!this.config.enabled) {
20478
+ return;
20479
+ }
20480
+ if (this.config.replicators && this.config.replicators.length > 0) {
20481
+ await this.initializeReplicators();
20482
+ }
20483
+ if (!database.resources.replication_logs) {
20484
+ this.replicationLog = await database.createResource({
20485
+ name: "replication_logs",
20486
+ attributes: {
20487
+ id: "string|required",
20488
+ resourceName: "string|required",
20489
+ operation: "string|required",
20490
+ recordId: "string|required",
20491
+ replicatorId: "string|required",
20492
+ status: "string|required",
20493
+ attempts: "number|required",
20494
+ lastAttempt: "string|required",
20495
+ error: "string|required",
20496
+ data: "object|required",
20497
+ timestamp: "string|required"
20498
+ }
20499
+ });
20500
+ } else {
20501
+ this.replicationLog = database.resources.replication_logs;
20502
+ }
20503
+ for (const resourceName in database.resources) {
20504
+ if (resourceName !== "replication_logs") {
20505
+ this.installHooks(database.resources[resourceName]);
20506
+ }
20507
+ }
20508
+ const originalCreateResource = database.createResource.bind(database);
20509
+ database.createResource = async (config) => {
20510
+ const resource = await originalCreateResource(config);
20511
+ if (resource && resource.name !== "replication_logs") {
20512
+ this.installHooks(resource);
20513
+ }
20514
+ return resource;
20515
+ };
20516
+ this.startQueueProcessor();
20517
+ }
20518
+ async initializeReplicators() {
20519
+ for (const replicatorConfig of this.config.replicators) {
20520
+ try {
20521
+ const { driver, config: replicatorConfigData, resources = [] } = replicatorConfig;
20522
+ const validation = validateReplicatorConfig(driver, replicatorConfigData, resources);
20523
+ if (!validation.isValid) {
20524
+ this.emit("replicator.validation.failed", {
20525
+ driver,
20526
+ errors: validation.errors
20527
+ });
20528
+ continue;
20529
+ }
20530
+ const replicator = createReplicator(driver, replicatorConfigData, resources);
20531
+ await replicator.initialize(this.database);
20532
+ replicator.on("replicated", (data) => {
20533
+ this.emit("replication.success", data);
20534
+ });
20535
+ replicator.on("replication_error", (data) => {
20536
+ this.emit("replication.failed", data);
20537
+ });
20538
+ this.replicators.push({
20539
+ id: `${driver}-${Date.now()}`,
20540
+ driver,
20541
+ config: replicatorConfigData,
20542
+ resources,
20543
+ instance: replicator
20544
+ });
20545
+ this.emit("replicator.initialized", {
20546
+ driver,
20547
+ config: replicatorConfigData,
20548
+ resources
20549
+ });
20550
+ } catch (error) {
20551
+ this.emit("replicator.initialization.failed", {
20552
+ driver: replicatorConfig.driver,
20553
+ error: error.message
20554
+ });
20555
+ }
20556
+ }
20557
+ }
20558
+ async start() {
20559
+ }
20560
+ async stop() {
20561
+ this.isProcessing = false;
20562
+ await this.processQueue();
20563
+ }
20564
+ installHooks(resource) {
20565
+ if (!resource || resource.name === "replication_logs") return;
20566
+ const originalDataMap = /* @__PURE__ */ new Map();
20567
+ resource.addHook("afterInsert", async (data) => {
20568
+ await this.queueReplication(resource.name, "insert", data.id, data);
20569
+ return data;
20570
+ });
20571
+ resource.addHook("preUpdate", async (data) => {
20572
+ if (data.id) {
20573
+ try {
20574
+ const originalData = await resource.get(data.id);
20575
+ originalDataMap.set(data.id, originalData);
20576
+ } catch (error) {
20577
+ originalDataMap.set(data.id, { id: data.id });
20578
+ }
20579
+ }
20580
+ return data;
20581
+ });
20582
+ resource.addHook("afterUpdate", async (data) => {
20583
+ const beforeData = originalDataMap.get(data.id);
20584
+ await this.queueReplication(resource.name, "update", data.id, data, beforeData);
20585
+ originalDataMap.delete(data.id);
20586
+ return data;
20587
+ });
20588
+ resource.addHook("afterDelete", async (data) => {
20589
+ await this.queueReplication(resource.name, "delete", data.id, data);
20590
+ return data;
20591
+ });
20592
+ const originalDeleteMany = resource.deleteMany.bind(resource);
20593
+ resource.deleteMany = async (ids) => {
20594
+ const result = await originalDeleteMany(ids);
20595
+ if (result && result.length > 0) {
20596
+ for (const id of ids) {
20597
+ await this.queueReplication(resource.name, "delete", id, { id });
20598
+ }
20599
+ }
20600
+ return result;
20601
+ };
20602
+ }
20603
+ async queueReplication(resourceName, operation, recordId, data, beforeData = null) {
20604
+ if (!this.config.enabled) {
20605
+ return;
20606
+ }
20607
+ if (this.replicators.length === 0) {
20608
+ return;
20609
+ }
20610
+ const applicableReplicators = this.replicators.filter(
20611
+ (replicator) => replicator.instance.shouldReplicateResource(resourceName)
20612
+ );
20613
+ if (applicableReplicators.length === 0) {
20614
+ return;
20615
+ }
20616
+ const item = {
20617
+ id: `repl-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
20618
+ resourceName,
20619
+ operation,
20620
+ recordId,
20621
+ data: lodashEs.isPlainObject(data) ? data : { raw: data },
20622
+ beforeData: beforeData ? lodashEs.isPlainObject(beforeData) ? beforeData : { raw: beforeData } : null,
20623
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
20624
+ attempts: 0
20625
+ };
20626
+ const logId = await this.logReplication(item);
20627
+ if (this.config.syncMode === "sync") {
20628
+ try {
20629
+ const result = await this.processReplicationItem(item);
20630
+ if (logId) {
20631
+ await this.updateReplicationLog(logId, {
20632
+ status: result.success ? "success" : "failed",
20633
+ attempts: 1,
20634
+ error: result.success ? "" : JSON.stringify(result.results)
20635
+ });
20636
+ }
20637
+ this.stats.totalOperations++;
20638
+ if (result.success) {
20639
+ this.stats.successfulOperations++;
20640
+ } else {
20641
+ this.stats.failedOperations++;
20642
+ }
20643
+ } catch (error) {
20644
+ if (logId) {
20645
+ await this.updateReplicationLog(logId, {
20646
+ status: "failed",
20647
+ attempts: 1,
20648
+ error: error.message
20649
+ });
20650
+ }
20651
+ this.stats.failedOperations++;
20652
+ }
20653
+ } else {
20654
+ this.queue.push(item);
20655
+ this.emit("replication.queued", { item, queueLength: this.queue.length });
20656
+ }
20657
+ }
20658
+ async processReplicationItem(item) {
20659
+ const { resourceName, operation, recordId, data, beforeData } = item;
20660
+ const applicableReplicators = this.replicators.filter(
20661
+ (replicator) => replicator.instance.shouldReplicateResource(resourceName)
20662
+ );
20663
+ if (applicableReplicators.length === 0) {
20664
+ return { success: true, skipped: true, reason: "no_applicable_replicators" };
20665
+ }
20666
+ const results = [];
20667
+ for (const replicator of applicableReplicators) {
20668
+ try {
20669
+ const result = await replicator.instance.replicate(resourceName, operation, data, recordId, beforeData);
20670
+ results.push({
20671
+ replicatorId: replicator.id,
20672
+ driver: replicator.driver,
20673
+ success: result.success,
20674
+ error: result.error,
20675
+ skipped: result.skipped
20676
+ });
20677
+ } catch (error) {
20678
+ results.push({
20679
+ replicatorId: replicator.id,
20680
+ driver: replicator.driver,
20681
+ success: false,
20682
+ error: error.message
20683
+ });
20684
+ }
20685
+ }
20686
+ return {
20687
+ success: results.every((r) => r.success || r.skipped),
20688
+ results
20689
+ };
20690
+ }
20691
+ async logReplication(item) {
20692
+ if (!this.replicationLog) return;
20693
+ try {
20694
+ const logId = `log-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
20695
+ await this.replicationLog.insert({
20696
+ id: logId,
20697
+ resourceName: item.resourceName,
20698
+ operation: item.operation,
20699
+ recordId: item.recordId,
20700
+ replicatorId: "all",
20701
+ // Will be updated with specific replicator results
20702
+ status: "queued",
20703
+ attempts: 0,
20704
+ lastAttempt: (/* @__PURE__ */ new Date()).toISOString(),
20705
+ error: "",
20706
+ data: lodashEs.isPlainObject(item.data) ? item.data : { raw: item.data },
20707
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
20708
+ });
20709
+ return logId;
20710
+ } catch (error) {
20711
+ this.emit("replication.log.failed", { error: error.message, item });
20712
+ return null;
20713
+ }
20714
+ }
20715
+ async updateReplicationLog(logId, updates) {
20716
+ if (!this.replicationLog) return;
20717
+ try {
20718
+ await this.replicationLog.update(logId, {
20719
+ ...updates,
20720
+ lastAttempt: (/* @__PURE__ */ new Date()).toISOString()
20721
+ });
20722
+ } catch (error) {
20723
+ this.emit("replication.updateLog.failed", { error: error.message, logId, updates });
20724
+ }
20725
+ }
20726
+ startQueueProcessor() {
20727
+ if (this.isProcessing) return;
20728
+ this.isProcessing = true;
20729
+ this.processQueueLoop();
20730
+ }
20731
+ async processQueueLoop() {
20732
+ while (this.isProcessing) {
20733
+ if (this.queue.length > 0) {
20734
+ const batch = this.queue.splice(0, this.config.batchSize);
20735
+ for (const item of batch) {
20736
+ await this.processReplicationItem(item);
20737
+ }
20738
+ } else {
20739
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
20740
+ }
20741
+ }
20742
+ }
20743
+ async processQueue() {
20744
+ if (this.queue.length === 0) return;
20745
+ const item = this.queue.shift();
20746
+ let attempts = 0;
20747
+ let lastError = null;
20748
+ while (attempts < this.config.retryAttempts) {
20749
+ try {
20750
+ attempts++;
20751
+ this.emit("replication.retry.started", {
20752
+ item,
20753
+ attempt: attempts,
20754
+ maxAttempts: this.config.retryAttempts
20755
+ });
20756
+ const result = await this.processReplicationItem(item);
20757
+ if (result.success) {
20758
+ this.stats.successfulOperations++;
20759
+ this.emit("replication.success", {
20760
+ item,
20761
+ attempts,
20762
+ results: result.results,
20763
+ stats: this.stats
20764
+ });
20765
+ return;
20766
+ } else {
20767
+ lastError = result.results;
20768
+ if (attempts < this.config.retryAttempts) {
20769
+ await new Promise((resolve) => setTimeout(resolve, this.config.retryDelay * attempts));
20770
+ }
20771
+ }
20772
+ } catch (error) {
20773
+ lastError = error.message;
20774
+ if (attempts < this.config.retryAttempts) {
20775
+ await new Promise((resolve) => setTimeout(resolve, this.config.retryDelay * attempts));
20776
+ } else {
20777
+ this.emit("replication.retry.exhausted", {
20778
+ attempts,
20779
+ lastError,
20780
+ item
20781
+ });
20782
+ }
20783
+ }
20784
+ }
20785
+ this.stats.failedOperations++;
20786
+ this.emit("replication.failed", {
20787
+ attempts,
20788
+ lastError,
20789
+ item,
20790
+ stats: this.stats
20791
+ });
20792
+ }
20793
+ // Utility methods
20794
+ async getReplicationStats() {
20795
+ const replicatorStats = await Promise.all(
20796
+ this.replicators.map(async (replicator) => {
20797
+ const status = await replicator.instance.getStatus();
20798
+ return {
20799
+ id: replicator.id,
20800
+ driver: replicator.driver,
20801
+ config: replicator.config,
20802
+ status
20803
+ };
20804
+ })
20805
+ );
20806
+ return {
20807
+ enabled: this.config.enabled,
20808
+ replicators: replicatorStats,
20809
+ queue: {
20810
+ length: this.queue.length,
20811
+ isProcessing: this.isProcessing
20812
+ },
20813
+ stats: this.stats,
20814
+ lastSync: this.stats.lastSync
20815
+ };
20816
+ }
20817
+ async getReplicationLogs(options = {}) {
20818
+ if (!this.replicationLog) {
20819
+ return [];
20820
+ }
20821
+ const {
20822
+ resourceName,
20823
+ operation,
20824
+ status,
20825
+ limit = 100,
20826
+ offset = 0
20827
+ } = options;
20828
+ let query = {};
20829
+ if (resourceName) {
20830
+ query.resourceName = resourceName;
20831
+ }
20832
+ if (operation) {
20833
+ query.operation = operation;
20834
+ }
20835
+ if (status) {
20836
+ query.status = status;
20837
+ }
20838
+ const logs = await this.replicationLog.list(query);
20839
+ return logs.slice(offset, offset + limit);
20840
+ }
20841
+ async retryFailedReplications() {
20842
+ if (!this.replicationLog) {
20843
+ return { retried: 0 };
20844
+ }
20845
+ const failedLogs = await this.replicationLog.list({
20846
+ status: "failed"
20847
+ });
20848
+ let retried = 0;
20849
+ for (const log of failedLogs) {
20850
+ try {
20851
+ await this.queueReplication(
20852
+ log.resourceName,
20853
+ log.operation,
20854
+ log.recordId,
20855
+ log.data
20856
+ );
20857
+ retried++;
20858
+ } catch (error) {
20859
+ console.error("Failed to retry replication:", error);
20860
+ }
20861
+ }
20862
+ return { retried };
20863
+ }
20864
+ async syncAllData(replicatorId) {
20865
+ const replicator = this.replicators.find((r) => r.id === replicatorId);
20866
+ if (!replicator) {
20867
+ throw new Error(`Replicator not found: ${replicatorId}`);
20868
+ }
20869
+ this.stats.lastSync = (/* @__PURE__ */ new Date()).toISOString();
20870
+ for (const resourceName in this.database.resources) {
20871
+ if (resourceName === "replication_logs") continue;
20872
+ if (replicator.instance.shouldReplicateResource(resourceName)) {
20873
+ this.emit("replication.sync.resource", { resourceName, replicatorId });
20874
+ const resource = this.database.resources[resourceName];
20875
+ const allRecords = await resource.getAll();
20876
+ for (const record of allRecords) {
20877
+ await replicator.instance.replicate(resourceName, "insert", record, record.id);
20878
+ }
20879
+ }
20880
+ }
20881
+ this.emit("replication.sync.completed", { replicatorId, stats: this.stats });
17829
20882
  }
17830
20883
  }
17831
20884
 
17832
20885
  exports.AVAILABLE_BEHAVIORS = AVAILABLE_BEHAVIORS;
20886
+ exports.AuditPlugin = AuditPlugin;
17833
20887
  exports.AuthenticationError = AuthenticationError;
17834
20888
  exports.BaseError = BaseError;
17835
20889
  exports.Cache = Cache;
@@ -17842,8 +20896,10 @@ exports.Database = Database;
17842
20896
  exports.DatabaseError = DatabaseError;
17843
20897
  exports.EncryptionError = EncryptionError;
17844
20898
  exports.ErrorMap = ErrorMap;
20899
+ exports.FullTextPlugin = FullTextPlugin;
17845
20900
  exports.InvalidResourceItem = InvalidResourceItem;
17846
20901
  exports.MemoryCache = MemoryCache;
20902
+ exports.MetricsPlugin = MetricsPlugin;
17847
20903
  exports.MissingMetadata = MissingMetadata;
17848
20904
  exports.NoSuchBucket = NoSuchBucket;
17849
20905
  exports.NoSuchKey = NoSuchKey;
@@ -17851,6 +20907,7 @@ exports.NotFound = NotFound;
17851
20907
  exports.PermissionError = PermissionError;
17852
20908
  exports.Plugin = Plugin;
17853
20909
  exports.PluginObject = PluginObject;
20910
+ exports.ReplicationPlugin = ReplicationPlugin;
17854
20911
  exports.Resource = Resource;
17855
20912
  exports.ResourceIdsPageReader = ResourceIdsPageReader;
17856
20913
  exports.ResourceIdsReader = ResourceIdsReader;