s3db.js 6.0.0 โ†’ 6.1.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
@@ -135,13 +135,17 @@ npm install s3db.js
135
135
  import { S3db } from "s3db.js";
136
136
 
137
137
  const s3db = new S3db({
138
- uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
138
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
139
139
  });
140
140
 
141
141
  await s3db.connect();
142
142
  console.log("๐ŸŽ‰ Connected to S3 database!");
143
143
  ```
144
144
 
145
+ > **โ„น๏ธ Note:** You do **not** need to provide `ACCESS_KEY` and `SECRET_KEY` in the connection string if your environment already has S3 permissions (e.g., via IAM Role on EKS, EC2, Lambda, or other compatible clouds). s3db.js will use the default AWS credential provider chain, so credentials can be omitted for role-based or environment-based authentication. This also applies to S3-compatible clouds (MinIO, DigitalOcean Spaces, etc.) if they support such mechanisms.
146
+
147
+ ---
148
+
145
149
  ### 3. Create your first resource
146
150
 
147
151
  ```javascript
@@ -151,24 +155,9 @@ const users = await s3db.createResource({
151
155
  name: "string|min:2|max:100",
152
156
  email: "email|unique",
153
157
  age: "number|integer|positive",
154
- isActive: "boolean",
155
- createdAt: "date"
156
- },
157
- timestamps: true,
158
- behavior: "user-management",
159
- partitions: {
160
- byRegion: { fields: { region: "string" } }
158
+ isActive: "boolean"
161
159
  },
162
- paranoid: true,
163
- autoDecrypt: true,
164
- cache: false,
165
- parallelism: 10,
166
- hooks: {
167
- preInsert: [async (data) => {
168
- console.log("Pre-insert:", data);
169
- return data;
170
- }]
171
- }
160
+ timestamps: true
172
161
  });
173
162
  ```
174
163
 
@@ -213,6 +202,28 @@ pnpm add s3db.js
213
202
  yarn add s3db.js
214
203
  ```
215
204
 
205
+ ### ๐Ÿ“ฆ Optional Dependencies
206
+
207
+ Some features require additional dependencies to be installed manually:
208
+
209
+ #### Replication Dependencies
210
+
211
+ If you plan to use the replication system with external services, install the corresponding dependencies:
212
+
213
+ ```bash
214
+ # For SQS replication (AWS SQS queues)
215
+ npm install @aws-sdk/client-sqs
216
+
217
+ # For BigQuery replication (Google BigQuery)
218
+ npm install @google-cloud/bigquery
219
+
220
+ # For PostgreSQL replication (PostgreSQL databases)
221
+ npm install pg
222
+ ```
223
+
224
+ **Why manual installation?** These are marked as `peerDependencies` to keep the main package lightweight. Only install what you need!
225
+ ```
226
+
216
227
  ### Environment Setup
217
228
 
218
229
  Create a `.env` file with your AWS credentials:
@@ -233,7 +244,7 @@ import dotenv from "dotenv";
233
244
  dotenv.config();
234
245
 
235
246
  const s3db = new S3db({
236
- uri: `s3://${process.env.AWS_ACCESS_KEY_ID}:${process.env.AWS_SECRET_ACCESS_KEY}@${process.env.AWS_BUCKET}/databases/${process.env.DATABASE_NAME}`
247
+ connectionString: `s3://${process.env.AWS_ACCESS_KEY_ID}:${process.env.AWS_SECRET_ACCESS_KEY}@${process.env.AWS_BUCKET}/databases/${process.env.DATABASE_NAME}`
237
248
  });
238
249
  ```
239
250
 
@@ -245,7 +256,7 @@ const s3db = new S3db({
245
256
  #### 1. Access Keys (Development)
246
257
  ```javascript
247
258
  const s3db = new S3db({
248
- uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
259
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
249
260
  });
250
261
  ```
251
262
 
@@ -253,14 +264,14 @@ const s3db = new S3db({
253
264
  ```javascript
254
265
  // No credentials needed - uses IAM role permissions
255
266
  const s3db = new S3db({
256
- uri: "s3://BUCKET_NAME/databases/myapp"
267
+ connectionString: "s3://BUCKET_NAME/databases/myapp"
257
268
  });
258
269
  ```
259
270
 
260
271
  #### 3. S3-Compatible Services (MinIO, etc.)
261
272
  ```javascript
262
273
  const s3db = new S3db({
263
- uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
274
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
264
275
  endpoint: "http://localhost:9000"
265
276
  });
266
277
  ```
@@ -276,92 +287,43 @@ A logical container for your resources, stored in a specific S3 prefix.
276
287
 
277
288
  ```javascript
278
289
  const s3db = new S3db({
279
- uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
290
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
280
291
  });
281
- // Creates/connects to: s3://bucket/databases/myapp/
282
292
  ```
283
293
 
284
294
  ### ๐Ÿ“‹ Resources (Collections)
285
295
  Resources define the structure of your documents, similar to tables in traditional databases.
286
296
 
287
- #### New Configuration Structure
288
-
289
- The Resource class now uses a unified configuration object where all options are passed directly in the config object:
290
-
291
297
  ```javascript
