s3db.js 6.2.0 β 7.0.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/PLUGINS.md +2724 -0
- package/README.md +372 -469
- package/UNLICENSE +24 -0
- package/dist/s3db.cjs.js +12105 -19396
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +373 -72
- package/dist/s3db.es.js +12090 -19393
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +12103 -19398
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +44 -38
- package/src/behaviors/body-only.js +110 -0
- package/src/behaviors/body-overflow.js +153 -0
- package/src/behaviors/enforce-limits.js +195 -0
- package/src/behaviors/index.js +39 -0
- package/src/behaviors/truncate-data.js +204 -0
- package/src/behaviors/user-managed.js +147 -0
- package/src/client.class.js +515 -0
- package/src/concerns/base62.js +61 -0
- package/src/concerns/calculator.js +204 -0
- package/src/concerns/crypto.js +159 -0
- package/src/concerns/id.js +8 -0
- package/src/concerns/index.js +5 -0
- package/src/concerns/try-fn.js +151 -0
- package/src/connection-string.class.js +75 -0
- package/src/database.class.js +599 -0
- package/src/errors.js +261 -0
- package/src/index.js +17 -0
- package/src/plugins/audit.plugin.js +442 -0
- package/src/plugins/cache/cache.class.js +53 -0
- package/src/plugins/cache/index.js +6 -0
- package/src/plugins/cache/memory-cache.class.js +164 -0
- package/src/plugins/cache/s3-cache.class.js +189 -0
- package/src/plugins/cache.plugin.js +275 -0
- package/src/plugins/consumers/index.js +24 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
- package/src/plugins/consumers/sqs-consumer.js +102 -0
- package/src/plugins/costs.plugin.js +81 -0
- package/src/plugins/fulltext.plugin.js +473 -0
- package/src/plugins/index.js +12 -0
- package/src/plugins/metrics.plugin.js +603 -0
- package/src/plugins/plugin.class.js +210 -0
- package/src/plugins/plugin.obj.js +13 -0
- package/src/plugins/queue-consumer.plugin.js +134 -0
- package/src/plugins/replicator.plugin.js +769 -0
- package/src/plugins/replicators/base-replicator.class.js +85 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
- package/src/plugins/replicators/index.js +44 -0
- package/src/plugins/replicators/postgres-replicator.class.js +427 -0
- package/src/plugins/replicators/s3db-replicator.class.js +352 -0
- package/src/plugins/replicators/sqs-replicator.class.js +427 -0
- package/src/resource.class.js +2626 -0
- package/src/s3db.d.ts +1263 -0
- package/src/schema.class.js +706 -0
- package/src/stream/index.js +16 -0
- package/src/stream/resource-ids-page-reader.class.js +10 -0
- package/src/stream/resource-ids-reader.class.js +63 -0
- package/src/stream/resource-reader.class.js +81 -0
- package/src/stream/resource-writer.class.js +92 -0
- package/src/validator.class.js +97 -0
package/README.md
CHANGED
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
- [π Binary Content Management](#-binary-content-management)
|
|
109
109
|
- [ποΈ Advanced Partitioning](#οΈ-advanced-partitioning)
|
|
110
110
|
- [π£ Advanced Hooks System](#-advanced-hooks-system)
|
|
111
|
+
- [π§© Resource Middlewares](#-resource-middlewares)
|
|
111
112
|
- [π API Reference](#-api-reference)
|
|
112
113
|
|
|
113
114
|
---
|
|
@@ -199,23 +200,22 @@ yarn add s3db.js
|
|
|
199
200
|
|
|
200
201
|
Some features require additional dependencies to be installed manually:
|
|
201
202
|
|
|
202
|
-
####
|
|
203
|
+
#### replicator Dependencies
|
|
203
204
|
|
|
204
|
-
If you plan to use the
|
|
205
|
+
If you plan to use the replicator system with external services, install the corresponding dependencies:
|
|
205
206
|
|
|
206
207
|
```bash
|
|
207
|
-
# For SQS
|
|
208
|
+
# For SQS replicator (AWS SQS queues)
|
|
208
209
|
npm install @aws-sdk/client-sqs
|
|
209
210
|
|
|
210
|
-
# For BigQuery
|
|
211
|
+
# For BigQuery replicator (Google BigQuery)
|
|
211
212
|
npm install @google-cloud/bigquery
|
|
212
213
|
|
|
213
|
-
# For PostgreSQL
|
|
214
|
+
# For PostgreSQL replicator (PostgreSQL databases)
|
|
214
215
|
npm install pg
|
|
215
216
|
```
|
|
216
217
|
|
|
217
218
|
**Why manual installation?** These are marked as `peerDependencies` to keep the main package lightweight. Only install what you need!
|
|
218
|
-
```
|
|
219
219
|
|
|
220
220
|
### Environment Setup
|
|
221
221
|
|
|
@@ -303,7 +303,7 @@ const users = await s3db.createResource({
|
|
|
303
303
|
password: "secret"
|
|
304
304
|
},
|
|
305
305
|
timestamps: true,
|
|
306
|
-
|
|
306
|
+
behavior: "user-managed",
|
|
307
307
|
partitions: {
|
|
308
308
|
byRegion: { fields: { region: "string" } }
|
|
309
309
|
}
|
|
@@ -351,7 +351,7 @@ const products = await s3db.createResource({
|
|
|
351
351
|
name: "products",
|
|
352
352
|
attributes: { name: "string", price: "number" },
|
|
353
353
|
hooks: {
|
|
354
|
-
|
|
354
|
+
beforeInsert: [async (data) => {
|
|
355
355
|
data.sku = `${data.category.toUpperCase()}-${Date.now()}`;
|
|
356
356
|
return data;
|
|
357
357
|
}],
|
|
@@ -377,21 +377,6 @@ readableStream.on("end", () => console.log("β
Export completed"));
|
|
|
377
377
|
const writableStream = await users.writable();
|
|
378
378
|
importData.forEach(userData => writableStream.write(userData));
|
|
379
379
|
writableStream.end();
|
|
380
|
-
```
|
|
381
|
-
value: "string"
|
|
382
|
-
},
|
|
383
|
-
behavior: "enforce-limits" // Ensures data stays within 2KB
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// Smart truncation - preserves structure, truncates content
|
|
387
|
-
const summaries = await s3db.createResource({
|
|
388
|
-
name: "summaries",
|
|
389
|
-
attributes: {
|
|
390
|
-
title: "string",
|
|
391
|
-
description: "string"
|
|
392
|
-
},
|
|
393
|
-
behavior: "data-truncate" // Truncates to fit within limits
|
|
394
|
-
});
|
|
395
380
|
```
|
|
396
381
|
|
|
397
382
|
### π Resource Versioning System
|
|
@@ -478,9 +463,25 @@ const timestampUsers = await s3db.createResource({
|
|
|
478
463
|
});
|
|
479
464
|
```
|
|
480
465
|
|
|
466
|
+
#### π **Intelligent Data Compression**
|
|
467
|
+
|
|
468
|
+
s3db.js automatically compresses numeric data using **Base62 encoding** to maximize your S3 metadata space (2KB limit):
|
|
469
|
+
|
|
470
|
+
| Data Type | Original | Compressed | Space Saved |
|
|
471
|
+
|-----------|----------|------------|-------------|
|
|
472
|
+
| `10000` | `10000` (5 digits) | `2Bi` (3 digits) | **40%** |
|
|
473
|
+
| `123456789` | `123456789` (9 digits) | `8m0Kx` (5 digits) | **44%** |
|
|
474
|
+
| Large arrays | `[1,2,3,999999]` (13 chars) | `1,2,3,hBxM` (9 chars) | **31%** |
|
|
475
|
+
|
|
476
|
+
**Performance Benefits:**
|
|
477
|
+
- β‘ **5x faster** encoding for large numbers vs Base36
|
|
478
|
+
- ποΈ **41% compression** for typical numeric data
|
|
479
|
+
- π **Space efficient** - more data fits in S3 metadata
|
|
480
|
+
- π **Automatic** - no configuration required
|
|
481
|
+
|
|
481
482
|
### π Plugin System
|
|
482
483
|
|
|
483
|
-
Extend
|
|
484
|
+
Extend s3db.js with powerful plugins for caching, monitoring, replication, search, and more:
|
|
484
485
|
|
|
485
486
|
```javascript
|
|
486
487
|
import {
|
|
@@ -488,444 +489,47 @@ import {
|
|
|
488
489
|
CostsPlugin,
|
|
489
490
|
FullTextPlugin,
|
|
490
491
|
MetricsPlugin,
|
|
491
|
-
|
|
492
|
+
ReplicatorPlugin,
|
|
492
493
|
AuditPlugin
|
|
493
494
|
} from 's3db.js';
|
|
494
495
|
|
|
495
496
|
const s3db = new S3db({
|
|
496
497
|
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
497
498
|
plugins: [
|
|
498
|
-
new CachePlugin(
|
|
499
|
-
CostsPlugin,
|
|
500
|
-
new FullTextPlugin({ fields: ['name'
|
|
501
|
-
new MetricsPlugin(
|
|
502
|
-
new
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
config: {
|
|
509
|
-
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
]
|
|
499
|
+
new CachePlugin(), // πΎ Intelligent caching
|
|
500
|
+
CostsPlugin, // π° Cost tracking
|
|
501
|
+
new FullTextPlugin({ fields: ['name'] }), // π Full-text search
|
|
502
|
+
new MetricsPlugin(), // π Performance monitoring
|
|
503
|
+
new ReplicatorPlugin({ // π Data replication
|
|
504
|
+
replicators: [{
|
|
505
|
+
driver: 's3db',
|
|
506
|
+
resources: ['users'],
|
|
507
|
+
config: { connectionString: "s3://backup-bucket/backup" }
|
|
508
|
+
}]
|
|
513
509
|
}),
|
|
514
|
-
new AuditPlugin(
|
|
510
|
+
new AuditPlugin() // π Audit logging
|
|
515
511
|
]
|
|
516
512
|
});
|
|
517
513
|
|
|
518
|
-
|
|
519
|
-
await users.insert({ name: "John", email: "john@example.com" });
|
|
520
|
-
// - Cache: Caches the operation
|
|
521
|
-
// - Costs: Tracks S3 costs
|
|
522
|
-
// - FullText: Indexes the data for search
|
|
523
|
-
// - Metrics: Records performance metrics
|
|
524
|
-
// - Replication: Syncs to configured replicators
|
|
525
|
-
// - Audit: Logs the operation
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
### π Replicator System
|
|
529
|
-
|
|
530
|
-
The Replication Plugin now supports a flexible driver-based system for replicating data to different targets. Each replicator driver handles a specific type of target system.
|
|
531
|
-
|
|
532
|
-
#### Available Replicators
|
|
533
|
-
|
|
534
|
-
**S3DB Replicator** - Replicates data to another s3db instance:
|
|
535
|
-
```javascript
|
|
536
|
-
{
|
|
537
|
-
driver: 's3db',
|
|
538
|
-
resources: ['users', 'products'], // <-- root level
|
|
539
|
-
config: {
|
|
540
|
-
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup",
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
**SQS Replicator** - Sends data to AWS SQS queues:
|
|
546
|
-
```javascript
|
|
547
|
-
{
|
|
548
|
-
driver: 'sqs',
|
|
549
|
-
resources: ['orders'],
|
|
550
|
-
config: {
|
|
551
|
-
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue',
|
|
552
|
-
region: 'us-east-1',
|
|
553
|
-
messageGroupId: 's3db-replication', // For FIFO queues
|
|
554
|
-
deduplicationId: true // Enable deduplication
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
**BigQuery Replicator** - Sends data to Google BigQuery:
|
|
560
|
-
```javascript
|
|
561
|
-
{
|
|
562
|
-
driver: 'bigquery',
|
|
563
|
-
config: {
|
|
564
|
-
projectId: 'my-project',
|
|
565
|
-
datasetId: 'analytics',
|
|
566
|
-
location: 'US',
|
|
567
|
-
logTable: 's3db_replication_log',
|
|
568
|
-
credentials: {
|
|
569
|
-
// Your Google Cloud service account credentials
|
|
570
|
-
client_email: 'service-account@project.iam.gserviceaccount.com',
|
|
571
|
-
private_key: '-----BEGIN PRIVATE KEY-----\n...'
|
|
572
|
-
}
|
|
573
|
-
},
|
|
574
|
-
resources: {
|
|
575
|
-
users: [
|
|
576
|
-
{ actions: ['insert', 'update', 'delete'], table: 'users_table' },
|
|
577
|
-
],
|
|
578
|
-
orders: [
|
|
579
|
-
{ actions: ['insert'], table: 'orders_table' },
|
|
580
|
-
{ actions: ['insert'], table: 'orders_analytics' }, // Also replicate to analytics table
|
|
581
|
-
],
|
|
582
|
-
products: 'products_table' // Short form: equivalent to { actions: ['insert'], table: 'products_table' }
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
**PostgreSQL Replicator** - Sends data to PostgreSQL databases:
|
|
588
|
-
```javascript
|
|
589
|
-
{
|
|
590
|
-
driver: 'postgres',
|
|
591
|
-
config: {
|
|
592
|
-
connectionString: 'postgresql://user:pass@localhost:5432/analytics',
|
|
593
|
-
// OR individual parameters:
|
|
594
|
-
// host: 'localhost',
|
|
595
|
-
// port: 5432,
|
|
596
|
-
// database: 'analytics',
|
|
597
|
-
// user: 'user',
|
|
598
|
-
// password: 'pass',
|
|
599
|
-
ssl: false,
|
|
600
|
-
logTable: 's3db_replication_log'
|
|
601
|
-
},
|
|
602
|
-
resources: {
|
|
603
|
-
users: [
|
|
604
|
-
{ actions: ['insert', 'update', 'delete'], table: 'users_table' },
|
|
605
|
-
],
|
|
606
|
-
orders: [
|
|
607
|
-
{ actions: ['insert'], table: 'orders_table' },
|
|
608
|
-
{ actions: ['insert'], table: 'orders_analytics' }, // Also replicate to analytics table
|
|
609
|
-
],
|
|
610
|
-
products: 'products_table' // Short form: equivalent to { actions: ['insert'], table: 'products_table' }
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
#### Replicator Features
|
|
616
|
-
|
|
617
|
-
- **Resource Filtering**: Each replicator can be configured to handle specific resources only
|
|
618
|
-
- **Event Emission**: All replicators emit events for monitoring and debugging
|
|
619
|
-
- **Connection Testing**: Test connections to replicators before use
|
|
620
|
-
- **Batch Operations**: Support for batch replication operations
|
|
621
|
-
- **Error Handling**: Comprehensive error handling and retry logic
|
|
622
|
-
- **Status Monitoring**: Get detailed status and statistics for each replicator
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
**β οΈ Important:** These dependencies are marked as `peerDependencies` in the package.json, which means they are not automatically installed with s3db.js. You must install them manually if you plan to use the corresponding replicators. If you don't install the required dependency, the replicator will throw an error when trying to initialize.
|
|
626
|
-
|
|
627
|
-
**Example error without dependency:**
|
|
628
|
-
```
|
|
629
|
-
Error: Cannot find module '@aws-sdk/client-sqs'
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
**Solution:** Install the missing dependency as shown above.
|
|
633
|
-
|
|
634
|
-
#### Example Usage
|
|
635
|
-
|
|
636
|
-
See `examples/e34-replicators.js` for a complete example using all four replicator types.
|
|
637
|
-
|
|
638
|
-
**Prerequisites:** Make sure to install the required dependencies before running the example:
|
|
639
|
-
|
|
640
|
-
```bash
|
|
641
|
-
# Install all replication dependencies for the full example
|
|
642
|
-
npm install @aws-sdk/client-sqs @google-cloud/bigquery pg
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
#### π Cache Plugin
|
|
646
|
-
Intelligent caching to reduce API calls and improve performance:
|
|
647
|
-
|
|
648
|
-
```javascript
|
|
649
|
-
const s3db = new S3db({
|
|
650
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
651
|
-
plugins: [new CachePlugin({
|
|
652
|
-
enabled: true,
|
|
653
|
-
ttl: 300000, // 5 minutes cache
|
|
654
|
-
maxSize: 1000, // Max 1000 items in cache
|
|
655
|
-
driverType: 'memory' // 'memory' or 's3'
|
|
656
|
-
})]
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
// Automatic caching for reads
|
|
660
|
-
await users.count(); // Cached for 5 minutes
|
|
661
|
-
await users.list(); // Cached for 5 minutes
|
|
662
|
-
await users.insert({...}); // Automatically clears cache
|
|
663
|
-
```
|
|
664
|
-
|
|
665
|
-
#### π° Costs Plugin
|
|
666
|
-
Track and monitor AWS S3 costs in real-time:
|
|
667
|
-
|
|
668
|
-
```javascript
|
|
669
|
-
const s3db = new S3db({
|
|
670
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
671
|
-
plugins: [CostsPlugin]
|
|
672
|
-
});
|
|
514
|
+
await s3db.connect();
|
|
673
515
|
|
|
674
|
-
//
|
|
516
|
+
// All plugins work together seamlessly
|
|
675
517
|
await users.insert({ name: "John", email: "john@example.com" });
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
// Get cost information
|
|
679
|
-
console.log(s3db.client.costs);
|
|
680
|
-
// { total: 0.000009, requests: { total: 3, get: 1, put: 1, list: 1 } }
|
|
518
|
+
// β
Data cached, costs tracked, indexed for search, metrics recorded, replicated, and audited
|
|
681
519
|
```
|
|
682
520
|
|
|
683
|
-
####
|
|
684
|
-
Powerful text search with automatic indexing:
|
|
521
|
+
#### Available Plugins
|
|
685
522
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
maxResults: 50,
|
|
694
|
-
language: 'en-US'
|
|
695
|
-
})]
|
|
696
|
-
});
|
|
523
|
+
- **πΎ Cache Plugin** - Intelligent caching (memory/S3) for performance
|
|
524
|
+
- **π° Costs Plugin** - Real-time AWS S3 cost tracking
|
|
525
|
+
- **π FullText Plugin** - Advanced search with automatic indexing
|
|
526
|
+
- **π Metrics Plugin** - Performance monitoring and analytics
|
|
527
|
+
- **π Replicator Plugin** - Multi-target replication (S3DB, SQS, BigQuery, PostgreSQL)
|
|
528
|
+
- **π Audit Plugin** - Comprehensive audit logging for compliance
|
|
529
|
+
- **π¬ Queue Consumer Plugin** - Message consumption from SQS/RabbitMQ
|
|
697
530
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
name: "products",
|
|
701
|
-
attributes: {
|
|
702
|
-
name: "string|required",
|
|
703
|
-
description: "string",
|
|
704
|
-
content: "string"
|
|
705
|
-
}
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
// Insert data (automatically indexed)
|
|
709
|
-
await products.insert({
|
|
710
|
-
name: "JavaScript Book",
|
|
711
|
-
description: "Learn JavaScript programming",
|
|
712
|
-
content: "Comprehensive guide to modern JavaScript"
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
// Search across all indexed fields
|
|
716
|
-
const results = await s3db.plugins.fulltext.searchRecords('products', 'javascript');
|
|
717
|
-
console.log(results); // Returns products with search scores
|
|
718
|
-
|
|
719
|
-
// Example of search results:
|
|
720
|
-
// [
|
|
721
|
-
// {
|
|
722
|
-
// id: "prod-123",
|
|
723
|
-
// name: "JavaScript Book",
|
|
724
|
-
// description: "Learn JavaScript programming",
|
|
725
|
-
// content: "Comprehensive guide to modern JavaScript",
|
|
726
|
-
// _searchScore: 0.85,
|
|
727
|
-
// _matchedFields: ["name", "description", "content"],
|
|
728
|
-
// _matchedWords: ["javascript"]
|
|
729
|
-
// },
|
|
730
|
-
// {
|
|
731
|
-
// id: "prod-456",
|
|
732
|
-
// name: "Web Development Guide",
|
|
733
|
-
// description: "Includes JavaScript, HTML, and CSS",
|
|
734
|
-
// content: "Complete web development with JavaScript",
|
|
735
|
-
// _searchScore: 0.72,
|
|
736
|
-
// _matchedFields: ["description", "content"],
|
|
737
|
-
// _matchedWords: ["javascript"]
|
|
738
|
-
// }
|
|
739
|
-
// ]
|
|
740
|
-
```
|
|
741
|
-
|
|
742
|
-
#### π Metrics Plugin
|
|
743
|
-
Monitor performance and usage metrics:
|
|
744
|
-
|
|
745
|
-
```javascript
|
|
746
|
-
const s3db = new S3db({
|
|
747
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
748
|
-
plugins: [new MetricsPlugin({
|
|
749
|
-
enabled: true,
|
|
750
|
-
collectPerformance: true,
|
|
751
|
-
collectErrors: true,
|
|
752
|
-
collectUsage: true,
|
|
753
|
-
flushInterval: 60000 // Flush every minute
|
|
754
|
-
})]
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
// Metrics are collected automatically
|
|
758
|
-
await users.insert({ name: "John" });
|
|
759
|
-
await users.list();
|
|
760
|
-
|
|
761
|
-
// Get metrics
|
|
762
|
-
const metrics = await s3db.plugins.metrics.getMetrics();
|
|
763
|
-
console.log(metrics); // Performance and usage data
|
|
764
|
-
|
|
765
|
-
// Example of metrics object:
|
|
766
|
-
// {
|
|
767
|
-
// performance: {
|
|
768
|
-
// averageResponseTime: 245, // milliseconds
|
|
769
|
-
// totalRequests: 1250,
|
|
770
|
-
// requestsPerSecond: 12.5,
|
|
771
|
-
// slowestOperations: [
|
|
772
|
-
// { operation: "list", resource: "users", avgTime: 450, count: 50 },
|
|
773
|
-
// { operation: "get", resource: "products", avgTime: 320, count: 200 }
|
|
774
|
-
// ]
|
|
775
|
-
// },
|
|
776
|
-
// usage: {
|
|
777
|
-
// resources: {
|
|
778
|
-
// users: { inserts: 150, updates: 75, deletes: 10, reads: 800 },
|
|
779
|
-
// products: { inserts: 300, updates: 120, deletes: 25, reads: 1200 }
|
|
780
|
-
// },
|
|
781
|
-
// totalOperations: 2680,
|
|
782
|
-
// mostActiveResource: "products",
|
|
783
|
-
// peakUsageHour: "14:00"
|
|
784
|
-
// },
|
|
785
|
-
// errors: {
|
|
786
|
-
// total: 15,
|
|
787
|
-
// byType: {
|
|
788
|
-
// "ValidationError": 8,
|
|
789
|
-
// "NotFoundError": 5,
|
|
790
|
-
// "PermissionError": 2
|
|
791
|
-
// },
|
|
792
|
-
// byResource: {
|
|
793
|
-
// users: 10,
|
|
794
|
-
// products: 5
|
|
795
|
-
// }
|
|
796
|
-
// },
|
|
797
|
-
// cache: {
|
|
798
|
-
// hitRate: 0.78, // 78% cache hit rate
|
|
799
|
-
// totalHits: 980,
|
|
800
|
-
// totalMisses: 270,
|
|
801
|
-
// averageCacheTime: 120 // milliseconds
|
|
802
|
-
// }
|
|
803
|
-
// }
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
#### π Replication Plugin
|
|
807
|
-
Replicate data to other buckets or regions:
|
|
808
|
-
|
|
809
|
-
```javascript
|
|
810
|
-
const s3db = new S3db({
|
|
811
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
812
|
-
plugins: [new ReplicationPlugin({
|
|
813
|
-
enabled: true,
|
|
814
|
-
replicators: [
|
|
815
|
-
{
|
|
816
|
-
driver: 's3db',
|
|
817
|
-
resources: ['users', 'products'],
|
|
818
|
-
config: {
|
|
819
|
-
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
|
|
820
|
-
}
|
|
821
|
-
},
|
|
822
|
-
{
|
|
823
|
-
driver: 'sqs',
|
|
824
|
-
resources: ['orders', 'users', 'products'],
|
|
825
|
-
config: {
|
|
826
|
-
// Resource-specific queues
|
|
827
|
-
queues: {
|
|
828
|
-
users: 'https://sqs.us-east-1.amazonaws.com/123456789012/users-events.fifo',
|
|
829
|
-
orders: 'https://sqs.us-east-1.amazonaws.com/123456789012/orders-events.fifo',
|
|
830
|
-
products: 'https://sqs.us-east-1.amazonaws.com/123456789012/products-events.fifo'
|
|
831
|
-
},
|
|
832
|
-
// Fallback queue for unspecified resources
|
|
833
|
-
defaultQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/default-events.fifo',
|
|
834
|
-
messageGroupId: 's3db-replication', // For FIFO queues
|
|
835
|
-
deduplicationId: true // Enable deduplication
|
|
836
|
-
}
|
|
837
|
-
},
|
|
838
|
-
{
|
|
839
|
-
driver: 'bigquery',
|
|
840
|
-
resources: ['users', 'orders'],
|
|
841
|
-
config: {
|
|
842
|
-
projectId: 'my-project',
|
|
843
|
-
datasetId: 'analytics',
|
|
844
|
-
tableId: 's3db_replication'
|
|
845
|
-
}
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
driver: 'postgres',
|
|
849
|
-
resources: ['users'],
|
|
850
|
-
config: {
|
|
851
|
-
connectionString: 'postgresql://user:pass@localhost:5432/analytics',
|
|
852
|
-
tableName: 's3db_replication'
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
],
|
|
856
|
-
syncInterval: 300000 // Sync every 5 minutes
|
|
857
|
-
})]
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
// Data is automatically replicated to all configured targets
|
|
861
|
-
await users.insert({ name: "John" }); // Synced to all replicators
|
|
862
|
-
```
|
|
863
|
-
|
|
864
|
-
**SQS Message Structure:**
|
|
865
|
-
|
|
866
|
-
The SQS replicator sends standardized messages with the following structure:
|
|
867
|
-
|
|
868
|
-
```javascript
|
|
869
|
-
// INSERT operation
|
|
870
|
-
{
|
|
871
|
-
resource: "users",
|
|
872
|
-
action: "insert",
|
|
873
|
-
data: { _v: 0, id: "user-001", name: "John", email: "john@example.com" },
|
|
874
|
-
timestamp: "2024-01-01T10:00:00.000Z",
|
|
875
|
-
source: "s3db-replication"
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// UPDATE operation (includes before/after data)
|
|
879
|
-
{
|
|
880
|
-
resource: "users",
|
|
881
|
-
action: "update",
|
|
882
|
-
before: { _v: 0, id: "user-001", name: "John", age: 30 },
|
|
883
|
-
data: { _v: 1, id: "user-001", name: "John", age: 31 },
|
|
884
|
-
timestamp: "2024-01-01T10:05:00.000Z",
|
|
885
|
-
source: "s3db-replication"
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// DELETE operation
|
|
889
|
-
{
|
|
890
|
-
resource: "users",
|
|
891
|
-
action: "delete",
|
|
892
|
-
data: { _v: 1, id: "user-001", name: "John", age: 31 },
|
|
893
|
-
timestamp: "2024-01-01T10:10:00.000Z",
|
|
894
|
-
source: "s3db-replication"
|
|
895
|
-
}
|
|
896
|
-
```
|
|
897
|
-
|
|
898
|
-
**Queue Routing:**
|
|
899
|
-
- Each resource can have its own dedicated queue
|
|
900
|
-
- Unspecified resources use the default queue
|
|
901
|
-
- FIFO queues supported with deduplication
|
|
902
|
-
- Messages are automatically routed to the appropriate queue
|
|
903
|
-
|
|
904
|
-
#### π Audit Plugin
|
|
905
|
-
Log all operations for compliance and traceability:
|
|
906
|
-
|
|
907
|
-
```javascript
|
|
908
|
-
const s3db = new S3db({
|
|
909
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
910
|
-
plugins: [new AuditPlugin({
|
|
911
|
-
enabled: true,
|
|
912
|
-
trackOperations: ['insert', 'update', 'delete', 'get'],
|
|
913
|
-
includeData: false, // Don't log sensitive data
|
|
914
|
-
retentionDays: 90
|
|
915
|
-
})]
|
|
916
|
-
});
|
|
917
|
-
|
|
918
|
-
// All operations are logged
|
|
919
|
-
await users.insert({ name: "John" });
|
|
920
|
-
await users.update(userId, { age: 31 });
|
|
921
|
-
|
|
922
|
-
// Get audit logs
|
|
923
|
-
const logs = await s3db.plugins.audit.getAuditLogs({
|
|
924
|
-
resourceName: 'users',
|
|
925
|
-
operation: 'insert'
|
|
926
|
-
});
|
|
927
|
-
console.log(logs); // Audit trail
|
|
928
|
-
```
|
|
531
|
+
**π For detailed documentation, configuration options, and advanced examples, see:**
|
|
532
|
+
**[π Complete Plugin Documentation](https://github.com/forattini-dev/s3db.js/blob/main/PLUGINS.md)**
|
|
929
533
|
|
|
930
534
|
### ποΈ Resource Behaviors
|
|
931
535
|
|
|
@@ -937,7 +541,7 @@ Choose the right behavior strategy for your use case:
|
|
|
937
541
|
|------------------|-------------|-----------|----------------|-------------------------|
|
|
938
542
|
| `user-managed` | None | Possible | Warns | Dev/Test/Advanced users |
|
|
939
543
|
| `enforce-limits` | Strict | No | Throws | Production |
|
|
940
|
-
| `data
|
|
544
|
+
| `truncate-data` | Truncates | Yes | Warns | Content Mgmt |
|
|
941
545
|
| `body-overflow` | Truncates/Splits | Yes | Warns | Large objects |
|
|
942
546
|
| `body-only` | Unlimited | No | No | Large JSON/Logs |
|
|
943
547
|
|
|
@@ -988,10 +592,10 @@ await users.insert({
|
|
|
988
592
|
**Best Practices & Warnings:**
|
|
989
593
|
- Exceeding S3 metadata limits will cause silent data loss or errors at the storage layer.
|
|
990
594
|
- Use this behavior only if you have custom logic to handle warnings and enforce limits.
|
|
991
|
-
- For production, prefer `enforce-limits` or `data
|
|
595
|
+
- For production, prefer `enforce-limits` or `truncate-data` to avoid data loss.
|
|
992
596
|
|
|
993
597
|
**Migration Tips:**
|
|
994
|
-
- To migrate to a stricter behavior, change the resource's behavior to `enforce-limits` or `data
|
|
598
|
+
- To migrate to a stricter behavior, change the resource's behavior to `enforce-limits` or `truncate-data`.
|
|
995
599
|
- Review emitted warnings to identify resources at risk of exceeding S3 limits.
|
|
996
600
|
|
|
997
601
|
#### Enforce Limits Behavior
|
|
@@ -1022,7 +626,7 @@ const summaries = await s3db.createResource({
|
|
|
1022
626
|
description: "string",
|
|
1023
627
|
content: "string"
|
|
1024
628
|
},
|
|
1025
|
-
behavior: "data
|
|
629
|
+
behavior: "truncate-data"
|
|
1026
630
|
});
|
|
1027
631
|
|
|
1028
632
|
// Automatically truncates to fit within 2KB
|
|
@@ -1330,6 +934,7 @@ const usUsers = await users.list({
|
|
|
1330
934
|
|
|
1331
935
|
// Note: The system automatically manages partition references internally
|
|
1332
936
|
// Users should use standard list() method with partition parameters
|
|
937
|
+
```
|
|
1333
938
|
|
|
1334
939
|
#### Automatic Timestamp Partitions
|
|
1335
940
|
|
|
@@ -1387,14 +992,14 @@ const users = await s3db.createResource({
|
|
|
1387
992
|
name: "users",
|
|
1388
993
|
attributes: { name: "string", email: "string" },
|
|
1389
994
|
hooks: {
|
|
1390
|
-
|
|
995
|
+
beforeInsert: [
|
|
1391
996
|
async (data) => {
|
|
1392
|
-
console.log('1.
|
|
997
|
+
console.log('1. Before-insert hook 1');
|
|
1393
998
|
data.timestamp = new Date().toISOString();
|
|
1394
999
|
return data;
|
|
1395
1000
|
},
|
|
1396
1001
|
async (data) => {
|
|
1397
|
-
console.log('2.
|
|
1002
|
+
console.log('2. Before-insert hook 2');
|
|
1398
1003
|
data.processed = true;
|
|
1399
1004
|
return data;
|
|
1400
1005
|
}
|
|
@@ -1412,7 +1017,7 @@ const users = await s3db.createResource({
|
|
|
1412
1017
|
}
|
|
1413
1018
|
});
|
|
1414
1019
|
|
|
1415
|
-
// Execution order:
|
|
1020
|
+
// Execution order: beforeInsert hooks β insert β afterInsert hooks
|
|
1416
1021
|
```
|
|
1417
1022
|
|
|
1418
1023
|
#### Version-Specific Hooks
|
|
@@ -1424,7 +1029,7 @@ const users = await s3db.createResource({
|
|
|
1424
1029
|
attributes: { name: "string", email: "string" },
|
|
1425
1030
|
versioningEnabled: true,
|
|
1426
1031
|
hooks: {
|
|
1427
|
-
|
|
1032
|
+
beforeInsert: [
|
|
1428
1033
|
async (data) => {
|
|
1429
1034
|
// Access resource context
|
|
1430
1035
|
console.log('Current version:', this.version);
|
|
@@ -1446,19 +1051,19 @@ users.on('versionUpdated', ({ oldVersion, newVersion }) => {
|
|
|
1446
1051
|
const users = await s3db.createResource({
|
|
1447
1052
|
name: "users",
|
|
1448
1053
|
attributes: { name: "string", email: "string" },
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1054
|
+
hooks: {
|
|
1055
|
+
beforeInsert: [
|
|
1056
|
+
async (data) => {
|
|
1057
|
+
try {
|
|
1058
|
+
// Validate external service
|
|
1059
|
+
await validateEmail(data.email);
|
|
1060
|
+
return data;
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
// Transform error or add context
|
|
1063
|
+
throw new Error(`Email validation failed: ${error.message}`);
|
|
1064
|
+
}
|
|
1459
1065
|
}
|
|
1460
|
-
|
|
1461
|
-
],
|
|
1066
|
+
],
|
|
1462
1067
|
afterInsert: [
|
|
1463
1068
|
async (data) => {
|
|
1464
1069
|
try {
|
|
@@ -1480,7 +1085,7 @@ const users = await s3db.createResource({
|
|
|
1480
1085
|
name: "users",
|
|
1481
1086
|
attributes: { name: "string", email: "string" },
|
|
1482
1087
|
hooks: {
|
|
1483
|
-
|
|
1088
|
+
beforeInsert: [
|
|
1484
1089
|
async function(data) {
|
|
1485
1090
|
// 'this' is bound to the resource instance
|
|
1486
1091
|
console.log('Resource name:', this.name);
|
|
@@ -1499,6 +1104,303 @@ const users = await s3db.createResource({
|
|
|
1499
1104
|
});
|
|
1500
1105
|
```
|
|
1501
1106
|
|
|
1107
|
+
### π§© Resource Middlewares
|
|
1108
|
+
|
|
1109
|
+
The Resource class supports a powerful middleware system, similar to Express/Koa, allowing you to intercept, modify, or extend the behavior of core methods like `insert`, `get`, `update`, `delete`, `list`, and more.
|
|
1110
|
+
|
|
1111
|
+
**Supported methods for middleware:**
|
|
1112
|
+
- `get`
|
|
1113
|
+
- `list`
|
|
1114
|
+
- `listIds`
|
|
1115
|
+
- `getAll`
|
|
1116
|
+
- `count`
|
|
1117
|
+
- `page`
|
|
1118
|
+
- `insert`
|
|
1119
|
+
- `update`
|
|
1120
|
+
- `delete`
|
|
1121
|
+
- `deleteMany`
|
|
1122
|
+
- `exists`
|
|
1123
|
+
- `getMany`
|
|
1124
|
+
|
|
1125
|
+
#### Middleware Signature
|
|
1126
|
+
```js
|
|
1127
|
+
async function middleware(ctx, next) {
|
|
1128
|
+
// ctx.resource: Resource instance
|
|
1129
|
+
// ctx.args: arguments array (for the method)
|
|
1130
|
+
// ctx.method: method name (e.g., 'insert')
|
|
1131
|
+
// next(): calls the next middleware or the original method
|
|
1132
|
+
}
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
#### Example: Logging Middleware for Insert
|
|
1136
|
+
```js
|
|
1137
|
+
const users = await s3db.createResource({
|
|
1138
|
+
name: "users",
|
|
1139
|
+
attributes: { name: "string", email: "string" }
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
users.useMiddleware('insert', async (ctx, next) => {
|
|
1143
|
+
console.log('Before insert:', ctx.args[0]);
|
|
1144
|
+
// You can modify ctx.args if needed
|
|
1145
|
+
ctx.args[0].name = ctx.args[0].name.toUpperCase();
|
|
1146
|
+
const result = await next();
|
|
1147
|
+
console.log('After insert:', result);
|
|
1148
|
+
return result;
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
await users.insert({ name: "john", email: "john@example.com" });
|
|
1152
|
+
// Output:
|
|
1153
|
+
// Before insert: { name: 'john', email: 'john@example.com' }
|
|
1154
|
+
// After insert: { id: '...', name: 'JOHN', email: 'john@example.com', ... }
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
#### Example: Validation or Metrics Middleware
|
|
1158
|
+
```js
|
|
1159
|
+
users.useMiddleware('update', async (ctx, next) => {
|
|
1160
|
+
if (!ctx.args[1].email) throw new Error('Email is required for update!');
|
|
1161
|
+
const start = Date.now();
|
|
1162
|
+
const result = await next();
|
|
1163
|
+
const duration = Date.now() - start;
|
|
1164
|
+
console.log(`Update took ${duration}ms`);
|
|
1165
|
+
return result;
|
|
1166
|
+
});
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
#### π Complete Example: Authentication & Audit Middleware
|
|
1170
|
+
|
|
1171
|
+
Here's a practical example showing how to implement authentication and audit logging with middleware:
|
|
1172
|
+
|
|
1173
|
+
```js
|
|
1174
|
+
import { S3db } from 's3db.js';
|
|
1175
|
+
|
|
1176
|
+
// Create database and resources
|
|
1177
|
+
const database = new S3db({ connectionString: 's3://my-bucket/my-app' });
|
|
1178
|
+
await database.connect();
|
|
1179
|
+
|
|
1180
|
+
const orders = await database.createResource({
|
|
1181
|
+
name: 'orders',
|
|
1182
|
+
attributes: {
|
|
1183
|
+
id: 'string|required',
|
|
1184
|
+
customerId: 'string|required',
|
|
1185
|
+
amount: 'number|required',
|
|
1186
|
+
status: 'string|required'
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
// Authentication middleware - runs on all operations
|
|
1191
|
+
['insert', 'update', 'delete', 'get'].forEach(method => {
|
|
1192
|
+
orders.useMiddleware(method, async (ctx, next) => {
|
|
1193
|
+
// Extract user from context (e.g., from JWT token)
|
|
1194
|
+
const user = ctx.user || ctx.args.find(arg => arg?.userId);
|
|
1195
|
+
|
|
1196
|
+
if (!user || !user.userId) {
|
|
1197
|
+
throw new Error(`Authentication required for ${method} operation`);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Add user info to context for other middlewares
|
|
1201
|
+
ctx.authenticatedUser = user;
|
|
1202
|
+
|
|
1203
|
+
return await next();
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// Audit logging middleware - tracks all changes
|
|
1208
|
+
['insert', 'update', 'delete'].forEach(method => {
|
|
1209
|
+
orders.useMiddleware(method, async (ctx, next) => {
|
|
1210
|
+
const startTime = Date.now();
|
|
1211
|
+
const user = ctx.authenticatedUser;
|
|
1212
|
+
|
|
1213
|
+
try {
|
|
1214
|
+
const result = await next();
|
|
1215
|
+
|
|
1216
|
+
// Log successful operation
|
|
1217
|
+
console.log(`[AUDIT] ${method.toUpperCase()}`, {
|
|
1218
|
+
resource: 'orders',
|
|
1219
|
+
userId: user.userId,
|
|
1220
|
+
method,
|
|
1221
|
+
args: ctx.args,
|
|
1222
|
+
duration: Date.now() - startTime,
|
|
1223
|
+
timestamp: new Date().toISOString(),
|
|
1224
|
+
success: true
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
return result;
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
// Log failed operation
|
|
1230
|
+
console.log(`[AUDIT] ${method.toUpperCase()} FAILED`, {
|
|
1231
|
+
resource: 'orders',
|
|
1232
|
+
userId: user.userId,
|
|
1233
|
+
method,
|
|
1234
|
+
error: error.message,
|
|
1235
|
+
duration: Date.now() - startTime,
|
|
1236
|
+
timestamp: new Date().toISOString(),
|
|
1237
|
+
success: false
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
throw error;
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
// Permission middleware for sensitive operations
|
|
1246
|
+
orders.useMiddleware('delete', async (ctx, next) => {
|
|
1247
|
+
const user = ctx.authenticatedUser;
|
|
1248
|
+
|
|
1249
|
+
if (user.role !== 'admin') {
|
|
1250
|
+
throw new Error('Only admins can delete orders');
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
return await next();
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
// Usage examples
|
|
1257
|
+
try {
|
|
1258
|
+
// This will require authentication and log the operation
|
|
1259
|
+
const order = await orders.insert(
|
|
1260
|
+
{
|
|
1261
|
+
id: 'order-123',
|
|
1262
|
+
customerId: 'cust-456',
|
|
1263
|
+
amount: 99.99,
|
|
1264
|
+
status: 'pending'
|
|
1265
|
+
},
|
|
1266
|
+
{ user: { userId: 'user-789', role: 'customer' } }
|
|
1267
|
+
);
|
|
1268
|
+
|
|
1269
|
+
// This will fail - only admins can delete
|
|
1270
|
+
await orders.delete('order-123', {
|
|
1271
|
+
user: { userId: 'user-789', role: 'customer' }
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
console.error('Operation failed:', error.message);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/*
|
|
1279
|
+
Expected output:
|
|
1280
|
+
[AUDIT] INSERT {
|
|
1281
|
+
resource: 'orders',
|
|
1282
|
+
userId: 'user-789',
|
|
1283
|
+
method: 'insert',
|
|
1284
|
+
args: [{ id: 'order-123', customerId: 'cust-456', amount: 99.99, status: 'pending' }],
|
|
1285
|
+
duration: 245,
|
|
1286
|
+
timestamp: '2024-01-15T10:30:45.123Z',
|
|
1287
|
+
success: true
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
Operation failed: Only admins can delete orders
|
|
1291
|
+
[AUDIT] DELETE FAILED {
|
|
1292
|
+
resource: 'orders',
|
|
1293
|
+
userId: 'user-789',
|
|
1294
|
+
method: 'delete',
|
|
1295
|
+
error: 'Only admins can delete orders',
|
|
1296
|
+
duration: 12,
|
|
1297
|
+
timestamp: '2024-01-15T10:30:45.456Z',
|
|
1298
|
+
success: false
|
|
1299
|
+
}
|
|
1300
|
+
*/
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
**Key Benefits of This Approach:**
|
|
1304
|
+
- π **Centralized Authentication**: One middleware handles auth for all operations
|
|
1305
|
+
- π **Comprehensive Auditing**: All operations are logged with timing and user info
|
|
1306
|
+
- π‘οΈ **Granular Permissions**: Different rules for different operations
|
|
1307
|
+
- β‘ **Performance Tracking**: Built-in timing for operation monitoring
|
|
1308
|
+
- π§ **Easy to Maintain**: Add/remove middlewares without changing business logic
|
|
1309
|
+
|
|
1310
|
+
- **Chaining:** You can add multiple middlewares for the same method; they run in registration order.
|
|
1311
|
+
- **Control:** You can short-circuit the chain by not calling `next()`, or modify arguments/results as needed.
|
|
1312
|
+
|
|
1313
|
+
This system is ideal for cross-cutting concerns like logging, access control, custom validation, metrics, or request shaping.
|
|
1314
|
+
|
|
1315
|
+
---
|
|
1316
|
+
|
|
1317
|
+
### π§© Hooks vs Middlewares: Differences, Usage, and Coexistence
|
|
1318
|
+
|
|
1319
|
+
s3db.js supports **both hooks and middlewares** for resources. They are complementary tools for customizing and extending resource behavior.
|
|
1320
|
+
|
|
1321
|
+
#### **What are Hooks?**
|
|
1322
|
+
- Hooks are functions that run **before or after** specific operations (e.g., `beforeInsert`, `afterUpdate`).
|
|
1323
|
+
- They are ideal for **side effects**: logging, notifications, analytics, validation, etc.
|
|
1324
|
+
- Hooks **cannot block or replace** the original operationβthey can only observe or modify the data passed to them.
|
|
1325
|
+
- Hooks are registered with `addHook(hookName, fn)` or via the `hooks` config.
|
|
1326
|
+
|
|
1327
|
+
> **π Note:** Don't confuse hooks with **events**. Hooks are lifecycle functions (`beforeInsert`, `afterUpdate`, etc.) while events are actual EventEmitter events (`exceedsLimit`, `truncate`, `overflow`) that you listen to with `.on(eventName, handler)`.
|
|
1328
|
+
|
|
1329
|
+
**Example:**
|
|
1330
|
+
```js
|
|
1331
|
+
users.addHook('afterInsert', async (data) => {
|
|
1332
|
+
await sendWelcomeEmail(data.email);
|
|
1333
|
+
return data;
|
|
1334
|
+
});
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
#### **What are Middlewares?**
|
|
1338
|
+
- Middlewares are functions that **wrap** the entire method call (like Express/Koa middlewares).
|
|
1339
|
+
- They can **intercept, modify, block, or replace** the operation.
|
|
1340
|
+
- Middlewares can transform arguments, short-circuit the call, or modify the result.
|
|
1341
|
+
- Middlewares are registered with `useMiddleware(method, fn)`.
|
|
1342
|
+
|
|
1343
|
+
**Example:**
|
|
1344
|
+
```js
|
|
1345
|
+
users.useMiddleware('insert', async (ctx, next) => {
|
|
1346
|
+
if (!ctx.args[0].email) throw new Error('Email required');
|
|
1347
|
+
ctx.args[0].name = ctx.args[0].name.toUpperCase();
|
|
1348
|
+
const result = await next();
|
|
1349
|
+
return result;
|
|
1350
|
+
});
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
#### **Key Differences**
|
|
1354
|
+
| Feature | Hooks | Middlewares |
|
|
1355
|
+
|----------------|------------------------------|------------------------------|
|
|
1356
|
+
| Placement | Before/after operation | Wraps the entire method |
|
|
1357
|
+
| Control | Cannot block/replace op | Can block/replace op |
|
|
1358
|
+
| Use case | Side effects, logging, etc. | Access control, transform |
|
|
1359
|
+
| Registration | `addHook(hookName, fn)` | `useMiddleware(method, fn)` |
|
|
1360
|
+
| Data access | Receives data only | Full context (args, method) |
|
|
1361
|
+
| Chaining | Runs in order, always passes | Runs in order, can short-circuit |
|
|
1362
|
+
|
|
1363
|
+
#### **How They Work Together**
|
|
1364
|
+
Hooks and middlewares can be used **together** on the same resource and method. The order of execution is:
|
|
1365
|
+
|
|
1366
|
+
1. **Middlewares** (before the operation)
|
|
1367
|
+
2. **Hooks** (`beforeX`)
|
|
1368
|
+
3. **Original operation**
|
|
1369
|
+
4. **Hooks** (`afterX`)
|
|
1370
|
+
5. **Middlewares** (after the operation, as the call stack unwinds)
|
|
1371
|
+
|
|
1372
|
+
**Example: Using Both**
|
|
1373
|
+
```js
|
|
1374
|
+
// Middleware: transforms input and checks permissions
|
|
1375
|
+
users.useMiddleware('insert', async (ctx, next) => {
|
|
1376
|
+
if (!userHasPermission(ctx.args[0])) throw new Error('Unauthorized');
|
|
1377
|
+
ctx.args[0].name = ctx.args[0].name.toUpperCase();
|
|
1378
|
+
const result = await next();
|
|
1379
|
+
return result;
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
// Hook: sends notification after insert
|
|
1383
|
+
users.addHook('afterInsert', async (data) => {
|
|
1384
|
+
await sendWelcomeEmail(data.email);
|
|
1385
|
+
return data;
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
await users.insert({ name: 'john', email: 'john@example.com' });
|
|
1389
|
+
// Output:
|
|
1390
|
+
// Middleware runs (transforms/checks)
|
|
1391
|
+
// Hook runs (sends email)
|
|
1392
|
+
```
|
|
1393
|
+
|
|
1394
|
+
#### **When to Use Each**
|
|
1395
|
+
- Use **hooks** for: logging, analytics, notifications, validation, side effects.
|
|
1396
|
+
- Use **middlewares** for: access control, input/output transformation, caching, rate limiting, blocking or replacing operations.
|
|
1397
|
+
- Use **both** for advanced scenarios: e.g., middleware for access control + hook for analytics.
|
|
1398
|
+
|
|
1399
|
+
#### **Best Practices**
|
|
1400
|
+
- Hooks are lightweight and ideal for observing or reacting to events.
|
|
1401
|
+
- Middlewares are powerful and ideal for controlling or transforming operations.
|
|
1402
|
+
- You can safely combine both for maximum flexibility.
|
|
1403
|
+
|
|
1502
1404
|
---
|
|
1503
1405
|
|
|
1504
1406
|
## π API Reference
|
|
@@ -1585,4 +1487,5 @@ console.log(`Total users: ${allUsers.length}`);
|
|
|
1585
1487
|
|
|
1586
1488
|
| Method | Description | Example |
|
|
1587
1489
|
|--------|-------------|---------|
|
|
1588
|
-
| `
|
|
1490
|
+
| `readable(options?)` | Create readable stream | `await users.readable()` |
|
|
1491
|
+
| `writable(options?)` | Create writable stream | `await users.writable()` |
|