s3db.js 4.0.1 โ†’ 4.0.2

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.
Files changed (2) hide show
  1. package/README.md +1359 -97
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,150 +1,1412 @@
1
1
  # s3db.js
2
2
 
3
- Use AWS S3, the world's most reliable document storage, as a database with this ORM.
3
+ [![license: unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) [![npm version](https://img.shields.io/npm/v/s3db.js.svg?style=flat)](https://www.npmjs.com/package/s3db.js) [![Maintainability](https://api.codeclimate.com/v1/badges/26e3dc46c42367d44f18/maintainability)](https://codeclimate.com/github/forattini-dev/s3db.js/maintainability) [![Coverage Status](https://coveralls.io/repos/github/forattini-dev/s3db.js/badge.svg?branch=main)](https://coveralls.io/github/forattini-dev/s3db.js?branch=main)
4
4
 
5
- ## Installation
5
+ **A document-based database built on AWS S3 with a powerful ORM-like interface**
6
+
7
+ Transform AWS S3 into a fully functional document database with automatic validation, encryption, caching, and streaming capabilities.
8
+
9
+ ## ๐Ÿš€ Quick Start
6
10
 
7
11
  ```bash
8
- npm install s3db.js
12
+ npm i s3db.js
13
+ ```
14
+
15
+ ```javascript
16
+ import { S3db } from "s3db.js";
17
+
18
+ // Connect to your S3 database
19
+ const s3db = new S3db({
20
+ uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
21
+ });
22
+
23
+ await s3db.connect();
24
+
25
+ // Create a resource (collection)
26
+ const users = await s3db.createResource({
27
+ name: "users",
28
+ attributes: {
29
+ name: "string|min:2|max:100",
30
+ email: "email|unique",
31
+ age: "number|integer|positive",
32
+ isActive: "boolean",
33
+ createdAt: "date"
34
+ }
35
+ });
36
+
37
+ // Insert data
38
+ const user = await users.insert({
39
+ name: "John Doe",
40
+ email: "john@example.com",
41
+ age: 30,
42
+ isActive: true,
43
+ createdAt: new Date()
44
+ });
45
+
46
+ // Query data
47
+ const foundUser = await users.get(user.id);
48
+ console.log(foundUser.name); // "John Doe"
49
+ ```
50
+
51
+ ## ๐Ÿ“‹ Table of Contents
52
+
53
+ - [๐ŸŽฏ What is s3db.js?](#-what-is-s3dbjs)
54
+ - [๐Ÿ’ก How it Works](#-how-it-works)
55
+ - [โšก Installation & Setup](#-installation--setup)
56
+ - [๐Ÿ”ง Configuration](#-configuration)
57
+ - [๐Ÿ“š Core Concepts](#-core-concepts)
58
+ - [๐Ÿ› ๏ธ API Reference](#๏ธ-api-reference)
59
+ - [๐Ÿ“Š Examples](#-examples)
60
+ - [๐Ÿ”„ Streaming](#-streaming)
61
+ - [๐Ÿ” Security & Encryption](#-security--encryption)
62
+ - [๐Ÿ’ฐ Cost Analysis](#-cost-analysis)
63
+ - [๐ŸŽ›๏ธ Advanced Features](#๏ธ-advanced-features)
64
+ - [๐Ÿšจ Limitations & Best Practices](#-limitations--best-practices)
65
+ - [๐Ÿงช Testing](#-testing)
66
+ - [๐Ÿ“… Version Compatibility](#-version-compatibility)
67
+
68
+ ## ๐ŸŽฏ What is s3db.js?
69
+
70
+ `s3db.js` is a document database that leverages AWS S3's metadata capabilities to store structured data. Instead of storing data in file bodies, it uses S3's metadata fields (up to 2KB) to store document data, making it extremely cost-effective for document storage.
71
+
72
+ ### Key Features
73
+
74
+ - **๐Ÿ”„ ORM-like Interface**: Familiar database operations (insert, get, update, delete)
75
+ - **โœ… Automatic Validation**: Built-in schema validation using fastest-validator
76
+ - **๐Ÿ” Encryption**: Optional field-level encryption for sensitive data
77
+ - **โšก Streaming**: Handle large datasets with readable/writable streams
78
+ - **๐Ÿ’พ Caching**: Reduce API calls with intelligent caching
79
+ - **๐Ÿ“Š Cost Tracking**: Monitor AWS costs with built-in plugins
80
+ - **๐Ÿ›ก๏ธ Type Safety**: Full TypeScript support
81
+ - **๐Ÿ”ง Robust Serialization**: Advanced handling of arrays and objects with edge cases
82
+ - **๐Ÿ“ Comprehensive Testing**: Complete test suite with journey-based scenarios
83
+ - **๐Ÿ•’ Automatic Timestamps**: Optional createdAt/updatedAt fields
84
+ - **๐Ÿ“ฆ Partitions**: Organize data by fields for efficient queries
85
+ - **๐ŸŽฃ Hooks**: Custom logic before/after operations
86
+ - **๐Ÿ”Œ Plugins**: Extensible architecture
87
+
88
+ ## ๐Ÿ’ก How it Works
89
+
90
+ ### The Magic Behind s3db.js
91
+
92
+ AWS S3 allows you to store metadata with each object:
93
+ - **Metadata**: Up to 2KB of UTF-8 encoded data
94
+
95
+ `s3db.js` cleverly uses these fields to store document data instead of file contents, making each S3 object act as a database record.
96
+
97
+ ### Data Storage Strategy
98
+
99
+ ```javascript
100
+ // Your document
101
+ {
102
+ id: "user-123",
103
+ name: "John Doe",
104
+ email: "john@example.com",
105
+ age: 30
106
+ }
107
+
108
+ // Stored in S3 as:
109
+ // Key: users/user-123
110
+ // Metadata: { "name": "John Doe", "email": "john@example.com", "age": "30", "id": "user-123" }
111
+ ```
112
+
113
+ ## โšก Installation & Setup
114
+
115
+ ### Install
116
+
117
+ ```bash
118
+ npm i s3db.js
119
+ # or
120
+ pnpm add s3db.js
121
+ # or
122
+ yarn add s3db.js
123
+ ```
124
+
125
+ ### Basic Setup
126
+
127
+ ```javascript
128
+ import { S3db } from "s3db.js";
129
+
130
+ const s3db = new S3db({
131
+ uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
132
+ });
133
+
134
+ await s3db.connect();
135
+ console.log("Connected to S3 database!");
136
+ ```
137
+
138
+ ### Environment Variables Setup
139
+
140
+ ```javascript
141
+ import * as dotenv from "dotenv";
142
+ dotenv.config();
143
+
144
+ import { S3db } from "s3db.js";
145
+
146
+ const s3db = new S3db({
147
+ uri: `s3://${process.env.AWS_ACCESS_KEY_ID}:${process.env.AWS_SECRET_ACCESS_KEY}@${process.env.AWS_BUCKET}/databases/${process.env.DATABASE_NAME}`
148
+ });
149
+ ```
150
+
151
+ ## ๐Ÿ”ง Configuration
152
+
153
+ ### Connection Options
154
+
155
+ | Option | Type | Default | Description |
156
+ |--------|------|---------|-------------|
157
+ | `uri` | `string` | **required** | S3 connection string |
158
+ | `parallelism` | `number` | `10` | Concurrent operations |
159
+ | `passphrase` | `string` | `"secret"` | Encryption key |
160
+ | `cache` | `boolean` | `false` | Enable caching |
161
+ | `ttl` | `number` | `86400` | Cache TTL in seconds |
162
+ | `plugins` | `array` | `[]` | Custom plugins |
163
+
164
+ ### ๐Ÿ” Authentication & Connectivity
165
+
166
+ `s3db.js` supports multiple authentication methods and can connect to various S3-compatible services:
167
+
168
+ #### Connection String Format
169
+
170
+ ```
171
+ s3://[ACCESS_KEY:SECRET_KEY@]BUCKET_NAME[/PREFIX]
9
172
  ```
10
173
 
11
- ## Quick Start
174
+ #### 1. AWS S3 with Access Keys
12
175
 
13
- ### Node.js (ES Modules)
176
+ ```javascript
177
+ const s3db = new S3db({
178
+ uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
179
+ });
180
+ ```
181
+
182
+ #### 2. AWS S3 with IAM Roles (EC2/EKS)
14
183
 
15
184
  ```javascript
16
- import S3db from 's3db.js';
185
+ // No credentials needed - uses IAM role permissions
186
+ const s3db = new S3db({
187
+ uri: "s3://BUCKET_NAME/databases/myapp"
188
+ });
189
+ ```
17
190
 
18
- const db = new S3db({
19
- region: 'us-east-1',
20
- accessKeyId: 'your-access-key',
21
- secretAccessKey: 'your-secret-key',
22
- bucket: 'your-bucket-name'
191
+ #### 3. MinIO or S3-Compatible Services
192
+
193
+ ```javascript
194
+ const s3db = new S3db({
195
+ uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
196
+ endpoint: "http://localhost:9000" // MinIO default endpoint
23
197
  });
198
+ ```
199
+
200
+ #### 4. Environment-Based Configuration
24
201
 
25
- await db.connect();
202
+ ```javascript
203
+ const s3db = new S3db({
204
+ uri: `s3://${process.env.AWS_ACCESS_KEY_ID}:${process.env.AWS_SECRET_ACCESS_KEY}@${process.env.AWS_BUCKET}/databases/${process.env.DATABASE_NAME}`,
205
+ endpoint: process.env.S3_ENDPOINT
206
+ });
207
+ ```
26
208
 
27
- const users = db.resource('users', {
28
- schema: {
29
- name: { type: 'string', required: true },
30
- email: { type: 'string', required: true },
31
- age: { type: 'number' }
209
+ #### Security Best Practices
210
+
211
+ - **IAM Roles**: Use IAM roles instead of access keys when possible (EC2, EKS, Lambda)
212
+ - **Environment Variables**: Store credentials in environment variables, not in code
213
+ - **Bucket Permissions**: Ensure your IAM role/user has the necessary S3 permissions:
214
+ - `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject`, `s3:ListBucket`, `s3:GetBucketLocation`
215
+
216
+ ### Advanced Configuration
217
+
218
+ ```javascript
219
+ import fs from "fs";
220
+
221
+ const s3db = new S3db({
222
+ uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
223
+ parallelism: 25, // Handle 25 concurrent operations
224
+ passphrase: fs.readFileSync("./cert.pem"), // Custom encryption key
225
+ cache: true, // Enable caching
226
+ ttl: 3600, // 1 hour cache TTL
227
+ plugins: [CostsPlugin] // Enable cost tracking
228
+ });
229
+ ```
230
+
231
+ ## ๐Ÿ“š Core Concepts
232
+
233
+ ### 1. Database
234
+
235
+ A database is a logical container for your resources, stored in a specific S3 prefix.
236
+
237
+ ```javascript
238
+ // This creates/connects to a database at:
239
+ // s3://bucket/databases/myapp/
240
+ const s3db = new S3db({
241
+ uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
242
+ });
243
+ ```
244
+
245
+ ### 2. Resources (Collections)
246
+
247
+ Resources are like tables in traditional databases - they define the structure of your documents.
248
+
249
+ ```javascript
250
+ const users = await s3db.createResource({
251
+ name: "users",
252
+ attributes: {
253
+ name: "string|min:2|max:100",
254
+ email: "email|unique",
255
+ age: "number|integer|positive",
256
+ profile: {
257
+ bio: "string|optional",
258
+ avatar: "url|optional"
259
+ },
260
+ tags: "array|items:string",
261
+ metadata: "object|optional"
32
262
  }
33
263
  });
264
+ ```
265
+
266
+ #### Automatic Timestamps
267
+
268
+ If you enable the `timestamps` option, `s3db.js` will automatically add `createdAt` and `updatedAt` fields to your resource, and keep them updated on insert and update operations.
269
+
270
+ ```js
271
+ const users = await s3db.createResource({
272
+ name: "users",
273
+ attributes: { name: "string", email: "email" },
274
+ options: { timestamps: true }
275
+ });
34
276
 
35
- // Insert data
277
+ const user = await users.insert({ name: "John", email: "john@example.com" });
278
+ console.log(user.createdAt); // e.g. "2024-06-27T12:34:56.789Z"
279
+ console.log(user.updatedAt); // same as createdAt on insert
280
+ ```
281
+
282
+ #### Resource Behaviors
283
+
284
+ `s3db.js` provides a powerful behavior system to handle how your data is managed when it approaches or exceeds S3's 2KB metadata limit. Each behavior implements different strategies for handling large documents.
285
+
286
+ ##### Available Behaviors
287
+
288
+ | Behavior | Description | Use Case |
289
+ |----------|-------------|----------|
290
+ | `user-management` | **Default** - Emits warnings but allows operations | Development and testing |
291
+ | `enforce-limits` | Throws errors when limit is exceeded | Strict data size control |
292
+ | `data-truncate` | Truncates data to fit within limits | Preserve structure, lose data |
293
+ | `body-overflow` | Stores excess data in S3 object body | Preserve all data |
294
+
295
+ ##### Behavior Configuration
296
+
297
+ ```javascript
298
+ const users = await s3db.createResource({
299
+ name: "users",
300
+ attributes: {
301
+ name: "string|min:2|max:100",
302
+ email: "email|unique",
303
+ bio: "string|optional",
304
+ preferences: "object|optional"
305
+ },
306
+ options: {
307
+ behavior: "body-overflow", // Choose behavior strategy
308
+ timestamps: true, // Enable automatic timestamps
309
+ partitions: { // Define data partitions
310
+ byRegion: {
311
+ fields: { region: "string" }
312
+ }
313
+ },
314
+ hooks: { // Custom operation hooks
315
+ preInsert: [async (data) => {
316
+ // Custom validation logic
317
+ return data;
318
+ }],
319
+ afterInsert: [async (data) => {
320
+ console.log("User created:", data.id);
321
+ }]
322
+ }
323
+ }
324
+ });
325
+ ```
326
+
327
+ ##### 1. User Management Behavior (Default)
328
+
329
+ The default behavior that gives you full control over data size management:
330
+
331
+ ```javascript
332
+ const users = await s3db.createResource({
333
+ name: "users",
334
+ attributes: { name: "string", email: "email" },
335
+ options: { behavior: "user-management" }
336
+ });
337
+
338
+ // Listen for limit warnings
339
+ users.on("exceedsLimit", (info) => {
340
+ console.log(`Document ${info.operation} exceeds 2KB limit:`, {
341
+ totalSize: info.totalSize,
342
+ limit: info.limit,
343
+ excess: info.excess
344
+ });
345
+ });
346
+
347
+ // Operations continue normally even if limit is exceeded
348
+ const user = await users.insert({
349
+ name: "John Doe",
350
+ email: "john@example.com",
351
+ largeBio: "Very long bio...".repeat(100) // Will trigger warning but succeed
352
+ });
353
+ ```
354
+
355
+ ##### 2. Enforce Limits Behavior
356
+
357
+ Strict behavior that prevents operations when data exceeds the limit:
358
+
359
+ ```javascript
360
+ const users = await s3db.createResource({
361
+ name: "users",
362
+ attributes: { name: "string", email: "email" },
363
+ options: { behavior: "enforce-limits" }
364
+ });
365
+
366
+ try {
367
+ const user = await users.insert({
368
+ name: "John Doe",
369
+ email: "john@example.com",
370
+ largeBio: "Very long bio...".repeat(100)
371
+ });
372
+ } catch (error) {
373
+ console.error("Operation failed:", error.message);
374
+ // Error: S3 metadata size exceeds 2KB limit. Current size: 2500 bytes, limit: 2048 bytes
375
+ }
376
+ ```
377
+
378
+ ##### 3. Data Truncate Behavior
379
+
380
+ Intelligently truncates data to fit within limits while preserving structure:
381
+
382
+ ```javascript
383
+ const users = await s3db.createResource({
384
+ name: "users",
385
+ attributes: { name: "string", email: "email", bio: "string" },
386
+ options: { behavior: "data-truncate" }
387
+ });
388
+
389
+ const user = await users.insert({
390
+ name: "John Doe",
391
+ email: "john@example.com",
392
+ bio: "This is a very long biography that will be truncated to fit within the 2KB metadata limit..."
393
+ });
394
+
395
+ console.log(user.bio); // "This is a very long biography that will be truncated to fit within the 2KB metadata limit..."
396
+ // Note: The bio will be truncated with "..." suffix if it exceeds available space
397
+ ```
398
+
399
+ ##### 4. Body Overflow Behavior
400
+
401
+ Stores excess data in the S3 object body, preserving all information:
402
+
403
+ ```javascript
404
+ const users = await s3db.createResource({
405
+ name: "users",
406
+ attributes: { name: "string", email: "email", bio: "string" },
407
+ options: { behavior: "body-overflow" }
408
+ });
409
+
410
+ const user = await users.insert({
411
+ name: "John Doe",
412
+ email: "john@example.com",
413
+ bio: "This is a very long biography that will be stored in the S3 object body..."
414
+ });
415
+
416
+ // All data is preserved and automatically merged when retrieved
417
+ console.log(user.bio); // Full biography preserved
418
+ ```
419
+
420
+ **How Body Overflow Works:**
421
+ - Small attributes stay in metadata for fast access
422
+ - Large attributes are moved to S3 object body
423
+ - Data is automatically merged when retrieved
424
+ - Maintains full data integrity
425
+
426
+ ##### Complete Resource Configuration Reference
427
+
428
+ ```javascript
429
+ const resource = await s3db.createResource({
430
+ // Required: Resource name (unique within database)
431
+ name: "users",
432
+
433
+ // Required: Schema definition
434
+ attributes: {
435
+ // Basic types
436
+ name: "string|min:2|max:100",
437
+ email: "email|unique",
438
+ age: "number|integer|positive",
439
+ isActive: "boolean",
440
+
441
+ // Advanced types
442
+ website: "url",
443
+ uuid: "uuid",
444
+ createdAt: "date",
445
+ price: "currency|symbol:$",
446
+
447
+ // Encrypted fields
448
+ password: "secret",
449
+ apiKey: "secret",
450
+
451
+ // Nested objects
452
+ address: {
453
+ street: "string",
454
+ city: "string",
455
+ country: "string",
456
+ zipCode: "string|optional"
457
+ },
458
+
459
+ // Arrays
460
+ tags: "array|items:string|unique",
461
+ scores: "array|items:number|min:1",
462
+
463
+ // Multiple types
464
+ id: ["string", "number"],
465
+
466
+ // Complex nested structures
467
+ metadata: {
468
+ settings: "object|optional",
469
+ preferences: "object|optional"
470
+ }
471
+ },
472
+
473
+ // Optional: Resource configuration
474
+ options: {
475
+ // Behavior strategy for handling 2KB metadata limits
476
+ behavior: "user-management", // "user-management" | "enforce-limits" | "data-truncate" | "body-overflow"
477
+
478
+ // Enable automatic timestamps
479
+ timestamps: true, // Adds createdAt and updatedAt fields
480
+
481
+ // Define data partitions for efficient querying
482
+ partitions: {
483
+ byRegion: {
484
+ fields: { region: "string" }
485
+ },
486
+ byAgeGroup: {
487
+ fields: { ageGroup: "string" }
488
+ },
489
+ byDate: {
490
+ fields: { createdAt: "date|maxlength:10" }
491
+ }
492
+ },
493
+
494
+ // Custom operation hooks
495
+ hooks: {
496
+ // Pre-operation hooks (can modify data)
497
+ preInsert: [
498
+ async (data) => {
499
+ // Validate or transform data before insert
500
+ if (!data.email.includes("@")) {
501
+ throw new Error("Invalid email format");
502
+ }
503
+ return data;
504
+ }
505
+ ],
506
+ preUpdate: [
507
+ async (id, data) => {
508
+ // Validate or transform data before update
509
+ return data;
510
+ }
511
+ ],
512
+ preDelete: [
513
+ async (id) => {
514
+ // Validate before deletion
515
+ return true; // Return false to abort
516
+ }
517
+ ],
518
+
519
+ // Post-operation hooks (cannot modify data)
520
+ afterInsert: [
521
+ async (data) => {
522
+ console.log("User created:", data.id);
523
+ }
524
+ ],
525
+ afterUpdate: [
526
+ async (id, data) => {
527
+ console.log("User updated:", id);
528
+ }
529
+ ],
530
+ afterDelete: [
531
+ async (id) => {
532
+ console.log("User deleted:", id);
533
+ }
534
+ ]
535
+ }
536
+ }
537
+ });
538
+ ```
539
+
540
+ ### 3. Schema Validation
541
+
542
+ `s3db.js` uses [fastest-validator](https://github.com/icebob/fastest-validator) for schema validation with robust handling of edge cases:
543
+
544
+ ```javascript
545
+ const attributes = {
546
+ // Basic types
547
+ name: "string|min:2|max:100|trim",
548
+ email: "email|nullable",
549
+ age: "number|integer|positive",
550
+ isActive: "boolean",
551
+
552
+ // Advanced types
553
+ website: "url",
554
+ uuid: "uuid",
555
+ createdAt: "date",
556
+ price: "currency|symbol:$",
557
+
558
+ // Custom s3db types
559
+ password: "secret", // Encrypted field
560
+
561
+ // Nested objects (supports empty objects and null values)
562
+ address: {
563
+ street: "string",
564
+ city: "string",
565
+ country: "string",
566
+ zipCode: "string|optional"
567
+ },
568
+
569
+ // Arrays (robust serialization with special character handling)
570
+ tags: "array|items:string|unique", // Handles empty arrays: []
571
+ scores: "array|items:number|min:1", // Handles null arrays
572
+ categories: "array|items:string", // Handles arrays with pipe characters: ['tag|special', 'normal']
573
+
574
+ // Multiple types
575
+ id: ["string", "number"],
576
+
577
+ // Complex nested structures
578
+ metadata: {
579
+ settings: "object|optional", // Can be empty: {}
580
+ preferences: "object|optional" // Can be null
581
+ }
582
+ };
583
+ ```
584
+
585
+ ### Enhanced Array and Object Handling
586
+
587
+ s3db.js now provides robust serialization for complex data structures:
588
+
589
+ ```javascript
590
+ // โœ… Supported: Empty arrays and objects
36
591
  const user = await users.insert({
37
- name: 'John Doe',
38
- email: 'john@example.com',
592
+ name: "John Doe",
593
+ tags: [], // Empty array - properly serialized
594
+ metadata: {}, // Empty object - properly handled
595
+ preferences: null // Null object - correctly preserved
596
+ });
597
+
598
+ // โœ… Supported: Arrays with special characters
599
+ const product = await products.insert({
600
+ name: "Widget",
601
+ categories: ["electronics|gadgets", "home|office"], // Pipe characters escaped
602
+ tags: ["tag|with|pipes", "normal-tag"] // Multiple pipes handled
603
+ });
604
+ ```
605
+
606
+ ## ๐Ÿ› ๏ธ API Reference
607
+
608
+ ### Database Operations
609
+
610
+ #### Connect to Database
611
+
612
+ ```javascript
613
+ await s3db.connect();
614
+ // Emits 'connected' event when ready
615
+ ```
616
+
617
+ #### Create Resource
618
+
619
+ ```javascript
620
+ const resource = await s3db.createResource({
621
+ name: "users",
622
+ attributes: {
623
+ name: "string",
624
+ email: "email"
625
+ }
626
+ });
627
+ ```
628
+
629
+ #### Get Resource Reference
630
+
631
+ ```javascript
632
+ const users = s3db.resource("users");
633
+ // or
634
+ const users = s3db.resources.users
635
+ ```
636
+
637
+ ### Resource Operations
638
+
639
+ #### Insert Document
640
+
641
+ ```javascript
642
+ // With custom ID
643
+ const user = await users.insert({
644
+ id: "user-123",
645
+ name: "John Doe",
646
+ email: "john@example.com"
647
+ });
648
+
649
+ // Auto-generated ID
650
+ const user = await users.insert({
651
+ name: "Jane Doe",
652
+ email: "jane@example.com"
653
+ });
654
+ // ID will be auto-generated using nanoid
655
+ ```
656
+
657
+ #### Get Document
658
+
659
+ ```javascript
660
+ const user = await users.get("user-123");
661
+ console.log(user.name); // "John Doe"
662
+ ```
663
+
664
+ #### Update Document
665
+
666
+ ```javascript
667
+ const updatedUser = await users.update("user-123", {
668
+ name: "John Smith",
669
+ age: 31
670
+ });
671
+ // Only specified fields are updated
672
+ ```
673
+
674
+ #### Upsert Document
675
+
676
+ ```javascript
677
+ // Insert if doesn't exist, update if exists
678
+ const user = await users.upsert("user-123", {
679
+ name: "John Doe",
680
+ email: "john@example.com",
39
681
  age: 30
40
682
  });
683
+ ```
41
684
 
42
- // Query data
43
- const allUsers = await users.find();
44
- const john = await users.findOne({ name: 'John Doe' });
685
+ #### Delete Document
686
+
687
+ ```javascript
688
+ await users.delete("user-123");
689
+ ```
690
+
691
+ #### Count Documents
692
+
693
+ ```javascript
694
+ const count = await users.count();
695
+ console.log(`Total users: ${count}`);
696
+ ```
697
+
698
+ ### Bulk Operations
699
+
700
+ #### Insert Many
701
+
702
+ ```javascript
703
+ const users = [
704
+ { name: "User 1", email: "user1@example.com" },
705
+ { name: "User 2", email: "user2@example.com" },
706
+ { name: "User 3", email: "user3@example.com" }
707
+ ];
708
+
709
+ await users.insertMany(users);
710
+ ```
711
+
712
+ #### Get Many
713
+
714
+ ```javascript
715
+ const userList = await users.getMany(["user-1", "user-2", "user-3"]);
716
+ ```
717
+
718
+ #### Delete Many
719
+
720
+ ```javascript
721
+ await users.deleteMany(["user-1", "user-2", "user-3"]);
722
+ ```
723
+
724
+ #### Get All
725
+
726
+ ```javascript
727
+ const allUsers = await users.getAll();
728
+ // Returns all documents in the resource
729
+ ```
730
+
731
+ #### List IDs
732
+
733
+ ```javascript
734
+ const userIds = await users.listIds();
735
+ // Returns array of all document IDs
736
+ ```
737
+
738
+ #### Delete All
739
+
740
+ ```javascript
741
+ await users.deleteAll();
742
+ // โš ๏ธ Destructive operation - removes all documents
743
+ ```
744
+
745
+ ## ๐Ÿ“Š Examples
746
+
747
+ ### E-commerce Application
748
+
749
+ ```javascript
750
+ // Create product resource with body-overflow behavior for long descriptions
751
+ const products = await s3db.createResource({
752
+ name: "products",
753
+ attributes: {
754
+ name: "string|min:2|max:200",
755
+ description: "string|optional",
756
+ price: "number|positive",
757
+ category: "string",
758
+ tags: "array|items:string",
759
+ inStock: "boolean",
760
+ images: "array|items:url",
761
+ metadata: "object|optional"
762
+ },
763
+ options: {
764
+ behavior: "body-overflow", // Handle long product descriptions
765
+ timestamps: true // Track creation and update times
766
+ }
767
+ });
768
+
769
+ // Create order resource with enforce-limits for strict data control
770
+ const orders = await s3db.createResource({
771
+ name: "orders",
772
+ attributes: {
773
+ customerId: "string",
774
+ products: "array|items:string",
775
+ total: "number|positive",
776
+ status: "string|enum:pending,paid,shipped,delivered",
777
+ shippingAddress: {
778
+ street: "string",
779
+ city: "string",
780
+ country: "string",
781
+ zipCode: "string"
782
+ },
783
+ createdAt: "date"
784
+ },
785
+ options: {
786
+ behavior: "enforce-limits", // Strict validation for order data
787
+ timestamps: true
788
+ }
789
+ });
790
+
791
+ // Insert products (long descriptions will be handled by body-overflow)
792
+ const product = await products.insert({
793
+ name: "Wireless Headphones",
794
+ description: "High-quality wireless headphones with noise cancellation, 30-hour battery life, premium comfort design, and crystal-clear audio quality. Perfect for music lovers, professionals, and gamers alike. Features include Bluetooth 5.0, active noise cancellation, touch controls, and a premium carrying case.",
795
+ price: 99.99,
796
+ category: "electronics",
797
+ tags: ["wireless", "bluetooth", "audio", "noise-cancellation"],
798
+ inStock: true,
799
+ images: ["https://example.com/headphones.jpg"]
800
+ });
801
+
802
+ // Create order (enforce-limits ensures data integrity)
803
+ const order = await orders.insert({
804
+ customerId: "customer-123",
805
+ products: [product.id],
806
+ total: 99.99,
807
+ status: "pending",
808
+ shippingAddress: {
809
+ street: "123 Main St",
810
+ city: "New York",
811
+ country: "USA",
812
+ zipCode: "10001"
813
+ },
814
+ createdAt: new Date()
815
+ });
816
+ ```
817
+
818
+ ### User Authentication System
819
+
820
+ ```javascript
821
+ // Create users resource with encrypted password and strict validation
822
+ const users = await s3db.createResource({
823
+ name: "users",
824
+ attributes: {
825
+ username: "string|min:3|max:50|unique",
826
+ email: "email|unique",
827
+ password: "secret", // Encrypted field
828
+ role: "string|enum:user,admin,moderator",
829
+ isActive: "boolean",
830
+ lastLogin: "date|optional",
831
+ profile: {
832
+ firstName: "string",
833
+ lastName: "string",
834
+ avatar: "url|optional",
835
+ bio: "string|optional"
836
+ }
837
+ },
838
+ options: {
839
+ behavior: "enforce-limits", // Strict validation for user data
840
+ timestamps: true // Track account creation and updates
841
+ }
842
+ });
843
+
844
+ // Create sessions resource with body-overflow for session data
845
+ const sessions = await s3db.createResource({
846
+ name: "sessions",
847
+ attributes: {
848
+ userId: "string",
849
+ token: "secret", // Encrypted session token
850
+ expiresAt: "date",
851
+ userAgent: "string|optional",
852
+ ipAddress: "string|optional",
853
+ sessionData: "object|optional" // Additional session metadata
854
+ },
855
+ options: {
856
+ behavior: "body-overflow", // Handle large session data
857
+ timestamps: true
858
+ }
859
+ });
860
+
861
+ // Register user (enforce-limits ensures data integrity)
862
+ const user = await users.insert({
863
+ username: "john_doe",
864
+ email: "john@example.com",
865
+ password: "secure_password_123",
866
+ role: "user",
867
+ isActive: true,
868
+ profile: {
869
+ firstName: "John",
870
+ lastName: "Doe"
871
+ }
872
+ });
873
+
874
+ // Create session (body-overflow preserves all session data)
875
+ const session = await sessions.insert({
876
+ userId: user.id,
877
+ token: "jwt_token_here",
878
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
879
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
880
+ ipAddress: "192.168.1.1",
881
+ sessionData: {
882
+ preferences: { theme: "dark", language: "en" },
883
+ lastActivity: new Date(),
884
+ deviceInfo: { type: "desktop", os: "Windows" }
885
+ }
886
+ });
887
+ ```
888
+
889
+ ## ๐Ÿ”„ Streaming
890
+
891
+ For large datasets, use streams to process data efficiently:
892
+
893
+ ### Readable Stream
894
+
895
+ ```javascript
896
+ const readableStream = await users.readable();
897
+
898
+ readableStream.on("id", (id) => {
899
+ console.log("Processing user ID:", id);
900
+ });
901
+
902
+ readableStream.on("data", (user) => {
903
+ console.log("User:", user.name);
904
+ // Process each user
905
+ });
906
+
907
+ readableStream.on("end", () => {
908
+ console.log("Finished processing all users");
909
+ });
910
+
911
+ readableStream.on("error", (error) => {
912
+ console.error("Stream error:", error);
913
+ });
914
+ ```
915
+
916
+ ### Writable Stream
917
+
918
+ ```javascript
919
+ const writableStream = await users.writable();
920
+
921
+ // Write data to stream
922
+ writableStream.write({
923
+ name: "User 1",
924
+ email: "user1@example.com"
925
+ });
926
+
927
+ writableStream.write({
928
+ name: "User 2",
929
+ email: "user2@example.com"
930
+ });
931
+
932
+ // End stream
933
+ writableStream.end();
934
+ ```
935
+
936
+ ### Stream to CSV
937
+
938
+ ```javascript
939
+ import fs from "fs";
940
+ import { createObjectCsvWriter } from "csv-writer";
941
+
942
+ const csvWriter = createObjectCsvWriter({
943
+ path: "users.csv",
944
+ header: [
945
+ { id: "id", title: "ID" },
946
+ { id: "name", title: "Name" },
947
+ { id: "email", title: "Email" }
948
+ ]
949
+ });
950
+
951
+ const readableStream = await users.readable();
952
+ const records = [];
953
+
954
+ readableStream.on("data", (user) => {
955
+ records.push(user);
956
+ });
957
+
958
+ readableStream.on("end", async () => {
959
+ await csvWriter.writeRecords(records);
960
+ console.log("CSV file created successfully");
961
+ });
962
+ ```
963
+
964
+ ## ๐Ÿ” Security & Encryption
965
+
966
+ ### Field-Level Encryption
967
+
968
+ Use the `"secret"` type for sensitive data:
969
+
970
+ ```javascript
971
+ const users = await s3db.createResource({
972
+ name: "users",
973
+ attributes: {
974
+ username: "string",
975
+ email: "email",
976
+ password: "secret", // Encrypted
977
+ apiKey: "secret", // Encrypted
978
+ creditCard: "secret" // Encrypted
979
+ }
980
+ });
981
+
982
+ // Data is automatically encrypted/decrypted
983
+ const user = await users.insert({
984
+ username: "john_doe",
985
+ email: "john@example.com",
986
+ password: "my_secure_password", // Stored encrypted
987
+ apiKey: "sk_live_123456789", // Stored encrypted
988
+ creditCard: "4111111111111111" // Stored encrypted
989
+ });
990
+
991
+ // Retrieved data is automatically decrypted
992
+ const retrieved = await users.get(user.id);
993
+ console.log(retrieved.password); // "my_secure_password" (decrypted)
994
+ ```
995
+
996
+ ### Custom Encryption Key
997
+
998
+ ```javascript
999
+ import fs from "fs";
1000
+
1001
+ const s3db = new S3db({
1002
+ uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
1003
+ passphrase: fs.readFileSync("./private-key.pem") // Custom encryption key
1004
+ });
1005
+ ```
1006
+
1007
+ ## ๐Ÿ’ฐ Cost Analysis
1008
+
1009
+ ### Understanding S3 Costs
1010
+
1011
+ - **PUT Requests**: $0.000005 per 1,000 requests
1012
+ - **GET Requests**: $0.0000004 per 1,000 requests
1013
+ - **Data Transfer**: $0.09 per GB
1014
+ - **Storage**: $0.023 per GB (but s3db.js uses 0-byte files)
1015
+
1016
+ ### Cost Examples
1017
+
1018
+ #### Small Application (1,000 users)
1019
+
1020
+ ```javascript
1021
+ // Setup cost (one-time)
1022
+ const setupCost = 0.005; // 1,000 PUT requests
1023
+
1024
+ // Monthly read cost
1025
+ const monthlyReadCost = 0.0004; // 1,000 GET requests
1026
+
1027
+ console.log(`Setup: $${setupCost}`);
1028
+ console.log(`Monthly reads: $${monthlyReadCost}`);
45
1029
  ```
46
1030
 
47
- ### Node.js (CommonJS)
1031
+ #### Large Application (1,000,000 users)
48
1032
 
49
1033
  ```javascript
50
- const S3db = require('s3db.js');
1034
+ // Setup cost (one-time)
1035
+ const setupCost = 5.00; // 1,000,000 PUT requests
1036
+
1037
+ // Monthly read cost
1038
+ const monthlyReadCost = 0.40; // 1,000,000 GET requests
1039
+
1040
+ console.log(`Setup: $${setupCost}`);
1041
+ console.log(`Monthly reads: $${monthlyReadCost}`);
1042
+ ```
1043
+
1044
+ ### Cost Tracking Plugin
1045
+
1046
+ ```javascript
1047
+ import { CostsPlugin } from "s3db.js";
1048
+
1049
+ const s3db = new S3db({
1050
+ uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
1051
+ plugins: [CostsPlugin]
1052
+ });
1053
+
1054
+ // After operations
1055
+ console.log("Total cost:", s3db.client.costs.total.toFixed(4), "USD");
1056
+ console.log("Requests made:", s3db.client.costs.requests.total);
1057
+ ```
1058
+
1059
+ ## ๐ŸŽ›๏ธ Advanced Features
1060
+
1061
+ ### AutoEncrypt / AutoDecrypt
1062
+
1063
+ Fields with the type `secret` are automatically encrypted and decrypted using the resource's passphrase. This ensures sensitive data is protected at rest.
1064
+
1065
+ ```js
1066
+ const users = await s3db.createResource({
1067
+ name: "users",
1068
+ attributes: {
1069
+ username: "string",
1070
+ password: "secret" // Will be encrypted
1071
+ }
1072
+ });
1073
+
1074
+ const user = await users.insert({
1075
+ username: "john_doe",
1076
+ password: "my_secret_password"
1077
+ });
1078
+
1079
+ // The password is stored encrypted in S3, but automatically decrypted when retrieved
1080
+ const retrieved = await users.get(user.id);
1081
+ console.log(retrieved.password); // "my_secret_password"
1082
+ ```
1083
+
1084
+ ### Resource Events
1085
+
1086
+ All resources emit events for key operations. You can listen to these events for logging, analytics, or custom workflows.
1087
+
1088
+ ```js
1089
+ users.on("insert", (data) => console.log("User inserted:", data.id));
1090
+ users.on("get", (data) => console.log("User retrieved:", data.id));
1091
+ users.on("update", (attrs, data) => console.log("User updated:", data.id));
1092
+ users.on("delete", (id) => console.log("User deleted:", id));
1093
+ ```
1094
+
1095
+ ### Resource Schema Export/Import
1096
+
1097
+ You can export and import resource schemas for backup, migration, or versioning purposes.
1098
+
1099
+ ```js
1100
+ // Export schema
1101
+ const schemaData = users.schema.export();
1102
+
1103
+ // Import schema
1104
+ const importedSchema = Schema.import(schemaData);
1105
+ ```
1106
+
1107
+ ## Partitions
1108
+
1109
+ `s3db.js` supports **partitions** to organize and query your data efficiently. Partitions allow you to group documents by one or more fields, making it easy to filter, archive, or manage large datasets.
1110
+
1111
+ ### Defining partitions
1112
+
1113
+ You can define partitions when creating a resource using the `options.partitions` property:
1114
+
1115
+ ```js
1116
+ const users = await s3db.createResource({
1117
+ name: "users",
1118
+ attributes: {
1119
+ name: "string",
1120
+ email: "email",
1121
+ region: "string",
1122
+ ageGroup: "string"
1123
+ },
1124
+ options: {
1125
+ partitions: {
1126
+ byRegion: {
1127
+ fields: { region: "string" }
1128
+ },
1129
+ byAgeGroup: {
1130
+ fields: { ageGroup: "string" }
1131
+ }
1132
+ }
1133
+ }
1134
+ });
1135
+ ```
1136
+
1137
+ ### Querying by partition
51
1138
 
52
- const db = new S3db({
53
- connectionString: 's3://access-key:secret-key@bucket-name/prefix?region=us-east-1'
1139
+ ```js
1140
+ // Find all users in the 'south' region
1141
+ const usersSouth = await users.query({ region: "south" });
1142
+
1143
+ // Find all users in the 'adult' age group
1144
+ const adults = await users.query({ ageGroup: "adult" });
1145
+ ```
1146
+
1147
+ ### Example: Time-based partition
1148
+
1149
+ ```js
1150
+ const logs = await s3db.createResource({
1151
+ name: "logs",
1152
+ attributes: {
1153
+ message: "string",
1154
+ level: "string",
1155
+ createdAt: "date"
1156
+ },
1157
+ options: {
1158
+ partitions: {
1159
+ byDate: {
1160
+ fields: { createdAt: "date|maxlength:10" }
1161
+ }
1162
+ }
1163
+ }
54
1164
  });
55
1165
 
56
- await db.connect();
57
- // ... rest of the code
1166
+ // Query logs for a specific day
1167
+ const logsToday = await logs.query({ createdAt: "2024-06-27" });
58
1168
  ```
59
1169
 
60
- ### Browser
1170
+ ## Hooks
1171
+
1172
+ `s3db.js` provides a powerful hooks system to let you run custom logic before and after key operations on your resources. Hooks can be used for validation, transformation, logging, or any custom workflow.
1173
+
1174
+ ### Supported hooks
1175
+ - `preInsert` / `afterInsert`
1176
+ - `preUpdate` / `afterUpdate`
1177
+ - `preDelete` / `afterDelete`
61
1178
 
62
- ```html
63
- <!DOCTYPE html>
64
- <html>
65
- <head>
66
- <script src="https://unpkg.com/s3db.js@latest/dist/s3db.iife.min.js"></script>
67
- </head>
68
- <body>
69
- <script>
70
- const db = new s3db.S3db({
71
- region: 'us-east-1',
72
- accessKeyId: 'your-access-key',
73
- secretAccessKey: 'your-secret-key',
74
- bucket: 'your-bucket-name'
75
- });
1179
+ ### Registering hooks
1180
+ You can register hooks when creating a resource or dynamically:
76
1181
 
77
- db.connect().then(() => {
78
- const users = db.resource('users');
79
- return users.insert({ name: 'John', email: 'john@example.com' });
80
- });
81
- </script>
82
- </body>
83
- </html>
1182
+ ```js
1183
+ const users = await s3db.createResource({
1184
+ name: "users",
1185
+ attributes: { name: "string", email: "email" },
1186
+ options: {
1187
+ hooks: {
1188
+ preInsert: [async (data) => {
1189
+ if (!data.email.includes("@")) throw new Error("Invalid email");
1190
+ return data;
1191
+ }],
1192
+ afterInsert: [async (data) => {
1193
+ console.log("User inserted:", data.id);
1194
+ }]
1195
+ }
1196
+ }
1197
+ });
1198
+
1199
+ // Or dynamically:
1200
+ users.addHook('preInsert', async (data) => {
1201
+ // Custom logic
1202
+ return data;
1203
+ });
84
1204
  ```
85
1205
 
86
- ## Features
1206
+ ### Hook execution order
1207
+ - Internal hooks run first, user hooks run last (in the order they were added).
1208
+ - Hooks can be async and can modify the data (for `pre*` hooks).
1209
+ - If a hook throws, the operation is aborted.
87
1210
 
88
- - ๐Ÿš€ **High Performance**: Optimized for large datasets with streaming support
89
- - ๐Ÿ”’ **Security**: Built-in encryption and compression
90
- - ๐Ÿ“Š **Schema Validation**: Automatic data validation with customizable schemas
91
- - ๐Ÿ”„ **Caching**: Intelligent caching with TTL support
92
- - ๐Ÿ“ฆ **Partitioning**: Automatic data partitioning for better performance
93
- - ๐Ÿ”Œ **Plugin System**: Extensible with custom plugins
94
- - ๐ŸŒ **Universal**: Works in Node.js and browsers
95
- - ๐Ÿ“ **TypeScript**: Full TypeScript support with autocomplete
1211
+ ## Plugins
96
1212
 
97
- ## API Reference
1213
+ `s3db.js` supports plugins to extend or customize its behavior. Plugins can hook into lifecycle events, add new methods, or integrate with external systems.
98
1214
 
99
- ### S3db Class
1215
+ ### Example: Custom plugin
100
1216
 
101
- The main database class for connecting to S3 and managing resources.
1217
+ ```js
1218
+ const MyPlugin = {
1219
+ setup(s3db) {
1220
+ console.log("Plugin setup");
1221
+ },
1222
+ start() {
1223
+ console.log("Plugin started");
1224
+ },
1225
+ onUserCreated(user) {
1226
+ console.log("New user created:", user.id);
1227
+ }
1228
+ };
102
1229
 
103
- #### Constructor
1230
+ const s3db = new S3db({
1231
+ uri: "s3://...",
1232
+ plugins: [MyPlugin]
1233
+ });
1234
+ ```
1235
+
1236
+ ## ๐Ÿšจ Limitations & Best Practices
1237
+
1238
+ ### Limitations
1239
+
1240
+ 1. **Document Size**: Maximum ~2KB per document (metadata only) - **๐Ÿ’ก Use behaviors to handle larger documents**
1241
+ 2. **No Complex Queries**: No SQL-like WHERE clauses or joins
1242
+ 3. **No Indexes**: No automatic indexing for fast lookups
1243
+ 4. **Sequential IDs**: Best performance with sequential IDs (00001, 00002, etc.)
1244
+ 5. **No Transactions**: No ACID transactions across multiple operations
1245
+ 6. **S3 Pagination**: S3 lists objects in pages of 1000 items maximum, and these operations are not parallelizable, which can make listing large datasets slow
1246
+
1247
+ **๐Ÿ’ก Overcoming the 2KB Limit**: Use resource behaviors to handle documents that exceed the 2KB metadata limit:
1248
+ - **`body-overflow`**: Stores excess data in S3 object body (preserves all data)
1249
+ - **`data-truncate`**: Intelligently truncates data to fit within limits
1250
+ - **`enforce-limits`**: Strict validation to prevent oversized documents
1251
+ - **`user-management`**: Default behavior with warnings and monitoring
1252
+
1253
+ ### โœ… Recent Improvements
1254
+
1255
+ **๐Ÿ”ง Enhanced Data Serialization (v3.3.2+)**
1256
+
1257
+ s3db.js now handles complex data structures robustly:
1258
+
1259
+ - **Empty Arrays**: `[]` correctly serialized and preserved
1260
+ - **Null Arrays**: `null` values maintained without corruption
1261
+ - **Special Characters**: Arrays with pipe `|` characters properly escaped
1262
+ - **Empty Objects**: `{}` correctly mapped and stored
1263
+ - **Null Objects**: `null` object values preserved during serialization
1264
+ - **Nested Structures**: Complex nested objects with mixed empty/null values supported
1265
+
1266
+ ### Best Practices
1267
+
1268
+ #### 1. Design for Document Storage
104
1269
 
105
1270
  ```javascript
106
- new S3db(config)
1271
+ // โœ… Good: Nested structure is fine
1272
+ const user = {
1273
+ id: "user-123",
1274
+ name: "John Doe",
1275
+ email: "john@example.com",
1276
+ profile: {
1277
+ bio: "Software developer",
1278
+ avatar: "https://example.com/avatar.jpg",
1279
+ preferences: {
1280
+ theme: "dark",
1281
+ notifications: true
1282
+ }
1283
+ }
1284
+ };
1285
+
1286
+ // โŒ Avoid: Large arrays in documents
1287
+ const user = {
1288
+ id: "user-123",
1289
+ name: "John Doe",
1290
+ // This could exceed metadata limits
1291
+ purchaseHistory: [
1292
+ { id: "order-1", date: "2023-01-01", total: 99.99 },
1293
+ { id: "order-2", date: "2023-01-15", total: 149.99 },
1294
+ // ... many more items
1295
+ ]
1296
+ };
107
1297
  ```
108
1298
 
109
- **Config Options:**
110
- - `connectionString` (string): S3 connection string
111
- - `region` (string): AWS region
112
- - `accessKeyId` (string): AWS access key
113
- - `secretAccessKey` (string): AWS secret key
114
- - `bucket` (string): S3 bucket name
115
- - `prefix` (string): Key prefix for all objects
116
- - `encryption` (boolean): Enable encryption (default: false)
117
- - `compression` (boolean): Enable compression (default: false)
118
- - `cache` (boolean): Enable caching (default: true)
119
- - `cacheTTL` (number): Cache TTL in seconds (default: 300)
1299
+ #### 2. Use Sequential IDs
120
1300
 
121
- #### Methods
1301
+ ```javascript
1302
+ // โœ… Good: Sequential IDs for better performance
1303
+ const users = ["00001", "00002", "00003", "00004"];
122
1304
 
123
- - `connect()`: Connect to S3
124
- - `disconnect()`: Disconnect from S3
125
- - `resource(name, config)`: Create or get a resource
126
- - `listResources()`: List all resources
127
- - `getVersion()`: Get package version
1305
+ // โš ๏ธ Acceptable: Random IDs (but ensure sufficient uniqueness)
1306
+ const users = ["abc123", "def456", "ghi789", "jkl012"];
128
1307
 
129
- ### Resource Class
1308
+ // โŒ Avoid: Random IDs with low combinations (risk of collisions)
1309
+ const users = ["a1", "b2", "c3", "d4"]; // Only 26*10 = 260 combinations
1310
+ ```
1311
+
1312
+ #### 3. Optimize for Read Patterns
1313
+
1314
+ ```javascript
1315
+ // โœ… Good: Store frequently accessed data together
1316
+ const order = {
1317
+ id: "order-123",
1318
+ customerId: "customer-456",
1319
+ customerName: "John Doe", // Denormalized for quick access
1320
+ items: ["product-1", "product-2"],
1321
+ total: 99.99
1322
+ };
1323
+
1324
+ // โŒ Avoid: Requiring multiple lookups
1325
+ const order = {
1326
+ id: "order-123",
1327
+ customerId: "customer-456", // Requires separate lookup
1328
+ items: ["product-1", "product-2"]
1329
+ };
1330
+ ```
1331
+
1332
+ #### 4. Use Streaming for Large Datasets
1333
+
1334
+ ```javascript
1335
+ // โœ… Good: Use streams for large operations
1336
+ const readableStream = await users.readable();
1337
+ readableStream.on("data", (user) => {
1338
+ // Process each user individually
1339
+ });
1340
+
1341
+ // โŒ Avoid: Loading all data at once
1342
+ const allUsers = await users.getAll(); // May timeout with large datasets
1343
+ ```
1344
+
1345
+ #### 5. Implement Proper Error Handling
130
1346
 
131
- Represents a collection of documents in S3.
1347
+ ```javascript
1348
+ // Method 1: Try-catch with get()
1349
+ try {
1350
+ const user = await users.get("non-existent-id");
1351
+ } catch (error) {
1352
+ if (error.message.includes("does not exist")) {
1353
+ console.log("User not found");
1354
+ } else {
1355
+ console.error("Unexpected error:", error);
1356
+ }
1357
+ }
1358
+
1359
+ // Method 2: Check existence first (โš ๏ธ Additional request cost)
1360
+ const userId = "user-123";
1361
+ if (await users.exists(userId)) {
1362
+ const user = await users.get(userId);
1363
+ console.log("User found:", user.name);
1364
+ } else {
1365
+ console.log("User not found");
1366
+ }
1367
+ ```
132
1368
 
133
- #### Methods
1369
+ **โš ๏ธ Cost Warning**: Using `exists()` creates an additional S3 request. For high-volume operations, prefer the try-catch approach to minimize costs.
134
1370
 
135
- - `insert(data, options)`: Insert a single document
136
- - `insertMany(data, options)`: Insert multiple documents
137
- - `find(query, options)`: Find documents
138
- - `findOne(query, options)`: Find a single document
139
- - `update(query, data, options)`: Update documents
140
- - `delete(query, options)`: Delete documents
141
- - `createReadStream(query, options)`: Create a read stream
142
- - `createWriteStream(options)`: Create a write stream
1371
+ #### 6. Choose the Right Behavior Strategy
1372
+
1373
+ ```javascript
1374
+ // โœ… For development and testing - allows flexibility
1375
+ const devUsers = await s3db.createResource({
1376
+ name: "users",
1377
+ attributes: { name: "string", email: "email" },
1378
+ options: { behavior: "user-management" }
1379
+ });
143
1380
 
144
- ## Examples
1381
+ // โœ… For production with strict data control
1382
+ const prodUsers = await s3db.createResource({
1383
+ name: "users",
1384
+ attributes: { name: "string", email: "email" },
1385
+ options: { behavior: "enforce-limits" }
1386
+ });
1387
+
1388
+ // โœ… For preserving all data with larger documents
1389
+ const blogPosts = await s3db.createResource({
1390
+ name: "posts",
1391
+ attributes: { title: "string", content: "string", author: "string" },
1392
+ options: { behavior: "body-overflow" }
1393
+ });
1394
+
1395
+ // โœ… For structured data where truncation is acceptable
1396
+ const productDescriptions = await s3db.createResource({
1397
+ name: "products",
1398
+ attributes: { name: "string", description: "string", price: "number" },
1399
+ options: { behavior: "data-truncate" }
1400
+ });
1401
+ ```
145
1402
 
146
- See the [examples](./examples) directory for more detailed usage examples.
1403
+ **Behavior Selection Guide:**
1404
+ - **`user-management`**: Development, testing, or when you want full control
1405
+ - **`enforce-limits`**: Production systems requiring strict data validation
1406
+ - **`body-overflow`**: When data integrity is critical and you need to preserve all information
1407
+ - **`data-truncate`**: When you can afford to lose some data but want to maintain structure
147
1408
 
148
- ## License
1409
+ ### Performance Tips
149
1410
 
150
- UNLICENSED
1411
+ 1. **Enable Caching**: Use `cache: true` for frequently accessed data
1412
+ 2. **Adjust Parallelism**: Increase `parallelism`