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 +164 -0
- package/dist/s3db.cjs.js +74 -4
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +6 -0
- package/dist/s3db.es.js +74 -4
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +74 -4
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +1 -1
- package/src/client.class.js +41 -1
- package/src/database.class.js +2 -1
- package/src/resource.class.js +49 -1
- package/src/s3db.d.ts +6 -0
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
|
-
|
|
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.
|
|
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;
|