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