s3db.js 7.4.2 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLUGINS.md +3 -3
- package/dist/s3db.cjs.js +516 -349
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +10 -0
- package/dist/s3db.es.js +516 -349
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +517 -351
- package/dist/s3db.iife.min.js +1 -1
- package/mcp/README.md +1062 -0
- package/mcp/server.js +1 -1
- package/package.json +31 -29
- package/src/client.class.js +26 -5
- package/src/database.class.js +151 -0
- package/src/plugins/audit.plugin.js +143 -310
- package/src/plugins/cache/filesystem-cache.class.js +40 -11
- package/src/plugins/cache.plugin.js +95 -47
- package/src/plugins/fulltext.plugin.js +21 -0
- package/src/plugins/metrics.plugin.js +21 -6
- package/src/plugins/replicator.plugin.js +65 -70
- package/src/resource.class.js +3 -2
- package/src/s3db.d.ts +10 -0
package/dist/s3db.cjs.js
CHANGED
|
@@ -10,6 +10,7 @@ var promises = require('fs/promises');
|
|
|
10
10
|
var crypto = require('crypto');
|
|
11
11
|
var lodashEs = require('lodash-es');
|
|
12
12
|
var jsonStableStringify = require('json-stable-stringify');
|
|
13
|
+
var nodeHttpHandler = require('@smithy/node-http-handler');
|
|
13
14
|
var clientS3 = require('@aws-sdk/client-s3');
|
|
14
15
|
var flat = require('flat');
|
|
15
16
|
var FastestValidator = require('fastest-validator');
|
|
@@ -1271,7 +1272,6 @@ class AuditPlugin extends plugin_class_default {
|
|
|
1271
1272
|
includeData: options.includeData !== false,
|
|
1272
1273
|
includePartitions: options.includePartitions !== false,
|
|
1273
1274
|
maxDataSize: options.maxDataSize || 1e4,
|
|
1274
|
-
// 10KB limit
|
|
1275
1275
|
...options
|
|
1276
1276
|
};
|
|
1277
1277
|
}
|
|
@@ -1292,327 +1292,193 @@ class AuditPlugin extends plugin_class_default {
|
|
|
1292
1292
|
metadata: "string|optional"
|
|
1293
1293
|
},
|
|
1294
1294
|
behavior: "body-overflow"
|
|
1295
|
-
// keyPrefix removido
|
|
1296
1295
|
}));
|
|
1297
1296
|
this.auditResource = ok ? auditResource : this.database.resources.audits || null;
|
|
1298
1297
|
if (!ok && !this.auditResource) return;
|
|
1299
|
-
this.
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
async onStart() {
|
|
1303
|
-
}
|
|
1304
|
-
async onStop() {
|
|
1305
|
-
}
|
|
1306
|
-
installDatabaseProxy() {
|
|
1307
|
-
if (this.database._auditProxyInstalled) {
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
|
-
const installEventListenersForResource = this.installEventListenersForResource.bind(this);
|
|
1311
|
-
this.database._originalCreateResource = this.database.createResource;
|
|
1312
|
-
this.database.createResource = async function(...args) {
|
|
1313
|
-
const resource = await this._originalCreateResource(...args);
|
|
1314
|
-
if (resource.name !== "audits") {
|
|
1315
|
-
installEventListenersForResource(resource);
|
|
1298
|
+
this.database.addHook("afterCreateResource", (context) => {
|
|
1299
|
+
if (context.resource.name !== "audits") {
|
|
1300
|
+
this.setupResourceAuditing(context.resource);
|
|
1316
1301
|
}
|
|
1317
|
-
|
|
1318
|
-
};
|
|
1319
|
-
this.database._auditProxyInstalled = true;
|
|
1320
|
-
}
|
|
1321
|
-
installEventListeners() {
|
|
1302
|
+
});
|
|
1322
1303
|
for (const resource of Object.values(this.database.resources)) {
|
|
1323
|
-
if (resource.name
|
|
1324
|
-
|
|
1304
|
+
if (resource.name !== "audits") {
|
|
1305
|
+
this.setupResourceAuditing(resource);
|
|
1325
1306
|
}
|
|
1326
|
-
this.installEventListenersForResource(resource);
|
|
1327
1307
|
}
|
|
1328
1308
|
}
|
|
1329
|
-
|
|
1309
|
+
async onStart() {
|
|
1310
|
+
}
|
|
1311
|
+
async onStop() {
|
|
1312
|
+
}
|
|
1313
|
+
setupResourceAuditing(resource) {
|
|
1330
1314
|
resource.on("insert", async (data) => {
|
|
1331
|
-
|
|
1332
|
-
const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
|
|
1333
|
-
const auditRecord = {
|
|
1334
|
-
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
1315
|
+
await this.logAudit({
|
|
1335
1316
|
resourceName: resource.name,
|
|
1336
1317
|
operation: "insert",
|
|
1337
|
-
recordId,
|
|
1338
|
-
userId: this.getCurrentUserId?.() || "system",
|
|
1339
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1318
|
+
recordId: data.id || "auto-generated",
|
|
1340
1319
|
oldData: null,
|
|
1341
|
-
newData: this.config.includeData
|
|
1342
|
-
partition: this.config.includePartitions ? this.getPrimaryPartition(
|
|
1343
|
-
partitionValues: this.config.includePartitions
|
|
1344
|
-
metadata: JSON.stringify({
|
|
1345
|
-
source: "audit-plugin",
|
|
1346
|
-
version: "2.0"
|
|
1347
|
-
})
|
|
1348
|
-
};
|
|
1349
|
-
this.logAudit(auditRecord).catch(() => {
|
|
1320
|
+
newData: this.config.includeData ? JSON.stringify(this.truncateData(data)) : null,
|
|
1321
|
+
partition: this.config.includePartitions && this.getPartitionValues(data, resource) ? this.getPrimaryPartition(this.getPartitionValues(data, resource)) : null,
|
|
1322
|
+
partitionValues: this.config.includePartitions && this.getPartitionValues(data, resource) ? JSON.stringify(this.getPartitionValues(data, resource)) : null
|
|
1350
1323
|
});
|
|
1351
1324
|
});
|
|
1352
1325
|
resource.on("update", async (data) => {
|
|
1353
|
-
const recordId = data.id;
|
|
1354
1326
|
let oldData = data.$before;
|
|
1355
1327
|
if (this.config.includeData && !oldData) {
|
|
1356
|
-
const [ok, err, fetched] = await try_fn_default(() => resource.get(
|
|
1328
|
+
const [ok, err, fetched] = await try_fn_default(() => resource.get(data.id));
|
|
1357
1329
|
if (ok) oldData = fetched;
|
|
1358
1330
|
}
|
|
1359
|
-
|
|
1360
|
-
const auditRecord = {
|
|
1361
|
-
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
1331
|
+
await this.logAudit({
|
|
1362
1332
|
resourceName: resource.name,
|
|
1363
1333
|
operation: "update",
|
|
1364
|
-
recordId,
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
|
|
1370
|
-
partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
|
|
1371
|
-
metadata: JSON.stringify({
|
|
1372
|
-
source: "audit-plugin",
|
|
1373
|
-
version: "2.0"
|
|
1374
|
-
})
|
|
1375
|
-
};
|
|
1376
|
-
this.logAudit(auditRecord).catch(() => {
|
|
1334
|
+
recordId: data.id,
|
|
1335
|
+
oldData: oldData && this.config.includeData ? JSON.stringify(this.truncateData(oldData)) : null,
|
|
1336
|
+
newData: this.config.includeData ? JSON.stringify(this.truncateData(data)) : null,
|
|
1337
|
+
partition: this.config.includePartitions && this.getPartitionValues(data, resource) ? this.getPrimaryPartition(this.getPartitionValues(data, resource)) : null,
|
|
1338
|
+
partitionValues: this.config.includePartitions && this.getPartitionValues(data, resource) ? JSON.stringify(this.getPartitionValues(data, resource)) : null
|
|
1377
1339
|
});
|
|
1378
1340
|
});
|
|
1379
1341
|
resource.on("delete", async (data) => {
|
|
1380
|
-
const recordId = data.id;
|
|
1381
1342
|
let oldData = data;
|
|
1382
1343
|
if (this.config.includeData && !oldData) {
|
|
1383
|
-
const [ok, err, fetched] = await try_fn_default(() => resource.get(
|
|
1344
|
+
const [ok, err, fetched] = await try_fn_default(() => resource.get(data.id));
|
|
1384
1345
|
if (ok) oldData = fetched;
|
|
1385
1346
|
}
|
|
1386
|
-
|
|
1387
|
-
const auditRecord = {
|
|
1388
|
-
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
1347
|
+
await this.logAudit({
|
|
1389
1348
|
resourceName: resource.name,
|
|
1390
1349
|
operation: "delete",
|
|
1391
|
-
recordId,
|
|
1392
|
-
|
|
1393
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1394
|
-
oldData: oldData && this.config.includeData === false ? null : oldData ? JSON.stringify(this.truncateData(oldData)) : null,
|
|
1350
|
+
recordId: data.id,
|
|
1351
|
+
oldData: oldData && this.config.includeData ? JSON.stringify(this.truncateData(oldData)) : null,
|
|
1395
1352
|
newData: null,
|
|
1396
|
-
partition: this.config.includePartitions ? this.getPrimaryPartition(
|
|
1397
|
-
partitionValues: this.config.includePartitions
|
|
1398
|
-
metadata: JSON.stringify({
|
|
1399
|
-
source: "audit-plugin",
|
|
1400
|
-
version: "2.0"
|
|
1401
|
-
})
|
|
1402
|
-
};
|
|
1403
|
-
this.logAudit(auditRecord).catch(() => {
|
|
1353
|
+
partition: oldData && this.config.includePartitions && this.getPartitionValues(oldData, resource) ? this.getPrimaryPartition(this.getPartitionValues(oldData, resource)) : null,
|
|
1354
|
+
partitionValues: oldData && this.config.includePartitions && this.getPartitionValues(oldData, resource) ? JSON.stringify(this.getPartitionValues(oldData, resource)) : null
|
|
1404
1355
|
});
|
|
1405
1356
|
});
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
return result;
|
|
1442
|
-
});
|
|
1357
|
+
}
|
|
1358
|
+
// Backward compatibility for tests
|
|
1359
|
+
installEventListenersForResource(resource) {
|
|
1360
|
+
return this.setupResourceAuditing(resource);
|
|
1361
|
+
}
|
|
1362
|
+
async logAudit(auditData) {
|
|
1363
|
+
if (!this.auditResource) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
const auditRecord = {
|
|
1367
|
+
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
1368
|
+
userId: this.getCurrentUserId?.() || "system",
|
|
1369
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1370
|
+
metadata: JSON.stringify({ source: "audit-plugin", version: "2.0" }),
|
|
1371
|
+
resourceName: auditData.resourceName,
|
|
1372
|
+
operation: auditData.operation,
|
|
1373
|
+
recordId: auditData.recordId
|
|
1374
|
+
};
|
|
1375
|
+
if (auditData.oldData !== null) {
|
|
1376
|
+
auditRecord.oldData = auditData.oldData;
|
|
1377
|
+
}
|
|
1378
|
+
if (auditData.newData !== null) {
|
|
1379
|
+
auditRecord.newData = auditData.newData;
|
|
1380
|
+
}
|
|
1381
|
+
if (auditData.partition !== null) {
|
|
1382
|
+
auditRecord.partition = auditData.partition;
|
|
1383
|
+
}
|
|
1384
|
+
if (auditData.partitionValues !== null) {
|
|
1385
|
+
auditRecord.partitionValues = auditData.partitionValues;
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
await this.auditResource.insert(auditRecord);
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
console.warn("Audit logging failed:", error.message);
|
|
1391
|
+
}
|
|
1443
1392
|
}
|
|
1444
1393
|
getPartitionValues(data, resource) {
|
|
1445
|
-
if (!
|
|
1446
|
-
const partitions = resource.config?.partitions || {};
|
|
1394
|
+
if (!this.config.includePartitions || !resource.partitions) return null;
|
|
1447
1395
|
const partitionValues = {};
|
|
1448
|
-
for (const [partitionName,
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
if (Object.keys(partitionData).length > 0) {
|
|
1458
|
-
partitionValues[partitionName] = partitionData;
|
|
1459
|
-
}
|
|
1396
|
+
for (const [partitionName, partitionConfig] of Object.entries(resource.partitions)) {
|
|
1397
|
+
const values = {};
|
|
1398
|
+
for (const field of Object.keys(partitionConfig.fields)) {
|
|
1399
|
+
values[field] = this.getNestedFieldValue(data, field);
|
|
1400
|
+
}
|
|
1401
|
+
if (Object.values(values).some((v) => v !== void 0 && v !== null)) {
|
|
1402
|
+
partitionValues[partitionName] = values;
|
|
1460
1403
|
}
|
|
1461
1404
|
}
|
|
1462
|
-
return partitionValues;
|
|
1405
|
+
return Object.keys(partitionValues).length > 0 ? partitionValues : null;
|
|
1463
1406
|
}
|
|
1464
1407
|
getNestedFieldValue(data, fieldPath) {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
if (!currentLevel || typeof currentLevel !== "object" || !(key in currentLevel)) {
|
|
1408
|
+
const parts = fieldPath.split(".");
|
|
1409
|
+
let value = data;
|
|
1410
|
+
for (const part of parts) {
|
|
1411
|
+
if (value && typeof value === "object" && part in value) {
|
|
1412
|
+
value = value[part];
|
|
1413
|
+
} else {
|
|
1472
1414
|
return void 0;
|
|
1473
1415
|
}
|
|
1474
|
-
currentLevel = currentLevel[key];
|
|
1475
1416
|
}
|
|
1476
|
-
return
|
|
1417
|
+
return value;
|
|
1477
1418
|
}
|
|
1478
1419
|
getPrimaryPartition(partitionValues) {
|
|
1479
1420
|
if (!partitionValues) return null;
|
|
1480
1421
|
const partitionNames = Object.keys(partitionValues);
|
|
1481
1422
|
return partitionNames.length > 0 ? partitionNames[0] : null;
|
|
1482
1423
|
}
|
|
1483
|
-
async logAudit(auditRecord) {
|
|
1484
|
-
if (!auditRecord.id) {
|
|
1485
|
-
auditRecord.id = `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1486
|
-
}
|
|
1487
|
-
const result = await this.auditResource.insert(auditRecord);
|
|
1488
|
-
return result;
|
|
1489
|
-
}
|
|
1490
1424
|
truncateData(data) {
|
|
1491
|
-
if (!
|
|
1492
|
-
const
|
|
1493
|
-
for (const [key, value] of Object.entries(data)) {
|
|
1494
|
-
if (!key.startsWith("_") && key !== "$overflow") {
|
|
1495
|
-
filteredData[key] = value;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
const dataStr = JSON.stringify(filteredData);
|
|
1425
|
+
if (!this.config.includeData) return null;
|
|
1426
|
+
const dataStr = JSON.stringify(data);
|
|
1499
1427
|
if (dataStr.length <= this.config.maxDataSize) {
|
|
1500
|
-
return
|
|
1501
|
-
}
|
|
1502
|
-
let truncatedData = { ...filteredData };
|
|
1503
|
-
let currentSize = JSON.stringify(truncatedData).length;
|
|
1504
|
-
const metadataOverhead = JSON.stringify({
|
|
1505
|
-
_truncated: true,
|
|
1506
|
-
_originalSize: dataStr.length,
|
|
1507
|
-
_truncatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1508
|
-
}).length;
|
|
1509
|
-
const targetSize = this.config.maxDataSize - metadataOverhead;
|
|
1510
|
-
for (const [key, value] of Object.entries(truncatedData)) {
|
|
1511
|
-
if (typeof value === "string" && currentSize > targetSize) {
|
|
1512
|
-
const excess = currentSize - targetSize;
|
|
1513
|
-
const newLength = Math.max(0, value.length - excess - 3);
|
|
1514
|
-
if (newLength < value.length) {
|
|
1515
|
-
truncatedData[key] = value.substring(0, newLength) + "...";
|
|
1516
|
-
currentSize = JSON.stringify(truncatedData).length;
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1428
|
+
return data;
|
|
1519
1429
|
}
|
|
1520
1430
|
return {
|
|
1521
|
-
...
|
|
1431
|
+
...data,
|
|
1522
1432
|
_truncated: true,
|
|
1523
1433
|
_originalSize: dataStr.length,
|
|
1524
1434
|
_truncatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1525
1435
|
};
|
|
1526
1436
|
}
|
|
1527
|
-
// Utility methods for querying audit logs
|
|
1528
1437
|
async getAuditLogs(options = {}) {
|
|
1529
1438
|
if (!this.auditResource) return [];
|
|
1530
|
-
const
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
let filtered = allAudits.filter((audit) => {
|
|
1544
|
-
if (resourceName && audit.resourceName !== resourceName) return false;
|
|
1545
|
-
if (operation && audit.operation !== operation) return false;
|
|
1546
|
-
if (recordId && audit.recordId !== recordId) return false;
|
|
1547
|
-
if (userId && audit.userId !== userId) return false;
|
|
1548
|
-
if (partition && audit.partition !== partition) return false;
|
|
1549
|
-
if (startDate && new Date(audit.timestamp) < new Date(startDate)) return false;
|
|
1550
|
-
if (endDate && new Date(audit.timestamp) > new Date(endDate)) return false;
|
|
1551
|
-
return true;
|
|
1552
|
-
});
|
|
1553
|
-
filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
1554
|
-
const deserialized = filtered.slice(offset, offset + limit).map((audit) => {
|
|
1555
|
-
const [okOld, , oldData] = typeof audit.oldData === "string" ? tryFnSync(() => JSON.parse(audit.oldData)) : [true, null, audit.oldData];
|
|
1556
|
-
const [okNew, , newData] = typeof audit.newData === "string" ? tryFnSync(() => JSON.parse(audit.newData)) : [true, null, audit.newData];
|
|
1557
|
-
const [okPart, , partitionValues] = audit.partitionValues && typeof audit.partitionValues === "string" ? tryFnSync(() => JSON.parse(audit.partitionValues)) : [true, null, audit.partitionValues];
|
|
1558
|
-
const [okMeta, , metadata] = audit.metadata && typeof audit.metadata === "string" ? tryFnSync(() => JSON.parse(audit.metadata)) : [true, null, audit.metadata];
|
|
1559
|
-
return {
|
|
1560
|
-
...audit,
|
|
1561
|
-
oldData: audit.oldData === null || audit.oldData === void 0 || audit.oldData === "null" ? null : okOld ? oldData : null,
|
|
1562
|
-
newData: audit.newData === null || audit.newData === void 0 || audit.newData === "null" ? null : okNew ? newData : null,
|
|
1563
|
-
partitionValues: okPart ? partitionValues : audit.partitionValues,
|
|
1564
|
-
metadata: okMeta ? metadata : audit.metadata
|
|
1565
|
-
};
|
|
1566
|
-
});
|
|
1567
|
-
return deserialized;
|
|
1568
|
-
});
|
|
1569
|
-
return ok ? result : [];
|
|
1439
|
+
const { resourceName, operation, recordId, partition, startDate, endDate, limit = 100 } = options;
|
|
1440
|
+
let query = {};
|
|
1441
|
+
if (resourceName) query.resourceName = resourceName;
|
|
1442
|
+
if (operation) query.operation = operation;
|
|
1443
|
+
if (recordId) query.recordId = recordId;
|
|
1444
|
+
if (partition) query.partition = partition;
|
|
1445
|
+
if (startDate || endDate) {
|
|
1446
|
+
query.timestamp = {};
|
|
1447
|
+
if (startDate) query.timestamp.$gte = startDate;
|
|
1448
|
+
if (endDate) query.timestamp.$lte = endDate;
|
|
1449
|
+
}
|
|
1450
|
+
const result = await this.auditResource.page({ query, limit });
|
|
1451
|
+
return result.items || [];
|
|
1570
1452
|
}
|
|
1571
1453
|
async getRecordHistory(resourceName, recordId) {
|
|
1572
|
-
return this.getAuditLogs({
|
|
1573
|
-
resourceName,
|
|
1574
|
-
recordId,
|
|
1575
|
-
limit: 1e3
|
|
1576
|
-
});
|
|
1454
|
+
return await this.getAuditLogs({ resourceName, recordId });
|
|
1577
1455
|
}
|
|
1578
1456
|
async getPartitionHistory(resourceName, partitionName, partitionValues) {
|
|
1579
|
-
return this.getAuditLogs({
|
|
1457
|
+
return await this.getAuditLogs({
|
|
1580
1458
|
resourceName,
|
|
1581
1459
|
partition: partitionName,
|
|
1582
|
-
|
|
1460
|
+
partitionValues: JSON.stringify(partitionValues)
|
|
1583
1461
|
});
|
|
1584
1462
|
}
|
|
1585
1463
|
async getAuditStats(options = {}) {
|
|
1586
|
-
const
|
|
1587
|
-
resourceName,
|
|
1588
|
-
startDate,
|
|
1589
|
-
endDate
|
|
1590
|
-
} = options;
|
|
1591
|
-
const allAudits = await this.getAuditLogs({
|
|
1592
|
-
resourceName,
|
|
1593
|
-
startDate,
|
|
1594
|
-
endDate,
|
|
1595
|
-
limit: 1e4
|
|
1596
|
-
});
|
|
1464
|
+
const logs = await this.getAuditLogs(options);
|
|
1597
1465
|
const stats = {
|
|
1598
|
-
total:
|
|
1466
|
+
total: logs.length,
|
|
1599
1467
|
byOperation: {},
|
|
1600
1468
|
byResource: {},
|
|
1601
1469
|
byPartition: {},
|
|
1602
1470
|
byUser: {},
|
|
1603
1471
|
timeline: {}
|
|
1604
1472
|
};
|
|
1605
|
-
for (const
|
|
1606
|
-
stats.byOperation[
|
|
1607
|
-
stats.byResource[
|
|
1608
|
-
if (
|
|
1609
|
-
stats.byPartition[
|
|
1610
|
-
}
|
|
1611
|
-
stats.byUser[audit.userId] = (stats.byUser[audit.userId] || 0) + 1;
|
|
1612
|
-
if (audit.timestamp) {
|
|
1613
|
-
const day = audit.timestamp.split("T")[0];
|
|
1614
|
-
stats.timeline[day] = (stats.timeline[day] || 0) + 1;
|
|
1473
|
+
for (const log of logs) {
|
|
1474
|
+
stats.byOperation[log.operation] = (stats.byOperation[log.operation] || 0) + 1;
|
|
1475
|
+
stats.byResource[log.resourceName] = (stats.byResource[log.resourceName] || 0) + 1;
|
|
1476
|
+
if (log.partition) {
|
|
1477
|
+
stats.byPartition[log.partition] = (stats.byPartition[log.partition] || 0) + 1;
|
|
1615
1478
|
}
|
|
1479
|
+
stats.byUser[log.userId] = (stats.byUser[log.userId] || 0) + 1;
|
|
1480
|
+
const date = log.timestamp.split("T")[0];
|
|
1481
|
+
stats.timeline[date] = (stats.timeline[date] || 0) + 1;
|
|
1616
1482
|
}
|
|
1617
1483
|
return stats;
|
|
1618
1484
|
}
|
|
@@ -7101,19 +6967,37 @@ class FilesystemCache extends Cache {
|
|
|
7101
6967
|
});
|
|
7102
6968
|
for (const file of cacheFiles) {
|
|
7103
6969
|
const filePath = path.join(this.directory, file);
|
|
7104
|
-
|
|
7105
|
-
await
|
|
6970
|
+
try {
|
|
6971
|
+
if (await this._fileExists(filePath)) {
|
|
6972
|
+
await promises.unlink(filePath);
|
|
6973
|
+
}
|
|
6974
|
+
} catch (error) {
|
|
6975
|
+
if (error.code !== "ENOENT") {
|
|
6976
|
+
throw error;
|
|
6977
|
+
}
|
|
7106
6978
|
}
|
|
7107
6979
|
if (this.enableMetadata) {
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
await
|
|
6980
|
+
try {
|
|
6981
|
+
const metadataPath = this._getMetadataPath(filePath);
|
|
6982
|
+
if (await this._fileExists(metadataPath)) {
|
|
6983
|
+
await promises.unlink(metadataPath);
|
|
6984
|
+
}
|
|
6985
|
+
} catch (error) {
|
|
6986
|
+
if (error.code !== "ENOENT") {
|
|
6987
|
+
throw error;
|
|
6988
|
+
}
|
|
7111
6989
|
}
|
|
7112
6990
|
}
|
|
7113
6991
|
if (this.enableBackup) {
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
await
|
|
6992
|
+
try {
|
|
6993
|
+
const backupPath = filePath + this.backupSuffix;
|
|
6994
|
+
if (await this._fileExists(backupPath)) {
|
|
6995
|
+
await promises.unlink(backupPath);
|
|
6996
|
+
}
|
|
6997
|
+
} catch (error) {
|
|
6998
|
+
if (error.code !== "ENOENT") {
|
|
6999
|
+
throw error;
|
|
7000
|
+
}
|
|
7117
7001
|
}
|
|
7118
7002
|
}
|
|
7119
7003
|
}
|
|
@@ -7125,6 +7009,12 @@ class FilesystemCache extends Cache {
|
|
|
7125
7009
|
}
|
|
7126
7010
|
return true;
|
|
7127
7011
|
} catch (error) {
|
|
7012
|
+
if (error.code === "ENOENT") {
|
|
7013
|
+
if (this.enableStats) {
|
|
7014
|
+
this.stats.clears++;
|
|
7015
|
+
}
|
|
7016
|
+
return true;
|
|
7017
|
+
}
|
|
7128
7018
|
if (this.enableStats) {
|
|
7129
7019
|
this.stats.errors++;
|
|
7130
7020
|
}
|
|
@@ -7610,11 +7500,11 @@ class CachePlugin extends plugin_class_default {
|
|
|
7610
7500
|
await super.setup(database);
|
|
7611
7501
|
}
|
|
7612
7502
|
async onSetup() {
|
|
7613
|
-
if (this.config.driver) {
|
|
7503
|
+
if (this.config.driver && typeof this.config.driver === "object") {
|
|
7614
7504
|
this.driver = this.config.driver;
|
|
7615
|
-
} else if (this.config.
|
|
7505
|
+
} else if (this.config.driver === "memory") {
|
|
7616
7506
|
this.driver = new memory_cache_class_default(this.config.memoryOptions || {});
|
|
7617
|
-
} else if (this.config.
|
|
7507
|
+
} else if (this.config.driver === "filesystem") {
|
|
7618
7508
|
if (this.config.partitionAware) {
|
|
7619
7509
|
this.driver = new PartitionAwareFilesystemCache({
|
|
7620
7510
|
partitionStrategy: this.config.partitionStrategy,
|
|
@@ -7628,26 +7518,22 @@ class CachePlugin extends plugin_class_default {
|
|
|
7628
7518
|
} else {
|
|
7629
7519
|
this.driver = new s3_cache_class_default({ client: this.database.client, ...this.config.s3Options || {} });
|
|
7630
7520
|
}
|
|
7631
|
-
this.
|
|
7521
|
+
this.installDatabaseHooks();
|
|
7632
7522
|
this.installResourceHooks();
|
|
7633
7523
|
}
|
|
7524
|
+
/**
|
|
7525
|
+
* Install database hooks to handle resource creation/updates
|
|
7526
|
+
*/
|
|
7527
|
+
installDatabaseHooks() {
|
|
7528
|
+
this.database.addHook("afterCreateResource", async ({ resource }) => {
|
|
7529
|
+
this.installResourceHooksForResource(resource);
|
|
7530
|
+
});
|
|
7531
|
+
}
|
|
7634
7532
|
async onStart() {
|
|
7635
7533
|
}
|
|
7636
7534
|
async onStop() {
|
|
7637
7535
|
}
|
|
7638
|
-
installDatabaseProxy
|
|
7639
|
-
if (this.database._cacheProxyInstalled) {
|
|
7640
|
-
return;
|
|
7641
|
-
}
|
|
7642
|
-
const installResourceHooks = this.installResourceHooks.bind(this);
|
|
7643
|
-
this.database._originalCreateResourceForCache = this.database.createResource;
|
|
7644
|
-
this.database.createResource = async function(...args) {
|
|
7645
|
-
const resource = await this._originalCreateResourceForCache(...args);
|
|
7646
|
-
installResourceHooks(resource);
|
|
7647
|
-
return resource;
|
|
7648
|
-
};
|
|
7649
|
-
this.database._cacheProxyInstalled = true;
|
|
7650
|
-
}
|
|
7536
|
+
// Remove the old installDatabaseProxy method
|
|
7651
7537
|
installResourceHooks() {
|
|
7652
7538
|
for (const resource of Object.values(this.database.resources)) {
|
|
7653
7539
|
this.installResourceHooksForResource(resource);
|
|
@@ -7686,7 +7572,12 @@ class CachePlugin extends plugin_class_default {
|
|
|
7686
7572
|
"getAll",
|
|
7687
7573
|
"page",
|
|
7688
7574
|
"list",
|
|
7689
|
-
"get"
|
|
7575
|
+
"get",
|
|
7576
|
+
"exists",
|
|
7577
|
+
"content",
|
|
7578
|
+
"hasContent",
|
|
7579
|
+
"query",
|
|
7580
|
+
"getFromPartition"
|
|
7690
7581
|
];
|
|
7691
7582
|
for (const method of cacheMethods) {
|
|
7692
7583
|
resource.useMiddleware(method, async (ctx, next) => {
|
|
@@ -7699,9 +7590,26 @@ class CachePlugin extends plugin_class_default {
|
|
|
7699
7590
|
} else if (method === "list" || method === "listIds" || method === "count") {
|
|
7700
7591
|
const { partition, partitionValues } = ctx.args[0] || {};
|
|
7701
7592
|
key = await resource.cacheKeyFor({ action: method, partition, partitionValues });
|
|
7593
|
+
} else if (method === "query") {
|
|
7594
|
+
const filter = ctx.args[0] || {};
|
|
7595
|
+
const options = ctx.args[1] || {};
|
|
7596
|
+
key = await resource.cacheKeyFor({
|
|
7597
|
+
action: method,
|
|
7598
|
+
params: { filter, options: { limit: options.limit, offset: options.offset } },
|
|
7599
|
+
partition: options.partition,
|
|
7600
|
+
partitionValues: options.partitionValues
|
|
7601
|
+
});
|
|
7602
|
+
} else if (method === "getFromPartition") {
|
|
7603
|
+
const { id, partitionName, partitionValues } = ctx.args[0] || {};
|
|
7604
|
+
key = await resource.cacheKeyFor({
|
|
7605
|
+
action: method,
|
|
7606
|
+
params: { id, partitionName },
|
|
7607
|
+
partition: partitionName,
|
|
7608
|
+
partitionValues
|
|
7609
|
+
});
|
|
7702
7610
|
} else if (method === "getAll") {
|
|
7703
7611
|
key = await resource.cacheKeyFor({ action: method });
|
|
7704
|
-
} else if (
|
|
7612
|
+
} else if (["get", "exists", "content", "hasContent"].includes(method)) {
|
|
7705
7613
|
key = await resource.cacheKeyFor({ action: method, params: { id: ctx.args[0] } });
|
|
7706
7614
|
}
|
|
7707
7615
|
if (this.driver instanceof PartitionAwareFilesystemCache) {
|
|
@@ -7710,6 +7618,14 @@ class CachePlugin extends plugin_class_default {
|
|
|
7710
7618
|
const args = ctx.args[0] || {};
|
|
7711
7619
|
partition = args.partition;
|
|
7712
7620
|
partitionValues = args.partitionValues;
|
|
7621
|
+
} else if (method === "query") {
|
|
7622
|
+
const options = ctx.args[1] || {};
|
|
7623
|
+
partition = options.partition;
|
|
7624
|
+
partitionValues = options.partitionValues;
|
|
7625
|
+
} else if (method === "getFromPartition") {
|
|
7626
|
+
const { partitionName, partitionValues: pValues } = ctx.args[0] || {};
|
|
7627
|
+
partition = partitionName;
|
|
7628
|
+
partitionValues = pValues;
|
|
7713
7629
|
}
|
|
7714
7630
|
const [ok, err, result] = await try_fn_default(() => resource.cache._get(key, {
|
|
7715
7631
|
resource: resource.name,
|
|
@@ -7737,7 +7653,7 @@ class CachePlugin extends plugin_class_default {
|
|
|
7737
7653
|
}
|
|
7738
7654
|
});
|
|
7739
7655
|
}
|
|
7740
|
-
const writeMethods = ["insert", "update", "delete", "deleteMany"];
|
|
7656
|
+
const writeMethods = ["insert", "update", "delete", "deleteMany", "setContent", "deleteContent", "replace"];
|
|
7741
7657
|
for (const method of writeMethods) {
|
|
7742
7658
|
resource.useMiddleware(method, async (ctx, next) => {
|
|
7743
7659
|
const result = await next();
|
|
@@ -7752,6 +7668,12 @@ class CachePlugin extends plugin_class_default {
|
|
|
7752
7668
|
if (ok && full) data = full;
|
|
7753
7669
|
}
|
|
7754
7670
|
await this.clearCacheForResource(resource, data);
|
|
7671
|
+
} else if (method === "setContent" || method === "deleteContent") {
|
|
7672
|
+
const id = ctx.args[0]?.id || ctx.args[0];
|
|
7673
|
+
await this.clearCacheForResource(resource, { id });
|
|
7674
|
+
} else if (method === "replace") {
|
|
7675
|
+
const id = ctx.args[0];
|
|
7676
|
+
await this.clearCacheForResource(resource, { id, ...ctx.args[1] });
|
|
7755
7677
|
} else if (method === "deleteMany") {
|
|
7756
7678
|
await this.clearCacheForResource(resource);
|
|
7757
7679
|
}
|
|
@@ -7762,23 +7684,40 @@ class CachePlugin extends plugin_class_default {
|
|
|
7762
7684
|
async clearCacheForResource(resource, data) {
|
|
7763
7685
|
if (!resource.cache) return;
|
|
7764
7686
|
const keyPrefix = `resource=${resource.name}`;
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
const
|
|
7770
|
-
await resource.cache.clear(
|
|
7687
|
+
if (data && data.id) {
|
|
7688
|
+
const itemSpecificMethods = ["get", "exists", "content", "hasContent"];
|
|
7689
|
+
for (const method of itemSpecificMethods) {
|
|
7690
|
+
try {
|
|
7691
|
+
const specificKey = await this.generateCacheKey(resource, method, { id: data.id });
|
|
7692
|
+
await resource.cache.clear(specificKey.replace(".json.gz", ""));
|
|
7693
|
+
} catch (error) {
|
|
7771
7694
|
}
|
|
7772
|
-
}
|
|
7695
|
+
}
|
|
7696
|
+
if (this.config.includePartitions === true && resource.config?.partitions && Object.keys(resource.config.partitions).length > 0) {
|
|
7773
7697
|
const partitionValues = this.getPartitionValues(data, resource);
|
|
7774
7698
|
for (const [partitionName, values] of Object.entries(partitionValues)) {
|
|
7775
7699
|
if (values && Object.keys(values).length > 0 && Object.values(values).some((v) => v !== null && v !== void 0)) {
|
|
7776
|
-
|
|
7777
|
-
|
|
7700
|
+
try {
|
|
7701
|
+
const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
|
|
7702
|
+
await resource.cache.clear(partitionKeyPrefix);
|
|
7703
|
+
} catch (error) {
|
|
7704
|
+
}
|
|
7778
7705
|
}
|
|
7779
7706
|
}
|
|
7780
7707
|
}
|
|
7781
7708
|
}
|
|
7709
|
+
try {
|
|
7710
|
+
await resource.cache.clear(keyPrefix);
|
|
7711
|
+
} catch (error) {
|
|
7712
|
+
const aggregateMethods = ["count", "list", "listIds", "getAll", "page", "query"];
|
|
7713
|
+
for (const method of aggregateMethods) {
|
|
7714
|
+
try {
|
|
7715
|
+
await resource.cache.clear(`${keyPrefix}/action=${method}`);
|
|
7716
|
+
await resource.cache.clear(`resource=${resource.name}/action=${method}`);
|
|
7717
|
+
} catch (methodError) {
|
|
7718
|
+
}
|
|
7719
|
+
}
|
|
7720
|
+
}
|
|
7782
7721
|
}
|
|
7783
7722
|
async generateCacheKey(resource, action, params = {}, partition = null, partitionValues = null) {
|
|
7784
7723
|
const keyParts = [
|
|
@@ -7800,7 +7739,7 @@ class CachePlugin extends plugin_class_default {
|
|
|
7800
7739
|
return join(...keyParts) + ".json.gz";
|
|
7801
7740
|
}
|
|
7802
7741
|
async hashParams(params) {
|
|
7803
|
-
const sortedParams = Object.keys(params).sort().map((key) => `${key}:${params[key]}`).join("|") || "empty";
|
|
7742
|
+
const sortedParams = Object.keys(params).sort().map((key) => `${key}:${JSON.stringify(params[key])}`).join("|") || "empty";
|
|
7804
7743
|
return await sha256(sortedParams);
|
|
7805
7744
|
}
|
|
7806
7745
|
// Utility methods
|
|
@@ -8005,12 +7944,14 @@ class FullTextPlugin extends plugin_class_default {
|
|
|
8005
7944
|
}));
|
|
8006
7945
|
this.indexResource = ok ? indexResource : database.resources.fulltext_indexes;
|
|
8007
7946
|
await this.loadIndexes();
|
|
7947
|
+
this.installDatabaseHooks();
|
|
8008
7948
|
this.installIndexingHooks();
|
|
8009
7949
|
}
|
|
8010
7950
|
async start() {
|
|
8011
7951
|
}
|
|
8012
7952
|
async stop() {
|
|
8013
7953
|
await this.saveIndexes();
|
|
7954
|
+
this.removeDatabaseHooks();
|
|
8014
7955
|
}
|
|
8015
7956
|
async loadIndexes() {
|
|
8016
7957
|
if (!this.indexResource) return;
|
|
@@ -8046,6 +7987,16 @@ class FullTextPlugin extends plugin_class_default {
|
|
|
8046
7987
|
}
|
|
8047
7988
|
});
|
|
8048
7989
|
}
|
|
7990
|
+
installDatabaseHooks() {
|
|
7991
|
+
this.database.addHook("afterCreateResource", (resource) => {
|
|
7992
|
+
if (resource.name !== "fulltext_indexes") {
|
|
7993
|
+
this.installResourceHooks(resource);
|
|
7994
|
+
}
|
|
7995
|
+
});
|
|
7996
|
+
}
|
|
7997
|
+
removeDatabaseHooks() {
|
|
7998
|
+
this.database.removeHook("afterCreateResource", this.installResourceHooks.bind(this));
|
|
7999
|
+
}
|
|
8049
8000
|
installIndexingHooks() {
|
|
8050
8001
|
if (!this.database.plugins) {
|
|
8051
8002
|
this.database.plugins = {};
|
|
@@ -8408,6 +8359,7 @@ class MetricsPlugin extends plugin_class_default {
|
|
|
8408
8359
|
this.errorsResource = database.resources.error_logs;
|
|
8409
8360
|
this.performanceResource = database.resources.performance_logs;
|
|
8410
8361
|
}
|
|
8362
|
+
this.installDatabaseHooks();
|
|
8411
8363
|
this.installMetricsHooks();
|
|
8412
8364
|
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
8413
8365
|
this.startFlushTimer();
|
|
@@ -8420,9 +8372,17 @@ class MetricsPlugin extends plugin_class_default {
|
|
|
8420
8372
|
clearInterval(this.flushTimer);
|
|
8421
8373
|
this.flushTimer = null;
|
|
8422
8374
|
}
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8375
|
+
this.removeDatabaseHooks();
|
|
8376
|
+
}
|
|
8377
|
+
installDatabaseHooks() {
|
|
8378
|
+
this.database.addHook("afterCreateResource", (resource) => {
|
|
8379
|
+
if (resource.name !== "metrics" && resource.name !== "error_logs" && resource.name !== "performance_logs") {
|
|
8380
|
+
this.installResourceHooks(resource);
|
|
8381
|
+
}
|
|
8382
|
+
});
|
|
8383
|
+
}
|
|
8384
|
+
removeDatabaseHooks() {
|
|
8385
|
+
this.database.removeHook("afterCreateResource", this.installResourceHooks.bind(this));
|
|
8426
8386
|
}
|
|
8427
8387
|
installMetricsHooks() {
|
|
8428
8388
|
for (const resource of Object.values(this.database.resources)) {
|
|
@@ -9523,6 +9483,72 @@ class PostgresReplicator extends base_replicator_class_default {
|
|
|
9523
9483
|
}
|
|
9524
9484
|
var postgres_replicator_class_default = PostgresReplicator;
|
|
9525
9485
|
|
|
9486
|
+
/*
|
|
9487
|
+
this and http-lib folder
|
|
9488
|
+
|
|
9489
|
+
The MIT License
|
|
9490
|
+
|
|
9491
|
+
Copyright (c) 2015 John Hiesey
|
|
9492
|
+
|
|
9493
|
+
Permission is hereby granted, free of charge,
|
|
9494
|
+
to any person obtaining a copy of this software and
|
|
9495
|
+
associated documentation files (the "Software"), to
|
|
9496
|
+
deal in the Software without restriction, including
|
|
9497
|
+
without limitation the rights to use, copy, modify,
|
|
9498
|
+
merge, publish, distribute, sublicense, and/or sell
|
|
9499
|
+
copies of the Software, and to permit persons to whom
|
|
9500
|
+
the Software is furnished to do so,
|
|
9501
|
+
subject to the following conditions:
|
|
9502
|
+
|
|
9503
|
+
The above copyright notice and this permission notice
|
|
9504
|
+
shall be included in all copies or substantial portions of the Software.
|
|
9505
|
+
|
|
9506
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
9507
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
9508
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
9509
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
|
9510
|
+
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
9511
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
9512
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
9513
|
+
|
|
9514
|
+
*/
|
|
9515
|
+
|
|
9516
|
+
function Agent$1() {}
|
|
9517
|
+
Agent$1.defaultMaxSockets = 4;
|
|
9518
|
+
|
|
9519
|
+
/*
|
|
9520
|
+
this and http-lib folder
|
|
9521
|
+
|
|
9522
|
+
The MIT License
|
|
9523
|
+
|
|
9524
|
+
Copyright (c) 2015 John Hiesey
|
|
9525
|
+
|
|
9526
|
+
Permission is hereby granted, free of charge,
|
|
9527
|
+
to any person obtaining a copy of this software and
|
|
9528
|
+
associated documentation files (the "Software"), to
|
|
9529
|
+
deal in the Software without restriction, including
|
|
9530
|
+
without limitation the rights to use, copy, modify,
|
|
9531
|
+
merge, publish, distribute, sublicense, and/or sell
|
|
9532
|
+
copies of the Software, and to permit persons to whom
|
|
9533
|
+
the Software is furnished to do so,
|
|
9534
|
+
subject to the following conditions:
|
|
9535
|
+
|
|
9536
|
+
The above copyright notice and this permission notice
|
|
9537
|
+
shall be included in all copies or substantial portions of the Software.
|
|
9538
|
+
|
|
9539
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
9540
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
9541
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
9542
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
|
9543
|
+
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
9544
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
9545
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
9546
|
+
|
|
9547
|
+
*/
|
|
9548
|
+
|
|
9549
|
+
function Agent() {}
|
|
9550
|
+
Agent.defaultMaxSockets = 4;
|
|
9551
|
+
|
|
9526
9552
|
const S3_DEFAULT_REGION = "us-east-1";
|
|
9527
9553
|
const S3_DEFAULT_ENDPOINT = "https://s3.us-east-1.amazonaws.com";
|
|
9528
9554
|
class ConnectionString {
|
|
@@ -9590,19 +9616,38 @@ class Client extends EventEmitter {
|
|
|
9590
9616
|
id = null,
|
|
9591
9617
|
AwsS3Client,
|
|
9592
9618
|
connectionString,
|
|
9593
|
-
parallelism = 10
|
|
9619
|
+
parallelism = 10,
|
|
9620
|
+
httpClientOptions = {}
|
|
9594
9621
|
}) {
|
|
9595
9622
|
super();
|
|
9596
9623
|
this.verbose = verbose;
|
|
9597
9624
|
this.id = id ?? idGenerator();
|
|
9598
9625
|
this.parallelism = parallelism;
|
|
9599
9626
|
this.config = new ConnectionString(connectionString);
|
|
9627
|
+
this.httpClientOptions = {
|
|
9628
|
+
keepAlive: false,
|
|
9629
|
+
// Disabled for maximum creation speed
|
|
9630
|
+
maxSockets: 10,
|
|
9631
|
+
// Minimal sockets
|
|
9632
|
+
maxFreeSockets: 2,
|
|
9633
|
+
// Minimal pool
|
|
9634
|
+
timeout: 15e3,
|
|
9635
|
+
// Short timeout
|
|
9636
|
+
...httpClientOptions
|
|
9637
|
+
};
|
|
9600
9638
|
this.client = AwsS3Client || this.createClient();
|
|
9601
9639
|
}
|
|
9602
9640
|
createClient() {
|
|
9641
|
+
const httpAgent = new Agent$1(this.httpClientOptions);
|
|
9642
|
+
const httpsAgent = new Agent(this.httpClientOptions);
|
|
9643
|
+
const httpHandler = new nodeHttpHandler.NodeHttpHandler({
|
|
9644
|
+
httpAgent,
|
|
9645
|
+
httpsAgent
|
|
9646
|
+
});
|
|
9603
9647
|
let options = {
|
|
9604
9648
|
region: this.config.region,
|
|
9605
|
-
endpoint: this.config.endpoint
|
|
9649
|
+
endpoint: this.config.endpoint,
|
|
9650
|
+
requestHandler: httpHandler
|
|
9606
9651
|
};
|
|
9607
9652
|
if (this.config.forcePathStyle) options.forcePathStyle = true;
|
|
9608
9653
|
if (this.config.accessKeyId) {
|
|
@@ -13151,7 +13196,14 @@ class Resource extends EventEmitter {
|
|
|
13151
13196
|
"delete",
|
|
13152
13197
|
"deleteMany",
|
|
13153
13198
|
"exists",
|
|
13154
|
-
"getMany"
|
|
13199
|
+
"getMany",
|
|
13200
|
+
"content",
|
|
13201
|
+
"hasContent",
|
|
13202
|
+
"query",
|
|
13203
|
+
"getFromPartition",
|
|
13204
|
+
"setContent",
|
|
13205
|
+
"deleteContent",
|
|
13206
|
+
"replace"
|
|
13155
13207
|
];
|
|
13156
13208
|
for (const method of this._middlewareMethods) {
|
|
13157
13209
|
this._middlewares.set(method, []);
|
|
@@ -13330,7 +13382,7 @@ class Database extends EventEmitter {
|
|
|
13330
13382
|
super();
|
|
13331
13383
|
this.version = "1";
|
|
13332
13384
|
this.s3dbVersion = (() => {
|
|
13333
|
-
const [ok, err, version] = try_fn_default(() => true ? "7.
|
|
13385
|
+
const [ok, err, version] = try_fn_default(() => true ? "7.5.0" : "latest");
|
|
13334
13386
|
return ok ? version : "latest";
|
|
13335
13387
|
})();
|
|
13336
13388
|
this.resources = {};
|
|
@@ -13343,6 +13395,7 @@ class Database extends EventEmitter {
|
|
|
13343
13395
|
this.cache = options.cache;
|
|
13344
13396
|
this.passphrase = options.passphrase || "secret";
|
|
13345
13397
|
this.versioningEnabled = options.versioningEnabled || false;
|
|
13398
|
+
this._initHooks();
|
|
13346
13399
|
let connectionString = options.connectionString;
|
|
13347
13400
|
if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
|
|
13348
13401
|
const { bucket, region, accessKeyId, secretAccessKey, endpoint, forcePathStyle } = options;
|
|
@@ -13820,6 +13873,131 @@ class Database extends EventEmitter {
|
|
|
13820
13873
|
} catch (err) {
|
|
13821
13874
|
}
|
|
13822
13875
|
}
|
|
13876
|
+
/**
|
|
13877
|
+
* Initialize hooks system for database operations
|
|
13878
|
+
* @private
|
|
13879
|
+
*/
|
|
13880
|
+
_initHooks() {
|
|
13881
|
+
this._hooks = /* @__PURE__ */ new Map();
|
|
13882
|
+
this._hookEvents = [
|
|
13883
|
+
"beforeConnect",
|
|
13884
|
+
"afterConnect",
|
|
13885
|
+
"beforeCreateResource",
|
|
13886
|
+
"afterCreateResource",
|
|
13887
|
+
"beforeUploadMetadata",
|
|
13888
|
+
"afterUploadMetadata",
|
|
13889
|
+
"beforeDisconnect",
|
|
13890
|
+
"afterDisconnect",
|
|
13891
|
+
"resourceCreated",
|
|
13892
|
+
"resourceUpdated"
|
|
13893
|
+
];
|
|
13894
|
+
for (const event of this._hookEvents) {
|
|
13895
|
+
this._hooks.set(event, []);
|
|
13896
|
+
}
|
|
13897
|
+
this._wrapHookableMethods();
|
|
13898
|
+
}
|
|
13899
|
+
/**
|
|
13900
|
+
* Wrap methods that can have hooks
|
|
13901
|
+
* @private
|
|
13902
|
+
*/
|
|
13903
|
+
_wrapHookableMethods() {
|
|
13904
|
+
if (this._hooksInstalled) return;
|
|
13905
|
+
this._originalConnect = this.connect.bind(this);
|
|
13906
|
+
this._originalCreateResource = this.createResource.bind(this);
|
|
13907
|
+
this._originalUploadMetadataFile = this.uploadMetadataFile.bind(this);
|
|
13908
|
+
this._originalDisconnect = this.disconnect.bind(this);
|
|
13909
|
+
this.connect = async (...args) => {
|
|
13910
|
+
await this._executeHooks("beforeConnect", { args });
|
|
13911
|
+
const result = await this._originalConnect(...args);
|
|
13912
|
+
await this._executeHooks("afterConnect", { result, args });
|
|
13913
|
+
return result;
|
|
13914
|
+
};
|
|
13915
|
+
this.createResource = async (config) => {
|
|
13916
|
+
await this._executeHooks("beforeCreateResource", { config });
|
|
13917
|
+
const resource = await this._originalCreateResource(config);
|
|
13918
|
+
await this._executeHooks("afterCreateResource", { resource, config });
|
|
13919
|
+
return resource;
|
|
13920
|
+
};
|
|
13921
|
+
this.uploadMetadataFile = async (...args) => {
|
|
13922
|
+
await this._executeHooks("beforeUploadMetadata", { args });
|
|
13923
|
+
const result = await this._originalUploadMetadataFile(...args);
|
|
13924
|
+
await this._executeHooks("afterUploadMetadata", { result, args });
|
|
13925
|
+
return result;
|
|
13926
|
+
};
|
|
13927
|
+
this.disconnect = async (...args) => {
|
|
13928
|
+
await this._executeHooks("beforeDisconnect", { args });
|
|
13929
|
+
const result = await this._originalDisconnect(...args);
|
|
13930
|
+
await this._executeHooks("afterDisconnect", { result, args });
|
|
13931
|
+
return result;
|
|
13932
|
+
};
|
|
13933
|
+
this._hooksInstalled = true;
|
|
13934
|
+
}
|
|
13935
|
+
/**
|
|
13936
|
+
* Add a hook for a specific database event
|
|
13937
|
+
* @param {string} event - Hook event name
|
|
13938
|
+
* @param {Function} fn - Hook function
|
|
13939
|
+
* @example
|
|
13940
|
+
* database.addHook('afterCreateResource', async ({ resource }) => {
|
|
13941
|
+
* console.log('Resource created:', resource.name);
|
|
13942
|
+
* });
|
|
13943
|
+
*/
|
|
13944
|
+
addHook(event, fn) {
|
|
13945
|
+
if (!this._hooks) this._initHooks();
|
|
13946
|
+
if (!this._hooks.has(event)) {
|
|
13947
|
+
throw new Error(`Unknown hook event: ${event}. Available events: ${this._hookEvents.join(", ")}`);
|
|
13948
|
+
}
|
|
13949
|
+
if (typeof fn !== "function") {
|
|
13950
|
+
throw new Error("Hook function must be a function");
|
|
13951
|
+
}
|
|
13952
|
+
this._hooks.get(event).push(fn);
|
|
13953
|
+
}
|
|
13954
|
+
/**
|
|
13955
|
+
* Execute hooks for a specific event
|
|
13956
|
+
* @param {string} event - Hook event name
|
|
13957
|
+
* @param {Object} context - Context data to pass to hooks
|
|
13958
|
+
* @private
|
|
13959
|
+
*/
|
|
13960
|
+
async _executeHooks(event, context = {}) {
|
|
13961
|
+
if (!this._hooks || !this._hooks.has(event)) return;
|
|
13962
|
+
const hooks = this._hooks.get(event);
|
|
13963
|
+
for (const hook of hooks) {
|
|
13964
|
+
try {
|
|
13965
|
+
await hook({ database: this, ...context });
|
|
13966
|
+
} catch (error) {
|
|
13967
|
+
this.emit("hookError", { event, error, context });
|
|
13968
|
+
}
|
|
13969
|
+
}
|
|
13970
|
+
}
|
|
13971
|
+
/**
|
|
13972
|
+
* Remove a hook for a specific event
|
|
13973
|
+
* @param {string} event - Hook event name
|
|
13974
|
+
* @param {Function} fn - Hook function to remove
|
|
13975
|
+
*/
|
|
13976
|
+
removeHook(event, fn) {
|
|
13977
|
+
if (!this._hooks || !this._hooks.has(event)) return;
|
|
13978
|
+
const hooks = this._hooks.get(event);
|
|
13979
|
+
const index = hooks.indexOf(fn);
|
|
13980
|
+
if (index > -1) {
|
|
13981
|
+
hooks.splice(index, 1);
|
|
13982
|
+
}
|
|
13983
|
+
}
|
|
13984
|
+
/**
|
|
13985
|
+
* Get all hooks for a specific event
|
|
13986
|
+
* @param {string} event - Hook event name
|
|
13987
|
+
* @returns {Function[]} Array of hook functions
|
|
13988
|
+
*/
|
|
13989
|
+
getHooks(event) {
|
|
13990
|
+
if (!this._hooks || !this._hooks.has(event)) return [];
|
|
13991
|
+
return [...this._hooks.get(event)];
|
|
13992
|
+
}
|
|
13993
|
+
/**
|
|
13994
|
+
* Clear all hooks for a specific event
|
|
13995
|
+
* @param {string} event - Hook event name
|
|
13996
|
+
*/
|
|
13997
|
+
clearHooks(event) {
|
|
13998
|
+
if (!this._hooks || !this._hooks.has(event)) return;
|
|
13999
|
+
this._hooks.get(event).length = 0;
|
|
14000
|
+
}
|
|
13823
14001
|
}
|
|
13824
14002
|
class S3db extends Database {
|
|
13825
14003
|
}
|
|
@@ -14552,6 +14730,10 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14552
14730
|
}
|
|
14553
14731
|
return filtered;
|
|
14554
14732
|
}
|
|
14733
|
+
async getCompleteData(resource, data) {
|
|
14734
|
+
const [ok, err, completeRecord] = await try_fn_default(() => resource.get(data.id));
|
|
14735
|
+
return ok ? completeRecord : data;
|
|
14736
|
+
}
|
|
14555
14737
|
installEventListeners(resource, database, plugin) {
|
|
14556
14738
|
if (!resource || this.eventListenersInstalled.has(resource.name) || resource.name === this.config.replicatorLogResource) {
|
|
14557
14739
|
return;
|
|
@@ -14570,8 +14752,9 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14570
14752
|
});
|
|
14571
14753
|
resource.on("update", async (data, beforeData) => {
|
|
14572
14754
|
const [ok, error] = await try_fn_default(async () => {
|
|
14573
|
-
const completeData =
|
|
14574
|
-
|
|
14755
|
+
const completeData = await plugin.getCompleteData(resource, data);
|
|
14756
|
+
const dataWithTimestamp = { ...completeData, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
14757
|
+
await plugin.processReplicatorEvent("update", resource.name, completeData.id, dataWithTimestamp, beforeData);
|
|
14575
14758
|
});
|
|
14576
14759
|
if (!ok) {
|
|
14577
14760
|
if (this.config.verbose) {
|
|
@@ -14593,67 +14776,55 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14593
14776
|
});
|
|
14594
14777
|
this.eventListenersInstalled.add(resource.name);
|
|
14595
14778
|
}
|
|
14596
|
-
/**
|
|
14597
|
-
* Get complete data by always fetching the full record from the resource
|
|
14598
|
-
* This ensures we always have the complete data regardless of behavior or data size
|
|
14599
|
-
*/
|
|
14600
|
-
async getCompleteData(resource, data) {
|
|
14601
|
-
const [ok, err, completeRecord] = await try_fn_default(() => resource.get(data.id));
|
|
14602
|
-
return ok ? completeRecord : data;
|
|
14603
|
-
}
|
|
14604
14779
|
async setup(database) {
|
|
14605
14780
|
this.database = database;
|
|
14606
|
-
|
|
14607
|
-
await
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14622
|
-
|
|
14623
|
-
resourceName: "string",
|
|
14624
|
-
recordId: "string",
|
|
14625
|
-
data: "string",
|
|
14626
|
-
error: "string|optional",
|
|
14627
|
-
replicator: "string",
|
|
14628
|
-
timestamp: "string",
|
|
14629
|
-
status: "string"
|
|
14630
|
-
}
|
|
14631
|
-
});
|
|
14632
|
-
}
|
|
14633
|
-
});
|
|
14634
|
-
if (!logOk) {
|
|
14635
|
-
if (this.config.verbose) {
|
|
14636
|
-
console.warn(`[ReplicatorPlugin] Failed to create log resource ${this.config.replicatorLogResource}: ${logError.message}`);
|
|
14781
|
+
if (this.config.persistReplicatorLog) {
|
|
14782
|
+
const [ok, err, logResource] = await try_fn_default(() => database.createResource({
|
|
14783
|
+
name: this.config.replicatorLogResource || "replicator_logs",
|
|
14784
|
+
attributes: {
|
|
14785
|
+
id: "string|required",
|
|
14786
|
+
resource: "string|required",
|
|
14787
|
+
action: "string|required",
|
|
14788
|
+
data: "json",
|
|
14789
|
+
timestamp: "number|required",
|
|
14790
|
+
createdAt: "string|required"
|
|
14791
|
+
},
|
|
14792
|
+
behavior: "truncate-data"
|
|
14793
|
+
}));
|
|
14794
|
+
if (ok) {
|
|
14795
|
+
this.replicatorLogResource = logResource;
|
|
14796
|
+
} else {
|
|
14797
|
+
this.replicatorLogResource = database.resources[this.config.replicatorLogResource || "replicator_logs"];
|
|
14637
14798
|
}
|
|
14638
|
-
this.emit("replicator_log_resource_creation_error", {
|
|
14639
|
-
resourceName: this.config.replicatorLogResource,
|
|
14640
|
-
error: logError.message
|
|
14641
|
-
});
|
|
14642
14799
|
}
|
|
14643
|
-
await this.
|
|
14644
|
-
|
|
14645
|
-
|
|
14646
|
-
|
|
14647
|
-
if (resource) {
|
|
14800
|
+
await this.initializeReplicators(database);
|
|
14801
|
+
this.installDatabaseHooks();
|
|
14802
|
+
for (const resource of Object.values(database.resources)) {
|
|
14803
|
+
if (resource.name !== (this.config.replicatorLogResource || "replicator_logs")) {
|
|
14648
14804
|
this.installEventListeners(resource, database, this);
|
|
14649
14805
|
}
|
|
14650
|
-
return resource;
|
|
14651
|
-
};
|
|
14652
|
-
for (const resourceName in database.resources) {
|
|
14653
|
-
const resource = database.resources[resourceName];
|
|
14654
|
-
this.installEventListeners(resource, database, this);
|
|
14655
14806
|
}
|
|
14656
14807
|
}
|
|
14808
|
+
async start() {
|
|
14809
|
+
}
|
|
14810
|
+
async stop() {
|
|
14811
|
+
for (const replicator of this.replicators || []) {
|
|
14812
|
+
if (replicator && typeof replicator.cleanup === "function") {
|
|
14813
|
+
await replicator.cleanup();
|
|
14814
|
+
}
|
|
14815
|
+
}
|
|
14816
|
+
this.removeDatabaseHooks();
|
|
14817
|
+
}
|
|
14818
|
+
installDatabaseHooks() {
|
|
14819
|
+
this.database.addHook("afterCreateResource", (resource) => {
|
|
14820
|
+
if (resource.name !== (this.config.replicatorLogResource || "replicator_logs")) {
|
|
14821
|
+
this.installEventListeners(resource, this.database, this);
|
|
14822
|
+
}
|
|
14823
|
+
});
|
|
14824
|
+
}
|
|
14825
|
+
removeDatabaseHooks() {
|
|
14826
|
+
this.database.removeHook("afterCreateResource", this.installEventListeners.bind(this));
|
|
14827
|
+
}
|
|
14657
14828
|
createReplicator(driver, config, resources, client) {
|
|
14658
14829
|
return createReplicator(driver, config, resources, client);
|
|
14659
14830
|
}
|
|
@@ -14669,10 +14840,6 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14669
14840
|
}
|
|
14670
14841
|
}
|
|
14671
14842
|
}
|
|
14672
|
-
async start() {
|
|
14673
|
-
}
|
|
14674
|
-
async stop() {
|
|
14675
|
-
}
|
|
14676
14843
|
async uploadMetadataFile(database) {
|
|
14677
14844
|
if (typeof database.uploadMetadataFile === "function") {
|
|
14678
14845
|
await database.uploadMetadataFile();
|