s3db.js 7.3.4 → 7.3.6
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/PLUGINS.md +1285 -157
- package/dist/s3db.cjs.js +322 -119
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.es.js +322 -119
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +323 -120
- package/dist/s3db.iife.min.js +1 -1
- package/mcp/server.js +1410 -0
- package/package.json +30 -24
- package/src/database.class.js +10 -8
- package/src/plugins/cache/filesystem-cache.class.js +9 -0
- package/src/plugins/metrics.plugin.js +18 -8
- package/src/plugins/replicator.plugin.js +130 -72
- package/src/plugins/replicators/bigquery-replicator.class.js +31 -5
- package/src/plugins/replicators/postgres-replicator.class.js +17 -2
- package/src/plugins/replicators/s3db-replicator.class.js +175 -71
- package/src/plugins/replicators/sqs-replicator.class.js +13 -1
package/dist/s3db.cjs.js
CHANGED
|
@@ -7,8 +7,8 @@ var zlib = require('node:zlib');
|
|
|
7
7
|
var promisePool = require('@supercharge/promise-pool');
|
|
8
8
|
var web = require('node:stream/web');
|
|
9
9
|
var promises = require('fs/promises');
|
|
10
|
-
var lodashEs = require('lodash-es');
|
|
11
10
|
var crypto = require('crypto');
|
|
11
|
+
var lodashEs = require('lodash-es');
|
|
12
12
|
var jsonStableStringify = require('json-stable-stringify');
|
|
13
13
|
var clientS3 = require('@aws-sdk/client-s3');
|
|
14
14
|
var flat = require('flat');
|
|
@@ -7083,6 +7083,12 @@ class FilesystemCache extends Cache {
|
|
|
7083
7083
|
}
|
|
7084
7084
|
async _clear(prefix) {
|
|
7085
7085
|
try {
|
|
7086
|
+
if (!await this._fileExists(this.directory)) {
|
|
7087
|
+
if (this.enableStats) {
|
|
7088
|
+
this.stats.clears++;
|
|
7089
|
+
}
|
|
7090
|
+
return true;
|
|
7091
|
+
}
|
|
7086
7092
|
const files = await promises.readdir(this.directory);
|
|
7087
7093
|
const cacheFiles = files.filter((file) => {
|
|
7088
7094
|
if (!file.startsWith(this.prefix)) return false;
|
|
@@ -8353,7 +8359,7 @@ class MetricsPlugin extends plugin_class_default {
|
|
|
8353
8359
|
}
|
|
8354
8360
|
async setup(database) {
|
|
8355
8361
|
this.database = database;
|
|
8356
|
-
if (process.env.NODE_ENV === "test") return;
|
|
8362
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "test") return;
|
|
8357
8363
|
const [ok, err] = await try_fn_default(async () => {
|
|
8358
8364
|
const [ok1, err1, metricsResource] = await try_fn_default(() => database.createResource({
|
|
8359
8365
|
name: "metrics",
|
|
@@ -8403,7 +8409,7 @@ class MetricsPlugin extends plugin_class_default {
|
|
|
8403
8409
|
this.performanceResource = database.resources.performance_logs;
|
|
8404
8410
|
}
|
|
8405
8411
|
this.installMetricsHooks();
|
|
8406
|
-
if (process.env.NODE_ENV !== "test") {
|
|
8412
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
8407
8413
|
this.startFlushTimer();
|
|
8408
8414
|
}
|
|
8409
8415
|
}
|
|
@@ -8414,7 +8420,7 @@ class MetricsPlugin extends plugin_class_default {
|
|
|
8414
8420
|
clearInterval(this.flushTimer);
|
|
8415
8421
|
this.flushTimer = null;
|
|
8416
8422
|
}
|
|
8417
|
-
if (process.env.NODE_ENV !== "test") {
|
|
8423
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
8418
8424
|
await this.flushMetrics();
|
|
8419
8425
|
}
|
|
8420
8426
|
}
|
|
@@ -8593,10 +8599,18 @@ class MetricsPlugin extends plugin_class_default {
|
|
|
8593
8599
|
async flushMetrics() {
|
|
8594
8600
|
if (!this.metricsResource) return;
|
|
8595
8601
|
const [ok, err] = await try_fn_default(async () => {
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8602
|
+
let metadata, perfMetadata, errorMetadata, resourceMetadata;
|
|
8603
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "test") {
|
|
8604
|
+
metadata = {};
|
|
8605
|
+
perfMetadata = {};
|
|
8606
|
+
errorMetadata = {};
|
|
8607
|
+
resourceMetadata = {};
|
|
8608
|
+
} else {
|
|
8609
|
+
metadata = { global: "true" };
|
|
8610
|
+
perfMetadata = { perf: "true" };
|
|
8611
|
+
errorMetadata = { error: "true" };
|
|
8612
|
+
resourceMetadata = { resource: "true" };
|
|
8613
|
+
}
|
|
8600
8614
|
for (const [operation, data] of Object.entries(this.metrics.operations)) {
|
|
8601
8615
|
if (data.count > 0) {
|
|
8602
8616
|
await this.metricsResource.insert({
|
|
@@ -8946,6 +8960,9 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
8946
8960
|
await super.initialize(database);
|
|
8947
8961
|
const [ok, err, sdk] = await try_fn_default(() => import('@google-cloud/bigquery'));
|
|
8948
8962
|
if (!ok) {
|
|
8963
|
+
if (this.config.verbose) {
|
|
8964
|
+
console.warn(`[BigqueryReplicator] Failed to import BigQuery SDK: ${err.message}`);
|
|
8965
|
+
}
|
|
8949
8966
|
this.emit("initialization_error", { replicator: this.name, error: err.message });
|
|
8950
8967
|
throw err;
|
|
8951
8968
|
}
|
|
@@ -9015,19 +9032,28 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
9015
9032
|
const maxRetries = 2;
|
|
9016
9033
|
let lastError = null;
|
|
9017
9034
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
9018
|
-
|
|
9035
|
+
const [ok2, error] = await try_fn_default(async () => {
|
|
9019
9036
|
const [updateJob] = await this.bigqueryClient.createQueryJob({
|
|
9020
9037
|
query,
|
|
9021
9038
|
params,
|
|
9022
9039
|
location: this.location
|
|
9023
9040
|
});
|
|
9024
9041
|
await updateJob.getQueryResults();
|
|
9025
|
-
|
|
9042
|
+
return [updateJob];
|
|
9043
|
+
});
|
|
9044
|
+
if (ok2) {
|
|
9045
|
+
job = ok2;
|
|
9026
9046
|
break;
|
|
9027
|
-
}
|
|
9047
|
+
} else {
|
|
9028
9048
|
lastError = error;
|
|
9049
|
+
if (this.config.verbose) {
|
|
9050
|
+
console.warn(`[BigqueryReplicator] Update attempt ${attempt} failed: ${error.message}`);
|
|
9051
|
+
}
|
|
9029
9052
|
if (error?.message?.includes("streaming buffer") && attempt < maxRetries) {
|
|
9030
9053
|
const delaySeconds = 30;
|
|
9054
|
+
if (this.config.verbose) {
|
|
9055
|
+
console.warn(`[BigqueryReplicator] Retrying in ${delaySeconds} seconds due to streaming buffer issue`);
|
|
9056
|
+
}
|
|
9031
9057
|
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1e3));
|
|
9032
9058
|
continue;
|
|
9033
9059
|
}
|
|
@@ -9094,6 +9120,9 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
9094
9120
|
};
|
|
9095
9121
|
});
|
|
9096
9122
|
if (ok) return result;
|
|
9123
|
+
if (this.config.verbose) {
|
|
9124
|
+
console.warn(`[BigqueryReplicator] Replication failed for ${resourceName}: ${err.message}`);
|
|
9125
|
+
}
|
|
9097
9126
|
this.emit("replicator_error", {
|
|
9098
9127
|
replicator: this.name,
|
|
9099
9128
|
resourceName,
|
|
@@ -9114,8 +9143,14 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
9114
9143
|
record.id,
|
|
9115
9144
|
record.beforeData
|
|
9116
9145
|
));
|
|
9117
|
-
if (ok)
|
|
9118
|
-
|
|
9146
|
+
if (ok) {
|
|
9147
|
+
results.push(res);
|
|
9148
|
+
} else {
|
|
9149
|
+
if (this.config.verbose) {
|
|
9150
|
+
console.warn(`[BigqueryReplicator] Batch replication failed for record ${record.id}: ${err.message}`);
|
|
9151
|
+
}
|
|
9152
|
+
errors.push({ id: record.id, error: err.message });
|
|
9153
|
+
}
|
|
9119
9154
|
}
|
|
9120
9155
|
return {
|
|
9121
9156
|
success: errors.length === 0,
|
|
@@ -9131,6 +9166,9 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
9131
9166
|
return true;
|
|
9132
9167
|
});
|
|
9133
9168
|
if (ok) return true;
|
|
9169
|
+
if (this.config.verbose) {
|
|
9170
|
+
console.warn(`[BigqueryReplicator] Connection test failed: ${err.message}`);
|
|
9171
|
+
}
|
|
9134
9172
|
this.emit("connection_error", { replicator: this.name, error: err.message });
|
|
9135
9173
|
return false;
|
|
9136
9174
|
}
|
|
@@ -9218,6 +9256,9 @@ class PostgresReplicator extends base_replicator_class_default {
|
|
|
9218
9256
|
await super.initialize(database);
|
|
9219
9257
|
const [ok, err, sdk] = await try_fn_default(() => import('pg'));
|
|
9220
9258
|
if (!ok) {
|
|
9259
|
+
if (this.config.verbose) {
|
|
9260
|
+
console.warn(`[PostgresReplicator] Failed to import pg SDK: ${err.message}`);
|
|
9261
|
+
}
|
|
9221
9262
|
this.emit("initialization_error", {
|
|
9222
9263
|
replicator: this.name,
|
|
9223
9264
|
error: err.message
|
|
@@ -9359,6 +9400,9 @@ class PostgresReplicator extends base_replicator_class_default {
|
|
|
9359
9400
|
};
|
|
9360
9401
|
});
|
|
9361
9402
|
if (ok) return result;
|
|
9403
|
+
if (this.config.verbose) {
|
|
9404
|
+
console.warn(`[PostgresReplicator] Replication failed for ${resourceName}: ${err.message}`);
|
|
9405
|
+
}
|
|
9362
9406
|
this.emit("replicator_error", {
|
|
9363
9407
|
replicator: this.name,
|
|
9364
9408
|
resourceName,
|
|
@@ -9379,8 +9423,14 @@ class PostgresReplicator extends base_replicator_class_default {
|
|
|
9379
9423
|
record.id,
|
|
9380
9424
|
record.beforeData
|
|
9381
9425
|
));
|
|
9382
|
-
if (ok)
|
|
9383
|
-
|
|
9426
|
+
if (ok) {
|
|
9427
|
+
results.push(res);
|
|
9428
|
+
} else {
|
|
9429
|
+
if (this.config.verbose) {
|
|
9430
|
+
console.warn(`[PostgresReplicator] Batch replication failed for record ${record.id}: ${err.message}`);
|
|
9431
|
+
}
|
|
9432
|
+
errors.push({ id: record.id, error: err.message });
|
|
9433
|
+
}
|
|
9384
9434
|
}
|
|
9385
9435
|
return {
|
|
9386
9436
|
success: errors.length === 0,
|
|
@@ -9395,6 +9445,9 @@ class PostgresReplicator extends base_replicator_class_default {
|
|
|
9395
9445
|
return true;
|
|
9396
9446
|
});
|
|
9397
9447
|
if (ok) return true;
|
|
9448
|
+
if (this.config.verbose) {
|
|
9449
|
+
console.warn(`[PostgresReplicator] Connection test failed: ${err.message}`);
|
|
9450
|
+
}
|
|
9398
9451
|
this.emit("connection_error", { replicator: this.name, error: err.message });
|
|
9399
9452
|
return false;
|
|
9400
9453
|
}
|
|
@@ -13129,7 +13182,7 @@ class Database extends EventEmitter {
|
|
|
13129
13182
|
super();
|
|
13130
13183
|
this.version = "1";
|
|
13131
13184
|
this.s3dbVersion = (() => {
|
|
13132
|
-
const [ok, err, version] = try_fn_default(() => true ? "7.3.
|
|
13185
|
+
const [ok, err, version] = try_fn_default(() => true ? "7.3.6" : "latest");
|
|
13133
13186
|
return ok ? version : "latest";
|
|
13134
13187
|
})();
|
|
13135
13188
|
this.resources = {};
|
|
@@ -13172,14 +13225,16 @@ class Database extends EventEmitter {
|
|
|
13172
13225
|
this.keyPrefix = this.client.keyPrefix;
|
|
13173
13226
|
if (!this._exitListenerRegistered) {
|
|
13174
13227
|
this._exitListenerRegistered = true;
|
|
13175
|
-
process
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13228
|
+
if (typeof process !== "undefined") {
|
|
13229
|
+
process.on("exit", async () => {
|
|
13230
|
+
if (this.isConnected()) {
|
|
13231
|
+
try {
|
|
13232
|
+
await this.disconnect();
|
|
13233
|
+
} catch (err) {
|
|
13234
|
+
}
|
|
13180
13235
|
}
|
|
13181
|
-
}
|
|
13182
|
-
}
|
|
13236
|
+
});
|
|
13237
|
+
}
|
|
13183
13238
|
}
|
|
13184
13239
|
}
|
|
13185
13240
|
async connect() {
|
|
@@ -13631,9 +13686,8 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13631
13686
|
const map = {};
|
|
13632
13687
|
for (const res of resources) {
|
|
13633
13688
|
if (typeof res === "string") map[normalizeResourceName$1(res)] = res;
|
|
13634
|
-
else if (Array.isArray(res) && typeof res[0] === "string") map[normalizeResourceName$1(res[0])] = res;
|
|
13635
13689
|
else if (typeof res === "object" && res.resource) {
|
|
13636
|
-
map[normalizeResourceName$1(res.resource)] =
|
|
13690
|
+
map[normalizeResourceName$1(res.resource)] = res;
|
|
13637
13691
|
}
|
|
13638
13692
|
}
|
|
13639
13693
|
return map;
|
|
@@ -13646,15 +13700,14 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13646
13700
|
else if (Array.isArray(dest)) {
|
|
13647
13701
|
map[normSrc] = dest.map((item) => {
|
|
13648
13702
|
if (typeof item === "string") return item;
|
|
13649
|
-
if (typeof item === "function") return item;
|
|
13650
13703
|
if (typeof item === "object" && item.resource) {
|
|
13651
|
-
return
|
|
13704
|
+
return item;
|
|
13652
13705
|
}
|
|
13653
13706
|
return item;
|
|
13654
13707
|
});
|
|
13655
13708
|
} else if (typeof dest === "function") map[normSrc] = dest;
|
|
13656
13709
|
else if (typeof dest === "object" && dest.resource) {
|
|
13657
|
-
map[normSrc] =
|
|
13710
|
+
map[normSrc] = dest;
|
|
13658
13711
|
}
|
|
13659
13712
|
}
|
|
13660
13713
|
return map;
|
|
@@ -13662,10 +13715,6 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13662
13715
|
if (typeof resources === "function") {
|
|
13663
13716
|
return resources;
|
|
13664
13717
|
}
|
|
13665
|
-
if (typeof resources === "string") {
|
|
13666
|
-
const map = { [normalizeResourceName$1(resources)]: resources };
|
|
13667
|
-
return map;
|
|
13668
|
-
}
|
|
13669
13718
|
return {};
|
|
13670
13719
|
}
|
|
13671
13720
|
validateConfig() {
|
|
@@ -13679,8 +13728,8 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13679
13728
|
return { isValid: errors.length === 0, errors };
|
|
13680
13729
|
}
|
|
13681
13730
|
async initialize(database) {
|
|
13682
|
-
|
|
13683
|
-
|
|
13731
|
+
await super.initialize(database);
|
|
13732
|
+
const [ok, err] = await try_fn_default(async () => {
|
|
13684
13733
|
if (this.client) {
|
|
13685
13734
|
this.targetDatabase = this.client;
|
|
13686
13735
|
} else if (this.connectionString) {
|
|
@@ -13699,7 +13748,11 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13699
13748
|
replicator: this.name,
|
|
13700
13749
|
target: this.connectionString || "client-provided"
|
|
13701
13750
|
});
|
|
13702
|
-
}
|
|
13751
|
+
});
|
|
13752
|
+
if (!ok) {
|
|
13753
|
+
if (this.config.verbose) {
|
|
13754
|
+
console.warn(`[S3dbReplicator] Initialization failed: ${err.message}`);
|
|
13755
|
+
}
|
|
13703
13756
|
throw err;
|
|
13704
13757
|
}
|
|
13705
13758
|
}
|
|
@@ -13718,18 +13771,77 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13718
13771
|
id = recordId;
|
|
13719
13772
|
}
|
|
13720
13773
|
const normResource = normalizeResourceName$1(resource);
|
|
13721
|
-
const
|
|
13722
|
-
|
|
13723
|
-
|
|
13774
|
+
const entry = this.resourcesMap[normResource];
|
|
13775
|
+
if (!entry) {
|
|
13776
|
+
throw new Error(`[S3dbReplicator] Resource not configured: ${resource}`);
|
|
13777
|
+
}
|
|
13778
|
+
if (Array.isArray(entry)) {
|
|
13779
|
+
const results = [];
|
|
13780
|
+
for (const destConfig of entry) {
|
|
13781
|
+
const [ok, error, result] = await try_fn_default(async () => {
|
|
13782
|
+
return await this._replicateToSingleDestination(destConfig, normResource, op, payload, id);
|
|
13783
|
+
});
|
|
13784
|
+
if (!ok) {
|
|
13785
|
+
if (this.config && this.config.verbose) {
|
|
13786
|
+
console.warn(`[S3dbReplicator] Failed to replicate to destination ${JSON.stringify(destConfig)}: ${error.message}`);
|
|
13787
|
+
}
|
|
13788
|
+
throw error;
|
|
13789
|
+
}
|
|
13790
|
+
results.push(result);
|
|
13791
|
+
}
|
|
13792
|
+
return results;
|
|
13793
|
+
} else {
|
|
13794
|
+
const [ok, error, result] = await try_fn_default(async () => {
|
|
13795
|
+
return await this._replicateToSingleDestination(entry, normResource, op, payload, id);
|
|
13796
|
+
});
|
|
13797
|
+
if (!ok) {
|
|
13798
|
+
if (this.config && this.config.verbose) {
|
|
13799
|
+
console.warn(`[S3dbReplicator] Failed to replicate to destination ${JSON.stringify(entry)}: ${error.message}`);
|
|
13800
|
+
}
|
|
13801
|
+
throw error;
|
|
13802
|
+
}
|
|
13803
|
+
return result;
|
|
13804
|
+
}
|
|
13805
|
+
}
|
|
13806
|
+
async _replicateToSingleDestination(destConfig, sourceResource, operation, data, recordId) {
|
|
13807
|
+
let destResourceName;
|
|
13808
|
+
if (typeof destConfig === "string") {
|
|
13809
|
+
destResourceName = destConfig;
|
|
13810
|
+
} else if (typeof destConfig === "object" && destConfig.resource) {
|
|
13811
|
+
destResourceName = destConfig.resource;
|
|
13812
|
+
} else {
|
|
13813
|
+
destResourceName = sourceResource;
|
|
13814
|
+
}
|
|
13815
|
+
if (typeof destConfig === "object" && destConfig.actions && Array.isArray(destConfig.actions)) {
|
|
13816
|
+
if (!destConfig.actions.includes(operation)) {
|
|
13817
|
+
return { skipped: true, reason: "action_not_supported", action: operation, destination: destResourceName };
|
|
13818
|
+
}
|
|
13819
|
+
}
|
|
13820
|
+
const destResourceObj = this._getDestResourceObj(destResourceName);
|
|
13821
|
+
let transformedData;
|
|
13822
|
+
if (typeof destConfig === "object" && destConfig.transform && typeof destConfig.transform === "function") {
|
|
13823
|
+
transformedData = destConfig.transform(data);
|
|
13824
|
+
if (transformedData && data && data.id && !transformedData.id) {
|
|
13825
|
+
transformedData.id = data.id;
|
|
13826
|
+
}
|
|
13827
|
+
} else if (typeof destConfig === "object" && destConfig.transformer && typeof destConfig.transformer === "function") {
|
|
13828
|
+
transformedData = destConfig.transformer(data);
|
|
13829
|
+
if (transformedData && data && data.id && !transformedData.id) {
|
|
13830
|
+
transformedData.id = data.id;
|
|
13831
|
+
}
|
|
13832
|
+
} else {
|
|
13833
|
+
transformedData = data;
|
|
13834
|
+
}
|
|
13835
|
+
if (!transformedData && data) transformedData = data;
|
|
13724
13836
|
let result;
|
|
13725
|
-
if (
|
|
13837
|
+
if (operation === "insert") {
|
|
13726
13838
|
result = await destResourceObj.insert(transformedData);
|
|
13727
|
-
} else if (
|
|
13728
|
-
result = await destResourceObj.update(
|
|
13729
|
-
} else if (
|
|
13730
|
-
result = await destResourceObj.delete(
|
|
13839
|
+
} else if (operation === "update") {
|
|
13840
|
+
result = await destResourceObj.update(recordId, transformedData);
|
|
13841
|
+
} else if (operation === "delete") {
|
|
13842
|
+
result = await destResourceObj.delete(recordId);
|
|
13731
13843
|
} else {
|
|
13732
|
-
throw new Error(`Invalid operation: ${
|
|
13844
|
+
throw new Error(`Invalid operation: ${operation}. Supported operations are: insert, update, delete`);
|
|
13733
13845
|
}
|
|
13734
13846
|
return result;
|
|
13735
13847
|
}
|
|
@@ -13738,13 +13850,25 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13738
13850
|
const entry = this.resourcesMap[normResource];
|
|
13739
13851
|
let result;
|
|
13740
13852
|
if (!entry) return data;
|
|
13741
|
-
if (Array.isArray(entry)
|
|
13742
|
-
|
|
13853
|
+
if (Array.isArray(entry)) {
|
|
13854
|
+
for (const item of entry) {
|
|
13855
|
+
if (typeof item === "object" && item.transform && typeof item.transform === "function") {
|
|
13856
|
+
result = item.transform(data);
|
|
13857
|
+
break;
|
|
13858
|
+
} else if (typeof item === "object" && item.transformer && typeof item.transformer === "function") {
|
|
13859
|
+
result = item.transformer(data);
|
|
13860
|
+
break;
|
|
13861
|
+
}
|
|
13862
|
+
}
|
|
13863
|
+
if (!result) result = data;
|
|
13864
|
+
} else if (typeof entry === "object") {
|
|
13865
|
+
if (typeof entry.transform === "function") {
|
|
13866
|
+
result = entry.transform(data);
|
|
13867
|
+
} else if (typeof entry.transformer === "function") {
|
|
13868
|
+
result = entry.transformer(data);
|
|
13869
|
+
}
|
|
13743
13870
|
} else if (typeof entry === "function") {
|
|
13744
13871
|
result = entry(data);
|
|
13745
|
-
} else if (typeof entry === "object") {
|
|
13746
|
-
if (typeof entry.transform === "function") result = entry.transform(data);
|
|
13747
|
-
else if (typeof entry.transformer === "function") result = entry.transformer(data);
|
|
13748
13872
|
} else {
|
|
13749
13873
|
result = data;
|
|
13750
13874
|
}
|
|
@@ -13757,18 +13881,19 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13757
13881
|
const entry = this.resourcesMap[normResource];
|
|
13758
13882
|
if (!entry) return resource;
|
|
13759
13883
|
if (Array.isArray(entry)) {
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
|
|
13884
|
+
for (const item of entry) {
|
|
13885
|
+
if (typeof item === "string") return item;
|
|
13886
|
+
if (typeof item === "object" && item.resource) return item.resource;
|
|
13887
|
+
}
|
|
13888
|
+
return resource;
|
|
13763
13889
|
}
|
|
13764
13890
|
if (typeof entry === "string") return entry;
|
|
13765
|
-
if (
|
|
13891
|
+
if (typeof entry === "function") return resource;
|
|
13766
13892
|
if (typeof entry === "object" && entry.resource) return entry.resource;
|
|
13767
13893
|
return resource;
|
|
13768
13894
|
}
|
|
13769
13895
|
_getDestResourceObj(resource) {
|
|
13770
|
-
|
|
13771
|
-
const available = Object.keys(this.client.resources);
|
|
13896
|
+
const available = Object.keys(this.client.resources || {});
|
|
13772
13897
|
const norm = normalizeResourceName$1(resource);
|
|
13773
13898
|
const found = available.find((r) => normalizeResourceName$1(r) === norm);
|
|
13774
13899
|
if (!found) {
|
|
@@ -13790,8 +13915,14 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13790
13915
|
data: record.data,
|
|
13791
13916
|
beforeData: record.beforeData
|
|
13792
13917
|
}));
|
|
13793
|
-
if (ok)
|
|
13794
|
-
|
|
13918
|
+
if (ok) {
|
|
13919
|
+
results.push(result);
|
|
13920
|
+
} else {
|
|
13921
|
+
if (this.config.verbose) {
|
|
13922
|
+
console.warn(`[S3dbReplicator] Batch replication failed for record ${record.id}: ${err.message}`);
|
|
13923
|
+
}
|
|
13924
|
+
errors.push({ id: record.id, error: err.message });
|
|
13925
|
+
}
|
|
13795
13926
|
}
|
|
13796
13927
|
this.emit("batch_replicated", {
|
|
13797
13928
|
replicator: this.name,
|
|
@@ -13809,18 +13940,20 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13809
13940
|
}
|
|
13810
13941
|
async testConnection() {
|
|
13811
13942
|
const [ok, err] = await try_fn_default(async () => {
|
|
13812
|
-
if (!this.targetDatabase)
|
|
13813
|
-
|
|
13943
|
+
if (!this.targetDatabase) throw new Error("No target database configured");
|
|
13944
|
+
if (typeof this.targetDatabase.connect === "function") {
|
|
13945
|
+
await this.targetDatabase.connect();
|
|
13814
13946
|
}
|
|
13815
|
-
await this.targetDatabase.listResources();
|
|
13816
13947
|
return true;
|
|
13817
13948
|
});
|
|
13818
|
-
if (ok)
|
|
13819
|
-
|
|
13820
|
-
|
|
13821
|
-
|
|
13822
|
-
|
|
13823
|
-
|
|
13949
|
+
if (!ok) {
|
|
13950
|
+
if (this.config.verbose) {
|
|
13951
|
+
console.warn(`[S3dbReplicator] Connection test failed: ${err.message}`);
|
|
13952
|
+
}
|
|
13953
|
+
this.emit("connection_error", { replicator: this.name, error: err.message });
|
|
13954
|
+
return false;
|
|
13955
|
+
}
|
|
13956
|
+
return true;
|
|
13824
13957
|
}
|
|
13825
13958
|
async getStatus() {
|
|
13826
13959
|
const baseStatus = await super.getStatus();
|
|
@@ -13852,7 +13985,7 @@ class S3dbReplicator extends base_replicator_class_default {
|
|
|
13852
13985
|
} else {
|
|
13853
13986
|
return true;
|
|
13854
13987
|
}
|
|
13855
|
-
} else if (typeof item === "string"
|
|
13988
|
+
} else if (typeof item === "string") {
|
|
13856
13989
|
return true;
|
|
13857
13990
|
}
|
|
13858
13991
|
}
|
|
@@ -13979,6 +14112,9 @@ class SqsReplicator extends base_replicator_class_default {
|
|
|
13979
14112
|
if (!this.sqsClient) {
|
|
13980
14113
|
const [ok, err, sdk] = await try_fn_default(() => import('@aws-sdk/client-sqs'));
|
|
13981
14114
|
if (!ok) {
|
|
14115
|
+
if (this.config.verbose) {
|
|
14116
|
+
console.warn(`[SqsReplicator] Failed to import SQS SDK: ${err.message}`);
|
|
14117
|
+
}
|
|
13982
14118
|
this.emit("initialization_error", {
|
|
13983
14119
|
replicator: this.name,
|
|
13984
14120
|
error: err.message
|
|
@@ -14030,6 +14166,9 @@ class SqsReplicator extends base_replicator_class_default {
|
|
|
14030
14166
|
return { success: true, results };
|
|
14031
14167
|
});
|
|
14032
14168
|
if (ok) return result;
|
|
14169
|
+
if (this.config.verbose) {
|
|
14170
|
+
console.warn(`[SqsReplicator] Replication failed for ${resource}: ${err.message}`);
|
|
14171
|
+
}
|
|
14033
14172
|
this.emit("replicator_error", {
|
|
14034
14173
|
replicator: this.name,
|
|
14035
14174
|
resource,
|
|
@@ -14102,6 +14241,9 @@ class SqsReplicator extends base_replicator_class_default {
|
|
|
14102
14241
|
});
|
|
14103
14242
|
if (ok) return result;
|
|
14104
14243
|
const errorMessage = err?.message || err || "Unknown error";
|
|
14244
|
+
if (this.config.verbose) {
|
|
14245
|
+
console.warn(`[SqsReplicator] Batch replication failed for ${resource}: ${errorMessage}`);
|
|
14246
|
+
}
|
|
14105
14247
|
this.emit("batch_replicator_error", {
|
|
14106
14248
|
replicator: this.name,
|
|
14107
14249
|
resource,
|
|
@@ -14123,6 +14265,9 @@ class SqsReplicator extends base_replicator_class_default {
|
|
|
14123
14265
|
return true;
|
|
14124
14266
|
});
|
|
14125
14267
|
if (ok) return true;
|
|
14268
|
+
if (this.config.verbose) {
|
|
14269
|
+
console.warn(`[SqsReplicator] Connection test failed: ${err.message}`);
|
|
14270
|
+
}
|
|
14126
14271
|
this.emit("connection_error", {
|
|
14127
14272
|
replicator: this.name,
|
|
14128
14273
|
error: err.message
|
|
@@ -14219,25 +14364,37 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14219
14364
|
return;
|
|
14220
14365
|
}
|
|
14221
14366
|
resource.on("insert", async (data) => {
|
|
14222
|
-
|
|
14367
|
+
const [ok, error] = await try_fn_default(async () => {
|
|
14223
14368
|
const completeData = { ...data, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
14224
14369
|
await plugin.processReplicatorEvent("insert", resource.name, completeData.id, completeData);
|
|
14225
|
-
}
|
|
14370
|
+
});
|
|
14371
|
+
if (!ok) {
|
|
14372
|
+
if (this.config.verbose) {
|
|
14373
|
+
console.warn(`[ReplicatorPlugin] Insert event failed for resource ${resource.name}: ${error.message}`);
|
|
14374
|
+
}
|
|
14226
14375
|
this.emit("error", { operation: "insert", error: error.message, resource: resource.name });
|
|
14227
14376
|
}
|
|
14228
14377
|
});
|
|
14229
14378
|
resource.on("update", async (data, beforeData) => {
|
|
14230
|
-
|
|
14379
|
+
const [ok, error] = await try_fn_default(async () => {
|
|
14231
14380
|
const completeData = { ...data, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
14232
14381
|
await plugin.processReplicatorEvent("update", resource.name, completeData.id, completeData, beforeData);
|
|
14233
|
-
}
|
|
14382
|
+
});
|
|
14383
|
+
if (!ok) {
|
|
14384
|
+
if (this.config.verbose) {
|
|
14385
|
+
console.warn(`[ReplicatorPlugin] Update event failed for resource ${resource.name}: ${error.message}`);
|
|
14386
|
+
}
|
|
14234
14387
|
this.emit("error", { operation: "update", error: error.message, resource: resource.name });
|
|
14235
14388
|
}
|
|
14236
14389
|
});
|
|
14237
14390
|
resource.on("delete", async (data) => {
|
|
14238
|
-
|
|
14391
|
+
const [ok, error] = await try_fn_default(async () => {
|
|
14239
14392
|
await plugin.processReplicatorEvent("delete", resource.name, data.id, data);
|
|
14240
|
-
}
|
|
14393
|
+
});
|
|
14394
|
+
if (!ok) {
|
|
14395
|
+
if (this.config.verbose) {
|
|
14396
|
+
console.warn(`[ReplicatorPlugin] Delete event failed for resource ${resource.name}: ${error.message}`);
|
|
14397
|
+
}
|
|
14241
14398
|
this.emit("error", { operation: "delete", error: error.message, resource: resource.name });
|
|
14242
14399
|
}
|
|
14243
14400
|
});
|
|
@@ -14253,13 +14410,17 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14253
14410
|
}
|
|
14254
14411
|
async setup(database) {
|
|
14255
14412
|
this.database = database;
|
|
14256
|
-
|
|
14413
|
+
const [initOk, initError] = await try_fn_default(async () => {
|
|
14257
14414
|
await this.initializeReplicators(database);
|
|
14258
|
-
}
|
|
14259
|
-
|
|
14260
|
-
|
|
14415
|
+
});
|
|
14416
|
+
if (!initOk) {
|
|
14417
|
+
if (this.config.verbose) {
|
|
14418
|
+
console.warn(`[ReplicatorPlugin] Replicator initialization failed: ${initError.message}`);
|
|
14419
|
+
}
|
|
14420
|
+
this.emit("error", { operation: "setup", error: initError.message });
|
|
14421
|
+
throw initError;
|
|
14261
14422
|
}
|
|
14262
|
-
|
|
14423
|
+
const [logOk, logError] = await try_fn_default(async () => {
|
|
14263
14424
|
if (this.config.replicatorLogResource) {
|
|
14264
14425
|
const logRes = await database.createResource({
|
|
14265
14426
|
name: this.config.replicatorLogResource,
|
|
@@ -14276,7 +14437,15 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14276
14437
|
}
|
|
14277
14438
|
});
|
|
14278
14439
|
}
|
|
14279
|
-
}
|
|
14440
|
+
});
|
|
14441
|
+
if (!logOk) {
|
|
14442
|
+
if (this.config.verbose) {
|
|
14443
|
+
console.warn(`[ReplicatorPlugin] Failed to create log resource ${this.config.replicatorLogResource}: ${logError.message}`);
|
|
14444
|
+
}
|
|
14445
|
+
this.emit("replicator_log_resource_creation_error", {
|
|
14446
|
+
resourceName: this.config.replicatorLogResource,
|
|
14447
|
+
error: logError.message
|
|
14448
|
+
});
|
|
14280
14449
|
}
|
|
14281
14450
|
await this.uploadMetadataFile(database);
|
|
14282
14451
|
const originalCreateResource = database.createResource.bind(database);
|
|
@@ -14311,49 +14480,36 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14311
14480
|
}
|
|
14312
14481
|
async stop() {
|
|
14313
14482
|
}
|
|
14314
|
-
filterInternalFields(data) {
|
|
14315
|
-
if (!data || typeof data !== "object") return data;
|
|
14316
|
-
const filtered = {};
|
|
14317
|
-
for (const [key, value] of Object.entries(data)) {
|
|
14318
|
-
if (!key.startsWith("_") && !key.startsWith("$")) {
|
|
14319
|
-
filtered[key] = value;
|
|
14320
|
-
}
|
|
14321
|
-
}
|
|
14322
|
-
return filtered;
|
|
14323
|
-
}
|
|
14324
14483
|
async uploadMetadataFile(database) {
|
|
14325
14484
|
if (typeof database.uploadMetadataFile === "function") {
|
|
14326
14485
|
await database.uploadMetadataFile();
|
|
14327
14486
|
}
|
|
14328
14487
|
}
|
|
14329
|
-
async getCompleteData(resource, data) {
|
|
14330
|
-
try {
|
|
14331
|
-
const [ok, err, record] = await try_fn_default(() => resource.get(data.id));
|
|
14332
|
-
if (ok && record) {
|
|
14333
|
-
return record;
|
|
14334
|
-
}
|
|
14335
|
-
} catch (error) {
|
|
14336
|
-
}
|
|
14337
|
-
return data;
|
|
14338
|
-
}
|
|
14339
14488
|
async retryWithBackoff(operation, maxRetries = 3) {
|
|
14340
14489
|
let lastError;
|
|
14341
14490
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
14342
|
-
|
|
14343
|
-
|
|
14344
|
-
|
|
14491
|
+
const [ok, error] = await try_fn_default(operation);
|
|
14492
|
+
if (ok) {
|
|
14493
|
+
return ok;
|
|
14494
|
+
} else {
|
|
14345
14495
|
lastError = error;
|
|
14496
|
+
if (this.config.verbose) {
|
|
14497
|
+
console.warn(`[ReplicatorPlugin] Retry attempt ${attempt}/${maxRetries} failed: ${error.message}`);
|
|
14498
|
+
}
|
|
14346
14499
|
if (attempt === maxRetries) {
|
|
14347
14500
|
throw error;
|
|
14348
14501
|
}
|
|
14349
14502
|
const delay = Math.pow(2, attempt - 1) * 1e3;
|
|
14503
|
+
if (this.config.verbose) {
|
|
14504
|
+
console.warn(`[ReplicatorPlugin] Waiting ${delay}ms before retry...`);
|
|
14505
|
+
}
|
|
14350
14506
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
14351
14507
|
}
|
|
14352
14508
|
}
|
|
14353
14509
|
throw lastError;
|
|
14354
14510
|
}
|
|
14355
14511
|
async logError(replicator, resourceName, operation, recordId, data, error) {
|
|
14356
|
-
|
|
14512
|
+
const [ok, logError] = await try_fn_default(async () => {
|
|
14357
14513
|
const logResourceName = this.config.replicatorLogResource;
|
|
14358
14514
|
if (this.database && this.database.resources && this.database.resources[logResourceName]) {
|
|
14359
14515
|
const logResource = this.database.resources[logResourceName];
|
|
@@ -14368,7 +14524,19 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14368
14524
|
status: "error"
|
|
14369
14525
|
});
|
|
14370
14526
|
}
|
|
14371
|
-
}
|
|
14527
|
+
});
|
|
14528
|
+
if (!ok) {
|
|
14529
|
+
if (this.config.verbose) {
|
|
14530
|
+
console.warn(`[ReplicatorPlugin] Failed to log error for ${resourceName}: ${logError.message}`);
|
|
14531
|
+
}
|
|
14532
|
+
this.emit("replicator_log_error", {
|
|
14533
|
+
replicator: replicator.name || replicator.id,
|
|
14534
|
+
resourceName,
|
|
14535
|
+
operation,
|
|
14536
|
+
recordId,
|
|
14537
|
+
originalError: error.message,
|
|
14538
|
+
logError: logError.message
|
|
14539
|
+
});
|
|
14372
14540
|
}
|
|
14373
14541
|
}
|
|
14374
14542
|
async processReplicatorEvent(operation, resourceName, recordId, data, beforeData = null) {
|
|
@@ -14381,8 +14549,8 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14381
14549
|
return;
|
|
14382
14550
|
}
|
|
14383
14551
|
const promises = applicableReplicators.map(async (replicator) => {
|
|
14384
|
-
|
|
14385
|
-
const
|
|
14552
|
+
const [ok, error, result] = await try_fn_default(async () => {
|
|
14553
|
+
const result2 = await this.retryWithBackoff(
|
|
14386
14554
|
() => replicator.replicate(resourceName, operation, data, recordId, beforeData),
|
|
14387
14555
|
this.config.maxRetries
|
|
14388
14556
|
);
|
|
@@ -14391,11 +14559,17 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14391
14559
|
resourceName,
|
|
14392
14560
|
operation,
|
|
14393
14561
|
recordId,
|
|
14394
|
-
result,
|
|
14562
|
+
result: result2,
|
|
14395
14563
|
success: true
|
|
14396
14564
|
});
|
|
14565
|
+
return result2;
|
|
14566
|
+
});
|
|
14567
|
+
if (ok) {
|
|
14397
14568
|
return result;
|
|
14398
|
-
}
|
|
14569
|
+
} else {
|
|
14570
|
+
if (this.config.verbose) {
|
|
14571
|
+
console.warn(`[ReplicatorPlugin] Replication failed for ${replicator.name || replicator.id} on ${resourceName}: ${error.message}`);
|
|
14572
|
+
}
|
|
14399
14573
|
this.emit("replicator_error", {
|
|
14400
14574
|
replicator: replicator.name || replicator.id,
|
|
14401
14575
|
resourceName,
|
|
@@ -14420,11 +14594,14 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14420
14594
|
return;
|
|
14421
14595
|
}
|
|
14422
14596
|
const promises = applicableReplicators.map(async (replicator) => {
|
|
14423
|
-
|
|
14597
|
+
const [wrapperOk, wrapperError] = await try_fn_default(async () => {
|
|
14424
14598
|
const [ok, err, result] = await try_fn_default(
|
|
14425
14599
|
() => replicator.replicate(item.resourceName, item.operation, item.data, item.recordId, item.beforeData)
|
|
14426
14600
|
);
|
|
14427
14601
|
if (!ok) {
|
|
14602
|
+
if (this.config.verbose) {
|
|
14603
|
+
console.warn(`[ReplicatorPlugin] Replicator item processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${err.message}`);
|
|
14604
|
+
}
|
|
14428
14605
|
this.emit("replicator_error", {
|
|
14429
14606
|
replicator: replicator.name || replicator.id,
|
|
14430
14607
|
resourceName: item.resourceName,
|
|
@@ -14446,18 +14623,24 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14446
14623
|
success: true
|
|
14447
14624
|
});
|
|
14448
14625
|
return { success: true, result };
|
|
14449
|
-
}
|
|
14626
|
+
});
|
|
14627
|
+
if (wrapperOk) {
|
|
14628
|
+
return wrapperOk;
|
|
14629
|
+
} else {
|
|
14630
|
+
if (this.config.verbose) {
|
|
14631
|
+
console.warn(`[ReplicatorPlugin] Wrapper processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${wrapperError.message}`);
|
|
14632
|
+
}
|
|
14450
14633
|
this.emit("replicator_error", {
|
|
14451
14634
|
replicator: replicator.name || replicator.id,
|
|
14452
14635
|
resourceName: item.resourceName,
|
|
14453
14636
|
operation: item.operation,
|
|
14454
14637
|
recordId: item.recordId,
|
|
14455
|
-
error:
|
|
14638
|
+
error: wrapperError.message
|
|
14456
14639
|
});
|
|
14457
14640
|
if (this.config.logErrors && this.database) {
|
|
14458
|
-
await this.logError(replicator, item.resourceName, item.operation, item.recordId, item.data,
|
|
14641
|
+
await this.logError(replicator, item.resourceName, item.operation, item.recordId, item.data, wrapperError);
|
|
14459
14642
|
}
|
|
14460
|
-
return { success: false, error:
|
|
14643
|
+
return { success: false, error: wrapperError.message };
|
|
14461
14644
|
}
|
|
14462
14645
|
});
|
|
14463
14646
|
return Promise.allSettled(promises);
|
|
@@ -14479,9 +14662,13 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14479
14662
|
timestamp: typeof item.timestamp === "number" ? item.timestamp : Date.now(),
|
|
14480
14663
|
createdAt: item.createdAt || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
14481
14664
|
};
|
|
14482
|
-
|
|
14665
|
+
const [ok, err] = await try_fn_default(async () => {
|
|
14483
14666
|
await logRes.insert(logItem);
|
|
14484
|
-
}
|
|
14667
|
+
});
|
|
14668
|
+
if (!ok) {
|
|
14669
|
+
if (this.config.verbose) {
|
|
14670
|
+
console.warn(`[ReplicatorPlugin] Failed to log replicator item: ${err.message}`);
|
|
14671
|
+
}
|
|
14485
14672
|
this.emit("replicator.log.failed", { error: err, item });
|
|
14486
14673
|
}
|
|
14487
14674
|
}
|
|
@@ -14587,14 +14774,23 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14587
14774
|
this.emit("replicator.sync.completed", { replicatorId, stats: this.stats });
|
|
14588
14775
|
}
|
|
14589
14776
|
async cleanup() {
|
|
14590
|
-
|
|
14777
|
+
const [ok, error] = await try_fn_default(async () => {
|
|
14591
14778
|
if (this.replicators && this.replicators.length > 0) {
|
|
14592
14779
|
const cleanupPromises = this.replicators.map(async (replicator) => {
|
|
14593
|
-
|
|
14780
|
+
const [replicatorOk, replicatorError] = await try_fn_default(async () => {
|
|
14594
14781
|
if (replicator && typeof replicator.cleanup === "function") {
|
|
14595
14782
|
await replicator.cleanup();
|
|
14596
14783
|
}
|
|
14597
|
-
}
|
|
14784
|
+
});
|
|
14785
|
+
if (!replicatorOk) {
|
|
14786
|
+
if (this.config.verbose) {
|
|
14787
|
+
console.warn(`[ReplicatorPlugin] Failed to cleanup replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
|
|
14788
|
+
}
|
|
14789
|
+
this.emit("replicator_cleanup_error", {
|
|
14790
|
+
replicator: replicator.name || replicator.id || "unknown",
|
|
14791
|
+
driver: replicator.driver || "unknown",
|
|
14792
|
+
error: replicatorError.message
|
|
14793
|
+
});
|
|
14598
14794
|
}
|
|
14599
14795
|
});
|
|
14600
14796
|
await Promise.allSettled(cleanupPromises);
|
|
@@ -14603,7 +14799,14 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14603
14799
|
this.database = null;
|
|
14604
14800
|
this.eventListenersInstalled.clear();
|
|
14605
14801
|
this.removeAllListeners();
|
|
14606
|
-
}
|
|
14802
|
+
});
|
|
14803
|
+
if (!ok) {
|
|
14804
|
+
if (this.config.verbose) {
|
|
14805
|
+
console.warn(`[ReplicatorPlugin] Failed to cleanup plugin: ${error.message}`);
|
|
14806
|
+
}
|
|
14807
|
+
this.emit("replicator_plugin_cleanup_error", {
|
|
14808
|
+
error: error.message
|
|
14809
|
+
});
|
|
14607
14810
|
}
|
|
14608
14811
|
}
|
|
14609
14812
|
}
|