292
298
  const users = await s3db.createResource({
293
299
  name: "users",
294
300
  attributes: {
295
- // Basic types
296
301
  name: "string|min:2|max:100",
297
302
  email: "email|unique",
298
303
  age: "number|integer|positive",
299
304
  isActive: "boolean",
300
-
301
- // Nested objects
302
305
  profile: {
303
306
  bio: "string|optional",
304
- avatar: "url|optional",
305
- preferences: {
306
- theme: "string|enum:light,dark|default:light",
307
- notifications: "boolean|default:true"
308
- }
307
+ avatar: "url|optional"
309
308
  },
310
-
311
- // Arrays
312
309
  tags: "array|items:string|unique",
313
-
314
- // Encrypted fields
315
310
  password: "secret"
316
311
  },
317
- // All options are now at the root level
318
- timestamps: true, // Automatic createdAt/updatedAt
319
- behavior: "user-management", // How to handle large documents
320
- partitions: { // Organize data for efficient queries
312
+ timestamps: true,
313
+ behavior: "user-managed",
314
+ partitions: {
321
315
  byRegion: { fields: { region: "string" } }
322
- },
323
- paranoid: true, // Security flag for dangerous operations
324
- autoDecrypt: true, // Auto-decrypt secret fields
325
- cache: false, // Enable caching
326
- parallelism: 10, // Parallelism for bulk operations
327
- hooks: { // Custom hooks
328
- preInsert: [async (data) => {
329
- console.log("Pre-insert:", data);
330
- return data;
331
- }]
332
316
  }
333
317
  });
334
318
  ```
335
319
 
336
320
  ### ๐Ÿ” Schema Validation
337
- Built-in validation using [@icebob/fastest-validator](https://github.com/icebob/fastest-validator) for resource creation and partition validation. This powerful validation engine provides comprehensive rule support, excellent performance, and detailed error reporting for all your data validation needs.
338
-
339
- ```javascript
340
- const product = await products.insert({
341
- name: "Wireless Headphones",
342
- price: 99.99,
343
- category: "electronics",
344
- features: ["bluetooth", "noise-cancellation"],
345
- specifications: {
346
- battery: "30 hours",
347
- connectivity: "Bluetooth 5.0"
348
- }
349
- });
350
- ```
351
-
352
- **Validation Features powered by fastest-validator:**
353
- - โœ… **Comprehensive Rules** - String, number, array, object, date validation
354
- - โœ… **Nested Objects** - Deep validation for complex data structures
355
- - โœ… **Custom Rules** - Extend with your own validation logic
356
- - โœ… **Performance** - Optimized validation engine for speed
357
- - โœ… **Error Messages** - Detailed validation error reporting
321
+ Built-in validation using [@icebob/fastest-validator](https://github.com/icebob/fastest-validator) with comprehensive rule support and excellent performance.
358
322
 
359
323
  ---
360
324
 
361
325
  ## โšก Advanced Features
362
326
 
363
- s3db.js leverages [@icebob/fastest-validator](https://github.com/icebob/fastest-validator) as its core validation engine for both resource schemas and partition field validation, ensuring high-performance data validation with comprehensive rule support.
364
-
365
327
  ### ๐Ÿ“ฆ Partitions
366
328
 
367
329
  Organize data efficiently with partitions for faster queries:
@@ -372,32 +334,16 @@ const analytics = await s3db.createResource({
372
334
  attributes: {
373
335
  userId: "string",
374
336
  event: "string",
375
- timestamp: "date",
376
- utm: {
377
- source: "string",
378
- medium: "string",
379
- campaign: "string"
380
- }
337
+ timestamp: "date"
381
338
  },
382
339
  partitions: {
383
340
  byDate: { fields: { timestamp: "date|maxlength:10" } },
384
- byUtmSource: { fields: { "utm.source": "string" } },
385
- byUserAndDate: {
386
- fields: {
387
- userId: "string",
388
- timestamp: "date|maxlength:10"
389
- }
390
- }
341
+ byUserAndDate: { fields: { userId: "string", timestamp: "date|maxlength:10" } }
391
342
  }
392
343
  });
393
344
 
394
345
  // Query by partition for better performance
395
- const googleEvents = await analytics.list({
396
- partition: "byUtmSource",
397
- partitionValues: { "utm.source": "google" }
398
- });
399
-
400
- const todayEvents = await analytics.count({
346
+ const todayEvents = await analytics.list({
401
347
  partition: "byDate",
402
348
  partitionValues: { timestamp: "2024-01-15" }
403
349
  });
@@ -410,105 +356,35 @@ Add custom logic with pre/post operation hooks:
410
356
  ```javascript
411
357
  const products = await s3db.createResource({
412
358
  name: "products",
413
- attributes: {
414
- name: "string",
415
- price: "number",
416
- category: "string"
417
- },
359
+ attributes: { name: "string", price: "number" },
418
360
  hooks: {
419
- preInsert: [
420
- async (data) => {
421
- // Auto-generate SKU
422
- data.sku = `${data.category.toUpperCase()}-${Date.now()}`;
423
- return data;
424
- }
425
- ],
426
- afterInsert: [
427
- async (data) => {
428
- console.log(`๐Ÿ“ฆ Product ${data.name} created with SKU: ${data.sku}`);
429
- // Send notification, update cache, etc.
430
- }
431
- ],
432
- preUpdate: [
433
- async (id, data) => {
434
- // Log price changes
435
- if (data.price) {
436
- console.log(`๐Ÿ’ฐ Price update for ${id}: $${data.price}`);
437
- }
438
- return data;
439
- }
440
- ]
441
- },
442
-
443
- // Optional: Security settings (default: true)
444
- paranoid: true,
445
-
446
- // Optional: Schema options (default: false)
447
- allNestedObjectsOptional: false,
448
-
449
- // Optional: Encryption settings (default: true)
450
- autoDecrypt: true,
451
-
452
- // Optional: Caching (default: false)
453
- cache: false
361
+ preInsert: [async (data) => {
362
+ data.sku = `${data.category.toUpperCase()}-${Date.now()}`;
363
+ return data;
364
+ }],
365
+ afterInsert: [async (data) => {
366
+ console.log(`๐Ÿ“ฆ Product ${data.name} created`);
367
+ }]
368
+ }
454
369
  });
455
370
  ```
456
371
 
457
372
  ### ๐Ÿ”„ Streaming API
458
373
 
459
- Handle large datasets efficiently with streams:
374
+ Handle large datasets efficiently:
460
375
 
461
376
  ```javascript
462
- // Export all users to CSV
377
+ // Export to CSV
463
378
  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
379
  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
- });
380
+ readableStream.on("data", (user) => records.push(user));
381
+ readableStream.on("end", () => console.log("โœ… Export completed"));
482
382
 
483
- // Bulk import from stream
383
+ // Bulk import
484
384
  const writableStream = await users.writable();
485
- importData.forEach(userData => {
486
- writableStream.write(userData);
487
- });
385
+ importData.forEach(userData => writableStream.write(userData));
488
386
  writableStream.end();
489
387
  ```
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
388
  value: "string"
513
389
  },
514
390
  behavior: "enforce-limits" // Ensures data stays within 2KB
@@ -527,33 +403,16 @@ const summaries = await s3db.createResource({
527
403
 
528
404
  ### ๐Ÿ”„ Resource Versioning System
529
405
 
530
- s3db.js includes a powerful versioning system that automatically manages schema evolution and data migration:
531
-
532
- #### Enable Versioning
406
+ Automatically manages schema evolution and data migration:
533
407
 
534
408
  ```javascript
535
- // Enable versioning at database level
409
+ // Enable versioning
536
410
  const s3db = new S3db({
537
- uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
538
- versioningEnabled: true // Enable versioning for all resources
411
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
412
+ versioningEnabled: true
539
413
  });
540
414
 
541
415
  // 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
