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 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
- // Export all users to CSV
454
- const readableStream = await users.readable();
455
- const csvWriter = createObjectCsvWriter({
456
- path: "users_export.csv",
457
- header: [
458
- { id: "id", title: "ID" },
459
- { id: "name", title: "Name" },
460
- { id: "email", title: "Email" }
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
- const records = [];
465
- readableStream.on("data", (user) => {
466
- records.push(user);
467
- });
1239
+ // Execution order: preInsert hooks โ†’ insert โ†’ afterInsert hooks
1240
+ ```
468
1241
 
469
- readableStream.on("end", async () => {
470
- await csvWriter.writeRecords(records);
471
- console.log("โœ… Export completed: users_export.csv");
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
- // Bulk import from stream
475
- const writableStream = await users.writable();
476
- importData.forEach(userData => {
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
- ### ๐Ÿ›ก๏ธ Document Behaviors
483
-
484
- Handle documents that exceed S3's 2KB metadata limit:
1267
+ #### Error Handling in Hooks
485
1268
 
486
1269
  ```javascript
487
- // Preserve all data by storing overflow in S3 body
488
- const blogs = await s3db.createResource({
489
- name: "blogs",
490
- attributes: {
491
- title: "string",
492
- content: "string", // Can be very large
493
- author: "string"
494
- },
495
- behavior: "body-overflow" // Handles large content automatically
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
- // Strict validation - throws error if limit exceeded
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
- // Smart truncation - preserves structure, truncates content
509
- const summaries = await s3db.createResource({
510
- name: "summaries",
511
- attributes: {
512
- title: "string",
513
- description: "string"
514
- },
515
- behavior: "data-truncate" // Truncates to fit within limits
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