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.es.js
CHANGED
|
@@ -6,6 +6,7 @@ import { mkdir, writeFile, readFile, stat, unlink, readdir, rm } from 'fs/promis
|
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { chunk, merge, isString as isString$1, isEmpty, invert, uniq, cloneDeep, get, set, isObject as isObject$1, isFunction as isFunction$1 } from 'lodash-es';
|
|
8
8
|
import jsonStableStringify from 'json-stable-stringify';
|
|
9
|
+
import { NodeHttpHandler } from '@smithy/node-http-handler';
|
|
9
10
|
import { S3Client, PutObjectCommand, GetObjectCommand, HeadObjectCommand, CopyObjectCommand, DeleteObjectCommand, DeleteObjectsCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
|
|
10
11
|
import { flatten, unflatten } from 'flat';
|
|
11
12
|
import FastestValidator from 'fastest-validator';
|
|
@@ -1267,7 +1268,6 @@ class AuditPlugin extends plugin_class_default {
|
|
|
1267
1268
|
includeData: options.includeData !== false,
|
|
1268
1269
|
includePartitions: options.includePartitions !== false,
|
|
1269
1270
|
maxDataSize: options.maxDataSize || 1e4,
|
|
1270
|
-
// 10KB limit
|
|
1271
1271
|
...options
|
|
1272
1272
|
};
|
|
1273
1273
|
}
|
|
@@ -1288,327 +1288,193 @@ class AuditPlugin extends plugin_class_default {
|
|
|
1288
1288
|
metadata: "string|optional"
|
|
1289
1289
|
},
|
|
1290
1290
|
behavior: "body-overflow"
|
|
1291
|
-
// keyPrefix removido
|
|
1292
1291
|
}));
|
|
1293
1292
|
this.auditResource = ok ? auditResource : this.database.resources.audits || null;
|
|
1294
1293
|
if (!ok && !this.auditResource) return;
|
|
1295
|
-
this.
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
async onStart() {
|
|
1299
|
-
}
|
|
1300
|
-
async onStop() {
|
|
1301
|
-
}
|
|
1302
|
-
installDatabaseProxy() {
|
|
1303
|
-
if (this.database._auditProxyInstalled) {
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
const installEventListenersForResource = this.installEventListenersForResource.bind(this);
|
|
1307
|
-
this.database._originalCreateResource = this.database.createResource;
|
|
1308
|
-
this.database.createResource = async function(...args) {
|
|
1309
|
-
const resource = await this._originalCreateResource(...args);
|
|
1310
|
-
if (resource.name !== "audits") {
|
|
1311
|
-
installEventListenersForResource(resource);
|
|
1294
|
+
this.database.addHook("afterCreateResource", (context) => {
|
|
1295
|
+
if (context.resource.name !== "audits") {
|
|
1296
|
+
this.setupResourceAuditing(context.resource);
|
|
1312
1297
|
}
|
|
1313
|
-
|
|
1314
|
-
};
|
|
1315
|
-
this.database._auditProxyInstalled = true;
|
|
1316
|
-
}
|
|
1317
|
-
installEventListeners() {
|
|
1298
|
+
});
|
|
1318
1299
|
for (const resource of Object.values(this.database.resources)) {
|
|
1319
|
-
if (resource.name
|
|
1320
|
-
|
|
1300
|
+
if (resource.name !== "audits") {
|
|
1301
|
+
this.setupResourceAuditing(resource);
|
|
1321
1302
|
}
|
|
1322
|
-
this.installEventListenersForResource(resource);
|
|
1323
1303
|
}
|
|
1324
1304
|
}
|
|
1325
|
-
|
|
1305
|
+
async onStart() {
|
|
1306
|
+
}
|
|
1307
|
+
async onStop() {
|
|
1308
|
+
}
|
|
1309
|
+
setupResourceAuditing(resource) {
|
|
1326
1310
|
resource.on("insert", async (data) => {
|
|
1327
|
-
|
|
1328
|
-
const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
|
|
1329
|
-
const auditRecord = {
|
|
1330
|
-
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
1311
|
+
await this.logAudit({
|
|
1331
1312
|
resourceName: resource.name,
|
|
1332
1313
|
operation: "insert",
|
|
1333
|
-
recordId,
|
|
1334
|
-
userId: this.getCurrentUserId?.() || "system",
|
|
1335
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1314
|
+
recordId: data.id || "auto-generated",
|
|
1336
1315
|
oldData: null,
|
|
1337
|
-
newData: this.config.includeData
|
|
1338
|
-
partition: this.config.includePartitions ? this.getPrimaryPartition(
|
|
1339
|
-
partitionValues: this.config.includePartitions
|
|
1340
|
-
metadata: JSON.stringify({
|
|
1341
|
-
source: "audit-plugin",
|
|
1342
|
-
version: "2.0"
|
|
1343
|
-
})
|
|
1344
|
-
};
|
|
1345
|
-
this.logAudit(auditRecord).catch(() => {
|
|
1316
|
+
newData: this.config.includeData ? JSON.stringify(this.truncateData(data)) : null,
|
|
1317
|
+
partition: this.config.includePartitions && this.getPartitionValues(data, resource) ? this.getPrimaryPartition(this.getPartitionValues(data, resource)) : null,
|
|
1318
|
+
partitionValues: this.config.includePartitions && this.getPartitionValues(data, resource) ? JSON.stringify(this.getPartitionValues(data, resource)) : null
|
|
1346
1319
|
});
|
|
1347
1320
|
});
|
|
1348
1321
|
resource.on("update", async (data) => {
|
|
1349
|
-
const recordId = data.id;
|
|
1350
1322
|
let oldData = data.$before;
|
|
1351
1323
|
if (this.config.includeData && !oldData) {
|
|
1352
|
-
const [ok, err, fetched] = await try_fn_default(() => resource.get(
|
|
1324
|
+
const [ok, err, fetched] = await try_fn_default(() => resource.get(data.id));
|
|
1353
1325
|
if (ok) oldData = fetched;
|
|
1354
1326
|
}
|
|
1355
|
-
|
|
1356
|
-
const auditRecord = {
|
|
1357
|
-
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
1327
|
+
await this.logAudit({
|
|
1358
1328
|
resourceName: resource.name,
|
|
1359
1329
|
operation: "update",
|
|
1360
|
-
recordId,
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
|
|
1366
|
-
partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
|
|
1367
|
-
metadata: JSON.stringify({
|
|
1368
|
-
source: "audit-plugin",
|
|
1369
|
-
version: "2.0"
|
|
1370
|
-
})
|
|
1371
|
-
};
|
|
1372
|
-
this.logAudit(auditRecord).catch(() => {
|
|
1330
|
+
recordId: data.id,
|
|
1331
|
+
oldData: oldData && this.config.includeData ? JSON.stringify(this.truncateData(oldData)) : null,
|
|
1332
|
+
newData: this.config.includeData ? JSON.stringify(this.truncateData(data)) : null,
|
|
1333
|
+
partition: this.config.includePartitions && this.getPartitionValues(data, resource) ? this.getPrimaryPartition(this.getPartitionValues(data, resource)) : null,
|
|
1334
|
+
partitionValues: this.config.includePartitions && this.getPartitionValues(data, resource) ? JSON.stringify(this.getPartitionValues(data, resource)) : null
|
|
1373
1335
|
});
|
|
1374
1336
|
});
|
|
1375
1337
|
resource.on("delete", async (data) => {
|
|
1376
|
-
const recordId = data.id;
|
|
1377
1338
|
let oldData = data;
|
|
1378
1339
|
if (this.config.includeData && !oldData) {
|
|
1379
|
-
const [ok, err, fetched] = await try_fn_default(() => resource.get(
|
|
1340
|
+
const [ok, err, fetched] = await try_fn_default(() => resource.get(data.id));
|
|
1380
1341
|
if (ok) oldData = fetched;
|
|
1381
1342
|
}
|
|
1382
|
-
|
|
1383
|
-
const auditRecord = {
|
|
1384
|
-
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
1343
|
+
await this.logAudit({
|
|
1385
1344
|
resourceName: resource.name,
|
|
1386
1345
|
operation: "delete",
|
|
1387
|
-
recordId,
|
|
1388
|
-
|
|
1389
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1390
|
-
oldData: oldData && this.config.includeData === false ? null : oldData ? JSON.stringify(this.truncateData(oldData)) : null,
|
|
1346
|
+
recordId: data.id,
|
|
1347
|
+
oldData: oldData && this.config.includeData ? JSON.stringify(this.truncateData(oldData)) : null,
|
|
1391
1348
|
newData: null,
|
|
1392
|
-
partition: this.config.includePartitions ? this.getPrimaryPartition(
|
|
1393
|
-
partitionValues: this.config.includePartitions
|
|
1394
|
-
metadata: JSON.stringify({
|
|
1395
|
-
source: "audit-plugin",
|
|
1396
|
-
version: "2.0"
|
|
1397
|
-
})
|
|
1398
|
-
};
|
|
1399
|
-
this.logAudit(auditRecord).catch(() => {
|
|
1349
|
+
partition: oldData && this.config.includePartitions && this.getPartitionValues(oldData, resource) ? this.getPrimaryPartition(this.getPartitionValues(oldData, resource)) : null,
|
|
1350
|
+
partitionValues: oldData && this.config.includePartitions && this.getPartitionValues(oldData, resource) ? JSON.stringify(this.getPartitionValues(oldData, resource)) : null
|
|
1400
1351
|
});
|
|
1401
1352
|
});
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
-
return result;
|
|
1438
|
-
});
|
|
1353
|
+
}
|
|
1354
|
+
// Backward compatibility for tests
|
|
1355
|
+
installEventListenersForResource(resource) {
|
|
1356
|
+
return this.setupResourceAuditing(resource);
|
|
1357
|
+
}
|
|
1358
|
+
async logAudit(auditData) {
|
|
1359
|
+
if (!this.auditResource) {
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const auditRecord = {
|
|
1363
|
+
id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
1364
|
+
userId: this.getCurrentUserId?.() || "system",
|
|
1365
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1366
|
+
metadata: JSON.stringify({ source: "audit-plugin", version: "2.0" }),
|
|
1367
|
+
resourceName: auditData.resourceName,
|
|
1368
|
+
operation: auditData.operation,
|
|
1369
|
+
recordId: auditData.recordId
|
|
1370
|
+
};
|
|
1371
|
+
if (auditData.oldData !== null) {
|
|
1372
|
+
auditRecord.oldData = auditData.oldData;
|
|
1373
|
+
}
|
|
1374
|
+
if (auditData.newData !== null) {
|
|
1375
|
+
auditRecord.newData = auditData.newData;
|
|
1376
|
+
}
|
|
1377
|
+
if (auditData.partition !== null) {
|
|
1378
|
+
auditRecord.partition = auditData.partition;
|
|
1379
|
+
}
|
|
1380
|
+
if (auditData.partitionValues !== null) {
|
|
1381
|
+
auditRecord.partitionValues = auditData.partitionValues;
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
await this.auditResource.insert(auditRecord);
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
console.warn("Audit logging failed:", error.message);
|
|
1387
|
+
}
|
|
1439
1388
|
}
|
|
1440
1389
|
getPartitionValues(data, resource) {
|
|
1441
|
-
if (!
|
|
1442
|
-
const partitions = resource.config?.partitions || {};
|
|
1390
|
+
if (!this.config.includePartitions || !resource.partitions) return null;
|
|
1443
1391
|
const partitionValues = {};
|
|
1444
|
-
for (const [partitionName,
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
if (Object.keys(partitionData).length > 0) {
|
|
1454
|
-
partitionValues[partitionName] = partitionData;
|
|
1455
|
-
}
|
|
1392
|
+
for (const [partitionName, partitionConfig] of Object.entries(resource.partitions)) {
|
|
1393
|
+
const values = {};
|
|
1394
|
+
for (const field of Object.keys(partitionConfig.fields)) {
|
|
1395
|
+
values[field] = this.getNestedFieldValue(data, field);
|
|
1396
|
+
}
|
|
1397
|
+
if (Object.values(values).some((v) => v !== void 0 && v !== null)) {
|
|
1398
|
+
partitionValues[partitionName] = values;
|
|
1456
1399
|
}
|
|
1457
1400
|
}
|
|
1458
|
-
return partitionValues;
|
|
1401
|
+
return Object.keys(partitionValues).length > 0 ? partitionValues : null;
|
|
1459
1402
|
}
|
|
1460
1403
|
getNestedFieldValue(data, fieldPath) {
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
if (!currentLevel || typeof currentLevel !== "object" || !(key in currentLevel)) {
|
|
1404
|
+
const parts = fieldPath.split(".");
|
|
1405
|
+
let value = data;
|
|
1406
|
+
for (const part of parts) {
|
|
1407
|
+
if (value && typeof value === "object" && part in value) {
|
|
1408
|
+
value = value[part];
|
|
1409
|
+
} else {
|
|
1468
1410
|
return void 0;
|
|
1469
1411
|
}
|
|
1470
|
-
currentLevel = currentLevel[key];
|
|
1471
1412
|
}
|
|
1472
|
-
return
|
|
1413
|
+
return value;
|
|
1473
1414
|
}
|
|
1474
1415
|
getPrimaryPartition(partitionValues) {
|
|
1475
1416
|
if (!partitionValues) return null;
|
|
1476
1417
|
const partitionNames = Object.keys(partitionValues);
|
|
1477
1418
|
return partitionNames.length > 0 ? partitionNames[0] : null;
|
|
1478
1419
|
}
|
|
1479
|
-
async logAudit(auditRecord) {
|
|
1480
|
-
if (!auditRecord.id) {
|
|
1481
|
-
auditRecord.id = `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1482
|
-
}
|
|
1483
|
-
const result = await this.auditResource.insert(auditRecord);
|
|
1484
|
-
return result;
|
|
1485
|
-
}
|
|
1486
1420
|
truncateData(data) {
|
|
1487
|
-
if (!
|
|
1488
|
-
const
|
|
1489
|
-
for (const [key, value] of Object.entries(data)) {
|
|
1490
|
-
if (!key.startsWith("_") && key !== "$overflow") {
|
|
1491
|
-
filteredData[key] = value;
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
const dataStr = JSON.stringify(filteredData);
|
|
1421
|
+
if (!this.config.includeData) return null;
|
|
1422
|
+
const dataStr = JSON.stringify(data);
|
|
1495
1423
|
if (dataStr.length <= this.config.maxDataSize) {
|
|
1496
|
-
return
|
|
1497
|
-
}
|
|
1498
|
-
let truncatedData = { ...filteredData };
|
|
1499
|
-
let currentSize = JSON.stringify(truncatedData).length;
|
|
1500
|
-
const metadataOverhead = JSON.stringify({
|
|
1501
|
-
_truncated: true,
|
|
1502
|
-
_originalSize: dataStr.length,
|
|
1503
|
-
_truncatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1504
|
-
}).length;
|
|
1505
|
-
const targetSize = this.config.maxDataSize - metadataOverhead;
|
|
1506
|
-
for (const [key, value] of Object.entries(truncatedData)) {
|
|
1507
|
-
if (typeof value === "string" && currentSize > targetSize) {
|
|
1508
|
-
const excess = currentSize - targetSize;
|
|
1509
|
-
const newLength = Math.max(0, value.length - excess - 3);
|
|
1510
|
-
if (newLength < value.length) {
|
|
1511
|
-
truncatedData[key] = value.substring(0, newLength) + "...";
|
|
1512
|
-
currentSize = JSON.stringify(truncatedData).length;
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1424
|
+
return data;
|
|
1515
1425
|
}
|
|
1516
1426
|
return {
|
|
1517
|
-
...
|
|
1427
|
+
...data,
|
|
1518
1428
|
_truncated: true,
|
|
1519
1429
|
_originalSize: dataStr.length,
|
|
1520
1430
|
_truncatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1521
1431
|
};
|
|
1522
1432
|
}
|
|
1523
|
-
// Utility methods for querying audit logs
|
|
1524
1433
|
async getAuditLogs(options = {}) {
|
|
1525
1434
|
if (!this.auditResource) return [];
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
let filtered = allAudits.filter((audit) => {
|
|
1540
|
-
if (resourceName && audit.resourceName !== resourceName) return false;
|
|
1541
|
-
if (operation && audit.operation !== operation) return false;
|
|
1542
|
-
if (recordId && audit.recordId !== recordId) return false;
|
|
1543
|
-
if (userId && audit.userId !== userId) return false;
|
|
1544
|
-
if (partition && audit.partition !== partition) return false;
|
|
1545
|
-
if (startDate && new Date(audit.timestamp) < new Date(startDate)) return false;
|
|
1546
|
-
if (endDate && new Date(audit.timestamp) > new Date(endDate)) return false;
|
|
1547
|
-
return true;
|
|
1548
|
-
});
|
|
1549
|
-
filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
1550
|
-
const deserialized = filtered.slice(offset, offset + limit).map((audit) => {
|
|
1551
|
-
const [okOld, , oldData] = typeof audit.oldData === "string" ? tryFnSync(() => JSON.parse(audit.oldData)) : [true, null, audit.oldData];
|
|
1552
|
-
const [okNew, , newData] = typeof audit.newData === "string" ? tryFnSync(() => JSON.parse(audit.newData)) : [true, null, audit.newData];
|
|
1553
|
-
const [okPart, , partitionValues] = audit.partitionValues && typeof audit.partitionValues === "string" ? tryFnSync(() => JSON.parse(audit.partitionValues)) : [true, null, audit.partitionValues];
|
|
1554
|
-
const [okMeta, , metadata] = audit.metadata && typeof audit.metadata === "string" ? tryFnSync(() => JSON.parse(audit.metadata)) : [true, null, audit.metadata];
|
|
1555
|
-
return {
|
|
1556
|
-
...audit,
|
|
1557
|
-
oldData: audit.oldData === null || audit.oldData === void 0 || audit.oldData === "null" ? null : okOld ? oldData : null,
|
|
1558
|
-
newData: audit.newData === null || audit.newData === void 0 || audit.newData === "null" ? null : okNew ? newData : null,
|
|
1559
|
-
partitionValues: okPart ? partitionValues : audit.partitionValues,
|
|
1560
|
-
metadata: okMeta ? metadata : audit.metadata
|
|
1561
|
-
};
|
|
1562
|
-
});
|
|
1563
|
-
return deserialized;
|
|
1564
|
-
});
|
|
1565
|
-
return ok ? result : [];
|
|
1435
|
+
const { resourceName, operation, recordId, partition, startDate, endDate, limit = 100 } = options;
|
|
1436
|
+
let query = {};
|
|
1437
|
+
if (resourceName) query.resourceName = resourceName;
|
|
1438
|
+
if (operation) query.operation = operation;
|
|
1439
|
+
if (recordId) query.recordId = recordId;
|
|
1440
|
+
if (partition) query.partition = partition;
|
|
1441
|
+
if (startDate || endDate) {
|
|
1442
|
+
query.timestamp = {};
|
|
1443
|
+
if (startDate) query.timestamp.$gte = startDate;
|
|
1444
|
+
if (endDate) query.timestamp.$lte = endDate;
|
|
1445
|
+
}
|
|
1446
|
+
const result = await this.auditResource.page({ query, limit });
|
|
1447
|
+
return result.items || [];
|
|
1566
1448
|
}
|
|
1567
1449
|
async getRecordHistory(resourceName, recordId) {
|
|
1568
|
-
return this.getAuditLogs({
|
|
1569
|
-
resourceName,
|
|
1570
|
-
recordId,
|
|
1571
|
-
limit: 1e3
|
|
1572
|
-
});
|
|
1450
|
+
return await this.getAuditLogs({ resourceName, recordId });
|
|
1573
1451
|
}
|
|
1574
1452
|
async getPartitionHistory(resourceName, partitionName, partitionValues) {
|
|
1575
|
-
return this.getAuditLogs({
|
|
1453
|
+
return await this.getAuditLogs({
|
|
1576
1454
|
resourceName,
|
|
1577
1455
|
partition: partitionName,
|
|
1578
|
-
|
|
1456
|
+
partitionValues: JSON.stringify(partitionValues)
|
|
1579
1457
|
});
|
|
1580
1458
|
}
|
|
1581
1459
|
async getAuditStats(options = {}) {
|
|
1582
|
-
const
|
|
1583
|
-
resourceName,
|
|
1584
|
-
startDate,
|
|
1585
|
-
endDate
|
|
1586
|
-
} = options;
|
|
1587
|
-
const allAudits = await this.getAuditLogs({
|
|
1588
|
-
resourceName,
|
|
1589
|
-
startDate,
|
|
1590
|
-
endDate,
|
|
1591
|
-
limit: 1e4
|
|
1592
|
-
});
|
|
1460
|
+
const logs = await this.getAuditLogs(options);
|
|
1593
1461
|
const stats = {
|
|
1594
|
-
total:
|
|
1462
|
+
total: logs.length,
|
|
1595
1463
|
byOperation: {},
|
|
1596
1464
|
byResource: {},
|
|
1597
1465
|
byPartition: {},
|
|
1598
1466
|
byUser: {},
|
|
1599
1467
|
timeline: {}
|
|
1600
1468
|
};
|
|
1601
|
-
for (const
|
|
1602
|
-
stats.byOperation[
|
|
1603
|
-
stats.byResource[
|
|
1604
|
-
if (
|
|
1605
|
-
stats.byPartition[
|
|
1606
|
-
}
|
|
1607
|
-
stats.byUser[audit.userId] = (stats.byUser[audit.userId] || 0) + 1;
|
|
1608
|
-
if (audit.timestamp) {
|
|
1609
|
-
const day = audit.timestamp.split("T")[0];
|
|
1610
|
-
stats.timeline[day] = (stats.timeline[day] || 0) + 1;
|
|
1469
|
+
for (const log of logs) {
|
|
1470
|
+
stats.byOperation[log.operation] = (stats.byOperation[log.operation] || 0) + 1;
|
|
1471
|
+
stats.byResource[log.resourceName] = (stats.byResource[log.resourceName] || 0) + 1;
|
|
1472
|
+
if (log.partition) {
|
|
1473
|
+
stats.byPartition[log.partition] = (stats.byPartition[log.partition] || 0) + 1;
|
|
1611
1474
|
}
|
|
1475
|
+
stats.byUser[log.userId] = (stats.byUser[log.userId] || 0) + 1;
|
|
1476
|
+
const date = log.timestamp.split("T")[0];
|
|
1477
|
+
stats.timeline[date] = (stats.timeline[date] || 0) + 1;
|
|
1612
1478
|
}
|
|
1613
1479
|
return stats;
|
|
1614
1480
|
}
|
|
@@ -7097,19 +6963,37 @@ class FilesystemCache extends Cache {
|
|
|
7097
6963
|
});
|
|
7098
6964
|
for (const file of cacheFiles) {
|
|
7099
6965
|
const filePath = path.join(this.directory, file);
|
|
7100
|
-
|
|
7101
|
-
await
|
|
6966
|
+
try {
|
|
6967
|
+
if (await this._fileExists(filePath)) {
|
|
6968
|
+
await unlink(filePath);
|
|
6969
|
+
}
|
|
6970
|
+
} catch (error) {
|
|
6971
|
+
if (error.code !== "ENOENT") {
|
|
6972
|
+
throw error;
|
|
6973
|
+
}
|
|
7102
6974
|
}
|
|
7103
6975
|
if (this.enableMetadata) {
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
await
|
|
6976
|
+
try {
|
|
6977
|
+
const metadataPath = this._getMetadataPath(filePath);
|
|
6978
|
+
if (await this._fileExists(metadataPath)) {
|
|
6979
|
+
await unlink(metadataPath);
|
|
6980
|
+
}
|
|
6981
|
+
} catch (error) {
|
|
6982
|
+
if (error.code !== "ENOENT") {
|
|
6983
|
+
throw error;
|
|
6984
|
+
}
|
|
7107
6985
|
}
|
|
7108
6986
|
}
|
|
7109
6987
|
if (this.enableBackup) {
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
await
|
|
6988
|
+
try {
|
|
6989
|
+
const backupPath = filePath + this.backupSuffix;
|
|
6990
|
+
if (await this._fileExists(backupPath)) {
|
|
6991
|
+
await unlink(backupPath);
|
|
6992
|
+
}
|
|
6993
|
+
} catch (error) {
|
|
6994
|
+
if (error.code !== "ENOENT") {
|
|
6995
|
+
throw error;
|
|
6996
|
+
}
|
|
7113
6997
|
}
|
|
7114
6998
|
}
|
|
7115
6999
|
}
|
|
@@ -7121,6 +7005,12 @@ class FilesystemCache extends Cache {
|
|
|
7121
7005
|
}
|
|
7122
7006
|
return true;
|
|
7123
7007
|
} catch (error) {
|
|
7008
|
+
if (error.code === "ENOENT") {
|
|
7009
|
+
if (this.enableStats) {
|
|
7010
|
+
this.stats.clears++;
|
|
7011
|
+
}
|
|
7012
|
+
return true;
|
|
7013
|
+
}
|
|
7124
7014
|
if (this.enableStats) {
|
|
7125
7015
|
this.stats.errors++;
|
|
7126
7016
|
}
|
|
@@ -7606,11 +7496,11 @@ class CachePlugin extends plugin_class_default {
|
|
|
7606
7496
|
await super.setup(database);
|
|
7607
7497
|
}
|
|
7608
7498
|
async onSetup() {
|
|
7609
|
-
if (this.config.driver) {
|
|
7499
|
+
if (this.config.driver && typeof this.config.driver === "object") {
|
|
7610
7500
|
this.driver = this.config.driver;
|
|
7611
|
-
} else if (this.config.
|
|
7501
|
+
} else if (this.config.driver === "memory") {
|
|
7612
7502
|
this.driver = new memory_cache_class_default(this.config.memoryOptions || {});
|
|
7613
|
-
} else if (this.config.
|
|
7503
|
+
} else if (this.config.driver === "filesystem") {
|
|
7614
7504
|
if (this.config.partitionAware) {
|
|
7615
7505
|
this.driver = new PartitionAwareFilesystemCache({
|
|
7616
7506
|
partitionStrategy: this.config.partitionStrategy,
|
|
@@ -7624,26 +7514,22 @@ class CachePlugin extends plugin_class_default {
|
|
|
7624
7514
|
} else {
|
|
7625
7515
|
this.driver = new s3_cache_class_default({ client: this.database.client, ...this.config.s3Options || {} });
|
|
7626
7516
|
}
|
|
7627
|
-
this.
|
|
7517
|
+
this.installDatabaseHooks();
|
|
7628
7518
|
this.installResourceHooks();
|
|
7629
7519
|
}
|
|
7520
|
+
/**
|
|
7521
|
+
* Install database hooks to handle resource creation/updates
|
|
7522
|
+
*/
|
|
7523
|
+
installDatabaseHooks() {
|
|
7524
|
+
this.database.addHook("afterCreateResource", async ({ resource }) => {
|
|
7525
|
+
this.installResourceHooksForResource(resource);
|
|
7526
|
+
});
|
|
7527
|
+
}
|
|
7630
7528
|
async onStart() {
|
|
7631
7529
|
}
|
|
7632
7530
|
async onStop() {
|
|
7633
7531
|
}
|
|
7634
|
-
installDatabaseProxy
|
|
7635
|
-
if (this.database._cacheProxyInstalled) {
|
|
7636
|
-
return;
|
|
7637
|
-
}
|
|
7638
|
-
const installResourceHooks = this.installResourceHooks.bind(this);
|
|
7639
|
-
this.database._originalCreateResourceForCache = this.database.createResource;
|
|
7640
|
-
this.database.createResource = async function(...args) {
|
|
7641
|
-
const resource = await this._originalCreateResourceForCache(...args);
|
|
7642
|
-
installResourceHooks(resource);
|
|
7643
|
-
return resource;
|
|
7644
|
-
};
|
|
7645
|
-
this.database._cacheProxyInstalled = true;
|
|
7646
|
-
}
|
|
7532
|
+
// Remove the old installDatabaseProxy method
|
|
7647
7533
|
installResourceHooks() {
|
|
7648
7534
|
for (const resource of Object.values(this.database.resources)) {
|
|
7649
7535
|
this.installResourceHooksForResource(resource);
|
|
@@ -7682,7 +7568,12 @@ class CachePlugin extends plugin_class_default {
|
|
|
7682
7568
|
"getAll",
|
|
7683
7569
|
"page",
|
|
7684
7570
|
"list",
|
|
7685
|
-
"get"
|
|
7571
|
+
"get",
|
|
7572
|
+
"exists",
|
|
7573
|
+
"content",
|
|
7574
|
+
"hasContent",
|
|
7575
|
+
"query",
|
|
7576
|
+
"getFromPartition"
|
|
7686
7577
|
];
|
|
7687
7578
|
for (const method of cacheMethods) {
|
|
7688
7579
|
resource.useMiddleware(method, async (ctx, next) => {
|
|
@@ -7695,9 +7586,26 @@ class CachePlugin extends plugin_class_default {
|
|
|
7695
7586
|
} else if (method === "list" || method === "listIds" || method === "count") {
|
|
7696
7587
|
const { partition, partitionValues } = ctx.args[0] || {};
|
|
7697
7588
|
key = await resource.cacheKeyFor({ action: method, partition, partitionValues });
|
|
7589
|
+
} else if (method === "query") {
|
|
7590
|
+
const filter = ctx.args[0] || {};
|
|
7591
|
+
const options = ctx.args[1] || {};
|
|
7592
|
+
key = await resource.cacheKeyFor({
|
|
7593
|
+
action: method,
|
|
7594
|
+
params: { filter, options: { limit: options.limit, offset: options.offset } },
|
|
7595
|
+
partition: options.partition,
|
|
7596
|
+
partitionValues: options.partitionValues
|
|
7597
|
+
});
|
|
7598
|
+
} else if (method === "getFromPartition") {
|
|
7599
|
+
const { id, partitionName, partitionValues } = ctx.args[0] || {};
|
|
7600
|
+
key = await resource.cacheKeyFor({
|
|
7601
|
+
action: method,
|
|
7602
|
+
params: { id, partitionName },
|
|
7603
|
+
partition: partitionName,
|
|
7604
|
+
partitionValues
|
|
7605
|
+
});
|
|
7698
7606
|
} else if (method === "getAll") {
|
|
7699
7607
|
key = await resource.cacheKeyFor({ action: method });
|
|
7700
|
-
} else if (
|
|
7608
|
+
} else if (["get", "exists", "content", "hasContent"].includes(method)) {
|
|
7701
7609
|
key = await resource.cacheKeyFor({ action: method, params: { id: ctx.args[0] } });
|
|
7702
7610
|
}
|
|
7703
7611
|
if (this.driver instanceof PartitionAwareFilesystemCache) {
|
|
@@ -7706,6 +7614,14 @@ class CachePlugin extends plugin_class_default {
|
|
|
7706
7614
|
const args = ctx.args[0] || {};
|
|
7707
7615
|
partition = args.partition;
|
|
7708
7616
|
partitionValues = args.partitionValues;
|
|
7617
|
+
} else if (method === "query") {
|
|
7618
|
+
const options = ctx.args[1] || {};
|
|
7619
|
+
partition = options.partition;
|
|
7620
|
+
partitionValues = options.partitionValues;
|
|
7621
|
+
} else if (method === "getFromPartition") {
|
|
7622
|
+
const { partitionName, partitionValues: pValues } = ctx.args[0] || {};
|
|
7623
|
+
partition = partitionName;
|
|
7624
|
+
partitionValues = pValues;
|
|
7709
7625
|
}
|
|
7710
7626
|
const [ok, err, result] = await try_fn_default(() => resource.cache._get(key, {
|
|
7711
7627
|
resource: resource.name,
|
|
@@ -7733,7 +7649,7 @@ class CachePlugin extends plugin_class_default {
|
|
|
7733
7649
|
}
|
|
7734
7650
|
});
|
|
7735
7651
|
}
|
|
7736
|
-
const writeMethods = ["insert", "update", "delete", "deleteMany"];
|
|
7652
|
+
const writeMethods = ["insert", "update", "delete", "deleteMany", "setContent", "deleteContent", "replace"];
|
|
7737
7653
|
for (const method of writeMethods) {
|
|
7738
7654
|
resource.useMiddleware(method, async (ctx, next) => {
|
|
7739
7655
|
const result = await next();
|
|
@@ -7748,6 +7664,12 @@ class CachePlugin extends plugin_class_default {
|
|
|
7748
7664
|
if (ok && full) data = full;
|
|
7749
7665
|
}
|
|
7750
7666
|
await this.clearCacheForResource(resource, data);
|
|
7667
|
+
} else if (method === "setContent" || method === "deleteContent") {
|
|
7668
|
+
const id = ctx.args[0]?.id || ctx.args[0];
|
|
7669
|
+
await this.clearCacheForResource(resource, { id });
|
|
7670
|
+
} else if (method === "replace") {
|
|
7671
|
+
const id = ctx.args[0];
|
|
7672
|
+
await this.clearCacheForResource(resource, { id, ...ctx.args[1] });
|
|
7751
7673
|
} else if (method === "deleteMany") {
|
|
7752
7674
|
await this.clearCacheForResource(resource);
|
|
7753
7675
|
}
|
|
@@ -7758,23 +7680,40 @@ class CachePlugin extends plugin_class_default {
|
|
|
7758
7680
|
async clearCacheForResource(resource, data) {
|
|
7759
7681
|
if (!resource.cache) return;
|
|
7760
7682
|
const keyPrefix = `resource=${resource.name}`;
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
const
|
|
7766
|
-
await resource.cache.clear(
|
|
7683
|
+
if (data && data.id) {
|
|
7684
|
+
const itemSpecificMethods = ["get", "exists", "content", "hasContent"];
|
|
7685
|
+
for (const method of itemSpecificMethods) {
|
|
7686
|
+
try {
|
|
7687
|
+
const specificKey = await this.generateCacheKey(resource, method, { id: data.id });
|
|
7688
|
+
await resource.cache.clear(specificKey.replace(".json.gz", ""));
|
|
7689
|
+
} catch (error) {
|
|
7767
7690
|
}
|
|
7768
|
-
}
|
|
7691
|
+
}
|
|
7692
|
+
if (this.config.includePartitions === true && resource.config?.partitions && Object.keys(resource.config.partitions).length > 0) {
|
|
7769
7693
|
const partitionValues = this.getPartitionValues(data, resource);
|
|
7770
7694
|
for (const [partitionName, values] of Object.entries(partitionValues)) {
|
|
7771
7695
|
if (values && Object.keys(values).length > 0 && Object.values(values).some((v) => v !== null && v !== void 0)) {
|
|
7772
|
-
|
|
7773
|
-
|
|
7696
|
+
try {
|
|
7697
|
+
const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
|
|
7698
|
+
await resource.cache.clear(partitionKeyPrefix);
|
|
7699
|
+
} catch (error) {
|
|
7700
|
+
}
|
|
7774
7701
|
}
|
|
7775
7702
|
}
|
|
7776
7703
|
}
|
|
7777
7704
|
}
|
|
7705
|
+
try {
|
|
7706
|
+
await resource.cache.clear(keyPrefix);
|
|
7707
|
+
} catch (error) {
|
|
7708
|
+
const aggregateMethods = ["count", "list", "listIds", "getAll", "page", "query"];
|
|
7709
|
+
for (const method of aggregateMethods) {
|
|
7710
|
+
try {
|
|
7711
|
+
await resource.cache.clear(`${keyPrefix}/action=${method}`);
|
|
7712
|
+
await resource.cache.clear(`resource=${resource.name}/action=${method}`);
|
|
7713
|
+
} catch (methodError) {
|
|
7714
|
+
}
|
|
7715
|
+
}
|
|
7716
|
+
}
|
|
7778
7717
|
}
|
|
7779
7718
|
async generateCacheKey(resource, action, params = {}, partition = null, partitionValues = null) {
|
|
7780
7719
|
const keyParts = [
|
|
@@ -7796,7 +7735,7 @@ class CachePlugin extends plugin_class_default {
|
|
|
7796
7735
|
return join(...keyParts) + ".json.gz";
|
|
7797
7736
|
}
|
|
7798
7737
|
async hashParams(params) {
|
|
7799
|
-
const sortedParams = Object.keys(params).sort().map((key) => `${key}:${params[key]}`).join("|") || "empty";
|
|
7738
|
+
const sortedParams = Object.keys(params).sort().map((key) => `${key}:${JSON.stringify(params[key])}`).join("|") || "empty";
|
|
7800
7739
|
return await sha256(sortedParams);
|
|
7801
7740
|
}
|
|
7802
7741
|
// Utility methods
|
|
@@ -8001,12 +7940,14 @@ class FullTextPlugin extends plugin_class_default {
|
|
|
8001
7940
|
}));
|
|
8002
7941
|
this.indexResource = ok ? indexResource : database.resources.fulltext_indexes;
|
|
8003
7942
|
await this.loadIndexes();
|
|
7943
|
+
this.installDatabaseHooks();
|
|
8004
7944
|
this.installIndexingHooks();
|
|
8005
7945
|
}
|
|
8006
7946
|
async start() {
|
|
8007
7947
|
}
|
|
8008
7948
|
async stop() {
|
|
8009
7949
|
await this.saveIndexes();
|
|
7950
|
+
this.removeDatabaseHooks();
|
|
8010
7951
|
}
|
|
8011
7952
|
async loadIndexes() {
|
|
8012
7953
|
if (!this.indexResource) return;
|
|
@@ -8042,6 +7983,16 @@ class FullTextPlugin extends plugin_class_default {
|
|
|
8042
7983
|
}
|
|
8043
7984
|
});
|
|
8044
7985
|
}
|
|
7986
|
+
installDatabaseHooks() {
|
|
7987
|
+
this.database.addHook("afterCreateResource", (resource) => {
|
|
7988
|
+
if (resource.name !== "fulltext_indexes") {
|
|
7989
|
+
this.installResourceHooks(resource);
|
|
7990
|
+
}
|
|
7991
|
+
});
|
|
7992
|
+
}
|
|
7993
|
+
removeDatabaseHooks() {
|
|
7994
|
+
this.database.removeHook("afterCreateResource", this.installResourceHooks.bind(this));
|
|
7995
|
+
}
|
|
8045
7996
|
installIndexingHooks() {
|
|
8046
7997
|
if (!this.database.plugins) {
|
|
8047
7998
|
this.database.plugins = {};
|
|
@@ -8404,6 +8355,7 @@ class MetricsPlugin extends plugin_class_default {
|
|
|
8404
8355
|
this.errorsResource = database.resources.error_logs;
|
|
8405
8356
|
this.performanceResource = database.resources.performance_logs;
|
|
8406
8357
|
}
|
|
8358
|
+
this.installDatabaseHooks();
|
|
8407
8359
|
this.installMetricsHooks();
|
|
8408
8360
|
if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
|
|
8409
8361
|
this.startFlushTimer();
|
|
@@ -8416,9 +8368,17 @@ class MetricsPlugin extends plugin_class_default {
|
|
|
8416
8368
|
clearInterval(this.flushTimer);
|
|
8417
8369
|
this.flushTimer = null;
|
|
8418
8370
|
}
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8371
|
+
this.removeDatabaseHooks();
|
|
8372
|
+
}
|
|
8373
|
+
installDatabaseHooks() {
|
|
8374
|
+
this.database.addHook("afterCreateResource", (resource) => {
|
|
8375
|
+
if (resource.name !== "metrics" && resource.name !== "error_logs" && resource.name !== "performance_logs") {
|
|
8376
|
+
this.installResourceHooks(resource);
|
|
8377
|
+
}
|
|
8378
|
+
});
|
|
8379
|
+
}
|
|
8380
|
+
removeDatabaseHooks() {
|
|
8381
|
+
this.database.removeHook("afterCreateResource", this.installResourceHooks.bind(this));
|
|
8422
8382
|
}
|
|
8423
8383
|
installMetricsHooks() {
|
|
8424
8384
|
for (const resource of Object.values(this.database.resources)) {
|
|
@@ -9519,6 +9479,72 @@ class PostgresReplicator extends base_replicator_class_default {
|
|
|
9519
9479
|
}
|
|
9520
9480
|
var postgres_replicator_class_default = PostgresReplicator;
|
|
9521
9481
|
|
|
9482
|
+
/*
|
|
9483
|
+
this and http-lib folder
|
|
9484
|
+
|
|
9485
|
+
The MIT License
|
|
9486
|
+
|
|
9487
|
+
Copyright (c) 2015 John Hiesey
|
|
9488
|
+
|
|
9489
|
+
Permission is hereby granted, free of charge,
|
|
9490
|
+
to any person obtaining a copy of this software and
|
|
9491
|
+
associated documentation files (the "Software"), to
|
|
9492
|
+
deal in the Software without restriction, including
|
|
9493
|
+
without limitation the rights to use, copy, modify,
|
|
9494
|
+
merge, publish, distribute, sublicense, and/or sell
|
|
9495
|
+
copies of the Software, and to permit persons to whom
|
|
9496
|
+
the Software is furnished to do so,
|
|
9497
|
+
subject to the following conditions:
|
|
9498
|
+
|
|
9499
|
+
The above copyright notice and this permission notice
|
|
9500
|
+
shall be included in all copies or substantial portions of the Software.
|
|
9501
|
+
|
|
9502
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
9503
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
9504
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
9505
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
|
9506
|
+
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
9507
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
9508
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
9509
|
+
|
|
9510
|
+
*/
|
|
9511
|
+
|
|
9512
|
+
function Agent$1() {}
|
|
9513
|
+
Agent$1.defaultMaxSockets = 4;
|
|
9514
|
+
|
|
9515
|
+
/*
|
|
9516
|
+
this and http-lib folder
|
|
9517
|
+
|
|
9518
|
+
The MIT License
|
|
9519
|
+
|
|
9520
|
+
Copyright (c) 2015 John Hiesey
|
|
9521
|
+
|
|
9522
|
+
Permission is hereby granted, free of charge,
|
|
9523
|
+
to any person obtaining a copy of this software and
|
|
9524
|
+
associated documentation files (the "Software"), to
|
|
9525
|
+
deal in the Software without restriction, including
|
|
9526
|
+
without limitation the rights to use, copy, modify,
|
|
9527
|
+
merge, publish, distribute, sublicense, and/or sell
|
|
9528
|
+
copies of the Software, and to permit persons to whom
|
|
9529
|
+
the Software is furnished to do so,
|
|
9530
|
+
subject to the following conditions:
|
|
9531
|
+
|
|
9532
|
+
The above copyright notice and this permission notice
|
|
9533
|
+
shall be included in all copies or substantial portions of the Software.
|
|
9534
|
+
|
|
9535
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
9536
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
9537
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
9538
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
|
9539
|
+
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
9540
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
9541
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
9542
|
+
|
|
9543
|
+
*/
|
|
9544
|
+
|
|
9545
|
+
function Agent() {}
|
|
9546
|
+
Agent.defaultMaxSockets = 4;
|
|
9547
|
+
|
|
9522
9548
|
const S3_DEFAULT_REGION = "us-east-1";
|
|
9523
9549
|
const S3_DEFAULT_ENDPOINT = "https://s3.us-east-1.amazonaws.com";
|
|
9524
9550
|
class ConnectionString {
|
|
@@ -9586,19 +9612,38 @@ class Client extends EventEmitter {
|
|
|
9586
9612
|
id = null,
|
|
9587
9613
|
AwsS3Client,
|
|
9588
9614
|
connectionString,
|
|
9589
|
-
parallelism = 10
|
|
9615
|
+
parallelism = 10,
|
|
9616
|
+
httpClientOptions = {}
|
|
9590
9617
|
}) {
|
|
9591
9618
|
super();
|
|
9592
9619
|
this.verbose = verbose;
|
|
9593
9620
|
this.id = id ?? idGenerator();
|
|
9594
9621
|
this.parallelism = parallelism;
|
|
9595
9622
|
this.config = new ConnectionString(connectionString);
|
|
9623
|
+
this.httpClientOptions = {
|
|
9624
|
+
keepAlive: false,
|
|
9625
|
+
// Disabled for maximum creation speed
|
|
9626
|
+
maxSockets: 10,
|
|
9627
|
+
// Minimal sockets
|
|
9628
|
+
maxFreeSockets: 2,
|
|
9629
|
+
// Minimal pool
|
|
9630
|
+
timeout: 15e3,
|
|
9631
|
+
// Short timeout
|
|
9632
|
+
...httpClientOptions
|
|
9633
|
+
};
|
|
9596
9634
|
this.client = AwsS3Client || this.createClient();
|
|
9597
9635
|
}
|
|
9598
9636
|
createClient() {
|
|
9637
|
+
const httpAgent = new Agent$1(this.httpClientOptions);
|
|
9638
|
+
const httpsAgent = new Agent(this.httpClientOptions);
|
|
9639
|
+
const httpHandler = new NodeHttpHandler({
|
|
9640
|
+
httpAgent,
|
|
9641
|
+
httpsAgent
|
|
9642
|
+
});
|
|
9599
9643
|
let options = {
|
|
9600
9644
|
region: this.config.region,
|
|
9601
|
-
endpoint: this.config.endpoint
|
|
9645
|
+
endpoint: this.config.endpoint,
|
|
9646
|
+
requestHandler: httpHandler
|
|
9602
9647
|
};
|
|
9603
9648
|
if (this.config.forcePathStyle) options.forcePathStyle = true;
|
|
9604
9649
|
if (this.config.accessKeyId) {
|
|
@@ -13147,7 +13192,14 @@ class Resource extends EventEmitter {
|
|
|
13147
13192
|
"delete",
|
|
13148
13193
|
"deleteMany",
|
|
13149
13194
|
"exists",
|
|
13150
|
-
"getMany"
|
|
13195
|
+
"getMany",
|
|
13196
|
+
"content",
|
|
13197
|
+
"hasContent",
|
|
13198
|
+
"query",
|
|
13199
|
+
"getFromPartition",
|
|
13200
|
+
"setContent",
|
|
13201
|
+
"deleteContent",
|
|
13202
|
+
"replace"
|
|
13151
13203
|
];
|
|
13152
13204
|
for (const method of this._middlewareMethods) {
|
|
13153
13205
|
this._middlewares.set(method, []);
|
|
@@ -13326,7 +13378,7 @@ class Database extends EventEmitter {
|
|
|
13326
13378
|
super();
|
|
13327
13379
|
this.version = "1";
|
|
13328
13380
|
this.s3dbVersion = (() => {
|
|
13329
|
-
const [ok, err, version] = try_fn_default(() => true ? "7.
|
|
13381
|
+
const [ok, err, version] = try_fn_default(() => true ? "7.5.0" : "latest");
|
|
13330
13382
|
return ok ? version : "latest";
|
|
13331
13383
|
})();
|
|
13332
13384
|
this.resources = {};
|
|
@@ -13339,6 +13391,7 @@ class Database extends EventEmitter {
|
|
|
13339
13391
|
this.cache = options.cache;
|
|
13340
13392
|
this.passphrase = options.passphrase || "secret";
|
|
13341
13393
|
this.versioningEnabled = options.versioningEnabled || false;
|
|
13394
|
+
this._initHooks();
|
|
13342
13395
|
let connectionString = options.connectionString;
|
|
13343
13396
|
if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
|
|
13344
13397
|
const { bucket, region, accessKeyId, secretAccessKey, endpoint, forcePathStyle } = options;
|
|
@@ -13816,6 +13869,131 @@ class Database extends EventEmitter {
|
|
|
13816
13869
|
} catch (err) {
|
|
13817
13870
|
}
|
|
13818
13871
|
}
|
|
13872
|
+
/**
|
|
13873
|
+
* Initialize hooks system for database operations
|
|
13874
|
+
* @private
|
|
13875
|
+
*/
|
|
13876
|
+
_initHooks() {
|
|
13877
|
+
this._hooks = /* @__PURE__ */ new Map();
|
|
13878
|
+
this._hookEvents = [
|
|
13879
|
+
"beforeConnect",
|
|
13880
|
+
"afterConnect",
|
|
13881
|
+
"beforeCreateResource",
|
|
13882
|
+
"afterCreateResource",
|
|
13883
|
+
"beforeUploadMetadata",
|
|
13884
|
+
"afterUploadMetadata",
|
|
13885
|
+
"beforeDisconnect",
|
|
13886
|
+
"afterDisconnect",
|
|
13887
|
+
"resourceCreated",
|
|
13888
|
+
"resourceUpdated"
|
|
13889
|
+
];
|
|
13890
|
+
for (const event of this._hookEvents) {
|
|
13891
|
+
this._hooks.set(event, []);
|
|
13892
|
+
}
|
|
13893
|
+
this._wrapHookableMethods();
|
|
13894
|
+
}
|
|
13895
|
+
/**
|
|
13896
|
+
* Wrap methods that can have hooks
|
|
13897
|
+
* @private
|
|
13898
|
+
*/
|
|
13899
|
+
_wrapHookableMethods() {
|
|
13900
|
+
if (this._hooksInstalled) return;
|
|
13901
|
+
this._originalConnect = this.connect.bind(this);
|
|
13902
|
+
this._originalCreateResource = this.createResource.bind(this);
|
|
13903
|
+
this._originalUploadMetadataFile = this.uploadMetadataFile.bind(this);
|
|
13904
|
+
this._originalDisconnect = this.disconnect.bind(this);
|
|
13905
|
+
this.connect = async (...args) => {
|
|
13906
|
+
await this._executeHooks("beforeConnect", { args });
|
|
13907
|
+
const result = await this._originalConnect(...args);
|
|
13908
|
+
await this._executeHooks("afterConnect", { result, args });
|
|
13909
|
+
return result;
|
|
13910
|
+
};
|
|
13911
|
+
this.createResource = async (config) => {
|
|
13912
|
+
await this._executeHooks("beforeCreateResource", { config });
|
|
13913
|
+
const resource = await this._originalCreateResource(config);
|
|
13914
|
+
await this._executeHooks("afterCreateResource", { resource, config });
|
|
13915
|
+
return resource;
|
|
13916
|
+
};
|
|
13917
|
+
this.uploadMetadataFile = async (...args) => {
|
|
13918
|
+
await this._executeHooks("beforeUploadMetadata", { args });
|
|
13919
|
+
const result = await this._originalUploadMetadataFile(...args);
|
|
13920
|
+
await this._executeHooks("afterUploadMetadata", { result, args });
|
|
13921
|
+
return result;
|
|
13922
|
+
};
|
|
13923
|
+
this.disconnect = async (...args) => {
|
|
13924
|
+
await this._executeHooks("beforeDisconnect", { args });
|
|
13925
|
+
const result = await this._originalDisconnect(...args);
|
|
13926
|
+
await this._executeHooks("afterDisconnect", { result, args });
|
|
13927
|
+
return result;
|
|
13928
|
+
};
|
|
13929
|
+
this._hooksInstalled = true;
|
|
13930
|
+
}
|
|
13931
|
+
/**
|
|
13932
|
+
* Add a hook for a specific database event
|
|
13933
|
+
* @param {string} event - Hook event name
|
|
13934
|
+
* @param {Function} fn - Hook function
|
|
13935
|
+
* @example
|
|
13936
|
+
* database.addHook('afterCreateResource', async ({ resource }) => {
|
|
13937
|
+
* console.log('Resource created:', resource.name);
|
|
13938
|
+
* });
|
|
13939
|
+
*/
|
|
13940
|
+
addHook(event, fn) {
|
|
13941
|
+
if (!this._hooks) this._initHooks();
|
|
13942
|
+
if (!this._hooks.has(event)) {
|
|
13943
|
+
throw new Error(`Unknown hook event: ${event}. Available events: ${this._hookEvents.join(", ")}`);
|
|
13944
|
+
}
|
|
13945
|
+
if (typeof fn !== "function") {
|
|
13946
|
+
throw new Error("Hook function must be a function");
|
|
13947
|
+
}
|
|
13948
|
+
this._hooks.get(event).push(fn);
|
|
13949
|
+
}
|
|
13950
|
+
/**
|
|
13951
|
+
* Execute hooks for a specific event
|
|
13952
|
+
* @param {string} event - Hook event name
|
|
13953
|
+
* @param {Object} context - Context data to pass to hooks
|
|
13954
|
+
* @private
|
|
13955
|
+
*/
|
|
13956
|
+
async _executeHooks(event, context = {}) {
|
|
13957
|
+
if (!this._hooks || !this._hooks.has(event)) return;
|
|
13958
|
+
const hooks = this._hooks.get(event);
|
|
13959
|
+
for (const hook of hooks) {
|
|
13960
|
+
try {
|
|
13961
|
+
await hook({ database: this, ...context });
|
|
13962
|
+
} catch (error) {
|
|
13963
|
+
this.emit("hookError", { event, error, context });
|
|
13964
|
+
}
|
|
13965
|
+
}
|
|
13966
|
+
}
|
|
13967
|
+
/**
|
|
13968
|
+
* Remove a hook for a specific event
|
|
13969
|
+
* @param {string} event - Hook event name
|
|
13970
|
+
* @param {Function} fn - Hook function to remove
|
|
13971
|
+
*/
|
|
13972
|
+
removeHook(event, fn) {
|
|
13973
|
+
if (!this._hooks || !this._hooks.has(event)) return;
|
|
13974
|
+
const hooks = this._hooks.get(event);
|
|
13975
|
+
const index = hooks.indexOf(fn);
|
|
13976
|
+
if (index > -1) {
|
|
13977
|
+
hooks.splice(index, 1);
|
|
13978
|
+
}
|
|
13979
|
+
}
|
|
13980
|
+
/**
|
|
13981
|
+
* Get all hooks for a specific event
|
|
13982
|
+
* @param {string} event - Hook event name
|
|
13983
|
+
* @returns {Function[]} Array of hook functions
|
|
13984
|
+
*/
|
|
13985
|
+
getHooks(event) {
|
|
13986
|
+
if (!this._hooks || !this._hooks.has(event)) return [];
|
|
13987
|
+
return [...this._hooks.get(event)];
|
|
13988
|
+
}
|
|
13989
|
+
/**
|
|
13990
|
+
* Clear all hooks for a specific event
|
|
13991
|
+
* @param {string} event - Hook event name
|
|
13992
|
+
*/
|
|
13993
|
+
clearHooks(event) {
|
|
13994
|
+
if (!this._hooks || !this._hooks.has(event)) return;
|
|
13995
|
+
this._hooks.get(event).length = 0;
|
|
13996
|
+
}
|
|
13819
13997
|
}
|
|
13820
13998
|
class S3db extends Database {
|
|
13821
13999
|
}
|
|
@@ -14548,6 +14726,10 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14548
14726
|
}
|
|
14549
14727
|
return filtered;
|
|
14550
14728
|
}
|
|
14729
|
+
async getCompleteData(resource, data) {
|
|
14730
|
+
const [ok, err, completeRecord] = await try_fn_default(() => resource.get(data.id));
|
|
14731
|
+
return ok ? completeRecord : data;
|
|
14732
|
+
}
|
|
14551
14733
|
installEventListeners(resource, database, plugin) {
|
|
14552
14734
|
if (!resource || this.eventListenersInstalled.has(resource.name) || resource.name === this.config.replicatorLogResource) {
|
|
14553
14735
|
return;
|
|
@@ -14566,8 +14748,9 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14566
14748
|
});
|
|
14567
14749
|
resource.on("update", async (data, beforeData) => {
|
|
14568
14750
|
const [ok, error] = await try_fn_default(async () => {
|
|
14569
|
-
const completeData =
|
|
14570
|
-
|
|
14751
|
+
const completeData = await plugin.getCompleteData(resource, data);
|
|
14752
|
+
const dataWithTimestamp = { ...completeData, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
14753
|
+
await plugin.processReplicatorEvent("update", resource.name, completeData.id, dataWithTimestamp, beforeData);
|
|
14571
14754
|
});
|
|
14572
14755
|
if (!ok) {
|
|
14573
14756
|
if (this.config.verbose) {
|
|
@@ -14589,67 +14772,55 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14589
14772
|
});
|
|
14590
14773
|
this.eventListenersInstalled.add(resource.name);
|
|
14591
14774
|
}
|
|
14592
|
-
/**
|
|
14593
|
-
* Get complete data by always fetching the full record from the resource
|
|
14594
|
-
* This ensures we always have the complete data regardless of behavior or data size
|
|
14595
|
-
*/
|
|
14596
|
-
async getCompleteData(resource, data) {
|
|
14597
|
-
const [ok, err, completeRecord] = await try_fn_default(() => resource.get(data.id));
|
|
14598
|
-
return ok ? completeRecord : data;
|
|
14599
|
-
}
|
|
14600
14775
|
async setup(database) {
|
|
14601
14776
|
this.database = database;
|
|
14602
|
-
|
|
14603
|
-
await
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
resourceName: "string",
|
|
14620
|
-
recordId: "string",
|
|
14621
|
-
data: "string",
|
|
14622
|
-
error: "string|optional",
|
|
14623
|
-
replicator: "string",
|
|
14624
|
-
timestamp: "string",
|
|
14625
|
-
status: "string"
|
|
14626
|
-
}
|
|
14627
|
-
});
|
|
14628
|
-
}
|
|
14629
|
-
});
|
|
14630
|
-
if (!logOk) {
|
|
14631
|
-
if (this.config.verbose) {
|
|
14632
|
-
console.warn(`[ReplicatorPlugin] Failed to create log resource ${this.config.replicatorLogResource}: ${logError.message}`);
|
|
14777
|
+
if (this.config.persistReplicatorLog) {
|
|
14778
|
+
const [ok, err, logResource] = await try_fn_default(() => database.createResource({
|
|
14779
|
+
name: this.config.replicatorLogResource || "replicator_logs",
|
|
14780
|
+
attributes: {
|
|
14781
|
+
id: "string|required",
|
|
14782
|
+
resource: "string|required",
|
|
14783
|
+
action: "string|required",
|
|
14784
|
+
data: "json",
|
|
14785
|
+
timestamp: "number|required",
|
|
14786
|
+
createdAt: "string|required"
|
|
14787
|
+
},
|
|
14788
|
+
behavior: "truncate-data"
|
|
14789
|
+
}));
|
|
14790
|
+
if (ok) {
|
|
14791
|
+
this.replicatorLogResource = logResource;
|
|
14792
|
+
} else {
|
|
14793
|
+
this.replicatorLogResource = database.resources[this.config.replicatorLogResource || "replicator_logs"];
|
|
14633
14794
|
}
|
|
14634
|
-
this.emit("replicator_log_resource_creation_error", {
|
|
14635
|
-
resourceName: this.config.replicatorLogResource,
|
|
14636
|
-
error: logError.message
|
|
14637
|
-
});
|
|
14638
14795
|
}
|
|
14639
|
-
await this.
|
|
14640
|
-
|
|
14641
|
-
|
|
14642
|
-
|
|
14643
|
-
if (resource) {
|
|
14796
|
+
await this.initializeReplicators(database);
|
|
14797
|
+
this.installDatabaseHooks();
|
|
14798
|
+
for (const resource of Object.values(database.resources)) {
|
|
14799
|
+
if (resource.name !== (this.config.replicatorLogResource || "replicator_logs")) {
|
|
14644
14800
|
this.installEventListeners(resource, database, this);
|
|
14645
14801
|
}
|
|
14646
|
-
return resource;
|
|
14647
|
-
};
|
|
14648
|
-
for (const resourceName in database.resources) {
|
|
14649
|
-
const resource = database.resources[resourceName];
|
|
14650
|
-
this.installEventListeners(resource, database, this);
|
|
14651
14802
|
}
|
|
14652
14803
|
}
|
|
14804
|
+
async start() {
|
|
14805
|
+
}
|
|
14806
|
+
async stop() {
|
|
14807
|
+
for (const replicator of this.replicators || []) {
|
|
14808
|
+
if (replicator && typeof replicator.cleanup === "function") {
|
|
14809
|
+
await replicator.cleanup();
|
|
14810
|
+
}
|
|
14811
|
+
}
|
|
14812
|
+
this.removeDatabaseHooks();
|
|
14813
|
+
}
|
|
14814
|
+
installDatabaseHooks() {
|
|
14815
|
+
this.database.addHook("afterCreateResource", (resource) => {
|
|
14816
|
+
if (resource.name !== (this.config.replicatorLogResource || "replicator_logs")) {
|
|
14817
|
+
this.installEventListeners(resource, this.database, this);
|
|
14818
|
+
}
|
|
14819
|
+
});
|
|
14820
|
+
}
|
|
14821
|
+
removeDatabaseHooks() {
|
|
14822
|
+
this.database.removeHook("afterCreateResource", this.installEventListeners.bind(this));
|
|
14823
|
+
}
|
|
14653
14824
|
createReplicator(driver, config, resources, client) {
|
|
14654
14825
|
return createReplicator(driver, config, resources, client);
|
|
14655
14826
|
}
|
|
@@ -14665,10 +14836,6 @@ class ReplicatorPlugin extends plugin_class_default {
|
|
|
14665
14836
|
}
|
|
14666
14837
|
}
|
|
14667
14838
|
}
|
|
14668
|
-
async start() {
|
|
14669
|
-
}
|
|
14670
|
-
async stop() {
|
|
14671
|
-
}
|
|
14672
14839
|
async uploadMetadataFile(database) {
|
|
14673
14840
|
if (typeof database.uploadMetadataFile === "function") {
|
|
14674
14841
|
await database.uploadMetadataFile();
|