416
  const users = await s3db.createResource({
558
417
  name: "users",
559
418
  attributes: {
@@ -563,114 +422,39 @@ const users = await s3db.createResource({
563
422
  versioningEnabled: true
564
423
  });
565
424
 
566
- // Insert users in v0
425
+ // Insert in version 0
567
426
  const user1 = await users.insert({
568
427
  name: "John Doe",
569
428
  email: "john@example.com"
570
429
  });
571
430
 
572
- // Update schema - automatically creates v1
431
+ // Update schema - creates version 1
573
432
  const updatedUsers = await s3db.createResource({
574
433
  name: "users",
575
434
  attributes: {
576
435
  name: "string|required",
577
436
  email: "string|required",
578
- age: "number|optional", // New field
579
- profile: "object|optional" // New nested object
437
+ age: "number|optional"
580
438
  },
581
439
  versioningEnabled: true
582
440
  });
583
441
 
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
442
+ // Automatic migration
598
443
  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" } }
444
+ console.log(migratedUser._v); // "1" - automatically migrated
647
445
 
648
446
  // Query by version
649
- const v0Users = await users.list({
447
+ const version0Users = await users.list({
650
448
  partition: "byVersion",
651
- partitionValues: { _v: "v0" }
652
- });
653
-
654
- const v1Users = await users.list({
655
- partition: "byVersion",
656
- partitionValues: { _v: "v1" }
449
+ partitionValues: { _v: "0" }
657
450
  });
658
451
  ```
659
452
 
660
453
  ### ๐Ÿ†” Custom ID Generation
661
454
 
662
- s3db.js supports flexible ID generation strategies:
663
-
664
- #### Built-in ID Sizes
455
+ Flexible ID generation strategies:
665
456
 
666
457
  ```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
458
  // Custom size IDs
675
459
  const shortUsers = await s3db.createResource({
676
460
  name: "short-users",
@@ -678,23 +462,12 @@ const shortUsers = await s3db.createResource({
678
462
  idSize: 8 // Generate 8-character IDs
679
463
  });
680
464
 
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)
465
+ // UUID support
466
+ import { v4 as uuidv4 } from 'uuid';
694
467
  const uuidUsers = await s3db.createResource({
695
468
  name: "uuid-users",
696
469
  attributes: { name: "string|required" },
697
- idGenerator: uuidv4 // Pass UUID function directly
470
+ idGenerator: uuidv4
698
471
  });
699
472
 
700
473
  // UUID v1 (time-based)
@@ -703,185 +476,560 @@ const timeUsers = await s3db.createResource({
703
476
  attributes: { name: "string|required" },
704
477
  idGenerator: uuidv1
705
478
  });
706
- ```
707
479
 
708
- #### Custom ID Functions
709
-
710
- ```javascript
711
- // Timestamp-based IDs
480
+ // Custom ID function
712
481
  const timestampUsers = await s3db.createResource({
713
482
  name: "timestamp-users",
714
483
  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()}`
484
+ idGenerator: () => `user_${Date.now()}`
731
485
  });
732
486
  ```
733
487
 
734
- #### ID Generator Priority
488
+ ### ๐Ÿ”Œ Plugin System
489
+
490
+ Extend functionality with powerful plugins. s3db.js supports multiple plugins working together seamlessly:
735
491
 
736
492
  ```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
493
+ import {
494
+ CachePlugin,
495
+ CostsPlugin,
496
+ FullTextPlugin,
497
+ MetricsPlugin,
498
+ ReplicationPlugin,
499
+ AuditPlugin
500
+ } from 's3db.js';
501
+
502
+ const s3db = new S3db({
503
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
504
+ plugins: [
505
+ new CachePlugin({ enabled: true }), // CachePlugin needs instantiation
506
+ CostsPlugin, // CostsPlugin is a static object
507
+ new FullTextPlugin({ fields: ['name', 'description'] }),
508
+ new MetricsPlugin({ enabled: true }),
509
+ new ReplicationPlugin({
510
+ enabled: true,
511
+ replicators: [
512
+ {
513
+ driver: 's3db',
514
+ resources: ['users', 'products'],
515
+ config: {
516
+ connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
517
+ }
518
+ }
519
+ ]
520
+ }),
521
+ new AuditPlugin({ enabled: true })
522
+ ]
743
523
  });
524
+
525
+ // All plugins work together seamlessly
526
+ await users.insert({ name: "John", email: "john@example.com" });
527
+ // - Cache: Caches the operation
528
+ // - Costs: Tracks S3 costs
529
+ // - FullText: Indexes the data for search
530
+ // - Metrics: Records performance metrics
531
+ // - Replication: Syncs to configured replicators
532
+ // - Audit: Logs the operation
744
533
  ```
745
534
 
746
- ### ๐Ÿ”Œ Plugin System
535
+ ### ๐Ÿ”„ Replicator System
747
536
 
748
- Extend s3db.js functionality with plugins:
537
+ 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.
749
538
 
750
- #### Built-in Plugins
539
+ #### Available Replicators
751
540
 
541
+ **S3DB Replicator** - Replicates data to another s3db instance:
752
542
  ```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
- });
543
+ {
544
+ driver: 's3db',
545
+ resources: ['users', 'products'], // <-- root level
546
+ config: {
547
+ connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup",
548
+ region: 'us-west-2'
549
+ }
550
+ }
760
551
  ```
761
552
 
762
- #### Cache Plugin
763
-
553
+ **SQS Replicator** - Sends data to AWS SQS queues:
764
554
  ```javascript
765
- // Automatic caching for read operations
766
- const users = await s3db.createResource({
767
- name: "users",
768
- attributes: { name: "string|required" }
769
- });
555
+ {
556
+ driver: 'sqs',
557
+ resources: ['orders'],
558
+ config: {
559
+ queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue',
560
+ region: 'us-east-1',
561
+ messageGroupId: 's3db-replication', // For FIFO queues
562
+ deduplicationId: true // Enable deduplication
563
+ }
564
+ }
565
+ ```
770
566
 
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
567
+ **BigQuery Replicator** - Sends data to Google BigQuery:
568
+ ```javascript
569
+ {
570
+ driver: 'bigquery',
571
+ resources: ['users', 'orders'],
572
+ config: {
573
+ projectId: 'my-project',
574
+ datasetId: 'analytics',
575
+ tableId: 's3db_replication',
576
+ location: 'US',
577
+ credentials: {
578
+ // Your Google Cloud service account credentials
579
+ client_email: 'service-account@project.iam.gserviceaccount.com',
580
+ private_key: '-----BEGIN PRIVATE KEY-----\n...'
581
+ }
582
+ }
583
+ }
584
+ ```
776
585
 
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
586
+ **PostgreSQL Replicator** - Sends data to PostgreSQL databases:
587
+ ```javascript
588
+ {
589
+ driver: 'postgres',
590
+ resources: ['users'],
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
+ tableName: 's3db_replication',
600
+ ssl: false
601
+ }
602
+ }
781
603
  ```
782
604
 
783
- #### Costs Plugin
605
+ #### Replicator Features
784
606
 
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
- });
607
+ - **Resource Filtering**: Each replicator can be configured to handle specific resources only
608
+ - **Event Emission**: All replicators emit events for monitoring and debugging
609
+ - **Connection Testing**: Test connections to replicators before use
610
+ - **Batch Operations**: Support for batch replication operations
611
+ - **Error Handling**: Comprehensive error handling and retry logic
612
+ - **Status Monitoring**: Get detailed status and statistics for each replicator
791
613
 
792
- // Monitor costs during operations
793
- await users.insert({ name: "John" });
794
- await users.get("user-123");
795
- await users.list();
614
+ #### Dependencies
796
615
 
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
- ```
616
+ The replicators use optional peer dependencies. Install only what you need:
805
617
 
806
- #### Custom Plugins
618
+ ```bash
619
+ # For SQS replicator
620
+ npm install @aws-sdk/client-sqs
621
+ # or
622
+ yarn add @aws-sdk/client-sqs
623
+ # or
624
+ pnpm add @aws-sdk/client-sqs
807
625
 
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
- };
626
+ # For BigQuery replicator
627
+ npm install @google-cloud/bigquery
628
+ # or
629
+ yarn add @google-cloud/bigquery
630
+ # or
631
+ pnpm add @google-cloud/bigquery
824
632
 
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
- });
633
+ # For PostgreSQL replicator
634
+ npm install pg
635
+ # or
636
+ yarn add pg
637
+ # or
638
+ pnpm add pg
830
639
  ```
831
640
 
832
- ### ๐ŸŽ›๏ธ Advanced Behaviors
641
+ **โš ๏ธ 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.
833
642
 
834
- Choose the right behavior strategy for your use case:
643
+ **Example error without dependency:**
644
+ ```
645
+ Error: Cannot find module '@aws-sdk/client-sqs'
646
+ ```
835
647
 
836
- #### Behavior Comparison
648
+ **Solution:** Install the missing dependency as shown above.
837
649
 
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 |
650
+ #### Example Usage
844
651
 
845
- #### User Management Behavior (Default)
652
+ See `examples/e34-replicators.js` for a complete example using all four replicator types.
846
653
 
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
- });
654
+ **Prerequisites:** Make sure to install the required dependencies before running the example:
854
655
 
855
- // Listen for limit warnings
856
- users.on('exceedsLimit', (data) => {
857
- console.warn(`Data exceeds 2KB limit by ${data.excess} bytes`);
858
- });
656
+ ```bash
657
+ # Install all replication dependencies for the full example
658
+ npm install @aws-sdk/client-sqs @google-cloud/bigquery pg
659
+ ```
859
660
 
860
- // Operation continues despite warning
861
- await users.insert({
862
- name: "John",
863
- bio: "A".repeat(3000) // > 2KB
661
+ #### ๐Ÿ”„ Cache Plugin
662
+
663
+ #### ๐Ÿ”„ Cache Plugin
664
+ Intelligent caching to reduce API calls and improve performance:
665
+
666
+ ```javascript
667
+ const s3db = new S3db({
668
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
669
+ plugins: [new CachePlugin({
670
+ enabled: true,
671
+ ttl: 300000, // 5 minutes cache
672
+ maxSize: 1000, // Max 1000 items in cache
673
+ driverType: 'memory' // 'memory' or 's3'
674
+ })]
864
675
  });
676
+
677
+ // Automatic caching for reads
678
+ await users.count(); // Cached for 5 minutes
679
+ await users.list(); // Cached for 5 minutes
680
+ await users.insert({...}); // Automatically clears cache
865
681
  ```
866
682
 
867
- #### Enforce Limits Behavior
683
+ #### ๐Ÿ’ฐ Costs Plugin
684
+ Track and monitor AWS S3 costs in real-time:
868
685
 
869
686
  ```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"
687
+ const s3db = new S3db({
688
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
689
+ plugins: [CostsPlugin]
875
690
  });
876
691
 
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
- });
692
+ // Track costs automatically
693
+ await users.insert({ name: "John", email: "john@example.com" });
694
+ await users.list();
695
+
696
+ // Get cost information
697
+ console.log(s3db.client.costs);
698
+ // { total: 0.000009, requests: { total: 3, get: 1, put: 1, list: 1 } }
882
699
  ```
883
700
 
884
- #### Data Truncate Behavior
701
+ #### ๐Ÿ” Full-Text Search Plugin
702
+ Powerful text search with automatic indexing:
703
+
704
+ ```javascript
705
+ const s3db = new S3db({
706
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
707
+ plugins: [new FullTextPlugin({
708
+ enabled: true,
709
+ fields: ['name', 'description', 'content'], // Fields to index
710
+ minWordLength: 3,
711
+ maxResults: 50,
712
+ language: 'en-US'
713
+ })]
714
+ });
715
+
716
+ // Create resource with searchable fields
717
+ const products = await s3db.createResource({
718
+ name: "products",
719
+ attributes: {
720
+ name: "string|required",
721
+ description: "string",
722
+ content: "string"
723
+ }
724
+ });
725
+
726
+ // Insert data (automatically indexed)
727
+ await products.insert({
728
+ name: "JavaScript Book",
729
+ description: "Learn JavaScript programming",
730
+ content: "Comprehensive guide to modern JavaScript"
731
+ });
732
+
733
+ // Search across all indexed fields
734
+ const results = await s3db.plugins.fulltext.searchRecords('products', 'javascript');
735
+ console.log(results); // Returns products with search scores
736
+
737
+ // Example of search results:
738
+ // [
739
+ // {
740
+ // id: "prod-123",
741
+ // name: "JavaScript Book",
742
+ // description: "Learn JavaScript programming",
743
+ // content: "Comprehensive guide to modern JavaScript",
744
+ // _searchScore: 0.85,
745
+ // _matchedFields: ["name", "description", "content"],
746
+ // _matchedWords: ["javascript"]
747
+ // },
748
+ // {
749
+ // id: "prod-456",
750
+ // name: "Web Development Guide",
751
+ // description: "Includes JavaScript, HTML, and CSS",
752
+ // content: "Complete web development with JavaScript",
753
+ // _searchScore: 0.72,
754
+ // _matchedFields: ["description", "content"],
755
+ // _matchedWords: ["javascript"]
756
+ // }
757
+ // ]
758
+ ```
759
+
760
+ #### ๐Ÿ“Š Metrics Plugin
761
+ Monitor performance and usage metrics:
762
+
763
+ ```javascript
764
+ const s3db = new S3db({
765
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
766
+ plugins: [new MetricsPlugin({
767
+ enabled: true,
768
+ collectPerformance: true,
769
+ collectErrors: true,
770
+ collectUsage: true,
771
+ flushInterval: 60000 // Flush every minute
772
+ })]
773
+ });
774
+
775
+ // Metrics are collected automatically
776
+ await users.insert({ name: "John" });
777
+ await users.list();
778
+
779
+ // Get metrics
780
+ const metrics = await s3db.plugins.metrics.getMetrics();
781
+ console.log(metrics); // Performance and usage data
782
+
783
+ // Example of metrics object:
784
+ // {
785
+ // performance: {
786
+ // averageResponseTime: 245, // milliseconds
787
+ // totalRequests: 1250,
788
+ // requestsPerSecond: 12.5,
789
+ // slowestOperations: [
790
+ // { operation: "list", resource: "users", avgTime: 450, count: 50 },
791
+ // { operation: "get", resource: "products", avgTime: 320, count: 200 }
792
+ // ]
793
+ // },
794
+ // usage: {
795
+ // resources: {
796
+ // users: { inserts: 150, updates: 75, deletes: 10, reads: 800 },
797
+ // products: { inserts: 300, updates: 120, deletes: 25, reads: 1200 }
798
+ // },
799
+ // totalOperations: 2680,
800
+ // mostActiveResource: "products",
801
+ // peakUsageHour: "14:00"
802
+ // },
803
+ // errors: {
804
+ // total: 15,
805
+ // byType: {
806
+ // "ValidationError": 8,
807
+ // "NotFoundError": 5,
808
+ // "PermissionError": 2
809
+ // },
810
+ // byResource: {
811
+ // users: 10,
812
+ // products: 5
813
+ // }
814
+ // },
815
+ // cache: {
816
+ // hitRate: 0.78, // 78% cache hit rate
817
+ // totalHits: 980,
818
+ // totalMisses: 270,
819
+ // averageCacheTime: 120 // milliseconds
820
+ // }
821
+ // }
822
+ ```
823
+
824
+ #### ๐Ÿ”„ Replication Plugin
825
+ Replicate data to other buckets or regions:
826
+
827
+ ```javascript
828
+ const s3db = new S3db({
829
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
830
+ plugins: [new ReplicationPlugin({
831
+ enabled: true,
832
+ replicators: [
833
+ {
834
+ driver: 's3db',
835
+ resources: ['users', 'products'],
836
+ config: {
837
+ connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
838
+ }
839
+ },
840
+ {
841
+ driver: 'sqs',
842
+ resources: ['orders', 'users', 'products'],
843
+ config: {
844
+ // Resource-specific queues
845
+ queues: {
846
+ users: 'https://sqs.us-east-1.amazonaws.com/123456789012/users-events.fifo',
847
+ orders: 'https://sqs.us-east-1.amazonaws.com/123456789012/orders-events.fifo',
848
+ products: 'https://sqs.us-east-1.amazonaws.com/123456789012/products-events.fifo'
849
+ },
850
+ // Fallback queue for unspecified resources
851
+ defaultQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/default-events.fifo',
852
+ messageGroupId: 's3db-replication', // For FIFO queues
853
+ deduplicationId: true // Enable deduplication
854
+ }
855
+ },
856
+ {
857
+ driver: 'bigquery',
858
+ resources: ['users', 'orders'],
859
+ config: {
860
+ projectId: 'my-project',
861
+ datasetId: 'analytics',
862
+ tableId: 's3db_replication'
863
+ }
864
+ },
865
+ {
866
+ driver: 'postgres',
867
+ resources: ['users'],
868
+ config: {
869
+ connectionString: 'postgresql://user:pass@localhost:5432/analytics',
870
+ tableName: 's3db_replication'
871
+ }
872
+ }
873
+ ],
874
+ syncInterval: 300000 // Sync every 5 minutes
875
+ })]
876
+ });
877
+
878
+ // Data is automatically replicated to all configured targets
879
+ await users.insert({ name: "John" }); // Synced to all replicators
880
+
881
+ **SQS Message Structure:**
882
+
883
+ The SQS replicator sends standardized messages with the following structure:
884
+
885
+ ```javascript
886
+ // INSERT operation
887
+ {
888
+ resource: "users",
889
+ action: "insert",
890
+ data: { _v: 0, id: "user-001", name: "John", email: "john@example.com" },
891
+ timestamp: "2024-01-01T10:00:00.000Z",
892
+ source: "s3db-replication"
893
+ }
894
+
895
+ // UPDATE operation (includes before/after data)
896
+ {
897
+ resource: "users",
898
+ action: "update",
899
+ before: { _v: 0, id: "user-001", name: "John", age: 30 },
900
+ data: { _v: 1, id: "user-001", name: "John", age: 31 },
901
+ timestamp: "2024-01-01T10:05:00.000Z",
902
+ source: "s3db-replication"
903
+ }
904
+
905
+ // DELETE operation
906
+ {
907
+ resource: "users",
908
+ action: "delete",
909
+ data: { _v: 1, id: "user-001", name: "John", age: 31 },
910
+ timestamp: "2024-01-01T10:10:00.000Z",
911
+ source: "s3db-replication"
912
+ }
913
+ ```
914
+
915
+ **Queue Routing:**
916
+ - Each resource can have its own dedicated queue
917
+ - Unspecified resources use the default queue
918
+ - FIFO queues supported with deduplication
919
+ - Messages are automatically routed to the appropriate queue
920
+ ```
921
+
922
+ #### ๐Ÿ“ Audit Plugin
923
+ Log all operations for compliance and traceability:
924
+
925
+ ```javascript
926
+ const s3db = new S3db({
927
+ connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
928
+ plugins: [new AuditPlugin({
929
+ enabled: true,
930
+ trackOperations: ['insert', 'update', 'delete', 'get'],
931
+ includeData: false, // Don't log sensitive data
932
+ retentionDays: 90
933
+ })]
934
+ });
935
+
936
+ // All operations are logged
937
+ await users.insert({ name: "John" });
938
+ await users.update(userId, { age: 31 });
939
+
940
+ // Get audit logs
941
+ const logs = await s3db.plugins.audit.getAuditLogs({
942
+ resourceName: 'users',
943
+ operation: 'insert'
944
+ });
945
+ console.log(logs); // Audit trail
946
+ ```
947
+
948
+ ### ๐ŸŽ›๏ธ Advanced Behaviors
949
+
950
+ Choose the right behavior strategy for your use case:
951
+
952
+ #### Behavior Comparison
953
+
954
+ | Behavior | Enforcement | Data Loss | Event Emission | Use Case |
955
+ |------------------|-------------|-----------|----------------|-------------------------|
956
+ | `user-managed` | None | Possible | Warns | Dev/Test/Advanced users |
957
+ | `enforce-limits` | Strict | No | Throws | Production |
958
+ | `data-truncate` | Truncates | Yes | Warns | Content Mgmt |
959
+ | `body-overflow` | Truncates/Splits | Yes | Warns | Large objects |
960
+ | `body-only` | Unlimited | No | No | Large JSON/Logs |
961
+
962
+ #### User Managed Behavior (Default)
963
+
964
+ The `user-managed` behavior is the default for s3db resources. It provides no automatic enforcement of S3 metadata or body size limits, and does not modify or truncate data. Instead, it emits warnings via the `exceedsLimit` event when S3 metadata limits are exceeded, but allows all operations to proceed.
965
+
966
+ **Purpose & Use Cases:**
967
+ - For development, testing, or advanced users who want full control over resource metadata and body size.
968
+ - Useful when you want to handle S3 metadata limits yourself, or implement custom logic for warnings.
969
+ - Not recommended for production unless you have custom enforcement or validation in place.
970
+
971
+ **How It Works:**
972
+ - Emits an `exceedsLimit` event (with details) when a resource's metadata size exceeds the S3 2KB limit.
973
+ - Does NOT block, truncate, or modify dataโ€”operations always proceed.
974
+ - No automatic enforcement of any limits; user is responsible for handling warnings and data integrity.
975
+
976
+ **Event Emission:**
977
+ - Event: `exceedsLimit`
978
+ - Payload:
979
+ - `operation`: 'insert' | 'update' | 'upsert'
980
+ - `id` (for update/upsert): resource id
981
+ - `totalSize`: total metadata size in bytes
982
+ - `limit`: S3 metadata limit (2048 bytes)
983
+ - `excess`: number of bytes over the limit
984
+ - `data`: the offending data object
985
+
986
+ ```javascript
987
+ // Flexible behavior - warns but doesn't block
988
+ const users = await s3db.createResource({
989
+ name: "users",
990
+ attributes: { name: "string", bio: "string" },
991
+ behavior: "user-managed" // Default
992
+ });
993
+
994
+ // Listen for limit warnings
995
+ users.on('exceedsLimit', (data) => {
996
+ console.warn(`Data exceeds 2KB limit by ${data.excess} bytes`, data);
997
+ });
998
+
999
+ // Operation continues despite warning
1000
+ await users.insert({
1001
+ name: "John",
1002
+ bio: "A".repeat(3000) // > 2KB
1003
+ });
1004
+ ```
1005
+
1006
+ **Best Practices & Warnings:**
1007
+ - Exceeding S3 metadata limits will cause silent data loss or errors at the storage layer.
1008
+ - Use this behavior only if you have custom logic to handle warnings and enforce limits.
1009
+ - For production, prefer `enforce-limits` or `data-truncate` to avoid data loss.
1010
+
1011
+ **Migration Tips:**
1012
+ - To migrate to a stricter behavior, change the resource's behavior to `enforce-limits` or `data-truncate`.
1013
+ - Review emitted warnings to identify resources at risk of exceeding S3 limits.
1014
+
1015
+ #### Enforce Limits Behavior
1016
+
1017
+ ```javascript
1018
+ // Strict validation - throws error if limit exceeded
1019
+ const settings = await s3db.createResource({
1020
+ name: "settings",
1021
+ attributes: { key: "string", value: "string" },
1022
+ behavior: "enforce-limits"
1023
+ });
1024
+
1025
+ // Throws error if data > 2KB
1026
+ await settings.insert({
1027
+ key: "large_setting",
1028
+ value: "A".repeat(3000) // Throws: "S3 metadata size exceeds 2KB limit"
1029
+ });
1030
+ ```
1031
+
1032
+ #### Data Truncate Behavior
885
1033
 
886
1034
  ```javascript
