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.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.installDatabaseProxy();
1296
- this.installEventListeners();
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
- return resource;
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 === "audits") {
1320
- continue;
1300
+ if (resource.name !== "audits") {
1301
+ this.setupResourceAuditing(resource);
1321
1302
  }
1322
- this.installEventListenersForResource(resource);
1323
1303
  }
1324
1304
  }
1325
- installEventListenersForResource(resource) {
1305
+ async onStart() {
1306
+ }
1307
+ async onStop() {
1308
+ }
1309
+ setupResourceAuditing(resource) {
1326
1310
  resource.on("insert", async (data) => {
1327
- const recordId = data.id || "auto-generated";
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 === false ? null : JSON.stringify(this.truncateData(data)),
1338
- partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
1339
- partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
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(recordId));
1324
+ const [ok, err, fetched] = await try_fn_default(() => resource.get(data.id));
1353
1325
  if (ok) oldData = fetched;
1354
1326
  }
1355
- const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
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
- userId: this.getCurrentUserId?.() || "system",
1362
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1363
- oldData: oldData && this.config.includeData === false ? null : oldData ? JSON.stringify(this.truncateData(oldData)) : null,
1364
- newData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(data)),
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(recordId));
1340
+ const [ok, err, fetched] = await try_fn_default(() => resource.get(data.id));
1380
1341
  if (ok) oldData = fetched;
1381
1342
  }
1382
- const partitionValues = oldData && this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null;
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
- userId: this.getCurrentUserId?.() || "system",
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(partitionValues) : null,
1393
- partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
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
- resource.useMiddleware("deleteMany", async (ctx, next) => {
1403
- const ids = ctx.args[0];
1404
- const oldDataMap = {};
1405
- if (this.config.includeData) {
1406
- for (const id of ids) {
1407
- const [ok, err, data] = await try_fn_default(() => resource.get(id));
1408
- oldDataMap[id] = ok ? data : null;
1409
- }
1410
- }
1411
- const result = await next();
1412
- if (result && result.length > 0 && this.config.includeData) {
1413
- for (const id of ids) {
1414
- const oldData = oldDataMap[id];
1415
- const partitionValues = oldData ? this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null : null;
1416
- const auditRecord = {
1417
- id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
1418
- resourceName: resource.name,
1419
- operation: "delete",
1420
- recordId: id,
1421
- userId: this.getCurrentUserId?.() || "system",
1422
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1423
- oldData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(oldData)),
1424
- newData: null,
1425
- partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
1426
- partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
1427
- metadata: JSON.stringify({
1428
- source: "audit-plugin",
1429
- version: "2.0",
1430
- batchOperation: true
1431
- })
1432
- };
1433
- this.logAudit(auditRecord).catch(() => {
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 (!data) return null;
1442
- const partitions = resource.config?.partitions || {};
1390
+ if (!this.config.includePartitions || !resource.partitions) return null;
1443
1391
  const partitionValues = {};
1444
- for (const [partitionName, partitionDef] of Object.entries(partitions)) {
1445
- if (partitionDef.fields) {
1446
- const partitionData = {};
1447
- for (const [fieldName, fieldRule] of Object.entries(partitionDef.fields)) {
1448
- const fieldValue = this.getNestedFieldValue(data, fieldName);
1449
- if (fieldValue !== void 0 && fieldValue !== null) {
1450
- partitionData[fieldName] = fieldValue;
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
- if (!fieldPath.includes(".")) {
1462
- return data[fieldPath];
1463
- }
1464
- const keys = fieldPath.split(".");
1465
- let currentLevel = data;
1466
- for (const key of keys) {
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 currentLevel;
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 (!data) return data;
1488
- const filteredData = {};
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 filteredData;
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
- ...truncatedData,
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 [ok, err, result] = await try_fn_default(async () => {
1527
- const {
1528
- resourceName,
1529
- operation,
1530
- recordId,
1531
- userId,
1532
- partition,
1533
- startDate,
1534
- endDate,
1535
- limit = 100,
1536
- offset = 0
1537
- } = options;
1538
- const allAudits = await this.auditResource.getAll();
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
- limit: 1e3
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: allAudits.length,
1462
+ total: logs.length,
1595
1463
  byOperation: {},
1596
1464
  byResource: {},
1597
1465
  byPartition: {},
1598
1466
  byUser: {},
1599
1467
  timeline: {}
1600
1468
  };
1601
- for (const audit of allAudits) {
1602
- stats.byOperation[audit.operation] = (stats.byOperation[audit.operation] || 0) + 1;
1603
- stats.byResource[audit.resourceName] = (stats.byResource[audit.resourceName] || 0) + 1;
1604
- if (audit.partition) {
1605
- stats.byPartition[audit.partition] = (stats.byPartition[audit.partition] || 0) + 1;
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
- if (await this._fileExists(filePath)) {
7101
- await unlink(filePath);
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
- const metadataPath = this._getMetadataPath(filePath);
7105
- if (await this._fileExists(metadataPath)) {
7106
- await unlink(metadataPath);
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
- const backupPath = filePath + this.backupSuffix;
7111
- if (await this._fileExists(backupPath)) {
7112
- await unlink(backupPath);
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.driverType === "memory") {
7501
+ } else if (this.config.driver === "memory") {
7612
7502
  this.driver = new memory_cache_class_default(this.config.memoryOptions || {});
7613
- } else if (this.config.driverType === "filesystem") {
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.installDatabaseProxy();
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 (method === "get") {
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
- await resource.cache.clear(keyPrefix);
7762
- if (this.config.includePartitions === true && resource.config?.partitions && Object.keys(resource.config.partitions).length > 0) {
7763
- if (!data) {
7764
- for (const partitionName of Object.keys(resource.config.partitions)) {
7765
- const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
7766
- await resource.cache.clear(partitionKeyPrefix);
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
- } else {
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
- const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
7773
- await resource.cache.clear(partitionKeyPrefix);
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
- if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
8420
- await this.flushMetrics();
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.4.1" : "latest");
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 = { ...data, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
14570
- await plugin.processReplicatorEvent("update", resource.name, completeData.id, completeData, beforeData);
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
- const [initOk, initError] = await try_fn_default(async () => {
14603
- await this.initializeReplicators(database);
14604
- });
14605
- if (!initOk) {
14606
- if (this.config.verbose) {
14607
- console.warn(`[ReplicatorPlugin] Replicator initialization failed: ${initError.message}`);
14608
- }
14609
- this.emit("error", { operation: "setup", error: initError.message });
14610
- throw initError;
14611
- }
14612
- const [logOk, logError] = await try_fn_default(async () => {
14613
- if (this.config.replicatorLogResource) {
14614
- const logRes = await database.createResource({
14615
- name: this.config.replicatorLogResource,
14616
- behavior: "body-overflow",
14617
- attributes: {
14618
- operation: "string",
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.uploadMetadataFile(database);
14640
- const originalCreateResource = database.createResource.bind(database);
14641
- database.createResource = async (config) => {
14642
- const resource = await originalCreateResource(config);
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();