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.iife.js CHANGED
@@ -1,4 +1,4 @@
1
- var S3DB = (function (exports, nanoid, zlib, promisePool, web, promises, crypto, lodashEs, jsonStableStringify, clientS3, flat, FastestValidator) {
1
+ var S3DB = (function (exports, nanoid, zlib, promisePool, web, promises, crypto, lodashEs, jsonStableStringify, nodeHttpHandler, clientS3, flat, FastestValidator) {
2
2
  'use strict';
3
3
 
4
4
  const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
@@ -1258,7 +1258,6 @@ ${JSON.stringify(validation, null, 2)}`,
1258
1258
  includeData: options.includeData !== false,
1259
1259
  includePartitions: options.includePartitions !== false,
1260
1260
  maxDataSize: options.maxDataSize || 1e4,
1261
- // 10KB limit
1262
1261
  ...options
1263
1262
  };
1264
1263
  }
@@ -1279,327 +1278,193 @@ ${JSON.stringify(validation, null, 2)}`,
1279
1278
  metadata: "string|optional"
1280
1279
  },
1281
1280
  behavior: "body-overflow"
1282
- // keyPrefix removido
1283
1281
  }));
1284
1282
  this.auditResource = ok ? auditResource : this.database.resources.audits || null;
1285
1283
  if (!ok && !this.auditResource) return;
