s3db.js 7.3.10 → 7.4.1

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/README.md CHANGED
@@ -109,6 +109,7 @@
109
109
  - [🗂️ Advanced Partitioning](#️-advanced-partitioning)
110
110
  - [🎣 Advanced Hooks System](#-advanced-hooks-system)
111
111
  - [🧩 Resource Middlewares](#-resource-middlewares)
112
+ - [🎧 Event Listeners Configuration](#-event-listeners-configuration)
112
113
  - [📖 API Reference](#-api-reference)
113
114
 
114
115
  ---
@@ -1398,6 +1399,169 @@ await users.insert({ name: 'john', email: 'john@example.com' });
1398
1399
 
1399
1400
  #### **Best Practices**
1400
1401
  - Hooks are lightweight and ideal for observing or reacting to events.
1402
+
1403
+ ---
1404
+
1405
+ ### 🎧 Event Listeners Configuration
1406
+
1407
+ s3db.js resources extend Node.js EventEmitter, providing a powerful event system for real-time monitoring and notifications. You can configure event listeners in **two ways**: programmatically using `.on()` or declaratively in the resource configuration.
1408
+
1409
+ #### **Programmatic Event Listeners**
1410
+ Traditional EventEmitter pattern using `.on()`, `.once()`, or `.off()`:
1411
+
1412
+ ```javascript
1413
+ const users = await s3db.createResource({
1414
+ name: "users",
1415
+ attributes: {
1416
+ name: "string|required",
1417
+ email: "string|required"
1418
+ }
1419
+ });
1420
+
1421
+ // Single event listener
1422
+ users.on('insert', (event) => {
1423
+ console.log('User created:', event.name);
1424
+ });
1425
+
1426
+ // Multiple listeners for the same event
1427
+ users.on('update', (event) => {
1428
+ console.log('Update detected:', event.id);
1429
+ });
1430
+
1431
+ users.on('update', (event) => {
1432
+ if (event.$before.email !== event.$after.email) {
1433
+ console.log('Email changed!');
1434
+ }
1435
+ });
1436
+ ```
1437
+
1438
+ #### **Declarative Event Listeners**
1439
+ Configure event listeners directly in the resource configuration for cleaner, more maintainable code:
1440
+
1441
+ ```javascript
1442
+ const users = await s3db.createResource({
1443
+ name: "users",
1444
+ attributes: {
1445
+ name: "string|required",
1446
+ email: "string|required"
1447
+ },
1448
+ events: {
1449
+ // Single event listener
1450
+ insert: (event) => {
1451
+ console.log('📝 User created:', {
1452
+ id: event.id,
1453
+ name: event.name,
1454
+ timestamp: new Date().toISOString()
1455
+ });
1456
+ },
1457
+
1458
+ // Multiple event listeners (array)
1459
+ update: [
1460
+ (event) => {
1461
+ console.log('⚠️ Update detected for user:', event.id);
1462
+ },
1463
+ (event) => {
1464
+ const changes = [];
1465
+ if (event.$before.name !== event.$after.name) {
1466
+ changes.push(`name: ${event.$before.name} → ${event.$after.name}`);
1467
+ }
1468
+ if (event.$before.email !== event.$after.email) {
1469
+ changes.push(`email: ${event.$before.email} → ${event.$after.email}`);
1470
+ }
1471
+ if (changes.length > 0) {
1472
+ console.log('📝 Changes:', changes.join(', '));
1473
+ }
1474
+ }
1475
+ ],
1476
+
1477
+ // Bulk operation listeners
1478
+ deleteMany: (count) => {
1479
+ console.log(`🗑️ Bulk delete: ${count} users deleted`);
1480
+ },
1481
+
1482
+ // Performance and monitoring
1483
+ list: (result) => {
1484
+ console.log(`📋 Listed ${result.count} users, ${result.errors} errors`);
1485
+ }
1486
+ }
1487
+ });
1488
+ ```
1489
+
1490
+ #### **Available Events**
1491
+
1492
+ | Event | Description | Data Passed |
1493
+ |-------|-------------|-------------|
1494
+ | `insert` | Single record inserted | Complete object with all fields |
1495
+ | `update` | Single record updated | Object with `$before` and `$after` states |
1496
+ | `delete` | Single record deleted | Object data before deletion |
1497
+ | `insertMany` | Bulk insert completed | Number of records inserted |
1498
+ | `deleteMany` | Bulk delete completed | Number of records deleted |
1499
+ | `list` | List operation completed | Result object with count and errors |
1500
+ | `count` | Count operation completed | Total count number |
1501
+ | `get` | Single record retrieved | Complete object data |
1502
+ | `getMany` | Multiple records retrieved | Count of records |
1503
+
1504
+ #### **Event Data Structure**
1505
+
1506
+ **Insert/Get Events:**
1507
+ ```javascript
1508
+ {
1509
+ id: 'user-123',
1510
+ name: 'John Doe',
1511
+ email: 'john@example.com',
1512
+ createdAt: '2023-12-01T10:00:00.000Z',
1513
+ // ... all other fields
1514
+ }
1515
+ ```
1516
+
1517
+ **Update Events:**
1518
+ ```javascript
1519
+ {
1520
+ id: 'user-123',
1521
+ name: 'John Updated',
1522
+ email: 'john.new@example.com',
1523
+ $before: {
1524
+ name: 'John Doe',
1525
+ email: 'john@example.com',
1526
+ // ... previous state
1527
+ },
1528
+ $after: {
1529
+ name: 'John Updated',
1530
+ email: 'john.new@example.com',
1531
+ // ... current state
1532
+ }
1533
+ }
1534
+ ```
1535
+
1536
+ #### **Combining Both Approaches**
1537
+ You can use both declarative and programmatic event listeners together:
1538
+
1539
+ ```javascript
1540
+ const users = await s3db.createResource({
1541
+ name: "users",
1542
+ attributes: { name: "string|required" },
1543
+ events: {
1544
+ insert: (event) => console.log('Config listener:', event.name)
1545
+ }
1546
+ });
1547
+
1548
+ // Add additional programmatic listeners
1549
+ users.on('insert', (event) => {
1550
+ console.log('Programmatic listener:', event.name);
1551
+ });
1552
+
1553
+ await users.insert({ name: 'John' });
1554
+ // Output:
1555
+ // Config listener: John
1556
+ // Programmatic listener: John
1557
+ ```
1558
+
1559
+ #### **Best Practices for Event Listeners**
1560
+ - **Declarative for core functionality**: Use the `events` config for essential listeners
1561
+ - **Programmatic for conditional/dynamic**: Use `.on()` for listeners that might change at runtime
1562
+ - **Error handling**: Listeners should handle their own errors to avoid breaking operations
1563
+ - **Performance**: Keep listeners lightweight; use async operations sparingly
1564
+ - **Debugging**: Event listeners are excellent for debugging and monitoring
1401
1565
  - Middlewares are powerful and ideal for controlling or transforming operations.
1402
1566
  - You can safely combine both for maximum flexibility.
1403
1567
 
package/dist/s3db.cjs.js CHANGED
@@ -9654,7 +9654,13 @@ class Client extends EventEmitter {
9654
9654
  if (metadata) {
9655
9655
  for (const [k, v] of Object.entries(metadata)) {
9656
9656
  const validKey = String(k).replace(/[^a-zA-Z0-9\-_]/g, "_");
9657
- stringMetadata[validKey] = String(v);
9657
+ const stringValue = String(v);
9658
+ const hasSpecialChars = /[^\x00-\x7F]/.test(stringValue);
9659
+ if (hasSpecialChars) {
9660
+ stringMetadata[validKey] = Buffer.from(stringValue, "utf8").toString("base64");
9661
+ } else {
9662
+ stringMetadata[validKey] = stringValue;
9663
+ }
9658
9664
  }
9659
9665
  }
9660
9666
  const options = {
@@ -9691,6 +9697,28 @@ class Client extends EventEmitter {
9691
9697
  let response, error;
9692
9698
  try {
9693
9699
  response = await this.sendCommand(new clientS3.GetObjectCommand(options));
9700
+ if (response.Metadata) {
9701
+ const decodedMetadata = {};
9702
+ for (const [key2, value] of Object.entries(response.Metadata)) {
9703
+ if (typeof value === "string") {
9704
+ try {
9705
+ const decoded = Buffer.from(value, "base64").toString("utf8");
9706
+ const hasSpecialChars = /[^\x00-\x7F]/.test(decoded);
9707
+ const isValidBase64 = Buffer.from(decoded, "utf8").toString("base64") === value;
9708
+ if (isValidBase64 && hasSpecialChars && decoded !== value) {
9709
+ decodedMetadata[key2] = decoded;
9710
+ } else {
9711
+ decodedMetadata[key2] = value;
9712
+ }
9713
+ } catch (decodeError) {
9714
+ decodedMetadata[key2] = value;
9715
+ }
9716
+ } else {
9717
+ decodedMetadata[key2] = value;
9718
+ }
9719
+ }
9720
+ response.Metadata = decodedMetadata;
9721
+ }
9694
9722
  return response;
9695
9723
  } catch (err) {
9696
9724
  error = err;
@@ -11074,6 +11102,7 @@ class Resource extends EventEmitter {
11074
11102
  * @param {Function} [config.idGenerator] - Custom ID generator function
11075
11103
  * @param {number} [config.idSize=22] - Size for auto-generated IDs
11076
11104
  * @param {boolean} [config.versioningEnabled=false] - Enable versioning for this resource
11105
+ * @param {Object} [config.events={}] - Event listeners to automatically add
11077
11106
  * @example
11078
11107
  * const users = new Resource({
11079
11108
  * name: 'users',
@@ -11095,6 +11124,14 @@ class Resource extends EventEmitter {
11095
11124
  * beforeInsert: [async (data) => {
11096
11125
  * return data;
11097
11126
  * }]
11127
+ * },
11128
+ * events: {
11129
+ * insert: (ev) => console.log('Inserted:', ev.id),
11130
+ * update: [
11131
+ * (ev) => console.warn('Update detected'),
11132
+ * (ev) => console.log('Updated:', ev.id)
11133
+ * ],
11134
+ * delete: (ev) => console.log('Deleted:', ev.id)
11098
11135
  * }
11099
11136
  * });
11100
11137
  *
@@ -11147,7 +11184,8 @@ class Resource extends EventEmitter {
11147
11184
  hooks = {},
11148
11185
  idGenerator: customIdGenerator,
11149
11186
  idSize = 22,
11150
- versioningEnabled = false
11187
+ versioningEnabled = false,
11188
+ events = {}
11151
11189
  } = config;
11152
11190
  this.name = name;
11153
11191
  this.client = client;
@@ -11189,6 +11227,19 @@ class Resource extends EventEmitter {
11189
11227
  }
11190
11228
  }
11191
11229
  }
11230
+ if (events && Object.keys(events).length > 0) {
11231
+ for (const [eventName, listeners] of Object.entries(events)) {
11232
+ if (Array.isArray(listeners)) {
11233
+ for (const listener of listeners) {
11234
+ if (typeof listener === "function") {
11235
+ this.on(eventName, listener);
11236
+ }
11237
+ }
11238
+ } else if (typeof listeners === "function") {
11239
+ this.on(eventName, listeners);
11240
+ }
11241
+ }
11242
+ }
11192
11243
  this._initMiddleware();
11193
11244
  }
11194
11245
  /**
@@ -13228,6 +13279,24 @@ function validateResourceConfig(config) {
13228
13279
  }
13229
13280
  }
13230
13281
  }
13282
+ if (config.events !== void 0) {
13283
+ if (typeof config.events !== "object" || Array.isArray(config.events)) {
13284
+ errors.push("Resource 'events' must be an object");
13285
+ } else {
13286
+ for (const [eventName, listeners] of Object.entries(config.events)) {
13287
+ if (Array.isArray(listeners)) {
13288
+ for (let i = 0; i < listeners.length; i++) {
13289
+ const listener = listeners[i];
13290
+ if (typeof listener !== "function") {
13291
+ errors.push(`Resource 'events.${eventName}[${i}]' must be a function`);
13292
+ }
13293
+ }
13294
+ } else if (typeof listeners !== "function") {
13295
+ errors.push(`Resource 'events.${eventName}' must be a function or array of functions`);
13296
+ }
13297
+ }
13298
+ }
13299
+ }
13231
13300
  return {
13232
13301
  isValid: errors.length === 0,
13233
13302
  errors
@@ -13240,7 +13309,7 @@ class Database extends EventEmitter {
13240
13309
  super();
13241
13310
  this.version = "1";
13242
13311
  this.s3dbVersion = (() => {
13243
- const [ok, err, version] = try_fn_default(() => true ? "7.3.10" : "latest");
13312
+ const [ok, err, version] = try_fn_default(() => true ? "7.4.1" : "latest");
13244
13313
  return ok ? version : "latest";
13245
13314
  })();
13246
13315
  this.resources = {};
@@ -13611,7 +13680,8 @@ class Database extends EventEmitter {
13611
13680
  versioningEnabled: this.versioningEnabled,
13612
13681
  map: config.map,
13613
13682
  idGenerator: config.idGenerator,
13614
- idSize: config.idSize
13683
+ idSize: config.idSize,
13684
+ events: config.events || {}
13615
13685
  });
13616
13686
  resource.database = this;
13617
13687
  this.resources[name] = resource;