s3db.js 4.0.1 โ†’ 4.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
@@ -1,150 +1,1868 @@
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
10
+
11
+ ```bash
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
6
116
 
7
117
  ```bash
8
- npm install s3db.js
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]
172
+ ```
173
+
174
+ #### 1. AWS S3 with Access Keys
175
+
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)
183
+
184
+ ```javascript
185
+ // No credentials needed - uses IAM role permissions
186
+ const s3db = new S3db({
187
+ uri: "s3://BUCKET_NAME/databases/myapp"
188
+ });
189
+ ```
190
+
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
197
+ });
198
+ ```
199
+
200
+ #### 4. Environment-Based Configuration
201
+
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
+ });
9
207
  ```
10
208
 
11
- ## Quick Start
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`
12
215
 
13
- ### Node.js (ES Modules)
216
+ ### Advanced Configuration
14
217
 
15
218
  ```javascript
16
- import S3db from 's3db.js';
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.
17
236
 
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'
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"
23
242
  });
243
+ ```
24
244
 
25
- await db.connect();
245
+ ### 2. Resources (Collections)
26
246
 
27
- const users = db.resource('users', {
28
- schema: {
29
- name: { type: 'string', required: true },
30
- email: { type: 'string', required: true },
31
- age: { type: 'number' }
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
+ ```
34
265
 
35
- // Insert data
266
+ #### Nested Attributes
267
+
268
+ `s3db.js` fully supports nested object attributes, allowing you to create complex document structures:
269
+
270
+ ```javascript
271
+ const users = await s3db.createResource({
272
+ name: "users",
273
+ attributes: {
274
+ name: "string|required",
275
+ email: "email|required",
276
+ utm: {
277
+ source: "string|required",
278
+ medium: "string|required",
279
+ campaign: "string|required",
280
+ term: "string|optional",
281
+ content: "string|optional"
282
+ },
283
+ address: {
284
+ street: "string|required",
285
+ city: "string|required",
286
+ state: "string|required",
287
+ country: "string|required",
288
+ zipCode: "string|optional"
289
+ },
290
+ metadata: {
291
+ category: "string|required",
292
+ priority: "string|required",
293
+ settings: "object|optional"
294
+ }
295
+ }
296
+ });
297
+
298
+ // Insert data with nested objects
36
299
  const user = await users.insert({
37
- name: 'John Doe',
38
- email: 'john@example.com',
300
+ name: "John Doe",
301
+ email: "john@example.com",
302
+ utm: {
303
+ source: "google",
304
+ medium: "cpc",
305
+ campaign: "brand_awareness",
306
+ term: "search term"
307
+ },
308
+ address: {
309
+ street: "123 Main St",
310
+ city: "San Francisco",
311
+ state: "California",
312
+ country: "US",
313
+ zipCode: "94105"
314
+ },
315
+ metadata: {
316
+ category: "premium",
317
+ priority: "high",
318
+ settings: { theme: "dark", notifications: true }
319
+ }
320
+ });
321
+
322
+ // Access nested data
323
+ console.log(user.utm.source); // "google"
324
+ console.log(user.address.city); // "San Francisco"
325
+ console.log(user.metadata.category); // "premium"
326
+ ```
327
+
328
+ **Key features of nested attributes:**
329
+ - **Deep nesting**: Support for multiple levels of nested objects
330
+ - **Validation**: Each nested field can have its own validation rules
331
+ - **Optional fields**: Nested objects can contain optional fields
332
+ - **Mixed types**: Combine simple types, arrays, and nested objects
333
+ - **Partition support**: Use dot notation for partitions on nested fields (e.g., `"utm.source"`, `"address.country"`)
334
+
335
+ #### Automatic Timestamps
336
+
337
+ 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.
338
+
339
+ ```js
340
+ const users = await s3db.createResource({
341
+ name: "users",
342
+ attributes: { name: "string", email: "email" },
343
+ options: { timestamps: true }
344
+ });
345
+
346
+ const user = await users.insert({ name: "John", email: "john@example.com" });
347
+ console.log(user.createdAt); // e.g. "2024-06-27T12:34:56.789Z"
348
+ console.log(user.updatedAt); // same as createdAt on insert
349
+ ```
350
+
351
+ #### Resource Behaviors
352
+
353
+ `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.
354
+
355
+ ##### Available Behaviors
356
+
357
+ | Behavior | Description | Use Case |
358
+ |----------|-------------|----------|
359
+ | `user-management` | **Default** - Emits warnings but allows operations | Development and testing |
360
+ | `enforce-limits` | Throws errors when limit is exceeded | Strict data size control |
361
+ | `data-truncate` | Truncates data to fit within limits | Preserve structure, lose data |
362
+ | `body-overflow` | Stores excess data in S3 object body | Preserve all data |
363
+
364
+ ##### Behavior Configuration
365
+
366
+ ```javascript
367
+ const users = await s3db.createResource({
368
+ name: "users",
369
+ attributes: {
370
+ name: "string|min:2|max:100",
371
+ email: "email|unique",
372
+ bio: "string|optional",
373
+ preferences: "object|optional"
374
+ },
375
+ options: {
376
+ behavior: "body-overflow", // Choose behavior strategy
377
+ timestamps: true, // Enable automatic timestamps
378
+ partitions: { // Define data partitions
379
+ byRegion: {
380
+ fields: { region: "string" }
381
+ }
382
+ },
383
+ hooks: { // Custom operation hooks
384
+ preInsert: [async (data) => {
385
+ // Custom validation logic
386
+ return data;
387
+ }],
388
+ afterInsert: [async (data) => {
389
+ console.log("User created:", data.id);
390
+ }]
391
+ }
392
+ }
393
+ });
394
+ ```
395
+
396
+ ##### 1. User Management Behavior (Default)
397
+
398
+ The default behavior that gives you full control over data size management:
399
+
400
+ ```javascript
401
+ const users = await s3db.createResource({
402
+ name: "users",
403
+ attributes: { name: "string", email: "email" },
404
+ options: { behavior: "user-management" }
405
+ });
406
+
407
+ // Listen for limit warnings
408
+ users.on("exceedsLimit", (info) => {
409
+ console.log(`Document ${info.operation} exceeds 2KB limit:`, {
410
+ totalSize: info.totalSize,
411
+ limit: info.limit,
412
+ excess: info.excess
413
+ });
414
+ });
415
+
416
+ // Operations continue normally even if limit is exceeded
417
+ const user = await users.insert({
418
+ name: "John Doe",
419
+ email: "john@example.com",
420
+ largeBio: "Very long bio...".repeat(100) // Will trigger warning but succeed
421
+ });
422
+ ```
423
+
424
+ ##### 2. Enforce Limits Behavior
425
+
426
+ Strict behavior that prevents operations when data exceeds the limit:
427
+
428
+ ```javascript
429
+ const users = await s3db.createResource({
430
+ name: "users",
431
+ attributes: { name: "string", email: "email" },
432
+ options: { behavior: "enforce-limits" }
433
+ });
434
+
435
+ try {
436
+ const user = await users.insert({
437
+ name: "John Doe",
438
+ email: "john@example.com",
439
+ largeBio: "Very long bio...".repeat(100)
440
+ });
441
+ } catch (error) {
442
+ console.error("Operation failed:", error.message);
443
+ // Error: S3 metadata size exceeds 2KB limit. Current size: 2500 bytes, limit: 2048 bytes
444
+ }
445
+ ```
446
+
447
+ ##### 3. Data Truncate Behavior
448
+
449
+ Intelligently truncates data to fit within limits while preserving structure:
450
+
451
+ ```javascript
452
+ const users = await s3db.createResource({
453
+ name: "users",
454
+ attributes: { name: "string", email: "email", bio: "string" },
455
+ options: { behavior: "data-truncate" }
456
+ });
457
+
458
+ const user = await users.insert({
459
+ name: "John Doe",
460
+ email: "john@example.com",
461
+ bio: "This is a very long biography that will be truncated to fit within the 2KB metadata limit..."
462
+ });
463
+
464
+ console.log(user.bio); // "This is a very long biography that will be truncated to fit within the 2KB metadata limit..."
465
+ // Note: The bio will be truncated with "..." suffix if it exceeds available space
466
+ ```
467
+
468
+ ##### 4. Body Overflow Behavior
469
+
470
+ Stores excess data in the S3 object body, preserving all information:
471
+
472
+ ```javascript
473
+ const users = await s3db.createResource({
474
+ name: "users",
475
+ attributes: { name: "string", email: "email", bio: "string" },
476
+ options: { behavior: "body-overflow" }
477
+ });
478
+
479
+ const user = await users.insert({
480
+ name: "John Doe",
481
+ email: "john@example.com",
482
+ bio: "This is a very long biography that will be stored in the S3 object body..."
483
+ });
484
+
485
+ // All data is preserved and automatically merged when retrieved
486
+ console.log(user.bio); // Full biography preserved
487
+ ```
488
+
489
+ **How Body Overflow Works:**
490
+ - Small attributes stay in metadata for fast access
491
+ - Large attributes are moved to S3 object body
492
+ - Data is automatically merged when retrieved
493
+ - Maintains full data integrity
494
+
495
+ ##### Complete Resource Configuration Reference
496
+
497
+ ```javascript
498
+ const resource = await s3db.createResource({
499
+ // Required: Resource name (unique within database)
500
+ name: "users",
501
+
502
+ // Required: Schema definition
503
+ attributes: {
504
+ // Basic types
505
+ name: "string|min:2|max:100",
506
+ email: "email|unique",
507
+ age: "number|integer|positive",
508
+ isActive: "boolean",
509
+
510
+ // Advanced types
511
+ website: "url",
512
+ uuid: "uuid",
513
+ createdAt: "date",
514
+ price: "currency|symbol:$",
515
+
516
+ // Encrypted fields
517
+ password: "secret",
518
+ apiKey: "secret",
519
+
520
+ // Nested objects
521
+ address: {
522
+ street: "string",
523
+ city: "string",
524
+ country: "string",
525
+ zipCode: "string|optional"
526
+ },
527
+
528
+ // Complex nested structures
529
+ profile: {
530
+ bio: "string|max:500|optional",
531
+ avatar: "url|optional",
532
+ birthDate: "date|optional",
533
+ preferences: {
534
+ theme: "string|enum:light,dark|default:light",
535
+ language: "string|enum:en,es,fr|default:en",
536
+ notifications: "boolean|default:true"
537
+ }
538
+ },
539
+
540
+ // Nested objects with validation
541
+ contact: {
542
+ phone: {
543
+ mobile: "string|pattern:^\\+?[1-9]\\d{1,14}$|optional",
544
+ work: "string|pattern:^\\+?[1-9]\\d{1,14}$|optional"
545
+ },
546
+ social: {
547
+ twitter: "string|optional",
548
+ linkedin: "url|optional",
549
+ github: "url|optional"
550
+ }
551
+ },
552
+
553
+ // Arrays
554
+ tags: "array|items:string|unique",
555
+ scores: "array|items:number|min:1",
556
+
557
+ // Multiple types
558
+ id: ["string", "number"],
559
+
560
+ // Complex nested structures
561
+ metadata: {
562
+ settings: "object|optional",
563
+ preferences: "object|optional"
564
+ },
565
+
566
+ // Analytics and tracking
567
+ analytics: {
568
+ utm: {
569
+ source: "string|optional",
570
+ medium: "string|optional",
571
+ campaign: "string|optional",
572
+ term: "string|optional",
573
+ content: "string|optional"
574
+ },
575
+ events: "array|items:object|optional",
576
+ lastVisit: "date|optional"
577
+ }
578
+ },
579
+
580
+ // Optional: Resource configuration
581
+ options: {
582
+ // Behavior strategy for handling 2KB metadata limits
583
+ behavior: "user-management", // "user-management" | "enforce-limits" | "data-truncate" | "body-overflow"
584
+
585
+ // Enable automatic timestamps
586
+ timestamps: true, // Adds createdAt and updatedAt fields
587
+
588
+ // Define data partitions for efficient querying
589
+ partitions: {
590
+ byRegion: {
591
+ fields: { region: "string" }
592
+ },
593
+ byAgeGroup: {
594
+ fields: { ageGroup: "string" }
595
+ },
596
+ byDate: {
597
+ fields: { createdAt: "date|maxlength:10" }
598
+ }
599
+ },
600
+
601
+ // Custom operation hooks
602
+ hooks: {
603
+ // Pre-operation hooks (can modify data)
604
+ preInsert: [
605
+ async (data) => {
606
+ // Validate or transform data before insert
607
+ if (!data.email.includes("@")) {
608
+ throw new Error("Invalid email format");
609
+ }
610
+ return data;
611
+ }
612
+ ],
613
+ preUpdate: [
614
+ async (id, data) => {
615
+ // Validate or transform data before update
616
+ return data;
617
+ }
618
+ ],
619
+ preDelete: [
620
+ async (id) => {
621
+ // Validate before deletion
622
+ return true; // Return false to abort
623
+ }
624
+ ],
625
+
626
+ // Post-operation hooks (cannot modify data)
627
+ afterInsert: [
628
+ async (data) => {
629
+ console.log("User created:", data.id);
630
+ }
631
+ ],
632
+ afterUpdate: [
633
+ async (id, data) => {
634
+ console.log("User updated:", id);
635
+ }
636
+ ],
637
+ afterDelete: [
638
+ async (id) => {
639
+ console.log("User deleted:", id);
640
+ }
641
+ ]
642
+ }
643
+ }
644
+ });
645
+ ```
646
+
647
+ ### 3. Schema Validation
648
+
649
+ `s3db.js` uses [fastest-validator](https://github.com/icebob/fastest-validator) for schema validation with robust handling of edge cases:
650
+
651
+ ```javascript
652
+ const attributes = {
653
+ // Basic types
654
+ name: "string|min:2|max:100|trim",
655
+ email: "email|nullable",
656
+ age: "number|integer|positive",
657
+ isActive: "boolean",
658
+
659
+ // Advanced types
660
+ website: "url",
661
+ uuid: "uuid",
662
+ createdAt: "date",
663
+ price: "currency|symbol:$",
664
+
665
+ // Custom s3db types
666
+ password: "secret", // Encrypted field
667
+
668
+ // Nested objects (supports empty objects and null values)
669
+ address: {
670
+ street: "string",
671
+ city: "string",
672
+ country: "string",
673
+ zipCode: "string|optional"
674
+ },
675
+
676
+ // Arrays (robust serialization with special character handling)
677
+ tags: "array|items:string|unique", // Handles empty arrays: []
678
+ scores: "array|items:number|min:1", // Handles null arrays
679
+ categories: "array|items:string", // Handles arrays with pipe characters: ['tag|special', 'normal']
680
+
681
+ // Multiple types
682
+ id: ["string", "number"],
683
+
684
+ // Complex nested structures
685
+ metadata: {
686
+ settings: "object|optional", // Can be empty: {}
687
+ preferences: "object|optional" // Can be null
688
+ }
689
+ };
690
+ ```
691
+
692
+ #### Nested Object Validation
693
+
694
+ Nested objects support comprehensive validation rules at each level:
695
+
696
+ ```javascript
697
+ const users = await s3db.createResource({
698
+ name: "users",
699
+ attributes: {
700
+ name: "string|min:2|max:100",
701
+ email: "email|unique",
702
+
703
+ // Simple nested object
704
+ profile: {
705
+ bio: "string|max:500|optional",
706
+ avatar: "url|optional",
707
+ birthDate: "date|optional"
708
+ },
709
+
710
+ // Complex nested structure with validation
711
+ contact: {
712
+ phone: {
713
+ mobile: "string|pattern:^\\+?[1-9]\\d{1,14}$|optional",
714
+ work: "string|pattern:^\\+?[1-9]\\d{1,14}$|optional"
715
+ },
716
+ social: {
717
+ twitter: "string|optional",
718
+ linkedin: "url|optional"
719
+ }
720
+ },
721
+
722
+ // Nested object with arrays
723
+ preferences: {
724
+ categories: "array|items:string|unique|optional",
725
+ notifications: {
726
+ email: "boolean|default:true",
727
+ sms: "boolean|default:false",
728
+ push: "boolean|default:true"
729
+ }
730
+ },
731
+
732
+ // Deep nesting with validation
733
+ analytics: {
734
+ tracking: {
735
+ utm: {
736
+ source: "string|optional",
737
+ medium: "string|optional",
738
+ campaign: "string|optional"
739
+ },
740
+ events: "array|items:object|optional"
741
+ }
742
+ }
743
+ }
744
+ });
745
+
746
+ // Insert data with complex nested structure
747
+ const user = await users.insert({
748
+ name: "John Doe",
749
+ email: "john@example.com",
750
+ profile: {
751
+ bio: "Software developer with 10+ years of experience",
752
+ avatar: "https://example.com/avatar.jpg",
753
+ birthDate: new Date("1990-01-15")
754
+ },
755
+ contact: {
756
+ phone: {
757
+ mobile: "+1234567890",
758
+ work: "+1987654321"
759
+ },
760
+ social: {
761
+ twitter: "@johndoe",
762
+ linkedin: "https://linkedin.com/in/johndoe"
763
+ }
764
+ },
765
+ preferences: {
766
+ categories: ["technology", "programming", "web-development"],
767
+ notifications: {
768
+ email: true,
769
+ sms: false,
770
+ push: true
771
+ }
772
+ },
773
+ analytics: {
774
+ tracking: {
775
+ utm: {
776
+ source: "google",
777
+ medium: "organic",
778
+ campaign: "brand"
779
+ },
780
+ events: [
781
+ { type: "page_view", timestamp: new Date() },
782
+ { type: "signup", timestamp: new Date() }
783
+ ]
784
+ }
785
+ }
786
+ });
787
+ ```
788
+
789
+ #### Enhanced Array and Object Handling
790
+
791
+ s3db.js now provides robust serialization for complex data structures:
792
+
793
+ ```javascript
794
+ // โœ… Supported: Empty arrays and objects
795
+ const user = await users.insert({
796
+ name: "John Doe",
797
+ tags: [], // Empty array - properly serialized
798
+ metadata: {}, // Empty object - properly handled
799
+ preferences: null // Null object - correctly preserved
800
+ });
801
+
802
+ // โœ… Supported: Arrays with special characters
803
+ const product = await products.insert({
804
+ name: "Widget",
805
+ categories: ["electronics|gadgets", "home|office"], // Pipe characters escaped
806
+ tags: ["tag|with|pipes", "normal-tag"] // Multiple pipes handled
807
+ });
808
+ ```
809
+
810
+ ## ๐Ÿ› ๏ธ API Reference
811
+
812
+ ### Database Operations
813
+
814
+ #### Connect to Database
815
+
816
+ ```javascript
817
+ await s3db.connect();
818
+ // Emits 'connected' event when ready
819
+ ```
820
+
821
+ #### Create Resource
822
+
823
+ ```javascript
824
+ const resource = await s3db.createResource({
825
+ name: "users",
826
+ attributes: {
827
+ name: "string",
828
+ email: "email"
829
+ }
830
+ });
831
+ ```
832
+
833
+ #### Get Resource Reference
834
+
835
+ ```javascript
836
+ const users = s3db.resource("users");
837
+ // or
838
+ const users = s3db.resources.users
839
+ ```
840
+
841
+ ### Resource Operations
842
+
843
+ #### Insert Document
844
+
845
+ ```javascript
846
+ // With custom ID
847
+ const user = await users.insert({
848
+ id: "user-123",
849
+ name: "John Doe",
850
+ email: "john@example.com"
851
+ });
852
+
853
+ // Auto-generated ID
854
+ const user = await users.insert({
855
+ name: "Jane Doe",
856
+ email: "jane@example.com"
857
+ });
858
+ // ID will be auto-generated using nanoid
859
+ ```
860
+
861
+ #### Get Document
862
+
863
+ ```javascript
864
+ const user = await users.get("user-123");
865
+ console.log(user.name); // "John Doe"
866
+ ```
867
+
868
+ #### Update Document
869
+
870
+ ```javascript
871
+ const updatedUser = await users.update("user-123", {
872
+ name: "John Smith",
873
+ age: 31
874
+ });
875
+ // Only specified fields are updated
876
+ ```
877
+
878
+ #### Upsert Document
879
+
880
+ ```javascript
881
+ // Insert if doesn't exist, update if exists
882
+ const user = await users.upsert("user-123", {
883
+ name: "John Doe",
884
+ email: "john@example.com",
39
885
  age: 30
40
886
  });
887
+ ```
41
888
 
42
- // Query data
43
- const allUsers = await users.find();
44
- const john = await users.findOne({ name: 'John Doe' });
889
+ #### Delete Document
890
+
891
+ ```javascript
892
+ await users.delete("user-123");
893
+ ```
894
+
895
+ #### Count Documents
896
+
897
+ ```javascript
898
+ const count = await users.count();
899
+ console.log(`Total users: ${count}`);
900
+ ```
901
+
902
+ ### Bulk Operations
903
+
904
+ #### Insert Many
905
+
906
+ ```javascript
907
+ const users = [
908
+ { name: "User 1", email: "user1@example.com" },
909
+ { name: "User 2", email: "user2@example.com" },
910
+ { name: "User 3", email: "user3@example.com" }
911
+ ];
912
+
913
+ await users.insertMany(users);
914
+ ```
915
+
916
+ #### Get Many
917
+
918
+ ```javascript
919
+ const userList = await users.getMany(["user-1", "user-2", "user-3"]);
920
+ ```
921
+
922
+ #### Delete Many
923
+
924
+ ```javascript
925
+ await users.deleteMany(["user-1", "user-2", "user-3"]);
926
+ ```
927
+
928
+ #### Get All
929
+
930
+ ```javascript
931
+ const allUsers = await users.getAll();
932
+ // Returns all documents in the resource
933
+ ```
934
+
935
+ #### List IDs
936
+
937
+ ```javascript
938
+ const userIds = await users.listIds();
939
+ // Returns array of all document IDs
940
+ ```
941
+
942
+ #### Delete All
943
+
944
+ ```javascript
945
+ await users.deleteAll();
946
+ // โš ๏ธ Destructive operation - removes all documents
947
+ ```
948
+
949
+ ## ๐Ÿ“Š Examples
950
+
951
+ ### E-commerce Application
952
+
953
+ ```javascript
954
+ // Create product resource with body-overflow behavior for long descriptions
955
+ const products = await s3db.createResource({
956
+ name: "products",
957
+ attributes: {
958
+ name: "string|min:2|max:200",
959
+ description: "string|optional",
960
+ price: "number|positive",
961
+ category: "string",
962
+ tags: "array|items:string",
963
+ inStock: "boolean",
964
+ images: "array|items:url",
965
+ metadata: "object|optional"
966
+ },
967
+ options: {
968
+ behavior: "body-overflow", // Handle long product descriptions
969
+ timestamps: true // Track creation and update times
970
+ }
971
+ });
972
+
973
+ // Create order resource with enforce-limits for strict data control
974
+ const orders = await s3db.createResource({
975
+ name: "orders",
976
+ attributes: {
977
+ customerId: "string",
978
+ products: "array|items:string",
979
+ total: "number|positive",
980
+ status: "string|enum:pending,paid,shipped,delivered",
981
+ shippingAddress: {
982
+ street: "string",
983
+ city: "string",
984
+ country: "string",
985
+ zipCode: "string"
986
+ },
987
+ createdAt: "date"
988
+ },
989
+ options: {
990
+ behavior: "enforce-limits", // Strict validation for order data
991
+ timestamps: true
992
+ }
993
+ });
994
+
995
+ // Insert products (long descriptions will be handled by body-overflow)
996
+ const product = await products.insert({
997
+ name: "Wireless Headphones",
998
+ 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.",
999
+ price: 99.99,
1000
+ category: "electronics",
1001
+ tags: ["wireless", "bluetooth", "audio", "noise-cancellation"],
1002
+ inStock: true,
1003
+ images: ["https://example.com/headphones.jpg"]
1004
+ });
1005
+
1006
+ // Create order (enforce-limits ensures data integrity)
1007
+ const order = await orders.insert({
1008
+ customerId: "customer-123",
1009
+ products: [product.id],
1010
+ total: 99.99,
1011
+ status: "pending",
1012
+ shippingAddress: {
1013
+ street: "123 Main St",
1014
+ city: "New York",
1015
+ country: "USA",
1016
+ zipCode: "10001"
1017
+ },
1018
+ createdAt: new Date()
1019
+ });
1020
+ ```
1021
+
1022
+ ### User Authentication System
1023
+
1024
+ ```javascript
1025
+ // Create users resource with encrypted password and strict validation
1026
+ const users = await s3db.createResource({
1027
+ name: "users",
1028
+ attributes: {
1029
+ username: "string|min:3|max:50|unique",
1030
+ email: "email|unique",
1031
+ password: "secret", // Encrypted field
1032
+ role: "string|enum:user,admin,moderator",
1033
+ isActive: "boolean",
1034
+ lastLogin: "date|optional",
1035
+ profile: {
1036
+ firstName: "string",
1037
+ lastName: "string",
1038
+ avatar: "url|optional",
1039
+ bio: "string|optional"
1040
+ }
1041
+ },
1042
+ options: {
1043
+ behavior: "enforce-limits", // Strict validation for user data
1044
+ timestamps: true // Track account creation and updates
1045
+ }
1046
+ });
1047
+
1048
+ // Create sessions resource with body-overflow for session data
1049
+ const sessions = await s3db.createResource({
1050
+ name: "sessions",
1051
+ attributes: {
1052
+ userId: "string",
1053
+ token: "secret", // Encrypted session token
1054
+ expiresAt: "date",
1055
+ userAgent: "string|optional",
1056
+ ipAddress: "string|optional",
1057
+ sessionData: "object|optional" // Additional session metadata
1058
+ },
1059
+ options: {
1060
+ behavior: "body-overflow", // Handle large session data
1061
+ timestamps: true
1062
+ }
1063
+ });
1064
+
1065
+ // Register user (enforce-limits ensures data integrity)
1066
+ const user = await users.insert({
1067
+ username: "john_doe",
1068
+ email: "john@example.com",
1069
+ password: "secure_password_123",
1070
+ role: "user",
1071
+ isActive: true,
1072
+ profile: {
1073
+ firstName: "John",
1074
+ lastName: "Doe"
1075
+ }
1076
+ });
1077
+
1078
+ // Create session (body-overflow preserves all session data)
1079
+ const session = await sessions.insert({
1080
+ userId: user.id,
1081
+ token: "jwt_token_here",
1082
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
1083
+ userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
1084
+ ipAddress: "192.168.1.1",
1085
+ sessionData: {
1086
+ preferences: { theme: "dark", language: "en" },
1087
+ lastActivity: new Date(),
1088
+ deviceInfo: { type: "desktop", os: "Windows" }
1089
+ }
1090
+ });
1091
+ ```
1092
+
1093
+ ## ๐Ÿ”„ Streaming
1094
+
1095
+ For large datasets, use streams to process data efficiently:
1096
+
1097
+ ### Readable Stream
1098
+
1099
+ ```javascript
1100
+ const readableStream = await users.readable();
1101
+
1102
+ readableStream.on("id", (id) => {
1103
+ console.log("Processing user ID:", id);
1104
+ });
1105
+
1106
+ readableStream.on("data", (user) => {
1107
+ console.log("User:", user.name);
1108
+ // Process each user
1109
+ });
1110
+
1111
+ readableStream.on("end", () => {
1112
+ console.log("Finished processing all users");
1113
+ });
1114
+
1115
+ readableStream.on("error", (error) => {
1116
+ console.error("Stream error:", error);
1117
+ });
1118
+ ```
1119
+
1120
+ ### Writable Stream
1121
+
1122
+ ```javascript
1123
+ const writableStream = await users.writable();
1124
+
1125
+ // Write data to stream
1126
+ writableStream.write({
1127
+ name: "User 1",
1128
+ email: "user1@example.com"
1129
+ });
1130
+
1131
+ writableStream.write({
1132
+ name: "User 2",
1133
+ email: "user2@example.com"
1134
+ });
1135
+
1136
+ // End stream
1137
+ writableStream.end();
1138
+ ```
1139
+
1140
+ ### Stream to CSV
1141
+
1142
+ ```javascript
1143
+ import fs from "fs";
1144
+ import { createObjectCsvWriter } from "csv-writer";
1145
+
1146
+ const csvWriter = createObjectCsvWriter({
1147
+ path: "users.csv",
1148
+ header: [
1149
+ { id: "id", title: "ID" },
1150
+ { id: "name", title: "Name" },
1151
+ { id: "email", title: "Email" }
1152
+ ]
1153
+ });
1154
+
1155
+ const readableStream = await users.readable();
1156
+ const records = [];
1157
+
1158
+ readableStream.on("data", (user) => {
1159
+ records.push(user);
1160
+ });
1161
+
1162
+ readableStream.on("end", async () => {
1163
+ await csvWriter.writeRecords(records);
1164
+ console.log("CSV file created successfully");
1165
+ });
45
1166
  ```
46
1167
 
47
- ### Node.js (CommonJS)
1168
+ ## ๐Ÿ” Security & Encryption
1169
+
1170
+ ### Field-Level Encryption
1171
+
1172
+ Use the `"secret"` type for sensitive data:
48
1173
 
49
1174
  ```javascript
50
- const S3db = require('s3db.js');
1175
+ const users = await s3db.createResource({
1176
+ name: "users",
1177
+ attributes: {
1178
+ username: "string",
1179
+ email: "email",
1180
+ password: "secret", // Encrypted
1181
+ apiKey: "secret", // Encrypted
1182
+ creditCard: "secret" // Encrypted
1183
+ }
1184
+ });
51
1185
 
52
- const db = new S3db({
53
- connectionString: 's3://access-key:secret-key@bucket-name/prefix?region=us-east-1'
1186
+ // Data is automatically encrypted/decrypted
1187
+ const user = await users.insert({
1188
+ username: "john_doe",
1189
+ email: "john@example.com",
1190
+ password: "my_secure_password", // Stored encrypted
1191
+ apiKey: "sk_live_123456789", // Stored encrypted
1192
+ creditCard: "4111111111111111" // Stored encrypted
54
1193
  });
55
1194
 
56
- await db.connect();
57
- // ... rest of the code
1195
+ // Retrieved data is automatically decrypted
1196
+ const retrieved = await users.get(user.id);
1197
+ console.log(retrieved.password); // "my_secure_password" (decrypted)
58
1198
  ```
59
1199
 
60
- ### Browser
1200
+ ### Custom Encryption Key
61
1201
 
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
- });
1202
+ ```javascript
1203
+ import fs from "fs";
76
1204
 
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>
1205
+ const s3db = new S3db({
1206
+ uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
1207
+ passphrase: fs.readFileSync("./private-key.pem") // Custom encryption key
1208
+ });
84
1209
  ```
85
1210
 
86
- ## Features
1211
+ ## ๐Ÿ’ฐ Cost Analysis
87
1212
 
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
1213
+ ### Understanding S3 Costs
96
1214
 
97
- ## API Reference
1215
+ - **PUT Requests**: $0.000005 per 1,000 requests
1216
+ - **GET Requests**: $0.0000004 per 1,000 requests
1217
+ - **Data Transfer**: $0.09 per GB
1218
+ - **Storage**: $0.023 per GB (but s3db.js uses 0-byte files)
98
1219
 
99
- ### S3db Class
1220
+ ### Cost Examples
100
1221
 
101
- The main database class for connecting to S3 and managing resources.
1222
+ #### Small Application (1,000 users)
102
1223
 
103
- #### Constructor
1224
+ ```javascript
1225
+ // Setup cost (one-time)
1226
+ const setupCost = 0.005; // 1,000 PUT requests
1227
+
1228
+ // Monthly read cost
1229
+ const monthlyReadCost = 0.0004; // 1,000 GET requests
1230
+
1231
+ console.log(`Setup: $${setupCost}`);
1232
+ console.log(`Monthly reads: $${monthlyReadCost}`);
1233
+ ```
1234
+
1235
+ #### Large Application (1,000,000 users)
1236
+
1237
+ ```javascript
1238
+ // Setup cost (one-time)
1239
+ const setupCost = 5.00; // 1,000,000 PUT requests
1240
+
1241
+ // Monthly read cost
1242
+ const monthlyReadCost = 0.40; // 1,000,000 GET requests
1243
+
1244
+ console.log(`Setup: $${setupCost}`);
1245
+ console.log(`Monthly reads: $${monthlyReadCost}`);
1246
+ ```
1247
+
1248
+ ### Cost Tracking Plugin
104
1249
 
105
1250
  ```javascript
106
- new S3db(config)
1251
+ import { CostsPlugin } from "s3db.js";
1252
+
1253
+ const s3db = new S3db({
1254
+ uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
1255
+ plugins: [CostsPlugin]
1256
+ });
1257
+
1258
+ // After operations
1259
+ console.log("Total cost:", s3db.client.costs.total.toFixed(4), "USD");
1260
+ console.log("Requests made:", s3db.client.costs.requests.total);
1261
+ ```
1262
+
1263
+ ## ๐ŸŽ›๏ธ Advanced Features
1264
+
1265
+ ### AutoEncrypt / AutoDecrypt
1266
+
1267
+ Fields with the type `secret` are automatically encrypted and decrypted using the resource's passphrase. This ensures sensitive data is protected at rest.
1268
+
1269
+ ```js
1270
+ const users = await s3db.createResource({
1271
+ name: "users",
1272
+ attributes: {
1273
+ username: "string",
1274
+ password: "secret" // Will be encrypted
1275
+ }
1276
+ });
1277
+
1278
+ const user = await users.insert({
1279
+ username: "john_doe",
1280
+ password: "my_secret_password"
1281
+ });
1282
+
1283
+ // The password is stored encrypted in S3, but automatically decrypted when retrieved
1284
+ const retrieved = await users.get(user.id);
1285
+ console.log(retrieved.password); // "my_secret_password"
1286
+ ```
1287
+
1288
+ ### Resource Events
1289
+
1290
+ All resources emit events for key operations. You can listen to these events for logging, analytics, or custom workflows.
1291
+
1292
+ ```js
1293
+ users.on("insert", (data) => console.log("User inserted:", data.id));
1294
+ users.on("get", (data) => console.log("User retrieved:", data.id));
1295
+ users.on("update", (attrs, data) => console.log("User updated:", data.id));
1296
+ users.on("delete", (id) => console.log("User deleted:", id));
1297
+ ```
1298
+
1299
+ ### Resource Schema Export/Import
1300
+
1301
+ You can export and import resource schemas for backup, migration, or versioning purposes.
1302
+
1303
+ ```js
1304
+ // Export schema
1305
+ const schemaData = users.schema.export();
1306
+
1307
+ // Import schema
1308
+ const importedSchema = Schema.import(schemaData);
1309
+ ```
1310
+
1311
+ ## Partitions
1312
+
1313
+ `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.
1314
+
1315
+ ### Defining partitions
1316
+
1317
+ You can define partitions when creating a resource using the `options.partitions` property:
1318
+
1319
+ ```js
1320
+ const users = await s3db.createResource({
1321
+ name: "users",
1322
+ attributes: {
1323
+ name: "string",
1324
+ email: "email",
1325
+ region: "string",
1326
+ ageGroup: "string",
1327
+ createdAt: "date"
1328
+ },
1329
+ options: {
1330
+ partitions: {
1331
+ byRegion: {
1332
+ fields: { region: "string" }
1333
+ },
1334
+ byAgeGroup: {
1335
+ fields: { ageGroup: "string" }
1336
+ },
1337
+ byDate: {
1338
+ fields: { createdAt: "date|maxlength:10" }
1339
+ }
1340
+ }
1341
+ }
1342
+ });
107
1343
  ```
108
1344
 
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)
1345
+ ### Querying by partition
120
1346
 
121
- #### Methods
1347
+ Partitions are automatically created when you insert documents, and you can query them using specific methods that accept partition parameters:
122
1348
 
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
1349
+ #### List IDs by partition
128
1350
 
129
- ### Resource Class
1351
+ ```js
1352
+ // Get all user IDs in the 'south' region
1353
+ const userIds = await users.listIds({
1354
+ partition: "byRegion",
1355
+ partitionValues: { region: "south" }
1356
+ });
130
1357
 
131
- Represents a collection of documents in S3.
1358
+ // Get all user IDs in the 'adult' age group
1359
+ const adultIds = await users.listIds({
1360
+ partition: "byAgeGroup",
1361
+ partitionValues: { ageGroup: "adult" }
1362
+ });
1363
+ ```
132
1364
 
133
- #### Methods
1365
+ #### Count documents by partition
134
1366
 
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
1367
+ ```js
1368
+ // Count users in the 'south' region
1369
+ const count = await users.count({
1370
+ partition: "byRegion",
1371
+ partitionValues: { region: "south" }
1372
+ });
143
1373
 
144
- ## Examples
1374
+ // Count adult users
1375
+ const adultCount = await users.count({
1376
+ partition: "byAgeGroup",
1377
+ partitionValues: { ageGroup: "adult" }
1378
+ });
1379
+ ```
1380
+
1381
+ #### List objects by partition
1382
+
1383
+ ```js
1384
+ // Get all users in the 'south' region
1385
+ const usersSouth = await users.listByPartition({
1386
+ partition: "byRegion",
1387
+ partitionValues: { region: "south" }
1388
+ });
1389
+
1390
+ // Get all adult users with pagination
1391
+ const adultUsers = await users.listByPartition(
1392
+ { partition: "byAgeGroup", partitionValues: { ageGroup: "adult" } },
1393
+ { limit: 10, offset: 0 }
1394
+ );
1395
+ ```
1396
+
1397
+ #### Page through partition data
1398
+
1399
+ ```js
1400
+ // Get first page of users in 'south' region
1401
+ const page = await users.page(0, 10, {
1402
+ partition: "byRegion",
1403
+ partitionValues: { region: "south" }
1404
+ });
1405
+
1406
+ console.log(page.items); // Array of user objects
1407
+ console.log(page.totalItems); // Total count in this partition
1408
+ console.log(page.totalPages); // Total pages available
1409
+ ```
1410
+
1411
+ ### Example: Time-based partition
1412
+
1413
+ ```js
1414
+ const logs = await s3db.createResource({
1415
+ name: "logs",
1416
+ attributes: {
1417
+ message: "string",
1418
+ level: "string",
1419
+ createdAt: "date"
1420
+ },
1421
+ options: {
1422
+ partitions: {
1423
+ byDate: {
1424
+ fields: { createdAt: "date|maxlength:10" }
1425
+ }
1426
+ }
1427
+ }
1428
+ });
1429
+
1430
+ // Insert logs (partitions are created automatically)
1431
+ await logs.insert({
1432
+ message: "User login",
1433
+ level: "info",
1434
+ createdAt: new Date("2024-06-27")
1435
+ });
1436
+
1437
+ // Query logs for a specific day
1438
+ const logsToday = await logs.listByPartition({
1439
+ partition: "byDate",
1440
+ partitionValues: { createdAt: "2024-06-27" }
1441
+ });
1442
+
1443
+ // Count logs for a specific day
1444
+ const count = await logs.count({
1445
+ partition: "byDate",
1446
+ partitionValues: { createdAt: "2024-06-27" }
1447
+ });
1448
+ ```
1449
+
1450
+ ### Partitions with Nested Fields
1451
+
1452
+ `s3db.js` supports partitions using nested object fields using dot notation, just like the schema mapper:
1453
+
1454
+ ```js
1455
+ const users = await s3db.createResource({
1456
+ name: "users",
1457
+ attributes: {
1458
+ name: "string|required",
1459
+ utm: {
1460
+ source: "string|required",
1461
+ medium: "string|required",
1462
+ campaign: "string|required"
1463
+ },
1464
+ address: {
1465
+ country: "string|required",
1466
+ state: "string|required",
1467
+ city: "string|required"
1468
+ },
1469
+ metadata: {
1470
+ category: "string|required",
1471
+ priority: "string|required"
1472
+ }
1473
+ },
1474
+ options: {
1475
+ partitions: {
1476
+ byUtmSource: {
1477
+ fields: {
1478
+ "utm.source": "string"
1479
+ }
1480
+ },
1481
+ byAddressCountry: {
1482
+ fields: {
1483
+ "address.country": "string|maxlength:2"
1484
+ }
1485
+ },
1486
+ byAddressState: {
1487
+ fields: {
1488
+ "address.country": "string|maxlength:2",
1489
+ "address.state": "string"
1490
+ }
1491
+ },
1492
+ byUtmAndAddress: {
1493
+ fields: {
1494
+ "utm.source": "string",
1495
+ "utm.medium": "string",
1496
+ "address.country": "string|maxlength:2"
1497
+ }
1498
+ }
1499
+ }
1500
+ }
1501
+ });
1502
+
1503
+ // Insert user with nested data
1504
+ await users.insert({
1505
+ name: "John Doe",
1506
+ utm: {
1507
+ source: "google",
1508
+ medium: "cpc",
1509
+ campaign: "brand"
1510
+ },
1511
+ address: {
1512
+ country: "US",
1513
+ state: "California",
1514
+ city: "San Francisco"
1515
+ },
1516
+ metadata: {
1517
+ category: "premium",
1518
+ priority: "high"
1519
+ }
1520
+ });
1521
+
1522
+ // Query by nested UTM source
1523
+ const googleUsers = await users.listIds({
1524
+ partition: "byUtmSource",
1525
+ partitionValues: { "utm.source": "google" }
1526
+ });
1527
+
1528
+ // Query by nested address country
1529
+ const usUsers = await users.listIds({
1530
+ partition: "byAddressCountry",
1531
+ partitionValues: { "address.country": "US" }
1532
+ });
1533
+
1534
+ // Query by multiple nested fields
1535
+ const usCaliforniaUsers = await users.listIds({
1536
+ partition: "byAddressState",
1537
+ partitionValues: {
1538
+ "address.country": "US",
1539
+ "address.state": "California"
1540
+ }
1541
+ });
1542
+
1543
+ // Complex query with UTM and address
1544
+ const googleCpcUsUsers = await users.listIds({
1545
+ partition: "byUtmAndAddress",
1546
+ partitionValues: {
1547
+ "utm.source": "google",
1548
+ "utm.medium": "cpc",
1549
+ "address.country": "US"
1550
+ }
1551
+ });
1552
+
1553
+ // Count and list operations work the same way
1554
+ const googleCount = await users.count({
1555
+ partition: "byUtmSource",
1556
+ partitionValues: { "utm.source": "google" }
1557
+ });
1558
+
1559
+ const googleUsersData = await users.listByPartition({
1560
+ partition: "byUtmSource",
1561
+ partitionValues: { "utm.source": "google" }
1562
+ });
1563
+ ```
1564
+
1565
+ **Key features of nested field partitions:**
1566
+
1567
+ - **Dot notation**: Use `"parent.child"` to access nested fields
1568
+ - **Multiple levels**: Support for deeply nested objects like `"address.country.state"`
1569
+ - **Mixed partitions**: Combine nested and flat fields in the same partition
1570
+ - **Rules support**: Apply maxlength, date formatting, etc. to nested fields
1571
+ - **Automatic flattening**: Uses the same flattening logic as the schema mapper
1572
+
1573
+ ### Partition rules and transformations
1574
+
1575
+ Partitions support various field rules that automatically transform values:
1576
+
1577
+ ```js
1578
+ const products = await s3db.createResource({
1579
+ name: "products",
1580
+ attributes: {
1581
+ name: "string",
1582
+ category: "string",
1583
+ price: "number",
1584
+ createdAt: "date"
1585
+ },
1586
+ options: {
1587
+ partitions: {
1588
+ byCategory: {
1589
+ fields: { category: "string" }
1590
+ },
1591
+ byDate: {
1592
+ fields: { createdAt: "date|maxlength:10" } // Truncates to YYYY-MM-DD
1593
+ }
1594
+ }
1595
+ }
1596
+ });
1597
+
1598
+ // Date values are automatically formatted
1599
+ await products.insert({
1600
+ name: "Widget",
1601
+ category: "electronics",
1602
+ price: 99.99,
1603
+ createdAt: new Date("2024-06-27T15:30:00Z") // Will be stored as "2024-06-27"
1604
+ });
1605
+ ```
1606
+
1607
+ ### Important notes about partitions
1608
+
1609
+ 1. **Automatic creation**: Partitions are automatically created when you insert documents
1610
+ 2. **Performance**: Partition queries are more efficient than filtering all documents
1611
+ 3. **Storage**: Each partition creates additional S3 objects, increasing storage costs
1612
+ 4. **Consistency**: Partition data is automatically kept in sync with main resource data
1613
+ 5. **Field requirements**: All partition fields must exist in your resource attributes
1614
+
1615
+ ### Available partition-aware methods
1616
+
1617
+ | Method | Description | Partition Support |
1618
+ |--------|-------------|-------------------|
1619
+ | `listIds()` | Get array of document IDs | โœ… `{ partition, partitionValues }` |
1620
+ | `count()` | Count documents | โœ… `{ partition, partitionValues }` |
1621
+ | `listByPartition()` | List documents by partition | โœ… `{ partition, partitionValues }` |
1622
+ | `page()` | Paginate documents | โœ… `{ partition, partitionValues }` |
1623
+ | `getFromPartition()` | Get single document from partition | โœ… Direct partition access |
1624
+ | `query()` | Filter documents in memory | โŒ No partition support |
1625
+
1626
+ ## Hooks
1627
+
1628
+ `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.
1629
+
1630
+ ### Supported hooks
1631
+ - `preInsert` / `afterInsert`
1632
+ - `preUpdate` / `afterUpdate`
1633
+ - `preDelete` / `afterDelete`
1634
+
1635
+ ### Registering hooks
1636
+ You can register hooks when creating a resource or dynamically:
1637
+
1638
+ ```js
1639
+ const users = await s3db.createResource({
1640
+ name: "users",
1641
+ attributes: { name: "string", email: "email" },
1642
+ options: {
1643
+ hooks: {
1644
+ preInsert: [async (data) => {
1645
+ if (!data.email.includes("@")) throw new Error("Invalid email");
1646
+ return data;
1647
+ }],
1648
+ afterInsert: [async (data) => {
1649
+ console.log("User inserted:", data.id);
1650
+ }]
1651
+ }
1652
+ }
1653
+ });
1654
+
1655
+ // Or dynamically:
1656
+ users.addHook('preInsert', async (data) => {
1657
+ // Custom logic
1658
+ return data;
1659
+ });
1660
+ ```
1661
+
1662
+ ### Hook execution order
1663
+ - Internal hooks run first, user hooks run last (in the order they were added).
1664
+ - Hooks can be async and can modify the data (for `pre*` hooks).
1665
+ - If a hook throws, the operation is aborted.
1666
+
1667
+ ## Plugins
1668
+
1669
+ `s3db.js` supports plugins to extend or customize its behavior. Plugins can hook into lifecycle events, add new methods, or integrate with external systems.
1670
+
1671
+ ### Example: Custom plugin
1672
+
1673
+ ```js
1674
+ const MyPlugin = {
1675
+ setup(s3db) {
1676
+ console.log("Plugin setup");
1677
+ },
1678
+ start() {
1679
+ console.log("Plugin started");
1680
+ },
1681
+ onUserCreated(user) {
1682
+ console.log("New user created:", user.id);
1683
+ }
1684
+ };
1685
+
1686
+ const s3db = new S3db({
1687
+ uri: "s3://...",
1688
+ plugins: [MyPlugin]
1689
+ });
1690
+ ```
1691
+
1692
+ ## ๐Ÿšจ Limitations & Best Practices
1693
+
1694
+ ### Limitations
1695
+
1696
+ 1. **Document Size**: Maximum ~2KB per document (metadata only) - **๐Ÿ’ก Use behaviors to handle larger documents**
1697
+ 2. **No Complex Queries**: No SQL-like WHERE clauses or joins
1698
+ 3. **No Indexes**: No automatic indexing for fast lookups
1699
+ 4. **Sequential IDs**: Best performance with sequential IDs (00001, 00002, etc.)
1700
+ 5. **No Transactions**: No ACID transactions across multiple operations
1701
+ 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
1702
+
1703
+ **๐Ÿ’ก Overcoming the 2KB Limit**: Use resource behaviors to handle documents that exceed the 2KB metadata limit:
1704
+ - **`body-overflow`**: Stores excess data in S3 object body (preserves all data)
1705
+ - **`data-truncate`**: Intelligently truncates data to fit within limits
1706
+ - **`enforce-limits`**: Strict validation to prevent oversized documents
1707
+ - **`user-management`**: Default behavior with warnings and monitoring
1708
+
1709
+ ### โœ… Recent Improvements
1710
+
1711
+ **๐Ÿ”ง Enhanced Data Serialization (v3.3.2+)**
1712
+
1713
+ s3db.js now handles complex data structures robustly:
1714
+
1715
+ - **Empty Arrays**: `[]` correctly serialized and preserved
1716
+ - **Null Arrays**: `null` values maintained without corruption
1717
+ - **Special Characters**: Arrays with pipe `|` characters properly escaped
1718
+ - **Empty Objects**: `{}` correctly mapped and stored
1719
+ - **Null Objects**: `null` object values preserved during serialization
1720
+ - **Nested Structures**: Complex nested objects with mixed empty/null values supported
1721
+
1722
+ ### Best Practices
1723
+
1724
+ #### 1. Design for Document Storage
1725
+
1726
+ ```javascript
1727
+ // โœ… Good: Nested structure is fine
1728
+ const user = {
1729
+ id: "user-123",
1730
+ name: "John Doe",
1731
+ email: "john@example.com",
1732
+ profile: {
1733
+ bio: "Software developer",
1734
+ avatar: "https://example.com/avatar.jpg",
1735
+ preferences: {
1736
+ theme: "dark",
1737
+ notifications: true
1738
+ }
1739
+ }
1740
+ };
1741
+
1742
+ // โŒ Avoid: Large arrays in documents
1743
+ const user = {
1744
+ id: "user-123",
1745
+ name: "John Doe",
1746
+ // This could exceed metadata limits
1747
+ purchaseHistory: [
1748
+ { id: "order-1", date: "2023-01-01", total: 99.99 },
1749
+ { id: "order-2", date: "2023-01-15", total: 149.99 },
1750
+ // ... many more items
1751
+ ]
1752
+ };
1753
+ ```
1754
+
1755
+ #### 2. Use Sequential IDs
1756
+
1757
+ ```javascript
1758
+ // โœ… Good: Sequential IDs for better performance
1759
+ const users = ["00001", "00002", "00003", "00004"];
1760
+
1761
+ // โš ๏ธ Acceptable: Random IDs (but ensure sufficient uniqueness)
1762
+ const users = ["abc123", "def456", "ghi789", "jkl012"];
1763
+
1764
+ // โŒ Avoid: Random IDs with low combinations (risk of collisions)
1765
+ const users = ["a1", "b2", "c3", "d4"]; // Only 26*10 = 260 combinations
1766
+ ```
1767
+
1768
+ #### 3. Optimize for Read Patterns
1769
+
1770
+ ```javascript
1771
+ // โœ… Good: Store frequently accessed data together
1772
+ const order = {
1773
+ id: "order-123",
1774
+ customerId: "customer-456",
1775
+ customerName: "John Doe", // Denormalized for quick access
1776
+ items: ["product-1", "product-2"],
1777
+ total: 99.99
1778
+ };
1779
+
1780
+ // โŒ Avoid: Requiring multiple lookups
1781
+ const order = {
1782
+ id: "order-123",
1783
+ customerId: "customer-456", // Requires separate lookup
1784
+ items: ["product-1", "product-2"]
1785
+ };
1786
+ ```
1787
+
1788
+ #### 4. Use Streaming for Large Datasets
1789
+
1790
+ ```javascript
1791
+ // โœ… Good: Use streams for large operations
1792
+ const readableStream = await users.readable();
1793
+ readableStream.on("data", (user) => {
1794
+ // Process each user individually
1795
+ });
1796
+
1797
+ // โŒ Avoid: Loading all data at once
1798
+ const allUsers = await users.getAll(); // May timeout with large datasets
1799
+ ```
1800
+
1801
+ #### 5. Implement Proper Error Handling
1802
+
1803
+ ```javascript
1804
+ // Method 1: Try-catch with get()
1805
+ try {
1806
+ const user = await users.get("non-existent-id");
1807
+ } catch (error) {
1808
+ if (error.message.includes("does not exist")) {
1809
+ console.log("User not found");
1810
+ } else {
1811
+ console.error("Unexpected error:", error);
1812
+ }
1813
+ }
1814
+
1815
+ // Method 2: Check existence first (โš ๏ธ Additional request cost)
1816
+ const userId = "user-123";
1817
+ if (await users.exists(userId)) {
1818
+ const user = await users.get(userId);
1819
+ console.log("User found:", user.name);
1820
+ } else {
1821
+ console.log("User not found");
1822
+ }
1823
+ ```
1824
+
1825
+ **โš ๏ธ Cost Warning**: Using `exists()` creates an additional S3 request. For high-volume operations, prefer the try-catch approach to minimize costs.
1826
+
1827
+ #### 6. Choose the Right Behavior Strategy
1828
+
1829
+ ```javascript
1830
+ // โœ… For development and testing - allows flexibility
1831
+ const devUsers = await s3db.createResource({
1832
+ name: "users",
1833
+ attributes: { name: "string", email: "email" },
1834
+ options: { behavior: "user-management" }
1835
+ });
1836
+
1837
+ // โœ… For production with strict data control
1838
+ const prodUsers = await s3db.createResource({
1839
+ name: "users",
1840
+ attributes: { name: "string", email: "email" },
1841
+ options: { behavior: "enforce-limits" }
1842
+ });
1843
+
1844
+ // โœ… For preserving all data with larger documents
1845
+ const blogPosts = await s3db.createResource({
1846
+ name: "posts",
1847
+ attributes: { title: "string", content: "string", author: "string" },
1848
+ options: { behavior: "body-overflow" }
1849
+ });
1850
+
1851
+ // โœ… For structured data where truncation is acceptable
1852
+ const productDescriptions = await s3db.createResource({
1853
+ name: "products",
1854
+ attributes: { name: "string", description: "string", price: "number" },
1855
+ options: { behavior: "data-truncate" }
1856
+ });
1857
+ ```
145
1858
 
146
- See the [examples](./examples) directory for more detailed usage examples.
1859
+ **Behavior Selection Guide:**
1860
+ - **`user-management`**: Development, testing, or when you want full control
1861
+ - **`enforce-limits`**: Production systems requiring strict data validation
1862
+ - **`body-overflow`**: When data integrity is critical and you need to preserve all information
1863
+ - **`data-truncate`**: When you can afford to lose some data but want to maintain structure
147
1864
 
148
- ## License
1865
+ ### Performance Tips
149
1866
 
150
- UNLICENSED
1867
+ 1. **Enable Caching**: Use `cache: true` for frequently accessed data
1868
+ 2. **Adjust Parallelism**: Increase `parallelism`