s3db.js 5.2.0 โ 6.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/README.md +1168 -54
- package/dist/s3db.cjs.js +128 -12
- package/dist/s3db.cjs.min.js +11 -11
- package/dist/s3db.es.js +128 -12
- package/dist/s3db.es.min.js +11 -11
- package/dist/s3db.iife.js +128 -12
- package/dist/s3db.iife.min.js +11 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,9 +97,20 @@
|
|
|
97
97
|
- [๐พ Installation](#-installation)
|
|
98
98
|
- [๐ฏ Core Concepts](#-core-concepts)
|
|
99
99
|
- [โก Advanced Features](#-advanced-features)
|
|
100
|
+
- [๐ Resource Versioning System](#-resource-versioning-system)
|
|
101
|
+
- [๐ Custom ID Generation](#-custom-id-generation)
|
|
102
|
+
- [๐ Plugin System](#-plugin-system)
|
|
103
|
+
- [๐๏ธ Advanced Behaviors](#๏ธ-advanced-behaviors)
|
|
104
|
+
- [๐ Advanced Streaming API](#-advanced-streaming-api)
|
|
105
|
+
- [๐ Binary Content Management](#-binary-content-management)
|
|
106
|
+
- [๐๏ธ Advanced Partitioning](#๏ธ-advanced-partitioning)
|
|
107
|
+
- [๐ฃ Advanced Hooks System](#-advanced-hooks-system)
|
|
100
108
|
- [๐ API Reference](#-api-reference)
|
|
101
109
|
- [๐จ Examples](#-examples)
|
|
102
110
|
- [๐ Security](#-security)
|
|
111
|
+
- [โ๏ธ Advanced Configuration Options](#๏ธ-advanced-configuration-options)
|
|
112
|
+
- [๐ก Events and Emitters](#-events-and-emitters)
|
|
113
|
+
- [๐ง Troubleshooting](#-troubleshooting)
|
|
103
114
|
- [๐ฐ Cost Analysis](#-cost-analysis)
|
|
104
115
|
- [๐จ Best Practices](#-best-practices)
|
|
105
116
|
- [๐งช Testing](#-testing)
|
|
@@ -196,10 +207,8 @@ console.log(`Total users: ${allUsers.length}`);
|
|
|
196
207
|
```bash
|
|
197
208
|
# npm
|
|
198
209
|
npm install s3db.js
|
|
199
|
-
|
|
200
210
|
# pnpm
|
|
201
211
|
pnpm add s3db.js
|
|
202
|
-
|
|
203
212
|
# yarn
|
|
204
213
|
yarn add s3db.js
|
|
205
214
|
```
|
|
@@ -447,72 +456,870 @@ const products = await s3db.createResource({
|
|
|
447
456
|
|
|
448
457
|
### ๐ Streaming API
|
|
449
458
|
|
|
450
|
-
Handle large datasets efficiently with streams:
|
|
459
|
+
Handle large datasets efficiently with streams:
|
|
460
|
+
|
|
461
|
+
```javascript
|
|
462
|
+
// Export all users to CSV
|
|
463
|
+
const readableStream = await users.readable();
|
|
464
|
+
const csvWriter = createObjectCsvWriter({
|
|
465
|
+
path: "users_export.csv",
|
|
466
|
+
header: [
|
|
467
|
+
{ id: "id", title: "ID" },
|
|
468
|
+
{ id: "name", title: "Name" },
|
|
469
|
+
{ id: "email", title: "Email" }
|
|
470
|
+
]
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const records = [];
|
|
474
|
+
readableStream.on("data", (user) => {
|
|
475
|
+
records.push(user);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
readableStream.on("end", async () => {
|
|
479
|
+
await csvWriter.writeRecords(records);
|
|
480
|
+
console.log("โ
Export completed: users_export.csv");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Bulk import from stream
|
|
484
|
+
const writableStream = await users.writable();
|
|
485
|
+
importData.forEach(userData => {
|
|
486
|
+
writableStream.write(userData);
|
|
487
|
+
});
|
|
488
|
+
writableStream.end();
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### ๐ก๏ธ Document Behaviors
|
|
492
|
+
|
|
493
|
+
Handle documents that exceed S3's 2KB metadata limit:
|
|
494
|
+
|
|
495
|
+
```javascript
|
|
496
|
+
// Preserve all data by storing overflow in S3 body
|
|
497
|
+
const blogs = await s3db.createResource({
|
|
498
|
+
name: "blogs",
|
|
499
|
+
attributes: {
|
|
500
|
+
title: "string",
|
|
501
|
+
content: "string", // Can be very large
|
|
502
|
+
author: "string"
|
|
503
|
+
},
|
|
504
|
+
behavior: "body-overflow" // Handles large content automatically
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Strict validation - throws error if limit exceeded
|
|
508
|
+
const settings = await s3db.createResource({
|
|
509
|
+
name: "settings",
|
|
510
|
+
attributes: {
|
|
511
|
+
key: "string",
|
|
512
|
+
value: "string"
|
|
513
|
+
},
|
|
514
|
+
behavior: "enforce-limits" // Ensures data stays within 2KB
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Smart truncation - preserves structure, truncates content
|
|
518
|
+
const summaries = await s3db.createResource({
|
|
519
|
+
name: "summaries",
|
|
520
|
+
attributes: {
|
|
521
|
+
title: "string",
|
|
522
|
+
description: "string"
|
|
523
|
+
},
|
|
524
|
+
behavior: "data-truncate" // Truncates to fit within limits
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### ๐ Resource Versioning System
|
|
529
|
+
|
|
530
|
+
s3db.js includes a powerful versioning system that automatically manages schema evolution and data migration:
|
|
531
|
+
|
|
532
|
+
#### Enable Versioning
|
|
533
|
+
|
|
534
|
+
```javascript
|
|
535
|
+
// Enable versioning at database level
|
|
536
|
+
const s3db = new S3db({
|
|
537
|
+
uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
538
|
+
versioningEnabled: true // Enable versioning for all resources
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Create versioned resource
|
|
542
|
+
const users = await s3db.createResource({
|
|
543
|
+
name: "users",
|
|
544
|
+
attributes: {
|
|
545
|
+
name: "string|required",
|
|
546
|
+
email: "string|required",
|
|
547
|
+
status: "string|required"
|
|
548
|
+
},
|
|
549
|
+
versioningEnabled: true // Enable for this specific resource
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
#### Automatic Version Management
|
|
554
|
+
|
|
555
|
+
```javascript
|
|
556
|
+
// Initial version (v0) - basic user data
|
|
557
|
+
const users = await s3db.createResource({
|
|
558
|
+
name: "users",
|
|
559
|
+
attributes: {
|
|
560
|
+
name: "string|required",
|
|
561
|
+
email: "string|required"
|
|
562
|
+
},
|
|
563
|
+
versioningEnabled: true
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Insert users in v0
|
|
567
|
+
const user1 = await users.insert({
|
|
568
|
+
name: "John Doe",
|
|
569
|
+
email: "john@example.com"
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Update schema - automatically creates v1
|
|
573
|
+
const updatedUsers = await s3db.createResource({
|
|
574
|
+
name: "users",
|
|
575
|
+
attributes: {
|
|
576
|
+
name: "string|required",
|
|
577
|
+
email: "string|required",
|
|
578
|
+
age: "number|optional", // New field
|
|
579
|
+
profile: "object|optional" // New nested object
|
|
580
|
+
},
|
|
581
|
+
versioningEnabled: true
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// User1 now has _v: "v0" metadata
|
|
585
|
+
// New users will have _v: "v1" metadata
|
|
586
|
+
const user2 = await updatedUsers.insert({
|
|
587
|
+
name: "Jane Smith",
|
|
588
|
+
email: "jane@example.com",
|
|
589
|
+
age: 30,
|
|
590
|
+
profile: { bio: "Software developer" }
|
|
591
|
+
});
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
#### Automatic Data Migration
|
|
595
|
+
|
|
596
|
+
```javascript
|
|
597
|
+
// Get user from old version - automatically migrated
|
|
598
|
+
const migratedUser = await updatedUsers.get(user1.id);
|
|
599
|
+
console.log(migratedUser._v); // "v1" - automatically migrated
|
|
600
|
+
console.log(migratedUser.age); // undefined (new field)
|
|
601
|
+
console.log(migratedUser.profile); // undefined (new field)
|
|
602
|
+
|
|
603
|
+
// Update user - migrates to current version
|
|
604
|
+
const updatedUser = await updatedUsers.update(user1.id, {
|
|
605
|
+
name: "John Doe",
|
|
606
|
+
email: "john@example.com",
|
|
607
|
+
age: 35, // Add new field
|
|
608
|
+
profile: { bio: "Updated bio" }
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
console.log(updatedUser._v); // "v1" - now on current version
|
|
612
|
+
console.log(updatedUser.age); // 35
|
|
613
|
+
console.log(updatedUser.profile); // { bio: "Updated bio" }
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
#### Historical Data Preservation
|
|
617
|
+
|
|
618
|
+
```javascript
|
|
619
|
+
// When versioning is enabled, old versions are preserved
|
|
620
|
+
// Historical data is stored in: ./resource=users/historical/id=user1
|
|
621
|
+
|
|
622
|
+
// The system automatically:
|
|
623
|
+
// 1. Detects schema changes via hash comparison
|
|
624
|
+
// 2. Increments version number (v0 โ v1 โ v2...)
|
|
625
|
+
// 3. Preserves old data in historical storage
|
|
626
|
+
// 4. Migrates data when accessed or updated
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
#### Version Partitions
|
|
630
|
+
|
|
631
|
+
```javascript
|
|
632
|
+
// Automatic version partition is created when versioning is enabled
|
|
633
|
+
const users = await s3db.createResource({
|
|
634
|
+
name: "users",
|
|
635
|
+
attributes: {
|
|
636
|
+
name: "string|required",
|
|
637
|
+
email: "string|required"
|
|
638
|
+
},
|
|
639
|
+
partitions: {
|
|
640
|
+
byStatus: { fields: { status: "string" } }
|
|
641
|
+
},
|
|
642
|
+
versioningEnabled: true
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Automatically adds: byVersion: { fields: { _v: "string" } }
|
|
646
|
+
console.log(users.config.partitions.byVersion); // { fields: { _v: "string" } }
|
|
647
|
+
|
|
648
|
+
// Query by version
|
|
649
|
+
const v0Users = await users.list({
|
|
650
|
+
partition: "byVersion",
|
|
651
|
+
partitionValues: { _v: "v0" }
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const v1Users = await users.list({
|
|
655
|
+
partition: "byVersion",
|
|
656
|
+
partitionValues: { _v: "v1" }
|
|
657
|
+
});
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### ๐ Custom ID Generation
|
|
661
|
+
|
|
662
|
+
s3db.js supports flexible ID generation strategies:
|
|
663
|
+
|
|
664
|
+
#### Built-in ID Sizes
|
|
665
|
+
|
|
666
|
+
```javascript
|
|
667
|
+
// Default 22-character IDs
|
|
668
|
+
const defaultUsers = await s3db.createResource({
|
|
669
|
+
name: "users",
|
|
670
|
+
attributes: { name: "string|required" }
|
|
671
|
+
// Uses default 22-character nanoid
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Custom size IDs
|
|
675
|
+
const shortUsers = await s3db.createResource({
|
|
676
|
+
name: "short-users",
|
|
677
|
+
attributes: { name: "string|required" },
|
|
678
|
+
idSize: 8 // Generate 8-character IDs
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
const longUsers = await s3db.createResource({
|
|
682
|
+
name: "long-users",
|
|
683
|
+
attributes: { name: "string|required" },
|
|
684
|
+
idSize: 32 // Generate 32-character IDs
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
#### UUID Support
|
|
689
|
+
|
|
690
|
+
```javascript
|
|
691
|
+
import { v4 as uuidv4, v1 as uuidv1 } from 'uuid';
|
|
692
|
+
|
|
693
|
+
// UUID v4 (random)
|
|
694
|
+
const uuidUsers = await s3db.createResource({
|
|
695
|
+
name: "uuid-users",
|
|
696
|
+
attributes: { name: "string|required" },
|
|
697
|
+
idGenerator: uuidv4 // Pass UUID function directly
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// UUID v1 (time-based)
|
|
701
|
+
const timeUsers = await s3db.createResource({
|
|
702
|
+
name: "time-users",
|
|
703
|
+
attributes: { name: "string|required" },
|
|
704
|
+
idGenerator: uuidv1
|
|
705
|
+
});
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
#### Custom ID Functions
|
|
709
|
+
|
|
710
|
+
```javascript
|
|
711
|
+
// Timestamp-based IDs
|
|
712
|
+
const timestampUsers = await s3db.createResource({
|
|
713
|
+
name: "timestamp-users",
|
|
714
|
+
attributes: { name: "string|required" },
|
|
715
|
+
idGenerator: () => `user_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// Sequential IDs
|
|
719
|
+
let counter = 0;
|
|
720
|
+
const sequentialUsers = await s3db.createResource({
|
|
721
|
+
name: "sequential-users",
|
|
722
|
+
attributes: { name: "string|required" },
|
|
723
|
+
idGenerator: () => `USER_${String(++counter).padStart(6, '0')}`
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// Prefixed IDs
|
|
727
|
+
const prefixedUsers = await s3db.createResource({
|
|
728
|
+
name: "prefixed-users",
|
|
729
|
+
attributes: { name: "string|required" },
|
|
730
|
+
idGenerator: () => `CUSTOM_${Math.random().toString(36).substr(2, 10).toUpperCase()}`
|
|
731
|
+
});
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
#### ID Generator Priority
|
|
735
|
+
|
|
736
|
+
```javascript
|
|
737
|
+
// Priority order: idGenerator function > idGenerator number > idSize > default
|
|
738
|
+
const users = await s3db.createResource({
|
|
739
|
+
name: "users",
|
|
740
|
+
attributes: { name: "string|required" },
|
|
741
|
+
idGenerator: () => "custom-id", // This takes precedence
|
|
742
|
+
idSize: 16 // This is ignored
|
|
743
|
+
});
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
### ๐ Plugin System
|
|
747
|
+
|
|
748
|
+
Extend s3db.js functionality with plugins:
|
|
749
|
+
|
|
750
|
+
#### Built-in Plugins
|
|
751
|
+
|
|
752
|
+
```javascript
|
|
753
|
+
import { CachePlugin, CostsPlugin } from 's3db.js';
|
|
754
|
+
|
|
755
|
+
// Enable caching and cost tracking
|
|
756
|
+
const s3db = new S3db({
|
|
757
|
+
uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
758
|
+
plugins: [CachePlugin, CostsPlugin]
|
|
759
|
+
});
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
#### Cache Plugin
|
|
763
|
+
|
|
764
|
+
```javascript
|
|
765
|
+
// Automatic caching for read operations
|
|
766
|
+
const users = await s3db.createResource({
|
|
767
|
+
name: "users",
|
|
768
|
+
attributes: { name: "string|required" }
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// These operations are automatically cached:
|
|
772
|
+
await users.count(); // Cached count
|
|
773
|
+
await users.list(); // Cached list
|
|
774
|
+
await users.getMany([...]); // Cached bulk get
|
|
775
|
+
await users.page({...}); // Cached pagination
|
|
776
|
+
|
|
777
|
+
// Write operations automatically clear cache:
|
|
778
|
+
await users.insert({...}); // Clears cache
|
|
779
|
+
await users.update(id, {...}); // Clears cache
|
|
780
|
+
await users.delete(id); // Clears cache
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
#### Costs Plugin
|
|
784
|
+
|
|
785
|
+
```javascript
|
|
786
|
+
// Track AWS S3 costs in real-time
|
|
787
|
+
const s3db = new S3db({
|
|
788
|
+
uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
789
|
+
plugins: [CostsPlugin]
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// Monitor costs during operations
|
|
793
|
+
await users.insert({ name: "John" });
|
|
794
|
+
await users.get("user-123");
|
|
795
|
+
await users.list();
|
|
796
|
+
|
|
797
|
+
// Check current costs
|
|
798
|
+
console.log(s3db.client.costs);
|
|
799
|
+
// {
|
|
800
|
+
// total: 0.000009,
|
|
801
|
+
// requests: { total: 3, put: 1, get: 2 },
|
|
802
|
+
// events: { PutObjectCommand: 1, GetObjectCommand: 1, HeadObjectCommand: 1 }
|
|
803
|
+
// }
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
#### Custom Plugins
|
|
807
|
+
|
|
808
|
+
```javascript
|
|
809
|
+
// Create custom plugin
|
|
810
|
+
const MyCustomPlugin = {
|
|
811
|
+
async setup(database) {
|
|
812
|
+
this.database = database;
|
|
813
|
+
console.log('Custom plugin setup');
|
|
814
|
+
},
|
|
815
|
+
|
|
816
|
+
async start() {
|
|
817
|
+
console.log('Custom plugin started');
|
|
818
|
+
},
|
|
819
|
+
|
|
820
|
+
async stop() {
|
|
821
|
+
console.log('Custom plugin stopped');
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// Use custom plugin
|
|
826
|
+
const s3db = new S3db({
|
|
827
|
+
uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
828
|
+
plugins: [MyCustomPlugin, CachePlugin]
|
|
829
|
+
});
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### ๐๏ธ Advanced Behaviors
|
|
833
|
+
|
|
834
|
+
Choose the right behavior strategy for your use case:
|
|
835
|
+
|
|
836
|
+
#### Behavior Comparison
|
|
837
|
+
|
|
838
|
+
| Behavior | Use Case | 2KB Limit | Data Loss | Performance |
|
|
839
|
+
|----------|----------|------------|-----------|-------------|
|
|
840
|
+
| `user-management` | Development/Testing | Warns | No | High |
|
|
841
|
+
| `enforce-limits` | Production/Strict | Throws Error | No | High |
|
|
842
|
+
| `data-truncate` | Content Management | Truncates | Yes | High |
|
|
843
|
+
| `body-overflow` | Large Documents | Uses S3 Body | No | Medium |
|
|
844
|
+
|
|
845
|
+
#### User Management Behavior (Default)
|
|
846
|
+
|
|
847
|
+
```javascript
|
|
848
|
+
// Flexible behavior - warns but doesn't block
|
|
849
|
+
const users = await s3db.createResource({
|
|
850
|
+
name: "users",
|
|
851
|
+
attributes: { name: "string", bio: "string" },
|
|
852
|
+
behavior: "user-management" // Default
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
// Listen for limit warnings
|
|
856
|
+
users.on('exceedsLimit', (data) => {
|
|
857
|
+
console.warn(`Data exceeds 2KB limit by ${data.excess} bytes`);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// Operation continues despite warning
|
|
861
|
+
await users.insert({
|
|
862
|
+
name: "John",
|
|
863
|
+
bio: "A".repeat(3000) // > 2KB
|
|
864
|
+
});
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
#### Enforce Limits Behavior
|
|
868
|
+
|
|
869
|
+
```javascript
|
|
870
|
+
// Strict validation - throws error if limit exceeded
|
|
871
|
+
const settings = await s3db.createResource({
|
|
872
|
+
name: "settings",
|
|
873
|
+
attributes: { key: "string", value: "string" },
|
|
874
|
+
behavior: "enforce-limits"
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
// Throws error if data > 2KB
|
|
878
|
+
await settings.insert({
|
|
879
|
+
key: "large_setting",
|
|
880
|
+
value: "A".repeat(3000) // Throws: "S3 metadata size exceeds 2KB limit"
|
|
881
|
+
});
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
#### Data Truncate Behavior
|
|
885
|
+
|
|
886
|
+
```javascript
|
|
887
|
+
// Smart truncation - preserves structure, truncates content
|
|
888
|
+
const summaries = await s3db.createResource({
|
|
889
|
+
name: "summaries",
|
|
890
|
+
attributes: {
|
|
891
|
+
title: "string",
|
|
892
|
+
description: "string",
|
|
893
|
+
content: "string"
|
|
894
|
+
},
|
|
895
|
+
behavior: "data-truncate"
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
// Automatically truncates to fit within 2KB
|
|
899
|
+
const result = await summaries.insert({
|
|
900
|
+
title: "Short Title",
|
|
901
|
+
description: "A".repeat(1000),
|
|
902
|
+
content: "B".repeat(2000) // Will be truncated with "..."
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
// Retrieved data shows truncation
|
|
906
|
+
const retrieved = await summaries.get(result.id);
|
|
907
|
+
console.log(retrieved.content); // "B...B..." (truncated)
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
#### Body Overflow Behavior
|
|
911
|
+
|
|
912
|
+
```javascript
|
|
913
|
+
// Preserve all data by using S3 object body
|
|
914
|
+
const blogs = await s3db.createResource({
|
|
915
|
+
name: "blogs",
|
|
916
|
+
attributes: {
|
|
917
|
+
title: "string",
|
|
918
|
+
content: "string", // Can be very large
|
|
919
|
+
author: "string"
|
|
920
|
+
},
|
|
921
|
+
behavior: "body-overflow"
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
// Large content is automatically split between metadata and body
|
|
925
|
+
const blog = await blogs.insert({
|
|
926
|
+
title: "My Blog Post",
|
|
927
|
+
content: "A".repeat(5000), // Large content
|
|
928
|
+
author: "John Doe"
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// All data is preserved and accessible
|
|
932
|
+
const retrieved = await blogs.get(blog.id);
|
|
933
|
+
console.log(retrieved.content.length); // 5000 (full content preserved)
|
|
934
|
+
console.log(retrieved._hasContent); // true (indicates body usage)
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
### ๐ Advanced Streaming API
|
|
938
|
+
|
|
939
|
+
Handle large datasets efficiently with advanced streaming capabilities:
|
|
940
|
+
|
|
941
|
+
#### Readable Streams
|
|
942
|
+
|
|
943
|
+
```javascript
|
|
944
|
+
// Configure streaming with custom batch size and concurrency
|
|
945
|
+
const readableStream = await users.readable({
|
|
946
|
+
batchSize: 50, // Process 50 items per batch
|
|
947
|
+
concurrency: 10 // 10 concurrent operations
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// Process data as it streams
|
|
951
|
+
readableStream.on('data', (user) => {
|
|
952
|
+
console.log(`Processing user: ${user.name}`);
|
|
953
|
+
// Process each user individually
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
readableStream.on('error', (error) => {
|
|
957
|
+
console.error('Stream error:', error);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
readableStream.on('end', () => {
|
|
961
|
+
console.log('Stream completed');
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// Pause and resume streaming
|
|
965
|
+
readableStream.pause();
|
|
966
|
+
setTimeout(() => readableStream.resume(), 1000);
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
#### Writable Streams
|
|
970
|
+
|
|
971
|
+
```javascript
|
|
972
|
+
// Configure writable stream for bulk operations
|
|
973
|
+
const writableStream = await users.writable({
|
|
974
|
+
batchSize: 25, // Write 25 items per batch
|
|
975
|
+
concurrency: 5 // 5 concurrent writes
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
// Write data to stream
|
|
979
|
+
const userData = [
|
|
980
|
+
{ name: 'User 1', email: 'user1@example.com' },
|
|
981
|
+
{ name: 'User 2', email: 'user2@example.com' },
|
|
982
|
+
// ... thousands more
|
|
983
|
+
];
|
|
984
|
+
|
|
985
|
+
userData.forEach(user => {
|
|
986
|
+
writableStream.write(user);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// End stream and wait for completion
|
|
990
|
+
writableStream.on('finish', () => {
|
|
991
|
+
console.log('All users written successfully');
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
writableStream.on('error', (error) => {
|
|
995
|
+
console.error('Write error:', error);
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
writableStream.end();
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
#### Stream Error Handling
|
|
1002
|
+
|
|
1003
|
+
```javascript
|
|
1004
|
+
// Handle errors gracefully in streams
|
|
1005
|
+
const stream = await users.readable();
|
|
1006
|
+
|
|
1007
|
+
stream.on('error', (error, item) => {
|
|
1008
|
+
console.error(`Error processing item:`, error);
|
|
1009
|
+
console.log('Problematic item:', item);
|
|
1010
|
+
// Continue processing other items
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// Custom error handling for specific operations
|
|
1014
|
+
stream.on('data', async (user) => {
|
|
1015
|
+
try {
|
|
1016
|
+
await processUser(user);
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
console.error(`Failed to process user ${user.id}:`, error);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
### ๐ Binary Content Management
|
|
1024
|
+
|
|
1025
|
+
Store and manage binary content alongside your metadata:
|
|
1026
|
+
|
|
1027
|
+
#### Set Binary Content
|
|
1028
|
+
|
|
1029
|
+
```javascript
|
|
1030
|
+
import fs from 'fs';
|
|
1031
|
+
|
|
1032
|
+
// Set image content for user profile
|
|
1033
|
+
const imageBuffer = fs.readFileSync('profile.jpg');
|
|
1034
|
+
await users.setContent({
|
|
1035
|
+
id: 'user-123',
|
|
1036
|
+
buffer: imageBuffer,
|
|
1037
|
+
contentType: 'image/jpeg'
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
// Set document content
|
|
1041
|
+
const documentBuffer = fs.readFileSync('document.pdf');
|
|
1042
|
+
await users.setContent({
|
|
1043
|
+
id: 'user-123',
|
|
1044
|
+
buffer: documentBuffer,
|
|
1045
|
+
contentType: 'application/pdf'
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
// Set text content
|
|
1049
|
+
await users.setContent({
|
|
1050
|
+
id: 'user-123',
|
|
1051
|
+
buffer: 'Hello World',
|
|
1052
|
+
contentType: 'text/plain'
|
|
1053
|
+
});
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
#### Retrieve Binary Content
|
|
1057
|
+
|
|
1058
|
+
```javascript
|
|
1059
|
+
// Get binary content
|
|
1060
|
+
const content = await users.content('user-123');
|
|
1061
|
+
|
|
1062
|
+
if (content.buffer) {
|
|
1063
|
+
console.log('Content type:', content.contentType);
|
|
1064
|
+
console.log('Content size:', content.buffer.length);
|
|
1065
|
+
|
|
1066
|
+
// Save to file
|
|
1067
|
+
fs.writeFileSync('downloaded.jpg', content.buffer);
|
|
1068
|
+
} else {
|
|
1069
|
+
console.log('No content found');
|
|
1070
|
+
}
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
#### Content Management
|
|
1074
|
+
|
|
1075
|
+
```javascript
|
|
1076
|
+
// Check if content exists
|
|
1077
|
+
const hasContent = await users.hasContent('user-123');
|
|
1078
|
+
console.log('Has content:', hasContent);
|
|
1079
|
+
|
|
1080
|
+
// Delete content but preserve metadata
|
|
1081
|
+
await users.deleteContent('user-123');
|
|
1082
|
+
// User metadata remains, but binary content is removed
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
### ๐๏ธ Advanced Partitioning
|
|
1086
|
+
|
|
1087
|
+
Organize data efficiently with complex partitioning strategies:
|
|
1088
|
+
|
|
1089
|
+
#### Composite Partitions
|
|
1090
|
+
|
|
1091
|
+
```javascript
|
|
1092
|
+
// Partition with multiple fields
|
|
1093
|
+
const analytics = await s3db.createResource({
|
|
1094
|
+
name: "analytics",
|
|
1095
|
+
attributes: {
|
|
1096
|
+
userId: "string",
|
|
1097
|
+
event: "string",
|
|
1098
|
+
timestamp: "date",
|
|
1099
|
+
region: "string",
|
|
1100
|
+
device: "string"
|
|
1101
|
+
},
|
|
1102
|
+
partitions: {
|
|
1103
|
+
// Single field partition
|
|
1104
|
+
byEvent: { fields: { event: "string" } },
|
|
1105
|
+
|
|
1106
|
+
// Two field partition
|
|
1107
|
+
byEventAndRegion: {
|
|
1108
|
+
fields: {
|
|
1109
|
+
event: "string",
|
|
1110
|
+
region: "string"
|
|
1111
|
+
}
|
|
1112
|
+
},
|
|
1113
|
+
|
|
1114
|
+
// Three field partition
|
|
1115
|
+
byEventRegionDevice: {
|
|
1116
|
+
fields: {
|
|
1117
|
+
event: "string",
|
|
1118
|
+
region: "string",
|
|
1119
|
+
device: "string"
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
#### Nested Field Partitions
|
|
1127
|
+
|
|
1128
|
+
```javascript
|
|
1129
|
+
// Partition by nested object fields
|
|
1130
|
+
const users = await s3db.createResource({
|
|
1131
|
+
name: "users",
|
|
1132
|
+
attributes: {
|
|
1133
|
+
name: "string",
|
|
1134
|
+
profile: {
|
|
1135
|
+
country: "string",
|
|
1136
|
+
city: "string",
|
|
1137
|
+
preferences: {
|
|
1138
|
+
theme: "string"
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
partitions: {
|
|
1143
|
+
byCountry: { fields: { "profile.country": "string" } },
|
|
1144
|
+
byCity: { fields: { "profile.city": "string" } },
|
|
1145
|
+
byTheme: { fields: { "profile.preferences.theme": "string" } }
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// Query by nested field
|
|
1150
|
+
const usUsers = await users.list({
|
|
1151
|
+
partition: "byCountry",
|
|
1152
|
+
partitionValues: { "profile.country": "US" }
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
// Note: The system automatically manages partition references internally
|
|
1156
|
+
// Users should use standard list() method with partition parameters
|
|
1157
|
+
|
|
1158
|
+
#### Automatic Timestamp Partitions
|
|
1159
|
+
|
|
1160
|
+
```javascript
|
|
1161
|
+
// Enable automatic timestamp partitions
|
|
1162
|
+
const events = await s3db.createResource({
|
|
1163
|
+
name: "events",
|
|
1164
|
+
attributes: {
|
|
1165
|
+
name: "string",
|
|
1166
|
+
data: "object"
|
|
1167
|
+
},
|
|
1168
|
+
timestamps: true // Automatically adds byCreatedDate and byUpdatedDate
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
// Query by creation date
|
|
1172
|
+
const todayEvents = await events.list({
|
|
1173
|
+
partition: "byCreatedDate",
|
|
1174
|
+
partitionValues: { createdAt: "2024-01-15" }
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
// Query by update date
|
|
1178
|
+
const recentlyUpdated = await events.list({
|
|
1179
|
+
partition: "byUpdatedDate",
|
|
1180
|
+
partitionValues: { updatedAt: "2024-01-15" }
|
|
1181
|
+
});
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
#### Partition Validation
|
|
1185
|
+
|
|
1186
|
+
```javascript
|
|
1187
|
+
// Partitions are automatically validated against attributes
|
|
1188
|
+
const users = await s3db.createResource({
|
|
1189
|
+
name: "users",
|
|
1190
|
+
attributes: {
|
|
1191
|
+
name: "string",
|
|
1192
|
+
email: "string",
|
|
1193
|
+
status: "string"
|
|
1194
|
+
},
|
|
1195
|
+
partitions: {
|
|
1196
|
+
byStatus: { fields: { status: "string" } }, // โ
Valid
|
|
1197
|
+
byEmail: { fields: { email: "string" } } // โ
Valid
|
|
1198
|
+
// byInvalid: { fields: { invalid: "string" } } // โ Would throw error
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
### ๐ฃ Advanced Hooks System
|
|
1204
|
+
|
|
1205
|
+
Extend functionality with comprehensive hook system:
|
|
1206
|
+
|
|
1207
|
+
#### Hook Execution Order
|
|
451
1208
|
|
|
452
1209
|
```javascript
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
1210
|
+
const users = await s3db.createResource({
|
|
1211
|
+
name: "users",
|
|
1212
|
+
attributes: { name: "string", email: "string" },
|
|
1213
|
+
hooks: {
|
|
1214
|
+
preInsert: [
|
|
1215
|
+
async (data) => {
|
|
1216
|
+
console.log('1. Pre-insert hook 1');
|
|
1217
|
+
data.timestamp = new Date().toISOString();
|
|
1218
|
+
return data;
|
|
1219
|
+
},
|
|
1220
|
+
async (data) => {
|
|
1221
|
+
console.log('2. Pre-insert hook 2');
|
|
1222
|
+
data.processed = true;
|
|
1223
|
+
return data;
|
|
1224
|
+
}
|
|
1225
|
+
],
|
|
1226
|
+
afterInsert: [
|
|
1227
|
+
async (data) => {
|
|
1228
|
+
console.log('3. After-insert hook 1');
|
|
1229
|
+
await sendWelcomeEmail(data.email);
|
|
1230
|
+
},
|
|
1231
|
+
async (data) => {
|
|
1232
|
+
console.log('4. After-insert hook 2');
|
|
1233
|
+
await updateAnalytics(data);
|
|
1234
|
+
}
|
|
1235
|
+
]
|
|
1236
|
+
}
|
|
462
1237
|
});
|
|
463
1238
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
records.push(user);
|
|
467
|
-
});
|
|
1239
|
+
// Execution order: preInsert hooks โ insert โ afterInsert hooks
|
|
1240
|
+
```
|
|
468
1241
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
1242
|
+
#### Version-Specific Hooks
|
|
1243
|
+
|
|
1244
|
+
```javascript
|
|
1245
|
+
// Hooks that respond to version changes
|
|
1246
|
+
const users = await s3db.createResource({
|
|
1247
|
+
name: "users",
|
|
1248
|
+
attributes: { name: "string", email: "string" },
|
|
1249
|
+
versioningEnabled: true,
|
|
1250
|
+
hooks: {
|
|
1251
|
+
preInsert: [
|
|
1252
|
+
async (data) => {
|
|
1253
|
+
// Access resource context
|
|
1254
|
+
console.log('Current version:', this.version);
|
|
1255
|
+
return data;
|
|
1256
|
+
}
|
|
1257
|
+
]
|
|
1258
|
+
}
|
|
472
1259
|
});
|
|
473
1260
|
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
writableStream.write(userData);
|
|
1261
|
+
// Listen for version updates
|
|
1262
|
+
users.on('versionUpdated', ({ oldVersion, newVersion }) => {
|
|
1263
|
+
console.log(`Resource updated from ${oldVersion} to ${newVersion}`);
|
|
478
1264
|
});
|
|
479
|
-
writableStream.end();
|
|
480
1265
|
```
|
|
481
1266
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
Handle documents that exceed S3's 2KB metadata limit:
|
|
1267
|
+
#### Error Handling in Hooks
|
|
485
1268
|
|
|
486
1269
|
```javascript
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
name: "
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
1270
|
+
const users = await s3db.createResource({
|
|
1271
|
+
name: "users",
|
|
1272
|
+
attributes: { name: "string", email: "string" },
|
|
1273
|
+
hooks: {
|
|
1274
|
+
preInsert: [
|
|
1275
|
+
async (data) => {
|
|
1276
|
+
try {
|
|
1277
|
+
// Validate external service
|
|
1278
|
+
await validateEmail(data.email);
|
|
1279
|
+
return data;
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
// Transform error or add context
|
|
1282
|
+
throw new Error(`Email validation failed: ${error.message}`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
],
|
|
1286
|
+
afterInsert: [
|
|
1287
|
+
async (data) => {
|
|
1288
|
+
try {
|
|
1289
|
+
await sendWelcomeEmail(data.email);
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
// Log but don't fail the operation
|
|
1292
|
+
console.error('Failed to send welcome email:', error);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
]
|
|
1296
|
+
}
|
|
496
1297
|
});
|
|
1298
|
+
```
|
|
497
1299
|
|
|
498
|
-
|
|
499
|
-
const settings = await s3db.createResource({
|
|
500
|
-
name: "settings",
|
|
501
|
-
attributes: {
|
|
502
|
-
key: "string",
|
|
503
|
-
value: "string"
|
|
504
|
-
},
|
|
505
|
-
behavior: "enforce-limits" // Ensures data stays within 2KB
|
|
506
|
-
});
|
|
1300
|
+
#### Hook Context and Binding
|
|
507
1301
|
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
name: "
|
|
511
|
-
attributes: {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1302
|
+
```javascript
|
|
1303
|
+
const users = await s3db.createResource({
|
|
1304
|
+
name: "users",
|
|
1305
|
+
attributes: { name: "string", email: "string" },
|
|
1306
|
+
hooks: {
|
|
1307
|
+
preInsert: [
|
|
1308
|
+
async function(data) {
|
|
1309
|
+
// 'this' is bound to the resource instance
|
|
1310
|
+
console.log('Resource name:', this.name);
|
|
1311
|
+
console.log('Resource version:', this.version);
|
|
1312
|
+
|
|
1313
|
+
// Access resource methods
|
|
1314
|
+
const exists = await this.exists(data.id);
|
|
1315
|
+
if (exists) {
|
|
1316
|
+
throw new Error('User already exists');
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
return data;
|
|
1320
|
+
}
|
|
1321
|
+
]
|
|
1322
|
+
}
|
|
516
1323
|
});
|
|
517
1324
|
```
|
|
518
1325
|
|
|
@@ -539,17 +1346,55 @@ const summaries = await s3db.createResource({
|
|
|
539
1346
|
| `upsert(id, data)` | Insert or update | `await users.upsert("user-123", {...})` |
|
|
540
1347
|
| `delete(id)` | Delete document | `await users.delete("user-123")` |
|
|
541
1348
|
| `exists(id)` | Check existence | `await users.exists("user-123")` |
|
|
1349
|
+
| `setContent({id, buffer, contentType})` | Set binary content | `await users.setContent({id: "123", buffer: imageBuffer})` |
|
|
1350
|
+
| `content(id)` | Get binary content | `await users.content("user-123")` |
|
|
1351
|
+
| `hasContent(id)` | Check if has content | `await users.hasContent("user-123")` |
|
|
1352
|
+
| `deleteContent(id)` | Remove content | `await users.deleteContent("user-123")` |
|
|
542
1353
|
|
|
543
1354
|
### ๐ Query Operations
|
|
544
1355
|
|
|
545
1356
|
| Method | Description | Example |
|
|
546
1357
|
|--------|-------------|---------|
|
|
547
|
-
| `list(options?)` | List documents | `await users.list()` |
|
|
1358
|
+
| `list(options?)` | List documents with pagination & partitions | `await users.list({limit: 10, offset: 0})` |
|
|
548
1359
|
| `listIds(options?)` | List document IDs | `await users.listIds()` |
|
|
549
1360
|
| `count(options?)` | Count documents | `await users.count()` |
|
|
550
1361
|
| `page(options)` | Paginate results | `await users.page({offset: 0, size: 10})` |
|
|
551
1362
|
| `query(filter, options?)` | Filter documents | `await users.query({isActive: true})` |
|
|
552
1363
|
|
|
1364
|
+
#### ๐ List vs GetAll - When to Use Each
|
|
1365
|
+
|
|
1366
|
+
**`list(options?)`** - Advanced listing with full control:
|
|
1367
|
+
```javascript
|
|
1368
|
+
// Simple listing (equivalent to getAll)
|
|
1369
|
+
const allUsers = await users.list();
|
|
1370
|
+
|
|
1371
|
+
// With pagination
|
|
1372
|
+
const first10 = await users.list({ limit: 10, offset: 0 });
|
|
1373
|
+
|
|
1374
|
+
// With partitions
|
|
1375
|
+
const usUsers = await users.list({
|
|
1376
|
+
partition: "byCountry",
|
|
1377
|
+
partitionValues: { "profile.country": "US" }
|
|
1378
|
+
});
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
**`getAll()`** - Simple listing for all documents:
|
|
1382
|
+
```javascript
|
|
1383
|
+
// Get all documents (no options, no pagination)
|
|
1384
|
+
const allUsers = await users.getAll();
|
|
1385
|
+
console.log(`Total users: ${allUsers.length}`);
|
|
1386
|
+
```
|
|
1387
|
+
|
|
1388
|
+
**Choose `getAll()` when:**
|
|
1389
|
+
- โ
You want all documents without pagination
|
|
1390
|
+
- โ
You don't need partition filtering
|
|
1391
|
+
- โ
You prefer simplicity over flexibility
|
|
1392
|
+
|
|
1393
|
+
**Choose `list()` when:**
|
|
1394
|
+
- โ
You need pagination control
|
|
1395
|
+
- โ
You want to filter by partitions
|
|
1396
|
+
- โ
You need more control over the query
|
|
1397
|
+
|
|
553
1398
|
### ๐ Bulk Operations
|
|
554
1399
|
|
|
555
1400
|
| Method | Description | Example |
|
|
@@ -560,6 +1405,15 @@ const summaries = await s3db.createResource({
|
|
|
560
1405
|
| `getAll()` | Get all documents | `await users.getAll()` |
|
|
561
1406
|
| `deleteAll()` | Delete all documents | `await users.deleteAll()` |
|
|
562
1407
|
|
|
1408
|
+
### ๐ Streaming Operations
|
|
1409
|
+
|
|
1410
|
+
| Method | Description | Example |
|
|
1411
|
+
|--------|-------------|---------|
|
|
1412
|
+
| `readable(options?)` | Create readable stream | `await users.readable({batchSize: 50})` |
|
|
1413
|
+
| `writable(options?)` | Create writable stream | `await users.writable({batchSize: 25})` |
|
|
1414
|
+
|
|
1415
|
+
|
|
1416
|
+
|
|
563
1417
|
---
|
|
564
1418
|
|
|
565
1419
|
## ๐จ Examples
|
|
@@ -601,6 +1455,14 @@ const johnsPosts = await posts.list({
|
|
|
601
1455
|
partition: "byAuthor",
|
|
602
1456
|
partitionValues: { author: "john_doe" }
|
|
603
1457
|
});
|
|
1458
|
+
|
|
1459
|
+
// Get all posts (simple approach)
|
|
1460
|
+
const allPosts = await posts.getAll();
|
|
1461
|
+
console.log(`Total posts: ${allPosts.length}`);
|
|
1462
|
+
|
|
1463
|
+
// Get posts with pagination (advanced approach)
|
|
1464
|
+
const firstPage = await posts.list({ limit: 10, offset: 0 });
|
|
1465
|
+
const secondPage = await posts.list({ limit: 10, offset: 10 });
|
|
604
1466
|
```
|
|
605
1467
|
|
|
606
1468
|
### ๐ E-commerce Store
|
|
@@ -662,6 +1524,16 @@ const product = await products.insert({
|
|
|
662
1524
|
images: ["https://example.com/headphones-1.jpg"]
|
|
663
1525
|
});
|
|
664
1526
|
|
|
1527
|
+
// Get all products (simple listing)
|
|
1528
|
+
const allProducts = await products.getAll();
|
|
1529
|
+
console.log(`Total products: ${allProducts.length}`);
|
|
1530
|
+
|
|
1531
|
+
// Get products by category (partitioned listing)
|
|
1532
|
+
const electronics = await products.list({
|
|
1533
|
+
partition: "byCategory",
|
|
1534
|
+
partitionValues: { category: "electronics" }
|
|
1535
|
+
});
|
|
1536
|
+
|
|
665
1537
|
// Create an order
|
|
666
1538
|
const order = await orders.insert({
|
|
667
1539
|
customerId: "customer-123",
|
|
@@ -808,6 +1680,61 @@ const s3db = new S3db({
|
|
|
808
1680
|
});
|
|
809
1681
|
```
|
|
810
1682
|
|
|
1683
|
+
### โ๏ธ Advanced Configuration Options
|
|
1684
|
+
|
|
1685
|
+
#### Database Configuration
|
|
1686
|
+
|
|
1687
|
+
```javascript
|
|
1688
|
+
const s3db = new S3db({
|
|
1689
|
+
uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
1690
|
+
|
|
1691
|
+
// Versioning
|
|
1692
|
+
versioningEnabled: true, // Enable versioning for all resources
|
|
1693
|
+
|
|
1694
|
+
// Performance
|
|
1695
|
+
parallelism: 25, // Concurrent operations (default: 10)
|
|
1696
|
+
|
|
1697
|
+
// Plugins
|
|
1698
|
+
plugins: [CachePlugin, CostsPlugin], // Enable plugins
|
|
1699
|
+
|
|
1700
|
+
// Security
|
|
1701
|
+
passphrase: "custom-secret-key", // Encryption key
|
|
1702
|
+
|
|
1703
|
+
// Debugging
|
|
1704
|
+
verbose: true, // Enable verbose logging
|
|
1705
|
+
});
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
#### Resource Configuration
|
|
1709
|
+
|
|
1710
|
+
```javascript
|
|
1711
|
+
const users = await s3db.createResource({
|
|
1712
|
+
name: "users",
|
|
1713
|
+
attributes: { name: "string", email: "string" },
|
|
1714
|
+
|
|
1715
|
+
// ID Generation
|
|
1716
|
+
idGenerator: uuidv4, // Custom ID generator function
|
|
1717
|
+
idSize: 16, // Custom ID size (if no idGenerator)
|
|
1718
|
+
|
|
1719
|
+
// Versioning
|
|
1720
|
+
versioningEnabled: true, // Enable for this resource
|
|
1721
|
+
|
|
1722
|
+
// Behavior Strategy
|
|
1723
|
+
behavior: "body-overflow", // How to handle large data
|
|
1724
|
+
|
|
1725
|
+
// Schema Options
|
|
1726
|
+
allNestedObjectsOptional: true, // Make nested objects optional
|
|
1727
|
+
autoDecrypt: true, // Auto-decrypt secret fields
|
|
1728
|
+
|
|
1729
|
+
// Security
|
|
1730
|
+
paranoid: true, // Security flag for dangerous operations
|
|
1731
|
+
|
|
1732
|
+
// Performance
|
|
1733
|
+
cache: false, // Enable caching for this resource
|
|
1734
|
+
parallelism: 10, // Resource-specific parallelism
|
|
1735
|
+
});
|
|
1736
|
+
```
|
|
1737
|
+
|
|
811
1738
|
---
|
|
812
1739
|
|
|
813
1740
|
## ๐ฐ Cost Analysis
|
|
@@ -981,6 +1908,193 @@ const page = await users.page({ offset: 0, size: 100 });
|
|
|
981
1908
|
});
|
|
982
1909
|
```
|
|
983
1910
|
|
|
1911
|
+
### ๐ง Troubleshooting
|
|
1912
|
+
|
|
1913
|
+
#### Common Issues and Solutions
|
|
1914
|
+
|
|
1915
|
+
**1. Data Exceeds 2KB Limit**
|
|
1916
|
+
```javascript
|
|
1917
|
+
// Problem: "S3 metadata size exceeds 2KB limit"
|
|
1918
|
+
// Solution: Use appropriate behavior strategy
|
|
1919
|
+
const users = await s3db.createResource({
|
|
1920
|
+
name: "users",
|
|
1921
|
+
attributes: { name: "string", bio: "string" },
|
|
1922
|
+
behavior: "body-overflow" // Handles large data automatically
|
|
1923
|
+
});
|
|
1924
|
+
```
|
|
1925
|
+
|
|
1926
|
+
**2. Version Conflicts**
|
|
1927
|
+
```javascript
|
|
1928
|
+
// Problem: Objects not migrating between versions
|
|
1929
|
+
// Solution: Ensure versioning is enabled and update objects
|
|
1930
|
+
const users = await s3db.createResource({
|
|
1931
|
+
name: "users",
|
|
1932
|
+
versioningEnabled: true
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
// Force migration by updating the object
|
|
1936
|
+
await users.update(userId, { ...existingData, newField: "value" });
|
|
1937
|
+
```
|
|
1938
|
+
|
|
1939
|
+
**3. Partition Validation Errors**
|
|
1940
|
+
```javascript
|
|
1941
|
+
// Problem: "Partition uses field that does not exist"
|
|
1942
|
+
// Solution: Ensure partition fields match attributes
|
|
1943
|
+
const users = await s3db.createResource({
|
|
1944
|
+
name: "users",
|
|
1945
|
+
attributes: {
|
|
1946
|
+
name: "string",
|
|
1947
|
+
email: "string",
|
|
1948
|
+
profile: { country: "string" }
|
|
1949
|
+
},
|
|
1950
|
+
partitions: {
|
|
1951
|
+
byEmail: { fields: { email: "string" } }, // โ
Valid
|
|
1952
|
+
byCountry: { fields: { "profile.country": "string" } } // โ
Valid
|
|
1953
|
+
// byInvalid: { fields: { invalid: "string" } } // โ Invalid
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
// Use standard list() method with partition parameters
|
|
1958
|
+
const results = await users.list({
|
|
1959
|
+
partition: "byEmail",
|
|
1960
|
+
partitionValues: { email: "user@example.com" }
|
|
1961
|
+
});
|
|
1962
|
+
```
|
|
1963
|
+
|
|
1964
|
+
**4. ID Generation Issues**
|
|
1965
|
+
```javascript
|
|
1966
|
+
// Problem: Custom ID generator not working
|
|
1967
|
+
// Solution: Check priority order and function signature
|
|
1968
|
+
const users = await s3db.createResource({
|
|
1969
|
+
name: "users",
|
|
1970
|
+
attributes: { name: "string" },
|
|
1971
|
+
idGenerator: () => `user_${Date.now()}`, // Must return string
|
|
1972
|
+
// idSize: 16 // This is ignored when idGenerator is provided
|
|
1973
|
+
});
|
|
1974
|
+
```
|
|
1975
|
+
|
|
1976
|
+
**5. Plugin Setup Issues**
|
|
1977
|
+
```javascript
|
|
1978
|
+
// Problem: Plugins not working
|
|
1979
|
+
// Solution: Ensure proper import and setup
|
|
1980
|
+
import { CachePlugin, CostsPlugin } from 's3db.js';
|
|
1981
|
+
|
|
1982
|
+
const s3db = new S3db({
|
|
1983
|
+
uri: "s3://...",
|
|
1984
|
+
plugins: [CachePlugin, CostsPlugin] // Array of plugin classes/functions
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
await s3db.connect(); // Plugins are initialized during connect
|
|
1988
|
+
```
|
|
1989
|
+
|
|
1990
|
+
**6. Streaming Performance Issues**
|
|
1991
|
+
```javascript
|
|
1992
|
+
// Problem: Streams too slow or memory intensive
|
|
1993
|
+
// Solution: Adjust batch size and concurrency
|
|
1994
|
+
const stream = await users.readable({
|
|
1995
|
+
batchSize: 10, // Smaller batches for memory
|
|
1996
|
+
concurrency: 5 // Fewer concurrent operations
|
|
1997
|
+
});
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
**7. Hook Execution Problems**
|
|
2001
|
+
```javascript
|
|
2002
|
+
// Problem: Hooks not executing or context issues
|
|
2003
|
+
// Solution: Use proper function binding and error handling
|
|
2004
|
+
const users = await s3db.createResource({
|
|
2005
|
+
name: "users",
|
|
2006
|
+
attributes: { name: "string" },
|
|
2007
|
+
hooks: {
|
|
2008
|
+
preInsert: [
|
|
2009
|
+
async function(data) { // Use function() for proper 'this' binding
|
|
2010
|
+
console.log('Resource name:', this.name);
|
|
2011
|
+
return data;
|
|
2012
|
+
}
|
|
2013
|
+
]
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
```
|
|
2017
|
+
|
|
2018
|
+
### ๐ก Events and Emitters
|
|
2019
|
+
|
|
2020
|
+
s3db.js uses Node.js EventEmitter for real-time notifications:
|
|
2021
|
+
|
|
2022
|
+
#### Resource Events
|
|
2023
|
+
|
|
2024
|
+
```javascript
|
|
2025
|
+
const users = await s3db.createResource({
|
|
2026
|
+
name: "users",
|
|
2027
|
+
attributes: { name: "string", email: "string" }
|
|
2028
|
+
});
|
|
2029
|
+
|
|
2030
|
+
// Listen for resource operations
|
|
2031
|
+
users.on('insert', (data) => {
|
|
2032
|
+
console.log('User inserted:', data.name);
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
users.on('update', (oldData, newData) => {
|
|
2036
|
+
console.log('User updated:', newData.name);
|
|
2037
|
+
});
|
|
2038
|
+
|
|
2039
|
+
users.on('delete', (id) => {
|
|
2040
|
+
console.log('User deleted:', id);
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
users.on('get', (data) => {
|
|
2044
|
+
console.log('User retrieved:', data.name);
|
|
2045
|
+
});
|
|
2046
|
+
```
|
|
2047
|
+
|
|
2048
|
+
#### Versioning Events
|
|
2049
|
+
|
|
2050
|
+
```javascript
|
|
2051
|
+
// Listen for version changes
|
|
2052
|
+
users.on('versionUpdated', ({ oldVersion, newVersion }) => {
|
|
2053
|
+
console.log(`Resource updated from ${oldVersion} to ${newVersion}`);
|
|
2054
|
+
});
|
|
2055
|
+
```
|
|
2056
|
+
|
|
2057
|
+
#### Behavior Events
|
|
2058
|
+
|
|
2059
|
+
```javascript
|
|
2060
|
+
// Listen for data limit warnings
|
|
2061
|
+
users.on('exceedsLimit', (data) => {
|
|
2062
|
+
console.warn(`Data exceeds 2KB limit by ${data.excess} bytes`);
|
|
2063
|
+
console.log('Operation:', data.operation);
|
|
2064
|
+
console.log('Resource ID:', data.id);
|
|
2065
|
+
});
|
|
2066
|
+
```
|
|
2067
|
+
|
|
2068
|
+
#### Database Events
|
|
2069
|
+
|
|
2070
|
+
```javascript
|
|
2071
|
+
// Listen for database-level events
|
|
2072
|
+
s3db.on('s3db.resourceCreated', (resourceName) => {
|
|
2073
|
+
console.log(`Resource created: ${resourceName}`);
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
s3db.on('s3db.resourceUpdated', (resourceName) => {
|
|
2077
|
+
console.log(`Resource updated: ${resourceName}`);
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
s3db.on('metadataUploaded', (metadata) => {
|
|
2081
|
+
console.log('Database metadata updated');
|
|
2082
|
+
});
|
|
2083
|
+
```
|
|
2084
|
+
|
|
2085
|
+
#### Plugin Events
|
|
2086
|
+
|
|
2087
|
+
```javascript
|
|
2088
|
+
// Listen for plugin-specific events
|
|
2089
|
+
s3db.on('cache.hit', (key) => {
|
|
2090
|
+
console.log('Cache hit:', key);
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
s3db.on('cache.miss', (key) => {
|
|
2094
|
+
console.log('Cache miss:', key);
|
|
2095
|
+
});
|
|
2096
|
+
```
|
|
2097
|
+
|
|
984
2098
|
---
|
|
985
2099
|
|
|
986
2100
|
## ๐งช Testing
|