887
1035
  // Smart truncation - preserves structure, truncates content
@@ -934,6 +1082,52 @@ console.log(retrieved.content.length); // 5000 (full content preserved)
934
1082
  console.log(retrieved._hasContent); // true (indicates body usage)
935
1083
  ```
936
1084
 
1085
+ #### Body Only Behavior
1086
+
1087
+ ```javascript
1088
+ // Store all data in S3 object body as JSON, keeping only version in metadata
1089
+ const documents = await s3db.createResource({
1090
+ name: "documents",
1091
+ attributes: {
1092
+ title: "string",
1093
+ content: "string", // Can be extremely large
1094
+ metadata: "object"
1095
+ },
1096
+ behavior: "body-only"
1097
+ });
1098
+
1099
+ // Store large documents without any size limits
1100
+ const document = await documents.insert({
1101
+ title: "Large Document",
1102
+ content: "A".repeat(100000), // 100KB content
1103
+ metadata: {
1104
+ author: "John Doe",
1105
+ tags: ["large", "document"],
1106
+ version: "1.0"
1107
+ }
1108
+ });
1109
+
1110
+ // All data is stored in the S3 object body
1111
+ const retrieved = await documents.get(document.id);
1112
+ console.log(retrieved.content.length); // 100000 (full content preserved)
1113
+ console.log(retrieved.metadata.author); // "John Doe"
1114
+ console.log(retrieved._hasContent); // true (indicates body usage)
1115
+
1116
+ // Perfect for storing large JSON documents, logs, or any large content
1117
+ const logEntry = await documents.insert({
1118
+ title: "Application Log",
1119
+ content: JSON.stringify({
1120
+ timestamp: new Date().toISOString(),
1121
+ level: "INFO",
1122
+ message: "Application started",
1123
+ details: {
1124
+ // ... large log details
1125
+ }
1126
+ }),
1127
+ metadata: { source: "api-server", environment: "production" }
1128
+ });
1129
+ ```
1130
+
937
1131
  ### ๐Ÿ”„ Advanced Streaming API
938
1132
 
939
1133
  Handle large datasets efficiently with advanced streaming capabilities:
@@ -1409,782 +1603,4 @@ console.log(`Total users: ${allUsers.length}`);
1409
1603
 
1410
1604
  | Method | Description | Example |
1411
1605
  |--------|-------------|---------|
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
-
1417
- ---
1418
-
1419
- ## ๐ŸŽจ Examples
1420
-
1421
- ### ๐Ÿ“ Blog Platform
1422
-
1423
- ```javascript
1424
- // Create blog posts with body-overflow behavior for long content
1425
- const posts = await s3db.createResource({
1426
- name: "posts",
1427
- attributes: {
1428
- title: "string|min:5|max:200",
1429
- content: "string",
1430
- author: "string",
1431
- tags: "array|items:string",
1432
- published: "boolean|default:false",
1433
- publishedAt: "date|optional"
1434
- },
1435
- behavior: "body-overflow", // Handle long content
1436
- timestamps: true,
1437
- partitions: {
1438
- byAuthor: { fields: { author: "string" } },
1439
- byTag: { fields: { "tags.0": "string" } }
1440
- }
1441
- });
1442
-
1443
- // Create a blog post
1444
- const post = await posts.insert({
1445
- title: "Getting Started with s3db.js",
1446
- content: "This is a comprehensive guide to using s3db.js for your next project...",
1447
- author: "john_doe",
1448
- tags: ["tutorial", "database", "s3"],
1449
- published: true,
1450
- publishedAt: new Date()
1451
- });
1452
-
1453
- // Query posts by author
1454
- const johnsPosts = await posts.list({
1455
- partition: "byAuthor",
1456
- partitionValues: { author: "john_doe" }
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 });
1466
- ```
1467
-
1468
- ### ๐Ÿ›’ E-commerce Store
1469
-
1470
- ```javascript
1471
- // Products with detailed specifications
1472
- const products = await s3db.createResource({
1473
- name: "products",
1474
- attributes: {
1475
- name: "string|min:2|max:200",
1476
- description: "string",
1477
- price: "number|positive",
1478
- category: "string",
1479
- inventory: {
1480
- stock: "number|integer|min:0",
1481
- reserved: "number|integer|min:0|default:0"
1482
- },
1483
- specifications: "object|optional",
1484
- images: "array|items:url"
1485
- },
1486
- behavior: "body-overflow",
1487
- timestamps: true,
1488
- partitions: {
1489
- byCategory: { fields: { category: "string" } }
1490
- }
1491
- });
1492
-
1493
- // Orders with customer information
1494
- const orders = await s3db.createResource({
1495
- name: "orders",
1496
- attributes: {
1497
- customerId: "string",
1498
- items: "array|items:object",
1499
- total: "number|positive",
1500
- status: "string|enum:pending,processing,shipped,delivered",
1501
- shipping: {
1502
- address: "string",
1503
- city: "string",
1504
- country: "string",
1505
- zipCode: "string"
1506
- }
1507
- },
1508
- behavior: "enforce-limits",
1509
- timestamps: true
1510
- });
1511
-
1512
- // Create a product
1513
- const product = await products.insert({
1514
- name: "Premium Wireless Headphones",
1515
- description: "High-quality audio with active noise cancellation",
1516
- price: 299.99,
1517
- category: "electronics",
1518
- inventory: { stock: 50 },
1519
- specifications: {
1520
- brand: "AudioTech",
1521
- model: "AT-WH1000",
1522
- features: ["ANC", "Bluetooth 5.0", "30h battery"]
1523
- },
1524
- images: ["https://example.com/headphones-1.jpg"]
1525
- });
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
-
1537
- // Create an order
1538
- const order = await orders.insert({
1539
- customerId: "customer-123",
1540
- items: [
1541
- { productId: product.id, quantity: 1, price: 299.99 }
1542
- ],
1543
- total: 299.99,
1544
- status: "pending",
1545
- shipping: {
1546
- address: "123 Main St",
1547
- city: "New York",
1548
- country: "USA",
1549
- zipCode: "10001"
1550
- }
1551
- });
1552
- ```
1553
-
1554
- ### ๐Ÿ‘ฅ User Management System
1555
-
1556
- ```javascript
1557
- // Users with authentication
1558
- const users = await s3db.createResource({
1559
- name: "users",
1560
- attributes: {
1561
- username: "string|min:3|max:50|unique",
1562
- email: "email|unique",
1563
- password: "secret", // Automatically encrypted
1564
- role: "string|enum:user,admin,moderator|default:user",
1565
- profile: {
1566
- firstName: "string",
1567
- lastName: "string",
1568
- avatar: "url|optional",
1569
- bio: "string|max:500|optional"
1570
- },
1571
- preferences: {
1572
- theme: "string|enum:light,dark|default:light",
1573
- language: "string|default:en",
1574
- notifications: "boolean|default:true"
1575
- },
1576
- lastLogin: "date|optional"
1577
- },
1578
- behavior: "enforce-limits",
1579
- timestamps: true,
1580
- hooks: {
1581
- preInsert: [async (data) => {
1582
- // Auto-generate secure password if not provided
1583
- if (!data.password) {
1584
- data.password = generateSecurePassword();
1585
- }
1586
- return data;
1587
- }],
1588
- afterInsert: [async (data) => {
1589
- console.log(`Welcome ${data.username}! ๐ŸŽ‰`);
1590
- }]
1591
- }
1592
- });
1593
-
1594
- // Register a new user
1595
- const user = await users.insert({
1596
- username: "jane_smith",
1597
- email: "jane@example.com",
1598
- profile: {
1599
- firstName: "Jane",
1600
- lastName: "Smith"
1601
- },
1602
- preferences: {
1603
- theme: "dark",
1604
- notifications: true
1605
- }
1606
- });
1607
-
1608
- // Password was auto-generated and encrypted
1609
- console.log("Generated password:", user.password);
1610
- ```
1611
-
1612
- ---
1613
-
1614
- ## ๐Ÿ” Security
1615
-
1616
- ### ๐Ÿ”’ Field-Level Encryption
1617
-
1618
- Sensitive data is automatically encrypted using the `"secret"` type:
1619
-
1620
- ```javascript
1621
- const users = await s3db.createResource({
1622
- name: "users",
1623
- attributes: {
1624
- email: "email",
1625
- password: "secret", // ๐Ÿ” Encrypted
1626
- apiKey: "secret", // ๐Ÿ” Encrypted
1627
- creditCard: "secret" // ๐Ÿ” Encrypted
1628
- }
1629
- });
1630
-
1631
- const user = await users.insert({
1632
- email: "john@example.com",
1633
- password: "my_secure_password",
1634
- apiKey: "sk_live_123456789",
1635
- creditCard: "4111111111111111"
1636
- });
1637
-
1638
- // Data is automatically decrypted when retrieved
1639
- const retrieved = await users.get(user.id);
1640
- console.log(retrieved.password); // "my_secure_password" โœ…
1641
- ```
1642
-
1643
- ### ๐ŸŽฒ Auto-Generated Secure Passwords
1644
-
1645
- s3db.js automatically generates secure passwords for `secret` fields when not provided:
1646
-
1647
- ```javascript
1648
- const accounts = await s3db.createResource({
1649
- name: "accounts",
1650
- attributes: {
1651
- name: "string",
1652
- password: "secret", // Auto-generated if not provided
1653
- apiKey: "secret" // Auto-generated if not provided
1654
- }
1655
- });
1656
-
1657
- const account = await accounts.insert({
1658
- name: "Service Account"
1659
- // password and apiKey will be auto-generated
1660
- });
1661
-
1662
- console.log(account.password); // "Ax7Kp9mN2qR3" (12-char secure password)
1663
- console.log(account.apiKey); // "Bc8Lq0nO3sS4" (12-char secure key)
1664
- ```
1665
-
1666
- **Features:**
1667
- - ๐ŸŽฏ **12-character passwords** with cryptographically secure randomness
1668
- - ๐Ÿšซ **No confusing characters** (excludes 0, O, 1, l, I)
1669
- - ๐Ÿ”„ **Unique every time** using nanoid generation
1670
- - ๐Ÿ›ก๏ธ **Custom passwords supported** when explicitly provided
1671
-
1672
- ### ๐Ÿ”‘ Custom Encryption Keys
1673
-
1674
- ```javascript
1675
- import fs from "fs";
1676
-
1677
- const s3db = new S3db({
1678
- uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
1679
- passphrase: fs.readFileSync("./private-key.pem") // Custom encryption key
1680
- });
1681
- ```
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
-
1738
- ---
1739
-
1740
- ## ๐Ÿ’ฐ Cost Analysis
1741
-
1742
- ### ๐Ÿ“Š Understanding S3 Costs
1743
-
1744
- s3db.js is incredibly cost-effective because it uses S3 metadata instead of file storage:
1745
-
1746
- | Operation | AWS Cost | s3db.js Usage |
1747
- |-----------|----------|---------------|
1748
- | **PUT Requests** | $0.0005 per 1,000 | Document inserts/updates |
1749
- | **GET Requests** | $0.0004 per 1,000 | Document retrievals |
1750
- | **Storage** | $0.023 per GB | ~$0 (uses 0-byte files) |
1751
- | **Data Transfer** | $0.09 per GB | Minimal (metadata only) |
1752
-
1753
- ### ๐Ÿ’ก Cost Examples
1754
-
1755
- <details>
1756
- <summary><strong>๐Ÿ“ˆ Small Application (1,000 users)</strong></summary>
1757
-
1758
- ```javascript
1759
- // One-time setup cost
1760
- const setupCost = 0.0005; // 1,000 PUT requests = $0.0005
1761
-
1762
- // Monthly operations (10 reads per user)
1763
- const monthlyReads = 0.004; // 10,000 GET requests = $0.004
1764
- const monthlyUpdates = 0.0005; // 1,000 PUT requests = $0.0005
1765
-
1766
- const totalMonthlyCost = monthlyReads + monthlyUpdates;
1767
- console.log(`Monthly cost: $${totalMonthlyCost.toFixed(4)}`); // $0.0045
1768
- ```
1769
-
1770
- </details>
1771
-
1772
- <details>
1773
- <summary><strong>๐Ÿš€ Large Application (1,000,000 users)</strong></summary>
1774
-
1775
- ```javascript
1776
- // One-time setup cost
1777
- const setupCost = 0.50; // 1,000,000 PUT requests = $0.50
1778
-
1779
- // Monthly operations (10 reads per user)
1780
- const monthlyReads = 4.00; // 10,000,000 GET requests = $4.00
1781
- const monthlyUpdates = 0.50; // 1,000,000 PUT requests = $0.50
1782
-
1783
- const totalMonthlyCost = monthlyReads + monthlyUpdates;
1784
- console.log(`Monthly cost: $${totalMonthlyCost.toFixed(2)}`); // $4.50
1785
- ```
1786
-
1787
- </details>
1788
-
1789
- ### ๐Ÿ“ˆ Cost Tracking
1790
-
1791
- Monitor your expenses with the built-in cost tracking plugin:
1792
-
1793
- ```javascript
1794
- import { CostsPlugin } from "s3db.js";
1795
-
1796
- const s3db = new S3db({
1797
- uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
1798
- plugins: [CostsPlugin]
1799
- });
1800
-
1801
- // After operations
1802
- console.log("๐Ÿ’ฐ Total cost:", s3db.client.costs.total.toFixed(4), "USD");
1803
- console.log("๐Ÿ“Š Requests made:", s3db.client.costs.requests.total);
1804
- console.log("๐Ÿ“ˆ Cost breakdown:", s3db.client.costs.breakdown);
1805
- ```
1806
-
1807
- ---
1808
-
1809
- ## ๐Ÿšจ Best Practices
1810
-
1811
- ### โœ… Do's
1812
-
1813
- #### **๐ŸŽฏ Design for Document Storage**
1814
- ```javascript
1815
- // โœ… Good: Well-structured documents
1816
- const user = {
1817
- id: "user-123",
1818
- name: "John Doe",
1819
- profile: {
1820
- bio: "Software developer",
1821
- preferences: { theme: "dark" }
1822
- }
1823
- };
1824
- ```
1825
-
1826
- #### **๐Ÿ“ˆ Use Sequential IDs for Performance**
1827
- ```javascript
1828
- // โœ… Best: Sequential IDs
1829
- const productIds = ["00001", "00002", "00003"];
1830
-
1831
- // โœ… Good: UUIDs with sufficient entropy
1832
- const userIds = ["a1b2c3d4", "e5f6g7h8", "i9j0k1l2"];
1833
- ```
1834
-
1835
- #### **๐Ÿ”„ Leverage Streaming for Large Operations**
1836
- ```javascript
1837
- // โœ… Good: Process large datasets with streams
1838
- const stream = await users.readable();
1839
- stream.on("data", (user) => {
1840
- // Process each user individually
1841
- });
1842
- ```
1843
-
1844
- #### **๐ŸŽ›๏ธ Choose the Right Behavior Strategy**
1845
- ```javascript
1846
- // โœ… Development: Flexible with warnings
1847
- { behavior: "user-management" }
1848
-
1849
- // โœ… Production: Strict validation
1850
- { behavior: "enforce-limits" }
1851
-
1852
- // โœ… Content: Preserve all data
1853
- { behavior: "body-overflow" }
1854
- ```
1855
-
1856
- ### โŒ Don'ts
1857
-
1858
- #### **๐Ÿšซ Avoid Large Arrays in Documents**
1859
- ```javascript
1860
- // โŒ Bad: Large arrays can exceed 2KB limit
1861
- const user = {
1862
- name: "John",
1863
- purchaseHistory: [/* hundreds of orders */]
1864
- };
1865
-
1866
- // โœ… Better: Use separate resource with references
1867
- const user = { name: "John", id: "user-123" };
1868
- const orders = [
1869
- { userId: "user-123", product: "...", date: "..." },
1870
- // Store orders separately
1871
- ];
1872
- ```
1873
-
1874
- #### **๐Ÿšซ Don't Load Everything at Once**
1875
- ```javascript
1876
- // โŒ Bad: Memory intensive
1877
- const allUsers = await users.getAll();
1878
-
1879
- // โœ… Better: Use pagination or streaming
1880
- const page = await users.page({ offset: 0, size: 100 });
1881
- ```
1882
-
1883
- ### ๐ŸŽฏ Performance Tips
1884
-
1885
- 1. **Enable caching** for frequently accessed data:
1886
- ```javascript
1887
- const s3db = new S3db({
1888
- uri: "s3://...",
1889
- cache: true,
1890
- ttl: 3600 // 1 hour
1891
- });
1892
- ```
1893
-
1894
- 2. **Adjust parallelism** for bulk operations:
1895
- ```javascript
1896
- const s3db = new S3db({
1897
- uri: "s3://...",
1898
- parallelism: 25 // Handle 25 concurrent operations
1899
- });
1900
- ```
1901
-
1902
- 3. **Use partitions** for efficient queries:
1903
- ```javascript
1904
- // Query specific partitions instead of scanning all data
1905
- const results = await users.list({
1906
- partition: "byRegion",
1907
- partitionValues: { region: "us-east" }
1908
- });
1909
- ```
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
-
2098
- ---
2099
-
2100
- ## ๐Ÿงช Testing
2101
-
2102
- s3db.js includes a comprehensive test suite. Run tests with:
2103
-
2104
- ```bash
2105
- # Run all tests
2106
- npm test
2107
-
2108
- # Run specific test file
2109
- npm test -- --testNamePattern="Resource"
2110
-
2111
- # Run with coverage
2112
- npm run test:coverage
2113
- ```
2114
-
2115
- ### Test Coverage
2116
-
2117
- - โœ… **Unit Tests** - Individual component testing
2118
- - โœ… **Integration Tests** - End-to-end workflows
2119
- - โœ… **Behavior Tests** - Document handling strategies
2120
- - โœ… **Performance Tests** - Large dataset operations
2121
- - โœ… **Security Tests** - Encryption and validation
2122
-
2123
- ---
2124
-
2125
- ## ๐Ÿค Contributing
2126
-
2127
- We'd love your help making s3db.js even better! Here's how you can contribute:
2128
-
2129
- ### ๐Ÿ› ๏ธ Development Setup
2130
-
2131
- ```bash
2132
- # Clone the repository
2133
- git clone https://github.com/forattini-dev/s3db.js.git
2134
- cd s3db.js
2135
-
2136
- # Install dependencies
2137
- npm install
2138
-
2139
- # Run tests
2140
- npm test
2141
-
2142
- # Start development server
2143
- npm run dev
2144
- ```
2145
-
2146
- ### ๐Ÿ“‹ Contribution Guidelines
2147
-
2148
- 1. **๐Ÿด Fork** the repository
2149
- 2. **๐ŸŒฟ Create** a feature branch (`git checkout -b feature/amazing-feature`)
2150
- 3. **โœจ Make** your changes with tests
2151
- 4. **โœ… Ensure** all tests pass (`npm test`)
2152
- 5. **๐Ÿ“ Commit** your changes (`git commit -m 'Add amazing feature'`)
2153
- 6. **๐Ÿš€ Push** to your branch (`git push origin feature/amazing-feature`)
2154
- 7. **๐Ÿ”„ Open** a Pull Request
2155
-
2156
- ### ๐Ÿ› Bug Reports
2157
-
2158
- Found a bug? Please open an issue with:
2159
- - Clear description of the problem
2160
- - Steps to reproduce
2161
- - Expected vs actual behavior
2162
- - Your environment details
2163
-
2164
- ### ๐Ÿ’ก Feature Requests
2165
-
2166
- Have an idea? We'd love to hear it! Open an issue describing:
2167
- - The problem you're trying to solve
2168
- - Your proposed solution
2169
- - Any alternatives you've considered
2170
-
2171
- ---
2172
-
2173
- ## ๐Ÿ“„ License
2174
-
2175
- This project is licensed under the **Unlicense** - see the [LICENSE](LICENSE) file for details.
2176
-
2177
- This means you can use, modify, and distribute this software for any purpose without any restrictions. It's truly free and open source! ๐ŸŽ‰
2178
-
2179
- ---
2180
-
2181
- <p align="center">
2182
- <strong>Made with โค๏ธ by developers, for developers</strong><br>
2183
- <a href="https://github.com/forattini-dev/s3db.js">โญ Star us on GitHub</a> โ€ข
2184
- <a href="https://www.npmjs.com/package/s3db.js">๐Ÿ“ฆ View on NPM</a> โ€ข
2185
- <a href="https://github.com/forattini-dev/s3db.js/issues">๐Ÿ› Report Issues</a>
2186
- </p>
2187
-
2188
- <p align="center">
2189
- <sub>Built with Node.js โ€ข Powered by AWS S3 โ€ข Streaming Ready</sub>
2190
- </p>
1606
+ | `