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/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.installDatabaseProxy();
1300
- this.installEventListeners();
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
- return resource;
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 === "audits") {
1324
- continue;
1304
+ if (resource.name !== "audits") {
1305
+ this.setupResourceAuditing(resource);
1325
1306
  }
1326
- this.installEventListenersForResource(resource);
1327
1307
  }
1328
1308
  }
1329
- installEventListenersForResource(resource) {
1309
+ async onStart() {
1310
+ }
1311
+ async onStop() {
1312
+ }
1313
+ setupResourceAuditing(resource) {
1330
1314
  resource.on("insert", async (data) => {
1331
- const recordId = data.id || "auto-generated";
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 === false ? null : JSON.stringify(this.truncateData(data)),
1342
- partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
1343
- partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
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(recordId));
1328
+ const [ok, err, fetched] = await try_fn_default(() => resource.get(data.id));
1357
1329
  if (ok) oldData = fetched;
1358
1330
  }
1359
- const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
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
- userId: this.getCurrentUserId?.() || "system",
1366
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1367
- oldData: oldData && this.config.includeData === false ? null : oldData ? JSON.stringify(this.truncateData(oldData)) : null,
1368
- newData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(data)),
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(recordId));
1344
+ const [ok, err, fetched] = await try_fn_default(() => resource.get(data.id));
1384
1345
  if (ok) oldData = fetched;
1385
1346
  }
1386
- const partitionValues = oldData && this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null;
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
- userId: this.getCurrentUserId?.() || "system",
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(partitionValues) : null,
1397
- partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
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
- resource.useMiddleware("deleteMany", async (ctx, next) => {
1407
- const ids = ctx.args[0];
1408
- const oldDataMap = {};
1409
- if (this.config.includeData) {
1410
- for (const id of ids) {
1411
- const [ok, err, data] = await try_fn_default(() => resource.get(id));
1412
- oldDataMap[id] = ok ? data : null;
1413
- }
1414
- }
1415
- const result = await next();
1416
- if (result && result.length > 0 && this.config.includeData) {
1417
- for (const id of ids) {
1418
- const oldData = oldDataMap[id];
1419
- const partitionValues = oldData ? this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null : null;
1420
- const auditRecord = {
1421
- id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
1422
- resourceName: resource.name,
1423
- operation: "delete",
1424
- recordId: id,
1425
- userId: this.getCurrentUserId?.() || "system",
1426
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1427
- oldData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(oldData)),
1428
- newData: null,
1429
- partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
1430
- partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
1431
- metadata: JSON.stringify({
1432
- source: "audit-plugin",
1433
- version: "2.0",
1434
- batchOperation: true
1435
- })
1436
- };
1437
- this.logAudit(auditRecord).catch(() => {
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 (!data) return null;
1446
- const partitions = resource.config?.partitions || {};
1394
+ if (!this.config.includePartitions || !resource.partitions) return null;
1447
1395
  const partitionValues = {};
1448
- for (const [partitionName, partitionDef] of Object.entries(partitions)) {
1449
- if (partitionDef.fields) {
1450
- const partitionData = {};
1451
- for (const [fieldName, fieldRule] of Object.entries(partitionDef.fields)) {
1452
- const fieldValue = this.getNestedFieldValue(data, fieldName);
1453
- if (fieldValue !== void 0 && fieldValue !== null) {
1454
- partitionData[fieldName] = fieldValue;
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
- if (!fieldPath.includes(".")) {
1466
- return data[fieldPath];
1467
- }
1468
- const keys = fieldPath.split(".");
1469
- let currentLevel = data;
1470
- for (const key of keys) {
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 currentLevel;
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 (!data) return data;
1492
- const filteredData = {};
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 filteredData;
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
- ...truncatedData,
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 [ok, err, result] = await try_fn_default(async () => {
1531
- const {
1532
- resourceName,
1533
- operation,
1534
- recordId,
1535
- userId,
1536
- partition,
1537
- startDate,
1538
- endDate,
1539
- limit = 100,
1540
- offset = 0
1541
- } = options;
1542
- const allAudits = await this.auditResource.getAll();
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
- limit: 1e3
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: allAudits.length,
1466
+ total: logs.length,
1599
1467
  byOperation: {},
1600
1468
  byResource: {},
1601
1469
  byPartition: {},
1602
1470
  byUser: {},
1603
1471
  timeline: {}
1604
1472
  };
1605
- for (const audit of allAudits) {
1606
- stats.byOperation[audit.operation] = (stats.byOperation[audit.operation] || 0) + 1;
1607
- stats.byResource[audit.resourceName] = (stats.byResource[audit.resourceName] || 0) + 1;
1608
- if (audit.partition) {
1609
- stats.byPartition[audit.partition] = (stats.byPartition[audit.partition] || 0) + 1;
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
- if (await this._fileExists(filePath)) {
7105
- await promises.unlink(filePath);
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
- const metadataPath = this._getMetadataPath(filePath);
7109
- if (await this._fileExists(metadataPath)) {
7110
- await promises.unlink(metadataPath);
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
- const backupPath = filePath + this.backupSuffix;
7115
- if (await this._fileExists(backupPath)) {
7116
- await promises.unlink(backupPath);
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.driverType === "memory") {
7505
+ } else if (this.config.driver === "memory") {
7616
7506
  this.driver = new memory_cache_class_default(this.config.memoryOptions || {});
7617
- } else if (this.config.driverType === "filesystem") {
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.installDatabaseProxy();
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 (method === "get") {
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
- await resource.cache.clear(keyPrefix);
7766
- if (this.config.includePartitions === true && resource.config?.partitions && Object.keys(resource.config.partitions).length > 0) {
7767
- if (!data) {
7768
- for (const partitionName of Object.keys(resource.config.partitions)) {
7769
- const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
7770
- await resource.cache.clear(partitionKeyPrefix);
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
- } else {
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
- const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
7777
- await resource.cache.clear(partitionKeyPrefix);
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
- if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
8424
- await this.flushMetrics();
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.4.1" : "latest");
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 = { ...data, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
14574
- await plugin.processReplicatorEvent("update", resource.name, completeData.id, completeData, beforeData);
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
- const [initOk, initError] = await try_fn_default(async () => {
14607
- await this.initializeReplicators(database);
14608
- });
14609
- if (!initOk) {
14610
- if (this.config.verbose) {
14611
- console.warn(`[ReplicatorPlugin] Replicator initialization failed: ${initError.message}`);
14612
- }
14613
- this.emit("error", { operation: "setup", error: initError.message });
14614
- throw initError;
14615
- }
14616
- const [logOk, logError] = await try_fn_default(async () => {
14617
- if (this.config.replicatorLogResource) {
14618
- const logRes = await database.createResource({
14619
- name: this.config.replicatorLogResource,
14620
- behavior: "body-overflow",
14621
- attributes: {
14622
- operation: "string",
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.uploadMetadataFile(database);
14644
- const originalCreateResource = database.createResource.bind(database);
14645
- database.createResource = async (config) => {
14646
- const resource = await originalCreateResource(config);
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();