1286
- this.installDatabaseProxy();
1287
- this.installEventListeners();
1288
- }
1289
- async onStart() {
1290
- }
1291
- async onStop() {
1292
- }
1293
- installDatabaseProxy() {
1294
- if (this.database._auditProxyInstalled) {
1295
- return;
1296
- }
1297
- const installEventListenersForResource = this.installEventListenersForResource.bind(this);
1298
- this.database._originalCreateResource = this.database.createResource;
1299
- this.database.createResource = async function(...args) {
1300
- const resource = await this._originalCreateResource(...args);
1301
- if (resource.name !== "audits") {
1302
- installEventListenersForResource(resource);
1284
+ this.database.addHook("afterCreateResource", (context) => {
1285
+ if (context.resource.name !== "audits") {
1286
+ this.setupResourceAuditing(context.resource);
1303
1287
  }
1304
- return resource;
1305
- };
1306
- this.database._auditProxyInstalled = true;
1307
- }
1308
- installEventListeners() {
1288
+ });
1309
1289
  for (const resource of Object.values(this.database.resources)) {
1310
- if (resource.name === "audits") {
1311
- continue;
1290
+ if (resource.name !== "audits") {
1291
+ this.setupResourceAuditing(resource);
1312
1292
  }
1313
- this.installEventListenersForResource(resource);
1314
1293
  }
1315
1294
  }
1316
- installEventListenersForResource(resource) {
1295
+ async onStart() {
1296
+ }
1297
+ async onStop() {
1298
+ }
1299
+ setupResourceAuditing(resource) {
1317
1300
  resource.on("insert", async (data) => {
1318
- const recordId = data.id || "auto-generated";
1319
- const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
1320
- const auditRecord = {
1321
- id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
1301
+ await this.logAudit({
1322
1302
  resourceName: resource.name,
1323
1303
  operation: "insert",
1324
- recordId,
1325
- userId: this.getCurrentUserId?.() || "system",
1326
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1304
+ recordId: data.id || "auto-generated",
1327
1305
  oldData: null,
1328
- newData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(data)),
1329
- partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
1330
- partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
1331
- metadata: JSON.stringify({
1332
- source: "audit-plugin",
1333
- version: "2.0"
1334
- })
1335
- };
1336
- this.logAudit(auditRecord).catch(() => {
1306
+ newData: this.config.includeData ? JSON.stringify(this.truncateData(data)) : null,
1307
+ partition: this.config.includePartitions && this.getPartitionValues(data, resource) ? this.getPrimaryPartition(this.getPartitionValues(data, resource)) : null,
1308
+ partitionValues: this.config.includePartitions && this.getPartitionValues(data, resource) ? JSON.stringify(this.getPartitionValues(data, resource)) : null
1337
1309
  });
1338
1310
  });
1339
1311
  resource.on("update", async (data) => {
1340
- const recordId = data.id;
1341
1312
  let oldData = data.$before;
1342
1313
  if (this.config.includeData && !oldData) {
1343
- const [ok, err, fetched] = await try_fn_default(() => resource.get(recordId));
1314
+ const [ok, err, fetched] = await try_fn_default(() => resource.get(data.id));
1344
1315
  if (ok) oldData = fetched;
1345
1316
  }
1346
- const partitionValues = this.config.includePartitions ? this.getPartitionValues(data, resource) : null;
1347
- const auditRecord = {
1348
- id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
1317
+ await this.logAudit({
1349
1318
  resourceName: resource.name,
1350
1319
  operation: "update",
1351
- recordId,
1352
- userId: this.getCurrentUserId?.() || "system",
1353
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1354
- oldData: oldData && this.config.includeData === false ? null : oldData ? JSON.stringify(this.truncateData(oldData)) : null,
1355
- newData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(data)),
1356
- partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
1357
- partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
1358
- metadata: JSON.stringify({
1359
- source: "audit-plugin",
1360
- version: "2.0"
1361
- })
1362
- };
1363
- this.logAudit(auditRecord).catch(() => {
1320
+ recordId: data.id,
1321
+ oldData: oldData && this.config.includeData ? JSON.stringify(this.truncateData(oldData)) : null,
1322
+ newData: this.config.includeData ? JSON.stringify(this.truncateData(data)) : null,
1323
+ partition: this.config.includePartitions && this.getPartitionValues(data, resource) ? this.getPrimaryPartition(this.getPartitionValues(data, resource)) : null,
1324
+ partitionValues: this.config.includePartitions && this.getPartitionValues(data, resource) ? JSON.stringify(this.getPartitionValues(data, resource)) : null
1364
1325
  });
1365
1326
  });
1366
1327
  resource.on("delete", async (data) => {
1367
- const recordId = data.id;
1368
1328
  let oldData = data;
1369
1329
  if (this.config.includeData && !oldData) {
1370
- const [ok, err, fetched] = await try_fn_default(() => resource.get(recordId));
1330
+ const [ok, err, fetched] = await try_fn_default(() => resource.get(data.id));
1371
1331
  if (ok) oldData = fetched;
1372
1332
  }
1373
- const partitionValues = oldData && this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null;
1374
- const auditRecord = {
1375
- id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
1333
+ await this.logAudit({
1376
1334
  resourceName: resource.name,
1377
1335
  operation: "delete",
1378
- recordId,
1379
- userId: this.getCurrentUserId?.() || "system",
1380
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1381
- oldData: oldData && this.config.includeData === false ? null : oldData ? JSON.stringify(this.truncateData(oldData)) : null,
1336
+ recordId: data.id,
1337
+ oldData: oldData && this.config.includeData ? JSON.stringify(this.truncateData(oldData)) : null,
1382
1338
  newData: null,
1383
- partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
1384
- partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
1385
- metadata: JSON.stringify({
1386
- source: "audit-plugin",
1387
- version: "2.0"
1388
- })
1389
- };
1390
- this.logAudit(auditRecord).catch(() => {
1339
+ partition: oldData && this.config.includePartitions && this.getPartitionValues(oldData, resource) ? this.getPrimaryPartition(this.getPartitionValues(oldData, resource)) : null,
1340
+ partitionValues: oldData && this.config.includePartitions && this.getPartitionValues(oldData, resource) ? JSON.stringify(this.getPartitionValues(oldData, resource)) : null
1391
1341
  });
1392
1342
  });
1393
- resource.useMiddleware("deleteMany", async (ctx, next) => {
1394
- const ids = ctx.args[0];
1395
- const oldDataMap = {};
1396
- if (this.config.includeData) {
1397
- for (const id of ids) {
1398
- const [ok, err, data] = await try_fn_default(() => resource.get(id));
1399
- oldDataMap[id] = ok ? data : null;
1400
- }
1401
- }
1402
- const result = await next();
1403
- if (result && result.length > 0 && this.config.includeData) {
1404
- for (const id of ids) {
1405
- const oldData = oldDataMap[id];
1406
- const partitionValues = oldData ? this.config.includePartitions ? this.getPartitionValues(oldData, resource) : null : null;
1407
- const auditRecord = {
1408
- id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
1409
- resourceName: resource.name,
1410
- operation: "delete",
1411
- recordId: id,
1412
- userId: this.getCurrentUserId?.() || "system",
1413
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1414
- oldData: this.config.includeData === false ? null : JSON.stringify(this.truncateData(oldData)),
1415
- newData: null,
1416
- partition: this.config.includePartitions ? this.getPrimaryPartition(partitionValues) : null,
1417
- partitionValues: this.config.includePartitions ? partitionValues ? Object.keys(partitionValues).length > 0 ? JSON.stringify(partitionValues) : null : null : null,
1418
- metadata: JSON.stringify({
1419
- source: "audit-plugin",
1420
- version: "2.0",
1421
- batchOperation: true
1422
- })
1423
- };
1424
- this.logAudit(auditRecord).catch(() => {
1425
- });
1426
- }
1427
- }
1428
- return result;
1429
- });
1343
+ }
1344
+ // Backward compatibility for tests
1345
+ installEventListenersForResource(resource) {
1346
+ return this.setupResourceAuditing(resource);
1347
+ }
1348
+ async logAudit(auditData) {
1349
+ if (!this.auditResource) {
1350
+ return;
1351
+ }
1352
+ const auditRecord = {
1353
+ id: `audit-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
1354
+ userId: this.getCurrentUserId?.() || "system",
1355
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1356
+ metadata: JSON.stringify({ source: "audit-plugin", version: "2.0" }),
1357
+ resourceName: auditData.resourceName,
1358
+ operation: auditData.operation,
1359
+ recordId: auditData.recordId
1360
+ };
1361
+ if (auditData.oldData !== null) {
1362
+ auditRecord.oldData = auditData.oldData;
1363
+ }
1364
+ if (auditData.newData !== null) {
1365
+ auditRecord.newData = auditData.newData;
1366
+ }
1367
+ if (auditData.partition !== null) {
1368
+ auditRecord.partition = auditData.partition;
1369
+ }
1370
+ if (auditData.partitionValues !== null) {
1371
+ auditRecord.partitionValues = auditData.partitionValues;
1372
+ }
1373
+ try {
1374
+ await this.auditResource.insert(auditRecord);
1375
+ } catch (error) {
1376
+ console.warn("Audit logging failed:", error.message);
1377
+ }
1430
1378
  }
1431
1379
  getPartitionValues(data, resource) {
1432
- if (!data) return null;
1433
- const partitions = resource.config?.partitions || {};
1380
+ if (!this.config.includePartitions || !resource.partitions) return null;
1434
1381
  const partitionValues = {};
1435
- for (const [partitionName, partitionDef] of Object.entries(partitions)) {
1436
- if (partitionDef.fields) {
1437
- const partitionData = {};
1438
- for (const [fieldName, fieldRule] of Object.entries(partitionDef.fields)) {
1439
- const fieldValue = this.getNestedFieldValue(data, fieldName);
1440
- if (fieldValue !== void 0 && fieldValue !== null) {
1441
- partitionData[fieldName] = fieldValue;
1442
- }
1443
- }
1444
- if (Object.keys(partitionData).length > 0) {
1445
- partitionValues[partitionName] = partitionData;
1446
- }
1382
+ for (const [partitionName, partitionConfig] of Object.entries(resource.partitions)) {
1383
+ const values = {};
1384
+ for (const field of Object.keys(partitionConfig.fields)) {
1385
+ values[field] = this.getNestedFieldValue(data, field);
1386
+ }
1387
+ if (Object.values(values).some((v) => v !== void 0 && v !== null)) {
1388
+ partitionValues[partitionName] = values;
1447
1389
  }
1448
1390
  }
1449
- return partitionValues;
1391
+ return Object.keys(partitionValues).length > 0 ? partitionValues : null;
1450
1392
  }
1451
1393
  getNestedFieldValue(data, fieldPath) {
1452
- if (!fieldPath.includes(".")) {
1453
- return data[fieldPath];
1454
- }
1455
- const keys = fieldPath.split(".");
1456
- let currentLevel = data;
1457
- for (const key of keys) {
1458
- if (!currentLevel || typeof currentLevel !== "object" || !(key in currentLevel)) {
1394
+ const parts = fieldPath.split(".");
1395
+ let value = data;
1396
+ for (const part of parts) {
1397
+ if (value && typeof value === "object" && part in value) {
1398
+ value = value[part];
1399
+ } else {
1459
1400
  return void 0;
1460
1401
  }
1461
- currentLevel = currentLevel[key];
1462
1402
  }
1463
- return currentLevel;
1403
+ return value;
1464
1404
  }
1465
1405
  getPrimaryPartition(partitionValues) {
1466
1406
  if (!partitionValues) return null;
1467
1407
  const partitionNames = Object.keys(partitionValues);
1468
1408
  return partitionNames.length > 0 ? partitionNames[0] : null;
1469
1409
  }
1470
- async logAudit(auditRecord) {
1471
- if (!auditRecord.id) {
1472
- auditRecord.id = `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1473
- }
1474
- const result = await this.auditResource.insert(auditRecord);
1475
- return result;
1476
- }
1477
1410
  truncateData(data) {
1478
- if (!data) return data;
1479
- const filteredData = {};
1480
- for (const [key, value] of Object.entries(data)) {
1481
- if (!key.startsWith("_") && key !== "$overflow") {
1482
- filteredData[key] = value;
1483
- }
1484
- }
1485
- const dataStr = JSON.stringify(filteredData);
1411
+ if (!this.config.includeData) return null;
1412
+ const dataStr = JSON.stringify(data);
1486
1413
  if (dataStr.length <= this.config.maxDataSize) {
1487
- return filteredData;
1488
- }
1489
- let truncatedData = { ...filteredData };
1490
- let currentSize = JSON.stringify(truncatedData).length;
1491
- const metadataOverhead = JSON.stringify({
1492
- _truncated: true,
1493
- _originalSize: dataStr.length,
1494
- _truncatedAt: (/* @__PURE__ */ new Date()).toISOString()
1495
- }).length;
1496
- const targetSize = this.config.maxDataSize - metadataOverhead;
1497
- for (const [key, value] of Object.entries(truncatedData)) {
1498
- if (typeof value === "string" && currentSize > targetSize) {
1499
- const excess = currentSize - targetSize;
1500
- const newLength = Math.max(0, value.length - excess - 3);
1501
- if (newLength < value.length) {
1502
- truncatedData[key] = value.substring(0, newLength) + "...";
1503
- currentSize = JSON.stringify(truncatedData).length;
1504
- }
1505
- }
1414
+ return data;
1506
1415
  }
1507
1416
  return {
1508
- ...truncatedData,
1417
+ ...data,
1509
1418
  _truncated: true,
1510
1419
  _originalSize: dataStr.length,
1511
1420
  _truncatedAt: (/* @__PURE__ */ new Date()).toISOString()
1512
1421
  };
1513
1422
  }
1514
- // Utility methods for querying audit logs
1515
1423
  async getAuditLogs(options = {}) {
1516
1424
  if (!this.auditResource) return [];
1517
- const [ok, err, result] = await try_fn_default(async () => {
1518
- const {
1519
- resourceName,
1520
- operation,
1521
- recordId,
1522
- userId,
1523
- partition,
1524
- startDate,
1525
- endDate,
1526
- limit = 100,
1527
- offset = 0
1528
- } = options;
1529
- const allAudits = await this.auditResource.getAll();
1530
- let filtered = allAudits.filter((audit) => {
1531
- if (resourceName && audit.resourceName !== resourceName) return false;
1532
- if (operation && audit.operation !== operation) return false;
1533
- if (recordId && audit.recordId !== recordId) return false;
1534
- if (userId && audit.userId !== userId) return false;
1535
- if (partition && audit.partition !== partition) return false;
1536
- if (startDate && new Date(audit.timestamp) < new Date(startDate)) return false;
1537
- if (endDate && new Date(audit.timestamp) > new Date(endDate)) return false;
1538
- return true;
1539
- });
1540
- filtered.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
1541
- const deserialized = filtered.slice(offset, offset + limit).map((audit) => {
1542
- const [okOld, , oldData] = typeof audit.oldData === "string" ? tryFnSync(() => JSON.parse(audit.oldData)) : [true, null, audit.oldData];
1543
- const [okNew, , newData] = typeof audit.newData === "string" ? tryFnSync(() => JSON.parse(audit.newData)) : [true, null, audit.newData];
1544
- const [okPart, , partitionValues] = audit.partitionValues && typeof audit.partitionValues === "string" ? tryFnSync(() => JSON.parse(audit.partitionValues)) : [true, null, audit.partitionValues];
1545
- const [okMeta, , metadata] = audit.metadata && typeof audit.metadata === "string" ? tryFnSync(() => JSON.parse(audit.metadata)) : [true, null, audit.metadata];
1546
- return {
1547
- ...audit,
1548
- oldData: audit.oldData === null || audit.oldData === void 0 || audit.oldData === "null" ? null : okOld ? oldData : null,
1549
- newData: audit.newData === null || audit.newData === void 0 || audit.newData === "null" ? null : okNew ? newData : null,
1550
- partitionValues: okPart ? partitionValues : audit.partitionValues,
1551
- metadata: okMeta ? metadata : audit.metadata
1552
- };
1553
- });
1554
- return deserialized;
1555
- });
1556
- return ok ? result : [];
1425
+ const { resourceName, operation, recordId, partition, startDate, endDate, limit = 100 } = options;
1426
+ let query = {};
1427
+ if (resourceName) query.resourceName = resourceName;
1428
+ if (operation) query.operation = operation;
1429
+ if (recordId) query.recordId = recordId;
1430
+ if (partition) query.partition = partition;
1431
+ if (startDate || endDate) {
1432
+ query.timestamp = {};
1433
+ if (startDate) query.timestamp.$gte = startDate;
1434
+ if (endDate) query.timestamp.$lte = endDate;
1435
+ }
1436
+ const result = await this.auditResource.page({ query, limit });
1437
+ return result.items || [];
1557
1438
  }
1558
1439
  async getRecordHistory(resourceName, recordId) {
1559
- return this.getAuditLogs({
1560
- resourceName,
1561
- recordId,
1562
- limit: 1e3
1563
- });
1440
+ return await this.getAuditLogs({ resourceName, recordId });
1564
1441
  }
1565
1442
  async getPartitionHistory(resourceName, partitionName, partitionValues) {
1566
- return this.getAuditLogs({
1443
+ return await this.getAuditLogs({
1567
1444
  resourceName,
1568
1445
  partition: partitionName,
1569
- limit: 1e3
1446
+ partitionValues: JSON.stringify(partitionValues)
1570
1447
  });
1571
1448
  }
1572
1449
  async getAuditStats(options = {}) {
1573
- const {
1574
- resourceName,
1575
- startDate,
1576
- endDate
1577
- } = options;
1578
- const allAudits = await this.getAuditLogs({
1579
- resourceName,
1580
- startDate,
1581
- endDate,
1582
- limit: 1e4
1583
- });
1450
+ const logs = await this.getAuditLogs(options);
1584
1451
  const stats = {
1585
- total: allAudits.length,
1452
+ total: logs.length,
1586
1453
  byOperation: {},
1587
1454
  byResource: {},
1588
1455
  byPartition: {},
1589
1456
  byUser: {},
1590
1457
  timeline: {}
1591
1458
  };
1592
- for (const audit of allAudits) {
1593
- stats.byOperation[audit.operation] = (stats.byOperation[audit.operation] || 0) + 1;
1594
- stats.byResource[audit.resourceName] = (stats.byResource[audit.resourceName] || 0) + 1;
1595
- if (audit.partition) {
1596
- stats.byPartition[audit.partition] = (stats.byPartition[audit.partition] || 0) + 1;
1597
- }
1598
- stats.byUser[audit.userId] = (stats.byUser[audit.userId] || 0) + 1;
1599
- if (audit.timestamp) {
1600
- const day = audit.timestamp.split("T")[0];
1601
- stats.timeline[day] = (stats.timeline[day] || 0) + 1;
1459
+ for (const log of logs) {
1460
+ stats.byOperation[log.operation] = (stats.byOperation[log.operation] || 0) + 1;
1461
+ stats.byResource[log.resourceName] = (stats.byResource[log.resourceName] || 0) + 1;
1462
+ if (log.partition) {
1463
+ stats.byPartition[log.partition] = (stats.byPartition[log.partition] || 0) + 1;
1602
1464
  }
1465
+ stats.byUser[log.userId] = (stats.byUser[log.userId] || 0) + 1;
1466
+ const date = log.timestamp.split("T")[0];
1467
+ stats.timeline[date] = (stats.timeline[date] || 0) + 1;
1603
1468
  }
1604
1469
  return stats;
1605
1470
  }
@@ -7088,19 +6953,37 @@ ${JSON.stringify(validation, null, 2)}`,
7088
6953
  });
7089
6954
  for (const file of cacheFiles) {
7090
6955
  const filePath = path.join(this.directory, file);
7091
- if (await this._fileExists(filePath)) {
7092
- await promises.unlink(filePath);
6956
+ try {
6957
+ if (await this._fileExists(filePath)) {
6958
+ await promises.unlink(filePath);
6959
+ }
6960
+ } catch (error) {
6961
+ if (error.code !== "ENOENT") {
6962
+ throw error;
6963
+ }
7093
6964
  }
7094
6965
  if (this.enableMetadata) {
7095
- const metadataPath = this._getMetadataPath(filePath);
7096
- if (await this._fileExists(metadataPath)) {
7097
- await promises.unlink(metadataPath);
6966
+ try {
6967
+ const metadataPath = this._getMetadataPath(filePath);
6968
+ if (await this._fileExists(metadataPath)) {
6969
+ await promises.unlink(metadataPath);
6970
+ }
6971
+ } catch (error) {
6972
+ if (error.code !== "ENOENT") {
6973
+ throw error;
6974
+ }
7098
6975
  }
7099
6976
  }
7100
6977
  if (this.enableBackup) {
7101
- const backupPath = filePath + this.backupSuffix;
7102
- if (await this._fileExists(backupPath)) {
7103
- await promises.unlink(backupPath);
6978
+ try {
6979
+ const backupPath = filePath + this.backupSuffix;
6980
+ if (await this._fileExists(backupPath)) {
6981
+ await promises.unlink(backupPath);
6982
+ }
6983
+ } catch (error) {
6984
+ if (error.code !== "ENOENT") {
6985
+ throw error;
6986
+ }
7104
6987
  }
7105
6988
  }
7106
6989
  }
@@ -7112,6 +6995,12 @@ ${JSON.stringify(validation, null, 2)}`,
7112
6995
  }
7113
6996
  return true;
7114
6997
  } catch (error) {
6998
+ if (error.code === "ENOENT") {
6999
+ if (this.enableStats) {
7000
+ this.stats.clears++;
7001
+ }
7002
+ return true;
7003
+ }
7115
7004
  if (this.enableStats) {
7116
7005
  this.stats.errors++;
7117
7006
  }
@@ -7597,11 +7486,11 @@ ${JSON.stringify(validation, null, 2)}`,
7597
7486
  await super.setup(database);
7598
7487
  }
7599
7488
  async onSetup() {
7600
- if (this.config.driver) {
7489
+ if (this.config.driver && typeof this.config.driver === "object") {
7601
7490
  this.driver = this.config.driver;
7602
- } else if (this.config.driverType === "memory") {
7491
+ } else if (this.config.driver === "memory") {
7603
7492
  this.driver = new memory_cache_class_default(this.config.memoryOptions || {});
7604
- } else if (this.config.driverType === "filesystem") {
7493
+ } else if (this.config.driver === "filesystem") {
7605
7494
  if (this.config.partitionAware) {
7606
7495
  this.driver = new PartitionAwareFilesystemCache({
7607
7496
  partitionStrategy: this.config.partitionStrategy,
@@ -7615,26 +7504,22 @@ ${JSON.stringify(validation, null, 2)}`,
7615
7504
  } else {
7616
7505
  this.driver = new s3_cache_class_default({ client: this.database.client, ...this.config.s3Options || {} });
7617
7506
  }
7618
- this.installDatabaseProxy();
7507
+ this.installDatabaseHooks();
7619
7508
  this.installResourceHooks();
7620
7509
  }
7510
+ /**
7511
+ * Install database hooks to handle resource creation/updates
7512
+ */
7513
+ installDatabaseHooks() {
7514
+ this.database.addHook("afterCreateResource", async ({ resource }) => {
7515
+ this.installResourceHooksForResource(resource);
7516
+ });
7517
+ }
7621
7518
  async onStart() {
7622
7519
  }
7623
7520
  async onStop() {
7624
7521
  }
7625
- installDatabaseProxy() {
7626
- if (this.database._cacheProxyInstalled) {
7627
- return;
7628
- }
7629
- const installResourceHooks = this.installResourceHooks.bind(this);
7630
- this.database._originalCreateResourceForCache = this.database.createResource;
7631
- this.database.createResource = async function(...args) {
7632
- const resource = await this._originalCreateResourceForCache(...args);
7633
- installResourceHooks(resource);
7634
- return resource;
7635
- };
7636
- this.database._cacheProxyInstalled = true;
7637
- }
7522
+ // Remove the old installDatabaseProxy method
7638
7523
  installResourceHooks() {
7639
7524
  for (const resource of Object.values(this.database.resources)) {
7640
7525
  this.installResourceHooksForResource(resource);
@@ -7673,7 +7558,12 @@ ${JSON.stringify(validation, null, 2)}`,
7673
7558
  "getAll",
7674
7559
  "page",
7675
7560
  "list",
7676
- "get"
7561
+ "get",
7562
+ "exists",
7563
+ "content",
7564
+ "hasContent",
7565
+ "query",
7566
+ "getFromPartition"
7677
7567
  ];
7678
7568
  for (const method of cacheMethods) {
7679
7569
  resource.useMiddleware(method, async (ctx, next) => {
@@ -7686,9 +7576,26 @@ ${JSON.stringify(validation, null, 2)}`,
7686
7576
  } else if (method === "list" || method === "listIds" || method === "count") {
7687
7577
  const { partition, partitionValues } = ctx.args[0] || {};
7688
7578
  key = await resource.cacheKeyFor({ action: method, partition, partitionValues });
7579
+ } else if (method === "query") {
7580
+ const filter = ctx.args[0] || {};
7581
+ const options = ctx.args[1] || {};
7582
+ key = await resource.cacheKeyFor({
7583
+ action: method,
7584
+ params: { filter, options: { limit: options.limit, offset: options.offset } },
7585
+ partition: options.partition,
7586
+ partitionValues: options.partitionValues
7587
+ });
7588
+ } else if (method === "getFromPartition") {
7589
+ const { id, partitionName, partitionValues } = ctx.args[0] || {};
7590
+ key = await resource.cacheKeyFor({
7591
+ action: method,
7592
+ params: { id, partitionName },
7593
+ partition: partitionName,
7594
+ partitionValues
7595
+ });
7689
7596
  } else if (method === "getAll") {
7690
7597
  key = await resource.cacheKeyFor({ action: method });
7691
- } else if (method === "get") {
7598
+ } else if (["get", "exists", "content", "hasContent"].includes(method)) {
7692
7599
  key = await resource.cacheKeyFor({ action: method, params: { id: ctx.args[0] } });
7693
7600
  }
7694
7601
  if (this.driver instanceof PartitionAwareFilesystemCache) {
@@ -7697,6 +7604,14 @@ ${JSON.stringify(validation, null, 2)}`,
7697
7604
  const args = ctx.args[0] || {};
7698
7605
  partition = args.partition;
7699
7606
  partitionValues = args.partitionValues;
7607
+ } else if (method === "query") {
7608
+ const options = ctx.args[1] || {};
7609
+ partition = options.partition;
7610
+ partitionValues = options.partitionValues;
7611
+ } else if (method === "getFromPartition") {
7612
+ const { partitionName, partitionValues: pValues } = ctx.args[0] || {};
7613
+ partition = partitionName;
7614
+ partitionValues = pValues;
7700
7615
  }
7701
7616
  const [ok, err, result] = await try_fn_default(() => resource.cache._get(key, {
7702
7617
  resource: resource.name,
@@ -7724,7 +7639,7 @@ ${JSON.stringify(validation, null, 2)}`,
7724
7639
  }
7725
7640
  });
7726
7641
  }
7727
- const writeMethods = ["insert", "update", "delete", "deleteMany"];
7642
+ const writeMethods = ["insert", "update", "delete", "deleteMany", "setContent", "deleteContent", "replace"];
7728
7643
  for (const method of writeMethods) {
7729
7644
  resource.useMiddleware(method, async (ctx, next) => {
7730
7645
  const result = await next();
@@ -7739,6 +7654,12 @@ ${JSON.stringify(validation, null, 2)}`,
7739
7654
  if (ok && full) data = full;
7740
7655
  }
7741
7656
  await this.clearCacheForResource(resource, data);
7657
+ } else if (method === "setContent" || method === "deleteContent") {
7658
+ const id = ctx.args[0]?.id || ctx.args[0];
7659
+ await this.clearCacheForResource(resource, { id });
7660
+ } else if (method === "replace") {
7661
+ const id = ctx.args[0];
7662
+ await this.clearCacheForResource(resource, { id, ...ctx.args[1] });
7742
7663
  } else if (method === "deleteMany") {
7743
7664
  await this.clearCacheForResource(resource);
7744
7665
  }
@@ -7749,23 +7670,40 @@ ${JSON.stringify(validation, null, 2)}`,
7749
7670
  async clearCacheForResource(resource, data) {
7750
7671
  if (!resource.cache) return;
7751
7672
  const keyPrefix = `resource=${resource.name}`;
7752
- await resource.cache.clear(keyPrefix);
7753
- if (this.config.includePartitions === true && resource.config?.partitions && Object.keys(resource.config.partitions).length > 0) {
7754
- if (!data) {
7755
- for (const partitionName of Object.keys(resource.config.partitions)) {
7756
- const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
7757
- await resource.cache.clear(partitionKeyPrefix);
7673
+ if (data && data.id) {
7674
+ const itemSpecificMethods = ["get", "exists", "content", "hasContent"];
7675
+ for (const method of itemSpecificMethods) {
7676
+ try {
7677
+ const specificKey = await this.generateCacheKey(resource, method, { id: data.id });
7678
+ await resource.cache.clear(specificKey.replace(".json.gz", ""));
7679
+ } catch (error) {
7758
7680
  }
7759
- } else {
7681
+ }
7682
+ if (this.config.includePartitions === true && resource.config?.partitions && Object.keys(resource.config.partitions).length > 0) {
7760
7683
  const partitionValues = this.getPartitionValues(data, resource);
7761
7684
  for (const [partitionName, values] of Object.entries(partitionValues)) {
7762
7685
  if (values && Object.keys(values).length > 0 && Object.values(values).some((v) => v !== null && v !== void 0)) {
7763
- const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
7764
- await resource.cache.clear(partitionKeyPrefix);
7686
+ try {
7687
+ const partitionKeyPrefix = join(keyPrefix, `partition=${partitionName}`);
7688
+ await resource.cache.clear(partitionKeyPrefix);
7689
+ } catch (error) {
7690
+ }
7765
7691
  }
7766
7692
  }
7767
7693
  }
7768
7694
  }
7695
+ try {
7696
+ await resource.cache.clear(keyPrefix);
7697
+ } catch (error) {
7698
+ const aggregateMethods = ["count", "list", "listIds", "getAll", "page", "query"];
7699
+ for (const method of aggregateMethods) {
7700
+ try {
7701
+ await resource.cache.clear(`${keyPrefix}/action=${method}`);
7702
+ await resource.cache.clear(`resource=${resource.name}/action=${method}`);
7703
+ } catch (methodError) {
7704
+ }
7705
+ }
7706
+ }
7769
7707
  }
7770
7708
  async generateCacheKey(resource, action, params = {}, partition = null, partitionValues = null) {
7771
7709
  const keyParts = [
@@ -7787,7 +7725,7 @@ ${JSON.stringify(validation, null, 2)}`,
7787
7725
  return join(...keyParts) + ".json.gz";
7788
7726
  }
7789
7727
  async hashParams(params) {
7790
- const sortedParams = Object.keys(params).sort().map((key) => `${key}:${params[key]}`).join("|") || "empty";
7728
+ const sortedParams = Object.keys(params).sort().map((key) => `${key}:${JSON.stringify(params[key])}`).join("|") || "empty";
7791
7729
  return await sha256(sortedParams);
7792
7730
  }
7793
7731
  // Utility methods
@@ -7992,12 +7930,14 @@ ${JSON.stringify(validation, null, 2)}`,
7992
7930
  }));
7993
7931
  this.indexResource = ok ? indexResource : database.resources.fulltext_indexes;
7994
7932
  await this.loadIndexes();
7933
+ this.installDatabaseHooks();
7995
7934
  this.installIndexingHooks();
7996
7935
  }
7997
7936
  async start() {
7998
7937
  }
7999
7938
  async stop() {
8000
7939
  await this.saveIndexes();
7940
+ this.removeDatabaseHooks();
8001
7941
  }
8002
7942
  async loadIndexes() {
8003
7943
  if (!this.indexResource) return;
@@ -8033,6 +7973,16 @@ ${JSON.stringify(validation, null, 2)}`,
8033
7973
  }
8034
7974
  });
8035
7975
  }
7976
+ installDatabaseHooks() {
7977
+ this.database.addHook("afterCreateResource", (resource) => {
7978
+ if (resource.name !== "fulltext_indexes") {
7979
+ this.installResourceHooks(resource);
7980
+ }
7981
+ });
7982
+ }
7983
+ removeDatabaseHooks() {
7984
+ this.database.removeHook("afterCreateResource", this.installResourceHooks.bind(this));
7985
+ }
8036
7986
  installIndexingHooks() {
8037
7987
  if (!this.database.plugins) {
8038
7988
  this.database.plugins = {};
@@ -8395,6 +8345,7 @@ ${JSON.stringify(validation, null, 2)}`,
8395
8345
  this.errorsResource = database.resources.error_logs;
8396
8346
  this.performanceResource = database.resources.performance_logs;
8397
8347
  }
8348
+ this.installDatabaseHooks();
8398
8349
  this.installMetricsHooks();
8399
8350
  if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
8400
8351
  this.startFlushTimer();
@@ -8407,9 +8358,17 @@ ${JSON.stringify(validation, null, 2)}`,
8407
8358
  clearInterval(this.flushTimer);
8408
8359
  this.flushTimer = null;
8409
8360
  }
8410
- if (typeof process !== "undefined" && process.env.NODE_ENV !== "test") {
8411
- await this.flushMetrics();
8412
- }
8361
+ this.removeDatabaseHooks();
8362
+ }
8363
+ installDatabaseHooks() {
8364
+ this.database.addHook("afterCreateResource", (resource) => {
8365
+ if (resource.name !== "metrics" && resource.name !== "error_logs" && resource.name !== "performance_logs") {
8366
+ this.installResourceHooks(resource);
8367
+ }
8368
+ });
8369
+ }
8370
+ removeDatabaseHooks() {
8371
+ this.database.removeHook("afterCreateResource", this.installResourceHooks.bind(this));
8413
8372
  }
8414
8373
  installMetricsHooks() {
8415
8374
  for (const resource of Object.values(this.database.resources)) {
@@ -9510,6 +9469,72 @@ ${JSON.stringify(validation, null, 2)}`,
9510
9469
  }
9511
9470
  var postgres_replicator_class_default = PostgresReplicator;
9512
9471
 
9472
+ /*
9473
+ this and http-lib folder
9474
+
9475
+ The MIT License
9476
+
9477
+ Copyright (c) 2015 John Hiesey
9478
+
9479
+ Permission is hereby granted, free of charge,
9480
+ to any person obtaining a copy of this software and
9481
+ associated documentation files (the "Software"), to
9482
+ deal in the Software without restriction, including
9483
+ without limitation the rights to use, copy, modify,
9484
+ merge, publish, distribute, sublicense, and/or sell
9485
+ copies of the Software, and to permit persons to whom
9486
+ the Software is furnished to do so,
9487
+ subject to the following conditions:
9488
+
9489
+ The above copyright notice and this permission notice
9490
+ shall be included in all copies or substantial portions of the Software.
9491
+
9492
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
9493
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
9494
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
9495
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
9496
+ ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
9497
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
9498
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9499
+
9500
+ */
9501
+
9502
+ function Agent$1() {}
9503
+ Agent$1.defaultMaxSockets = 4;
9504
+
9505
+ /*
9506
+ this and http-lib folder
9507
+
9508
+ The MIT License
9509
+
9510
+ Copyright (c) 2015 John Hiesey
9511
+
9512
+ Permission is hereby granted, free of charge,
9513
+ to any person obtaining a copy of this software and
9514
+ associated documentation files (the "Software"), to
9515
+ deal in the Software without restriction, including
9516
+ without limitation the rights to use, copy, modify,
9517
+ merge, publish, distribute, sublicense, and/or sell
9518
+ copies of the Software, and to permit persons to whom
9519
+ the Software is furnished to do so,
9520
+ subject to the following conditions:
9521
+
9522
+ The above copyright notice and this permission notice
9523
+ shall be included in all copies or substantial portions of the Software.
9524
+
9525
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
9526
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
9527
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
9528
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
9529
+ ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
9530
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
9531
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9532
+
9533
+ */
9534
+
9535
+ function Agent() {}
9536
+ Agent.defaultMaxSockets = 4;
9537
+
9513
9538
  const S3_DEFAULT_REGION = "us-east-1";
9514
9539
  const S3_DEFAULT_ENDPOINT = "https://s3.us-east-1.amazonaws.com";
9515
9540
  class ConnectionString {
@@ -9577,19 +9602,38 @@ ${JSON.stringify(validation, null, 2)}`,
9577
9602
  id = null,
9578
9603
  AwsS3Client,
9579
9604
  connectionString,
9580
- parallelism = 10
9605
+ parallelism = 10,
9606
+ httpClientOptions = {}
9581
9607
  }) {
9582
9608
  super();
9583
9609
  this.verbose = verbose;
9584
9610
  this.id = id ?? idGenerator();
9585
9611
  this.parallelism = parallelism;
9586
9612
  this.config = new ConnectionString(connectionString);
9613
+ this.httpClientOptions = {
9614
+ keepAlive: false,
9615
+ // Disabled for maximum creation speed
9616
+ maxSockets: 10,
9617
+ // Minimal sockets
9618
+ maxFreeSockets: 2,
9619
+ // Minimal pool
9620
+ timeout: 15e3,
9621
+ // Short timeout
9622
+ ...httpClientOptions
9623
+ };
9587
9624
  this.client = AwsS3Client || this.createClient();
9588
9625
  }
9589
9626
  createClient() {
9627
+ const httpAgent = new Agent$1(this.httpClientOptions);
9628
+ const httpsAgent = new Agent(this.httpClientOptions);
9629
+ const httpHandler = new nodeHttpHandler.NodeHttpHandler({
9630
+ httpAgent,
9631
+ httpsAgent
9632
+ });
9590
9633
  let options = {
9591
9634
  region: this.config.region,
9592
- endpoint: this.config.endpoint
9635
+ endpoint: this.config.endpoint,
9636
+ requestHandler: httpHandler
9593
9637
  };
9594
9638
  if (this.config.forcePathStyle) options.forcePathStyle = true;
9595
9639
  if (this.config.accessKeyId) {
@@ -13138,7 +13182,14 @@ ${JSON.stringify(validation, null, 2)}`,
13138
13182
  "delete",
13139
13183
  "deleteMany",
13140
13184
  "exists",
13141
- "getMany"
13185
+ "getMany",
13186
+ "content",
13187
+ "hasContent",
13188
+ "query",
13189
+ "getFromPartition",
13190
+ "setContent",
13191
+ "deleteContent",
13192
+ "replace"
13142
13193
  ];
13143
13194
  for (const method of this._middlewareMethods) {
13144
13195
  this._middlewares.set(method, []);
@@ -13317,7 +13368,7 @@ ${JSON.stringify(validation, null, 2)}`,
13317
13368
  super();
13318
13369
  this.version = "1";
13319
13370
  this.s3dbVersion = (() => {
13320
- const [ok, err, version] = try_fn_default(() => true ? "7.4.1" : "latest");
13371
+ const [ok, err, version] = try_fn_default(() => true ? "7.5.0" : "latest");
13321
13372
  return ok ? version : "latest";
13322
13373
  })();
13323
13374
  this.resources = {};
@@ -13330,6 +13381,7 @@ ${JSON.stringify(validation, null, 2)}`,
13330
13381
  this.cache = options.cache;
13331
13382
  this.passphrase = options.passphrase || "secret";
13332
13383
  this.versioningEnabled = options.versioningEnabled || false;
13384
+ this._initHooks();
13333
13385
  let connectionString = options.connectionString;
13334
13386
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
13335
13387
  const { bucket, region, accessKeyId, secretAccessKey, endpoint, forcePathStyle } = options;
@@ -13807,6 +13859,131 @@ ${JSON.stringify(validation, null, 2)}`,
13807
13859
  } catch (err) {
13808
13860
  }
13809
13861
  }
13862
+ /**
13863
+ * Initialize hooks system for database operations
13864
+ * @private
13865
+ */
13866
+ _initHooks() {
13867
+ this._hooks = /* @__PURE__ */ new Map();
13868
+ this._hookEvents = [
13869
+ "beforeConnect",
13870
+ "afterConnect",
13871
+ "beforeCreateResource",
13872
+ "afterCreateResource",
13873
+ "beforeUploadMetadata",
13874
+ "afterUploadMetadata",
13875
+ "beforeDisconnect",
13876
+ "afterDisconnect",
13877
+ "resourceCreated",
13878
+ "resourceUpdated"
13879
+ ];
13880
+ for (const event of this._hookEvents) {
13881
+ this._hooks.set(event, []);
13882
+ }
13883
+ this._wrapHookableMethods();
13884
+ }
13885
+ /**
13886
+ * Wrap methods that can have hooks
13887
+ * @private
13888
+ */
13889
+ _wrapHookableMethods() {
13890
+ if (this._hooksInstalled) return;
13891
+ this._originalConnect = this.connect.bind(this);
13892
+ this._originalCreateResource = this.createResource.bind(this);
13893
+ this._originalUploadMetadataFile = this.uploadMetadataFile.bind(this);
13894
+ this._originalDisconnect = this.disconnect.bind(this);
13895
+ this.connect = async (...args) => {
13896
+ await this._executeHooks("beforeConnect", { args });
13897
+ const result = await this._originalConnect(...args);
13898
+ await this._executeHooks("afterConnect", { result, args });
13899
+ return result;
13900
+ };
13901
+ this.createResource = async (config) => {
13902
+ await this._executeHooks("beforeCreateResource", { config });
13903
+ const resource = await this._originalCreateResource(config);
13904
+ await this._executeHooks("afterCreateResource", { resource, config });
13905
+ return resource;
13906
+ };
13907
+ this.uploadMetadataFile = async (...args) => {
13908
+ await this._executeHooks("beforeUploadMetadata", { args });
13909
+ const result = await this._originalUploadMetadataFile(...args);
13910
+ await this._executeHooks("afterUploadMetadata", { result, args });
13911
+ return result;
13912
+ };
13913
+ this.disconnect = async (...args) => {
13914
+ await this._executeHooks("beforeDisconnect", { args });
13915
+ const result = await this._originalDisconnect(...args);
13916
+ await this._executeHooks("afterDisconnect", { result, args });
13917
+ return result;
13918
+ };
13919
+ this._hooksInstalled = true;
13920
+ }
13921
+ /**
13922
+ * Add a hook for a specific database event
13923
+ * @param {string} event - Hook event name
13924
+ * @param {Function} fn - Hook function
13925
+ * @example
13926
+ * database.addHook('afterCreateResource', async ({ resource }) => {
13927
+ * console.log('Resource created:', resource.name);
13928
+ * });
13929
+ */
13930
+ addHook(event, fn) {
13931
+ if (!this._hooks) this._initHooks();
13932
+ if (!this._hooks.has(event)) {
13933
+ throw new Error(`Unknown hook event: ${event}. Available events: ${this._hookEvents.join(", ")}`);
13934
+ }
13935
+ if (typeof fn !== "function") {
13936
+ throw new Error("Hook function must be a function");
13937
+ }
13938
+ this._hooks.get(event).push(fn);
13939
+ }
13940
+ /**
13941
+ * Execute hooks for a specific event
13942
+ * @param {string} event - Hook event name
13943
+ * @param {Object} context - Context data to pass to hooks
13944
+ * @private
13945
+ */
13946
+ async _executeHooks(event, context = {}) {
13947
+ if (!this._hooks || !this._hooks.has(event)) return;
13948
+ const hooks = this._hooks.get(event);
13949
+ for (const hook of hooks) {
13950
+ try {
13951
+ await hook({ database: this, ...context });
13952
+ } catch (error) {
13953
+ this.emit("hookError", { event, error, context });
13954
+ }
13955
+ }
13956
+ }
13957
+ /**
13958
+ * Remove a hook for a specific event
13959
+ * @param {string} event - Hook event name
13960
+ * @param {Function} fn - Hook function to remove
13961
+ */
13962
+ removeHook(event, fn) {
13963
+ if (!this._hooks || !this._hooks.has(event)) return;
13964
+ const hooks = this._hooks.get(event);
13965
+ const index = hooks.indexOf(fn);
13966
+ if (index > -1) {
13967
+ hooks.splice(index, 1);
13968
+ }
13969
+ }
13970
+ /**
13971
+ * Get all hooks for a specific event
13972
+ * @param {string} event - Hook event name
13973
+ * @returns {Function[]} Array of hook functions
13974
+ */
13975
+ getHooks(event) {
13976
+ if (!this._hooks || !this._hooks.has(event)) return [];
13977
+ return [...this._hooks.get(event)];
13978
+ }
13979
+ /**
13980
+ * Clear all hooks for a specific event
13981
+ * @param {string} event - Hook event name
13982
+ */
13983
+ clearHooks(event) {
13984
+ if (!this._hooks || !this._hooks.has(event)) return;
13985
+ this._hooks.get(event).length = 0;
13986
+ }
13810
13987
  }
13811
13988
  class S3db extends Database {
13812
13989
  }
@@ -14539,6 +14716,10 @@ ${JSON.stringify(validation, null, 2)}`,
14539
14716
  }
14540
14717
  return filtered;
14541
14718
  }
14719
+ async getCompleteData(resource, data) {
14720
+ const [ok, err, completeRecord] = await try_fn_default(() => resource.get(data.id));
14721
+ return ok ? completeRecord : data;
14722
+ }
14542
14723
  installEventListeners(resource, database, plugin) {
14543
14724
  if (!resource || this.eventListenersInstalled.has(resource.name) || resource.name === this.config.replicatorLogResource) {
14544
14725
  return;
@@ -14557,8 +14738,9 @@ ${JSON.stringify(validation, null, 2)}`,
14557
14738
  });
14558
14739
  resource.on("update", async (data, beforeData) => {
14559
14740
  const [ok, error] = await try_fn_default(async () => {
14560
- const completeData = { ...data, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
14561
- await plugin.processReplicatorEvent("update", resource.name, completeData.id, completeData, beforeData);
14741
+ const completeData = await plugin.getCompleteData(resource, data);
14742
+ const dataWithTimestamp = { ...completeData, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
14743
+ await plugin.processReplicatorEvent("update", resource.name, completeData.id, dataWithTimestamp, beforeData);
14562
14744
  });
14563
14745
  if (!ok) {
14564
14746
  if (this.config.verbose) {
@@ -14580,67 +14762,55 @@ ${JSON.stringify(validation, null, 2)}`,
14580
14762
  });
14581
14763
  this.eventListenersInstalled.add(resource.name);
14582
14764
  }
14583
- /**
14584
- * Get complete data by always fetching the full record from the resource
14585
- * This ensures we always have the complete data regardless of behavior or data size
14586
- */
14587
- async getCompleteData(resource, data) {
14588
- const [ok, err, completeRecord] = await try_fn_default(() => resource.get(data.id));
14589
- return ok ? completeRecord : data;
14590
- }
14591
14765
  async setup(database) {
14592
14766
  this.database = database;
14593
- const [initOk, initError] = await try_fn_default(async () => {
14594
- await this.initializeReplicators(database);
14595
- });
14596
- if (!initOk) {
14597
- if (this.config.verbose) {
14598
- console.warn(`[ReplicatorPlugin] Replicator initialization failed: ${initError.message}`);
14599
- }
14600
- this.emit("error", { operation: "setup", error: initError.message });
14601
- throw initError;
14602
- }
14603
- const [logOk, logError] = await try_fn_default(async () => {
14604
- if (this.config.replicatorLogResource) {
14605
- const logRes = await database.createResource({
14606
- name: this.config.replicatorLogResource,
14607
- behavior: "body-overflow",
14608
- attributes: {
14609
- operation: "string",
14610
- resourceName: "string",
14611
- recordId: "string",
14612
- data: "string",
14613
- error: "string|optional",
14614
- replicator: "string",
14615
- timestamp: "string",
14616
- status: "string"
14617
- }
14618
- });
14619
- }
14620
- });
14621
- if (!logOk) {
14622
- if (this.config.verbose) {
14623
- console.warn(`[ReplicatorPlugin] Failed to create log resource ${this.config.replicatorLogResource}: ${logError.message}`);
14767
+ if (this.config.persistReplicatorLog) {
14768
+ const [ok, err, logResource] = await try_fn_default(() => database.createResource({
14769
+ name: this.config.replicatorLogResource || "replicator_logs",
14770
+ attributes: {
14771
+ id: "string|required",
14772
+ resource: "string|required",
14773
+ action: "string|required",
14774
+ data: "json",
14775
+ timestamp: "number|required",
14776
+ createdAt: "string|required"
14777
+ },
14778
+ behavior: "truncate-data"
14779
+ }));
14780
+ if (ok) {
14781
+ this.replicatorLogResource = logResource;
14782
+ } else {
14783
+ this.replicatorLogResource = database.resources[this.config.replicatorLogResource || "replicator_logs"];
14624
14784
  }
14625
- this.emit("replicator_log_resource_creation_error", {
14626
- resourceName: this.config.replicatorLogResource,
14627
- error: logError.message
14628
- });
14629
14785
  }
14630
- await this.uploadMetadataFile(database);
14631
- const originalCreateResource = database.createResource.bind(database);
14632
- database.createResource = async (config) => {
14633
- const resource = await originalCreateResource(config);
14634
- if (resource) {
14786
+ await this.initializeReplicators(database);
14787
+ this.installDatabaseHooks();
14788
+ for (const resource of Object.values(database.resources)) {
14789
+ if (resource.name !== (this.config.replicatorLogResource || "replicator_logs")) {
14635
14790
  this.installEventListeners(resource, database, this);
14636
14791
  }
14637
- return resource;
14638
- };
14639
- for (const resourceName in database.resources) {
14640
- const resource = database.resources[resourceName];
14641
- this.installEventListeners(resource, database, this);
14642
14792
  }
14643
14793
  }
14794
+ async start() {
14795
+ }
14796
+ async stop() {
14797
+ for (const replicator of this.replicators || []) {
14798
+ if (replicator && typeof replicator.cleanup === "function") {
14799
+ await replicator.cleanup();
14800
+ }
14801
+ }
14802
+ this.removeDatabaseHooks();
14803
+ }
14804
+ installDatabaseHooks() {
14805
+ this.database.addHook("afterCreateResource", (resource) => {
14806
+ if (resource.name !== (this.config.replicatorLogResource || "replicator_logs")) {
14807
+ this.installEventListeners(resource, this.database, this);
14808
+ }
14809
+ });
14810
+ }
14811
+ removeDatabaseHooks() {
14812
+ this.database.removeHook("afterCreateResource", this.installEventListeners.bind(this));
14813
+ }
14644
14814
  createReplicator(driver, config, resources, client) {
14645
14815
  return createReplicator(driver, config, resources, client);
14646
14816
  }
@@ -14656,10 +14826,6 @@ ${JSON.stringify(validation, null, 2)}`,
14656
14826
  }
14657
14827
  }
14658
14828
  }
14659
- async start() {
14660
- }
14661
- async stop() {
14662
- }
14663
14829
  async uploadMetadataFile(database) {
14664
14830
  if (typeof database.uploadMetadataFile === "function") {
14665
14831
  await database.uploadMetadataFile();
@@ -15062,4 +15228,4 @@ ${JSON.stringify(validation, null, 2)}`,
15062
15228
 
15063
15229
  return exports;
15064
15230
 
15065
- })({}, nanoid, zlib, PromisePool, streams, promises, crypto, _, stringify, AWS, flat, FastestValidator);
15231
+ })({}, nanoid, zlib, PromisePool, streams, promises, crypto, _, stringify, nodeHttpHandler, AWS, flat, FastestValidator);