s3db.js 5.2.0 โ 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1142 -612
- package/dist/s3db.cjs.js +3541 -484
- package/dist/s3db.cjs.min.js +1 -31
- package/dist/s3db.d.ts +891 -62
- package/dist/s3db.es.js +3539 -486
- package/dist/s3db.es.min.js +1 -31
- package/dist/s3db.iife.js +3541 -484
- package/dist/s3db.iife.min.js +1 -31
- package/package.json +33 -14
package/README.md
CHANGED
|
@@ -97,9 +97,20 @@
|
|
|
97
97
|
- [๐พ Installation](#-installation)
|
|
98
98
|
- [๐ฏ Core Concepts](#-core-concepts)
|
|
99
99
|
- [โก Advanced Features](#-advanced-features)
|
|
100
|
+
- [๐ Resource Versioning System](#-resource-versioning-system)
|
|
101
|
+
- [๐ Custom ID Generation](#-custom-id-generation)
|
|
102
|
+
- [๐ Plugin System](#-plugin-system)
|
|
103
|
+
- [๐๏ธ Advanced Behaviors](#๏ธ-advanced-behaviors)
|
|
104
|
+
- [๐ Advanced Streaming API](#-advanced-streaming-api)
|
|
105
|
+
- [๐ Binary Content Management](#-binary-content-management)
|
|
106
|
+
- [๐๏ธ Advanced Partitioning](#๏ธ-advanced-partitioning)
|
|
107
|
+
- [๐ฃ Advanced Hooks System](#-advanced-hooks-system)
|
|
100
108
|
- [๐ API Reference](#-api-reference)
|
|
101
109
|
- [๐จ Examples](#-examples)
|
|
102
110
|
- [๐ Security](#-security)
|
|
111
|
+
- [โ๏ธ Advanced Configuration Options](#๏ธ-advanced-configuration-options)
|
|
112
|
+
- [๐ก Events and Emitters](#-events-and-emitters)
|
|
113
|
+
- [๐ง Troubleshooting](#-troubleshooting)
|
|
103
114
|
- [๐ฐ Cost Analysis](#-cost-analysis)
|
|
104
115
|
- [๐จ Best Practices](#-best-practices)
|
|
105
116
|
- [๐งช Testing](#-testing)
|
|
@@ -124,13 +135,17 @@ npm install s3db.js
|
|
|
124
135
|
import { S3db } from "s3db.js";
|
|
125
136
|
|
|
126
137
|
const s3db = new S3db({
|
|
127
|
-
|
|
138
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
|
|
128
139
|
});
|
|
129
140
|
|
|
130
141
|
await s3db.connect();
|
|
131
142
|
console.log("๐ Connected to S3 database!");
|
|
132
143
|
```
|
|
133
144
|
|
|
145
|
+
> **โน๏ธ Note:** You do **not** need to provide `ACCESS_KEY` and `SECRET_KEY` in the connection string if your environment already has S3 permissions (e.g., via IAM Role on EKS, EC2, Lambda, or other compatible clouds). s3db.js will use the default AWS credential provider chain, so credentials can be omitted for role-based or environment-based authentication. This also applies to S3-compatible clouds (MinIO, DigitalOcean Spaces, etc.) if they support such mechanisms.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
134
149
|
### 3. Create your first resource
|
|
135
150
|
|
|
136
151
|
```javascript
|
|
@@ -140,24 +155,9 @@ const users = await s3db.createResource({
|
|
|
140
155
|
name: "string|min:2|max:100",
|
|
141
156
|
email: "email|unique",
|
|
142
157
|
age: "number|integer|positive",
|
|
143
|
-
isActive: "boolean"
|
|
144
|
-
createdAt: "date"
|
|
145
|
-
},
|
|
146
|
-
timestamps: true,
|
|
147
|
-
behavior: "user-management",
|
|
148
|
-
partitions: {
|
|
149
|
-
byRegion: { fields: { region: "string" } }
|
|
158
|
+
isActive: "boolean"
|
|
150
159
|
},
|
|
151
|
-
|
|
152
|
-
autoDecrypt: true,
|
|
153
|
-
cache: false,
|
|
154
|
-
parallelism: 10,
|
|
155
|
-
hooks: {
|
|
156
|
-
preInsert: [async (data) => {
|
|
157
|
-
console.log("Pre-insert:", data);
|
|
158
|
-
return data;
|
|
159
|
-
}]
|
|
160
|
-
}
|
|
160
|
+
timestamps: true
|
|
161
161
|
});
|
|
162
162
|
```
|
|
163
163
|
|
|
@@ -196,14 +196,34 @@ console.log(`Total users: ${allUsers.length}`);
|
|
|
196
196
|
```bash
|
|
197
197
|
# npm
|
|
198
198
|
npm install s3db.js
|
|
199
|
-
|
|
200
199
|
# pnpm
|
|
201
200
|
pnpm add s3db.js
|
|
202
|
-
|
|
203
201
|
# yarn
|
|
204
202
|
yarn add s3db.js
|
|
205
203
|
```
|
|
206
204
|
|
|
205
|
+
### ๐ฆ Optional Dependencies
|
|
206
|
+
|
|
207
|
+
Some features require additional dependencies to be installed manually:
|
|
208
|
+
|
|
209
|
+
#### Replication Dependencies
|
|
210
|
+
|
|
211
|
+
If you plan to use the replication system with external services, install the corresponding dependencies:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# For SQS replication (AWS SQS queues)
|
|
215
|
+
npm install @aws-sdk/client-sqs
|
|
216
|
+
|
|
217
|
+
# For BigQuery replication (Google BigQuery)
|
|
218
|
+
npm install @google-cloud/bigquery
|
|
219
|
+
|
|
220
|
+
# For PostgreSQL replication (PostgreSQL databases)
|
|
221
|
+
npm install pg
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Why manual installation?** These are marked as `peerDependencies` to keep the main package lightweight. Only install what you need!
|
|
225
|
+
```
|
|
226
|
+
|
|
207
227
|
### Environment Setup
|
|
208
228
|
|
|
209
229
|
Create a `.env` file with your AWS credentials:
|
|
@@ -224,7 +244,7 @@ import dotenv from "dotenv";
|
|
|
224
244
|
dotenv.config();
|
|
225
245
|
|
|
226
246
|
const s3db = new S3db({
|
|
227
|
-
|
|
247
|
+
connectionString: `s3://${process.env.AWS_ACCESS_KEY_ID}:${process.env.AWS_SECRET_ACCESS_KEY}@${process.env.AWS_BUCKET}/databases/${process.env.DATABASE_NAME}`
|
|
228
248
|
});
|
|
229
249
|
```
|
|
230
250
|
|
|
@@ -236,7 +256,7 @@ const s3db = new S3db({
|
|
|
236
256
|
#### 1. Access Keys (Development)
|
|
237
257
|
```javascript
|
|
238
258
|
const s3db = new S3db({
|
|
239
|
-
|
|
259
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
|
|
240
260
|
});
|
|
241
261
|
```
|
|
242
262
|
|
|
@@ -244,14 +264,14 @@ const s3db = new S3db({
|
|
|
244
264
|
```javascript
|
|
245
265
|
// No credentials needed - uses IAM role permissions
|
|
246
266
|
const s3db = new S3db({
|
|
247
|
-
|
|
267
|
+
connectionString: "s3://BUCKET_NAME/databases/myapp"
|
|
248
268
|
});
|
|
249
269
|
```
|
|
250
270
|
|
|
251
271
|
#### 3. S3-Compatible Services (MinIO, etc.)
|
|
252
272
|
```javascript
|
|
253
273
|
const s3db = new S3db({
|
|
254
|
-
|
|
274
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
255
275
|
endpoint: "http://localhost:9000"
|
|
256
276
|
});
|
|
257
277
|
```
|
|
@@ -267,92 +287,43 @@ A logical container for your resources, stored in a specific S3 prefix.
|
|
|
267
287
|
|
|
268
288
|
```javascript
|
|
269
289
|
const s3db = new S3db({
|
|
270
|
-
|
|
290
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
|
|
271
291
|
});
|
|
272
|
-
// Creates/connects to: s3://bucket/databases/myapp/
|
|
273
292
|
```
|
|
274
293
|
|
|
275
294
|
### ๐ Resources (Collections)
|
|
276
295
|
Resources define the structure of your documents, similar to tables in traditional databases.
|
|
277
296
|
|
|
278
|
-
#### New Configuration Structure
|
|
279
|
-
|
|
280
|
-
The Resource class now uses a unified configuration object where all options are passed directly in the config object:
|
|
281
|
-
|
|
282
297
|
```javascript
|
|
283
298
|
const users = await s3db.createResource({
|
|
284
299
|
name: "users",
|
|
285
300
|
attributes: {
|
|
286
|
-
// Basic types
|
|
287
301
|
name: "string|min:2|max:100",
|
|
288
302
|
email: "email|unique",
|
|
289
303
|
age: "number|integer|positive",
|
|
290
304
|
isActive: "boolean",
|
|
291
|
-
|
|
292
|
-
// Nested objects
|
|
293
305
|
profile: {
|
|
294
306
|
bio: "string|optional",
|
|
295
|
-
avatar: "url|optional"
|
|
296
|
-
preferences: {
|
|
297
|
-
theme: "string|enum:light,dark|default:light",
|
|
298
|
-
notifications: "boolean|default:true"
|
|
299
|
-
}
|
|
307
|
+
avatar: "url|optional"
|
|
300
308
|
},
|
|
301
|
-
|
|
302
|
-
// Arrays
|
|
303
309
|
tags: "array|items:string|unique",
|
|
304
|
-
|
|
305
|
-
// Encrypted fields
|
|
306
310
|
password: "secret"
|
|
307
311
|
},
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
partitions: { // Organize data for efficient queries
|
|
312
|
+
timestamps: true,
|
|
313
|
+
behavior: "user-managed",
|
|
314
|
+
partitions: {
|
|
312
315
|
byRegion: { fields: { region: "string" } }
|
|
313
|
-
},
|
|
314
|
-
paranoid: true, // Security flag for dangerous operations
|
|
315
|
-
autoDecrypt: true, // Auto-decrypt secret fields
|
|
316
|
-
cache: false, // Enable caching
|
|
317
|
-
parallelism: 10, // Parallelism for bulk operations
|
|
318
|
-
hooks: { // Custom hooks
|
|
319
|
-
preInsert: [async (data) => {
|
|
320
|
-
console.log("Pre-insert:", data);
|
|
321
|
-
return data;
|
|
322
|
-
}]
|
|
323
316
|
}
|
|
324
317
|
});
|
|
325
318
|
```
|
|
326
319
|
|
|
327
320
|
### ๐ Schema Validation
|
|
328
|
-
Built-in validation using [@icebob/fastest-validator](https://github.com/icebob/fastest-validator)
|
|
329
|
-
|
|
330
|
-
```javascript
|
|
331
|
-
const product = await products.insert({
|
|
332
|
-
name: "Wireless Headphones",
|
|
333
|
-
price: 99.99,
|
|
334
|
-
category: "electronics",
|
|
335
|
-
features: ["bluetooth", "noise-cancellation"],
|
|
336
|
-
specifications: {
|
|
337
|
-
battery: "30 hours",
|
|
338
|
-
connectivity: "Bluetooth 5.0"
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
**Validation Features powered by fastest-validator:**
|
|
344
|
-
- โ
**Comprehensive Rules** - String, number, array, object, date validation
|
|
345
|
-
- โ
**Nested Objects** - Deep validation for complex data structures
|
|
346
|
-
- โ
**Custom Rules** - Extend with your own validation logic
|
|
347
|
-
- โ
**Performance** - Optimized validation engine for speed
|
|
348
|
-
- โ
**Error Messages** - Detailed validation error reporting
|
|
321
|
+
Built-in validation using [@icebob/fastest-validator](https://github.com/icebob/fastest-validator) with comprehensive rule support and excellent performance.
|
|
349
322
|
|
|
350
323
|
---
|
|
351
324
|
|
|
352
325
|
## โก Advanced Features
|
|
353
326
|
|
|
354
|
-
s3db.js leverages [@icebob/fastest-validator](https://github.com/icebob/fastest-validator) as its core validation engine for both resource schemas and partition field validation, ensuring high-performance data validation with comprehensive rule support.
|
|
355
|
-
|
|
356
327
|
### ๐ฆ Partitions
|
|
357
328
|
|
|
358
329
|
Organize data efficiently with partitions for faster queries:
|
|
@@ -363,32 +334,16 @@ const analytics = await s3db.createResource({
|
|
|
363
334
|
attributes: {
|
|
364
335
|
userId: "string",
|
|
365
336
|
event: "string",
|
|
366
|
-
timestamp: "date"
|
|
367
|
-
utm: {
|
|
368
|
-
source: "string",
|
|
369
|
-
medium: "string",
|
|
370
|
-
campaign: "string"
|
|
371
|
-
}
|
|
337
|
+
timestamp: "date"
|
|
372
338
|
},
|
|
373
339
|
partitions: {
|
|
374
340
|
byDate: { fields: { timestamp: "date|maxlength:10" } },
|
|
375
|
-
|
|
376
|
-
byUserAndDate: {
|
|
377
|
-
fields: {
|
|
378
|
-
userId: "string",
|
|
379
|
-
timestamp: "date|maxlength:10"
|
|
380
|
-
}
|
|
381
|
-
}
|
|
341
|
+
byUserAndDate: { fields: { userId: "string", timestamp: "date|maxlength:10" } }
|
|
382
342
|
}
|
|
383
343
|
});
|
|
384
344
|
|
|
385
345
|
// Query by partition for better performance
|
|
386
|
-
const
|
|
387
|
-
partition: "byUtmSource",
|
|
388
|
-
partitionValues: { "utm.source": "google" }
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
const todayEvents = await analytics.count({
|
|
346
|
+
const todayEvents = await analytics.list({
|
|
392
347
|
partition: "byDate",
|
|
393
348
|
partitionValues: { timestamp: "2024-01-15" }
|
|
394
349
|
});
|
|
@@ -401,105 +356,35 @@ Add custom logic with pre/post operation hooks:
|
|
|
401
356
|
```javascript
|
|
402
357
|
const products = await s3db.createResource({
|
|
403
358
|
name: "products",
|
|
404
|
-
attributes: {
|
|
405
|
-
name: "string",
|
|
406
|
-
price: "number",
|
|
407
|
-
category: "string"
|
|
408
|
-
},
|
|
359
|
+
attributes: { name: "string", price: "number" },
|
|
409
360
|
hooks: {
|
|
410
|
-
preInsert: [
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
]
|
|
417
|
-
|
|
418
|
-
async (data) => {
|
|
419
|
-
console.log(`๐ฆ Product ${data.name} created with SKU: ${data.sku}`);
|
|
420
|
-
// Send notification, update cache, etc.
|
|
421
|
-
}
|
|
422
|
-
],
|
|
423
|
-
preUpdate: [
|
|
424
|
-
async (id, data) => {
|
|
425
|
-
// Log price changes
|
|
426
|
-
if (data.price) {
|
|
427
|
-
console.log(`๐ฐ Price update for ${id}: $${data.price}`);
|
|
428
|
-
}
|
|
429
|
-
return data;
|
|
430
|
-
}
|
|
431
|
-
]
|
|
432
|
-
},
|
|
433
|
-
|
|
434
|
-
// Optional: Security settings (default: true)
|
|
435
|
-
paranoid: true,
|
|
436
|
-
|
|
437
|
-
// Optional: Schema options (default: false)
|
|
438
|
-
allNestedObjectsOptional: false,
|
|
439
|
-
|
|
440
|
-
// Optional: Encryption settings (default: true)
|
|
441
|
-
autoDecrypt: true,
|
|
442
|
-
|
|
443
|
-
// Optional: Caching (default: false)
|
|
444
|
-
cache: false
|
|
361
|
+
preInsert: [async (data) => {
|
|
362
|
+
data.sku = `${data.category.toUpperCase()}-${Date.now()}`;
|
|
363
|
+
return data;
|
|
364
|
+
}],
|
|
365
|
+
afterInsert: [async (data) => {
|
|
366
|
+
console.log(`๐ฆ Product ${data.name} created`);
|
|
367
|
+
}]
|
|
368
|
+
}
|
|
445
369
|
});
|
|
446
370
|
```
|
|
447
371
|
|
|
448
372
|
### ๐ Streaming API
|
|
449
373
|
|
|
450
|
-
Handle large datasets efficiently
|
|
374
|
+
Handle large datasets efficiently:
|
|
451
375
|
|
|
452
376
|
```javascript
|
|
453
|
-
// Export
|
|
377
|
+
// Export to CSV
|
|
454
378
|
const readableStream = await users.readable();
|
|
455
|
-
const csvWriter = createObjectCsvWriter({
|
|
456
|
-
path: "users_export.csv",
|
|
457
|
-
header: [
|
|
458
|
-
{ id: "id", title: "ID" },
|
|
459
|
-
{ id: "name", title: "Name" },
|
|
460
|
-
{ id: "email", title: "Email" }
|
|
461
|
-
]
|
|
462
|
-
});
|
|
463
|
-
|
|
464
379
|
const records = [];
|
|
465
|
-
readableStream.on("data", (user) =>
|
|
466
|
-
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
readableStream.on("end", async () => {
|
|
470
|
-
await csvWriter.writeRecords(records);
|
|
471
|
-
console.log("โ
Export completed: users_export.csv");
|
|
472
|
-
});
|
|
380
|
+
readableStream.on("data", (user) => records.push(user));
|
|
381
|
+
readableStream.on("end", () => console.log("โ
Export completed"));
|
|
473
382
|
|
|
474
|
-
// Bulk import
|
|
383
|
+
// Bulk import
|
|
475
384
|
const writableStream = await users.writable();
|
|
476
|
-
importData.forEach(userData =>
|
|
477
|
-
writableStream.write(userData);
|
|
478
|
-
});
|
|
385
|
+
importData.forEach(userData => writableStream.write(userData));
|
|
479
386
|
writableStream.end();
|
|
480
387
|
```
|
|
481
|
-
|
|
482
|
-
### ๐ก๏ธ Document Behaviors
|
|
483
|
-
|
|
484
|
-
Handle documents that exceed S3's 2KB metadata limit:
|
|
485
|
-
|
|
486
|
-
```javascript
|
|
487
|
-
// Preserve all data by storing overflow in S3 body
|
|
488
|
-
const blogs = await s3db.createResource({
|
|
489
|
-
name: "blogs",
|
|
490
|
-
attributes: {
|
|
491
|
-
title: "string",
|
|
492
|
-
content: "string", // Can be very large
|
|
493
|
-
author: "string"
|
|
494
|
-
},
|
|
495
|
-
behavior: "body-overflow" // Handles large content automatically
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
// Strict validation - throws error if limit exceeded
|
|
499
|
-
const settings = await s3db.createResource({
|
|
500
|
-
name: "settings",
|
|
501
|
-
attributes: {
|
|
502
|
-
key: "string",
|
|
503
388
|
value: "string"
|
|
504
389
|
},
|
|
505
390
|
behavior: "enforce-limits" // Ensures data stays within 2KB
|
|
@@ -516,561 +401,1206 @@ const summaries = await s3db.createResource({
|
|
|
516
401
|
});
|
|
517
402
|
```
|
|
518
403
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
## ๐ API Reference
|
|
522
|
-
|
|
523
|
-
### ๐ Database Operations
|
|
524
|
-
|
|
525
|
-
| Method | Description | Example |
|
|
526
|
-
|--------|-------------|---------|
|
|
527
|
-
| `connect()` | Connect to database | `await s3db.connect()` |
|
|
528
|
-
| `createResource(config)` | Create new resource | `await s3db.createResource({...})` |
|
|
529
|
-
| `resource(name)` | Get resource reference | `const users = s3db.resource("users")` |
|
|
530
|
-
| `resourceExists(name)` | Check if resource exists | `s3db.resourceExists("users")` |
|
|
531
|
-
|
|
532
|
-
### ๐ Resource Operations
|
|
533
|
-
|
|
534
|
-
| Method | Description | Example |
|
|
535
|
-
|--------|-------------|---------|
|
|
536
|
-
| `insert(data)` | Create document | `await users.insert({name: "John"})` |
|
|
537
|
-
| `get(id)` | Retrieve document | `await users.get("user-123")` |
|
|
538
|
-
| `update(id, data)` | Update document | `await users.update("user-123", {age: 31})` |
|
|
539
|
-
| `upsert(id, data)` | Insert or update | `await users.upsert("user-123", {...})` |
|
|
540
|
-
| `delete(id)` | Delete document | `await users.delete("user-123")` |
|
|
541
|
-
| `exists(id)` | Check existence | `await users.exists("user-123")` |
|
|
542
|
-
|
|
543
|
-
### ๐ Query Operations
|
|
544
|
-
|
|
545
|
-
| Method | Description | Example |
|
|
546
|
-
|--------|-------------|---------|
|
|
547
|
-
| `list(options?)` | List documents | `await users.list()` |
|
|
548
|
-
| `listIds(options?)` | List document IDs | `await users.listIds()` |
|
|
549
|
-
| `count(options?)` | Count documents | `await users.count()` |
|
|
550
|
-
| `page(options)` | Paginate results | `await users.page({offset: 0, size: 10})` |
|
|
551
|
-
| `query(filter, options?)` | Filter documents | `await users.query({isActive: true})` |
|
|
552
|
-
|
|
553
|
-
### ๐ Bulk Operations
|
|
554
|
-
|
|
555
|
-
| Method | Description | Example |
|
|
556
|
-
|--------|-------------|---------|
|
|
557
|
-
| `insertMany(docs)` | Insert multiple | `await users.insertMany([{...}, {...}])` |
|
|
558
|
-
| `getMany(ids)` | Get multiple | `await users.getMany(["id1", "id2"])` |
|
|
559
|
-
| `deleteMany(ids)` | Delete multiple | `await users.deleteMany(["id1", "id2"])` |
|
|
560
|
-
| `getAll()` | Get all documents | `await users.getAll()` |
|
|
561
|
-
| `deleteAll()` | Delete all documents | `await users.deleteAll()` |
|
|
562
|
-
|
|
563
|
-
---
|
|
564
|
-
|
|
565
|
-
## ๐จ Examples
|
|
404
|
+
### ๐ Resource Versioning System
|
|
566
405
|
|
|
567
|
-
|
|
406
|
+
Automatically manages schema evolution and data migration:
|
|
568
407
|
|
|
569
408
|
```javascript
|
|
570
|
-
//
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
title: "string|min:5|max:200",
|
|
575
|
-
content: "string",
|
|
576
|
-
author: "string",
|
|
577
|
-
tags: "array|items:string",
|
|
578
|
-
published: "boolean|default:false",
|
|
579
|
-
publishedAt: "date|optional"
|
|
580
|
-
},
|
|
581
|
-
behavior: "body-overflow", // Handle long content
|
|
582
|
-
timestamps: true,
|
|
583
|
-
partitions: {
|
|
584
|
-
byAuthor: { fields: { author: "string" } },
|
|
585
|
-
byTag: { fields: { "tags.0": "string" } }
|
|
586
|
-
}
|
|
409
|
+
// Enable versioning
|
|
410
|
+
const s3db = new S3db({
|
|
411
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
412
|
+
versioningEnabled: true
|
|
587
413
|
});
|
|
588
414
|
|
|
589
|
-
// Create
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
415
|
+
// Create versioned resource
|
|
416
|
+
const users = await s3db.createResource({
|
|
417
|
+
name: "users",
|
|
418
|
+
attributes: {
|
|
419
|
+
name: "string|required",
|
|
420
|
+
email: "string|required"
|
|
421
|
+
},
|
|
422
|
+
versioningEnabled: true
|
|
597
423
|
});
|
|
598
424
|
|
|
599
|
-
//
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
425
|
+
// Insert in version 0
|
|
426
|
+
const user1 = await users.insert({
|
|
427
|
+
name: "John Doe",
|
|
428
|
+
email: "john@example.com"
|
|
603
429
|
});
|
|
604
|
-
```
|
|
605
430
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
// Products with detailed specifications
|
|
610
|
-
const products = await s3db.createResource({
|
|
611
|
-
name: "products",
|
|
431
|
+
// Update schema - creates version 1
|
|
432
|
+
const updatedUsers = await s3db.createResource({
|
|
433
|
+
name: "users",
|
|
612
434
|
attributes: {
|
|
613
|
-
name: "string|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
category: "string",
|
|
617
|
-
inventory: {
|
|
618
|
-
stock: "number|integer|min:0",
|
|
619
|
-
reserved: "number|integer|min:0|default:0"
|
|
620
|
-
},
|
|
621
|
-
specifications: "object|optional",
|
|
622
|
-
images: "array|items:url"
|
|
435
|
+
name: "string|required",
|
|
436
|
+
email: "string|required",
|
|
437
|
+
age: "number|optional"
|
|
623
438
|
},
|
|
624
|
-
|
|
625
|
-
timestamps: true,
|
|
626
|
-
partitions: {
|
|
627
|
-
byCategory: { fields: { category: "string" } }
|
|
628
|
-
}
|
|
439
|
+
versioningEnabled: true
|
|
629
440
|
});
|
|
630
441
|
|
|
631
|
-
//
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
attributes: {
|
|
635
|
-
customerId: "string",
|
|
636
|
-
items: "array|items:object",
|
|
637
|
-
total: "number|positive",
|
|
638
|
-
status: "string|enum:pending,processing,shipped,delivered",
|
|
639
|
-
shipping: {
|
|
640
|
-
address: "string",
|
|
641
|
-
city: "string",
|
|
642
|
-
country: "string",
|
|
643
|
-
zipCode: "string"
|
|
644
|
-
}
|
|
645
|
-
},
|
|
646
|
-
behavior: "enforce-limits",
|
|
647
|
-
timestamps: true
|
|
648
|
-
});
|
|
442
|
+
// Automatic migration
|
|
443
|
+
const migratedUser = await updatedUsers.get(user1.id);
|
|
444
|
+
console.log(migratedUser._v); // "1" - automatically migrated
|
|
649
445
|
|
|
650
|
-
//
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
price: 299.99,
|
|
655
|
-
category: "electronics",
|
|
656
|
-
inventory: { stock: 50 },
|
|
657
|
-
specifications: {
|
|
658
|
-
brand: "AudioTech",
|
|
659
|
-
model: "AT-WH1000",
|
|
660
|
-
features: ["ANC", "Bluetooth 5.0", "30h battery"]
|
|
661
|
-
},
|
|
662
|
-
images: ["https://example.com/headphones-1.jpg"]
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
// Create an order
|
|
666
|
-
const order = await orders.insert({
|
|
667
|
-
customerId: "customer-123",
|
|
668
|
-
items: [
|
|
669
|
-
{ productId: product.id, quantity: 1, price: 299.99 }
|
|
670
|
-
],
|
|
671
|
-
total: 299.99,
|
|
672
|
-
status: "pending",
|
|
673
|
-
shipping: {
|
|
674
|
-
address: "123 Main St",
|
|
675
|
-
city: "New York",
|
|
676
|
-
country: "USA",
|
|
677
|
-
zipCode: "10001"
|
|
678
|
-
}
|
|
446
|
+
// Query by version
|
|
447
|
+
const version0Users = await users.list({
|
|
448
|
+
partition: "byVersion",
|
|
449
|
+
partitionValues: { _v: "0" }
|
|
679
450
|
});
|
|
680
451
|
```
|
|
681
452
|
|
|
682
|
-
###
|
|
453
|
+
### ๐ Custom ID Generation
|
|
454
|
+
|
|
455
|
+
Flexible ID generation strategies:
|
|
683
456
|
|
|
684
457
|
```javascript
|
|
685
|
-
//
|
|
686
|
-
const
|
|
687
|
-
name: "users",
|
|
688
|
-
attributes: {
|
|
689
|
-
|
|
690
|
-
email: "email|unique",
|
|
691
|
-
password: "secret", // Automatically encrypted
|
|
692
|
-
role: "string|enum:user,admin,moderator|default:user",
|
|
693
|
-
profile: {
|
|
694
|
-
firstName: "string",
|
|
695
|
-
lastName: "string",
|
|
696
|
-
avatar: "url|optional",
|
|
697
|
-
bio: "string|max:500|optional"
|
|
698
|
-
},
|
|
699
|
-
preferences: {
|
|
700
|
-
theme: "string|enum:light,dark|default:light",
|
|
701
|
-
language: "string|default:en",
|
|
702
|
-
notifications: "boolean|default:true"
|
|
703
|
-
},
|
|
704
|
-
lastLogin: "date|optional"
|
|
705
|
-
},
|
|
706
|
-
behavior: "enforce-limits",
|
|
707
|
-
timestamps: true,
|
|
708
|
-
hooks: {
|
|
709
|
-
preInsert: [async (data) => {
|
|
710
|
-
// Auto-generate secure password if not provided
|
|
711
|
-
if (!data.password) {
|
|
712
|
-
data.password = generateSecurePassword();
|
|
713
|
-
}
|
|
714
|
-
return data;
|
|
715
|
-
}],
|
|
716
|
-
afterInsert: [async (data) => {
|
|
717
|
-
console.log(`Welcome ${data.username}! ๐`);
|
|
718
|
-
}]
|
|
719
|
-
}
|
|
458
|
+
// Custom size IDs
|
|
459
|
+
const shortUsers = await s3db.createResource({
|
|
460
|
+
name: "short-users",
|
|
461
|
+
attributes: { name: "string|required" },
|
|
462
|
+
idSize: 8 // Generate 8-character IDs
|
|
720
463
|
});
|
|
721
464
|
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
lastName: "Smith"
|
|
729
|
-
},
|
|
730
|
-
preferences: {
|
|
731
|
-
theme: "dark",
|
|
732
|
-
notifications: true
|
|
733
|
-
}
|
|
465
|
+
// UUID support
|
|
466
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
467
|
+
const uuidUsers = await s3db.createResource({
|
|
468
|
+
name: "uuid-users",
|
|
469
|
+
attributes: { name: "string|required" },
|
|
470
|
+
idGenerator: uuidv4
|
|
734
471
|
});
|
|
735
472
|
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
473
|
+
// UUID v1 (time-based)
|
|
474
|
+
const timeUsers = await s3db.createResource({
|
|
475
|
+
name: "time-users",
|
|
476
|
+
attributes: { name: "string|required" },
|
|
477
|
+
idGenerator: uuidv1
|
|
478
|
+
});
|
|
741
479
|
|
|
742
|
-
|
|
480
|
+
// Custom ID function
|
|
481
|
+
const timestampUsers = await s3db.createResource({
|
|
482
|
+
name: "timestamp-users",
|
|
483
|
+
attributes: { name: "string|required" },
|
|
484
|
+
idGenerator: () => `user_${Date.now()}`
|
|
485
|
+
});
|
|
486
|
+
```
|
|
743
487
|
|
|
744
|
-
###
|
|
488
|
+
### ๐ Plugin System
|
|
745
489
|
|
|
746
|
-
|
|
490
|
+
Extend functionality with powerful plugins. s3db.js supports multiple plugins working together seamlessly:
|
|
747
491
|
|
|
748
492
|
```javascript
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
});
|
|
493
|
+
import {
|
|
494
|
+
CachePlugin,
|
|
495
|
+
CostsPlugin,
|
|
496
|
+
FullTextPlugin,
|
|
497
|
+
MetricsPlugin,
|
|
498
|
+
ReplicationPlugin,
|
|
499
|
+
AuditPlugin
|
|
500
|
+
} from 's3db.js';
|
|
758
501
|
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
502
|
+
const s3db = new S3db({
|
|
503
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
504
|
+
plugins: [
|
|
505
|
+
new CachePlugin({ enabled: true }), // CachePlugin needs instantiation
|
|
506
|
+
CostsPlugin, // CostsPlugin is a static object
|
|
507
|
+
new FullTextPlugin({ fields: ['name', 'description'] }),
|
|
508
|
+
new MetricsPlugin({ enabled: true }),
|
|
509
|
+
new ReplicationPlugin({
|
|
510
|
+
enabled: true,
|
|
511
|
+
replicators: [
|
|
512
|
+
{
|
|
513
|
+
driver: 's3db',
|
|
514
|
+
resources: ['users', 'products'],
|
|
515
|
+
config: {
|
|
516
|
+
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
]
|
|
520
|
+
}),
|
|
521
|
+
new AuditPlugin({ enabled: true })
|
|
522
|
+
]
|
|
764
523
|
});
|
|
765
524
|
|
|
766
|
-
//
|
|
767
|
-
|
|
768
|
-
|
|
525
|
+
// All plugins work together seamlessly
|
|
526
|
+
await users.insert({ name: "John", email: "john@example.com" });
|
|
527
|
+
// - Cache: Caches the operation
|
|
528
|
+
// - Costs: Tracks S3 costs
|
|
529
|
+
// - FullText: Indexes the data for search
|
|
530
|
+
// - Metrics: Records performance metrics
|
|
531
|
+
// - Replication: Syncs to configured replicators
|
|
532
|
+
// - Audit: Logs the operation
|
|
769
533
|
```
|
|
770
534
|
|
|
771
|
-
###
|
|
535
|
+
### ๐ Replicator System
|
|
772
536
|
|
|
773
|
-
|
|
537
|
+
The Replication Plugin now supports a flexible driver-based system for replicating data to different targets. Each replicator driver handles a specific type of target system.
|
|
774
538
|
|
|
539
|
+
#### Available Replicators
|
|
540
|
+
|
|
541
|
+
**S3DB Replicator** - Replicates data to another s3db instance:
|
|
775
542
|
```javascript
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
543
|
+
{
|
|
544
|
+
driver: 's3db',
|
|
545
|
+
resources: ['users', 'products'], // <-- root level
|
|
546
|
+
config: {
|
|
547
|
+
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup",
|
|
548
|
+
region: 'us-west-2'
|
|
782
549
|
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const account = await accounts.insert({
|
|
786
|
-
name: "Service Account"
|
|
787
|
-
// password and apiKey will be auto-generated
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
console.log(account.password); // "Ax7Kp9mN2qR3" (12-char secure password)
|
|
791
|
-
console.log(account.apiKey); // "Bc8Lq0nO3sS4" (12-char secure key)
|
|
550
|
+
}
|
|
792
551
|
```
|
|
793
552
|
|
|
794
|
-
**
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
553
|
+
**SQS Replicator** - Sends data to AWS SQS queues:
|
|
554
|
+
```javascript
|
|
555
|
+
{
|
|
556
|
+
driver: 'sqs',
|
|
557
|
+
resources: ['orders'],
|
|
558
|
+
config: {
|
|
559
|
+
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue',
|
|
560
|
+
region: 'us-east-1',
|
|
561
|
+
messageGroupId: 's3db-replication', // For FIFO queues
|
|
562
|
+
deduplicationId: true // Enable deduplication
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
```
|
|
801
566
|
|
|
567
|
+
**BigQuery Replicator** - Sends data to Google BigQuery:
|
|
802
568
|
```javascript
|
|
803
|
-
|
|
569
|
+
{
|
|
570
|
+
driver: 'bigquery',
|
|
571
|
+
resources: ['users', 'orders'],
|
|
572
|
+
config: {
|
|
573
|
+
projectId: 'my-project',
|
|
574
|
+
datasetId: 'analytics',
|
|
575
|
+
tableId: 's3db_replication',
|
|
576
|
+
location: 'US',
|
|
577
|
+
credentials: {
|
|
578
|
+
// Your Google Cloud service account credentials
|
|
579
|
+
client_email: 'service-account@project.iam.gserviceaccount.com',
|
|
580
|
+
private_key: '-----BEGIN PRIVATE KEY-----\n...'
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
```
|
|
804
585
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
586
|
+
**PostgreSQL Replicator** - Sends data to PostgreSQL databases:
|
|
587
|
+
```javascript
|
|
588
|
+
{
|
|
589
|
+
driver: 'postgres',
|
|
590
|
+
resources: ['users'],
|
|
591
|
+
config: {
|
|
592
|
+
connectionString: 'postgresql://user:pass@localhost:5432/analytics',
|
|
593
|
+
// OR individual parameters:
|
|
594
|
+
// host: 'localhost',
|
|
595
|
+
// port: 5432,
|
|
596
|
+
// database: 'analytics',
|
|
597
|
+
// user: 'user',
|
|
598
|
+
// password: 'pass',
|
|
599
|
+
tableName: 's3db_replication',
|
|
600
|
+
ssl: false
|
|
601
|
+
}
|
|
602
|
+
}
|
|
809
603
|
```
|
|
810
604
|
|
|
811
|
-
|
|
605
|
+
#### Replicator Features
|
|
812
606
|
|
|
813
|
-
|
|
607
|
+
- **Resource Filtering**: Each replicator can be configured to handle specific resources only
|
|
608
|
+
- **Event Emission**: All replicators emit events for monitoring and debugging
|
|
609
|
+
- **Connection Testing**: Test connections to replicators before use
|
|
610
|
+
- **Batch Operations**: Support for batch replication operations
|
|
611
|
+
- **Error Handling**: Comprehensive error handling and retry logic
|
|
612
|
+
- **Status Monitoring**: Get detailed status and statistics for each replicator
|
|
814
613
|
|
|
815
|
-
|
|
614
|
+
#### Dependencies
|
|
816
615
|
|
|
817
|
-
|
|
616
|
+
The replicators use optional peer dependencies. Install only what you need:
|
|
818
617
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
618
|
+
```bash
|
|
619
|
+
# For SQS replicator
|
|
620
|
+
npm install @aws-sdk/client-sqs
|
|
621
|
+
# or
|
|
622
|
+
yarn add @aws-sdk/client-sqs
|
|
623
|
+
# or
|
|
624
|
+
pnpm add @aws-sdk/client-sqs
|
|
625
|
+
|
|
626
|
+
# For BigQuery replicator
|
|
627
|
+
npm install @google-cloud/bigquery
|
|
628
|
+
# or
|
|
629
|
+
yarn add @google-cloud/bigquery
|
|
630
|
+
# or
|
|
631
|
+
pnpm add @google-cloud/bigquery
|
|
632
|
+
|
|
633
|
+
# For PostgreSQL replicator
|
|
634
|
+
npm install pg
|
|
635
|
+
# or
|
|
636
|
+
yarn add pg
|
|
637
|
+
# or
|
|
638
|
+
pnpm add pg
|
|
639
|
+
```
|
|
825
640
|
|
|
826
|
-
|
|
641
|
+
**โ ๏ธ Important:** These dependencies are marked as `peerDependencies` in the package.json, which means they are not automatically installed with s3db.js. You must install them manually if you plan to use the corresponding replicators. If you don't install the required dependency, the replicator will throw an error when trying to initialize.
|
|
827
642
|
|
|
828
|
-
|
|
829
|
-
|
|
643
|
+
**Example error without dependency:**
|
|
644
|
+
```
|
|
645
|
+
Error: Cannot find module '@aws-sdk/client-sqs'
|
|
646
|
+
```
|
|
830
647
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
648
|
+
**Solution:** Install the missing dependency as shown above.
|
|
649
|
+
|
|
650
|
+
#### Example Usage
|
|
834
651
|
|
|
835
|
-
|
|
836
|
-
const monthlyReads = 0.004; // 10,000 GET requests = $0.004
|
|
837
|
-
const monthlyUpdates = 0.0005; // 1,000 PUT requests = $0.0005
|
|
652
|
+
See `examples/e34-replicators.js` for a complete example using all four replicator types.
|
|
838
653
|
|
|
839
|
-
|
|
840
|
-
|
|
654
|
+
**Prerequisites:** Make sure to install the required dependencies before running the example:
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
# Install all replication dependencies for the full example
|
|
658
|
+
npm install @aws-sdk/client-sqs @google-cloud/bigquery pg
|
|
841
659
|
```
|
|
842
660
|
|
|
843
|
-
|
|
661
|
+
#### ๐ Cache Plugin
|
|
844
662
|
|
|
845
|
-
|
|
846
|
-
|
|
663
|
+
#### ๐ Cache Plugin
|
|
664
|
+
Intelligent caching to reduce API calls and improve performance:
|
|
847
665
|
|
|
848
666
|
```javascript
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
667
|
+
const s3db = new S3db({
|
|
668
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
669
|
+
plugins: [new CachePlugin({
|
|
670
|
+
enabled: true,
|
|
671
|
+
ttl: 300000, // 5 minutes cache
|
|
672
|
+
maxSize: 1000, // Max 1000 items in cache
|
|
673
|
+
driverType: 'memory' // 'memory' or 's3'
|
|
674
|
+
})]
|
|
675
|
+
});
|
|
855
676
|
|
|
856
|
-
|
|
857
|
-
|
|
677
|
+
// Automatic caching for reads
|
|
678
|
+
await users.count(); // Cached for 5 minutes
|
|
679
|
+
await users.list(); // Cached for 5 minutes
|
|
680
|
+
await users.insert({...}); // Automatically clears cache
|
|
858
681
|
```
|
|
859
682
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
### ๐ Cost Tracking
|
|
863
|
-
|
|
864
|
-
Monitor your expenses with the built-in cost tracking plugin:
|
|
683
|
+
#### ๐ฐ Costs Plugin
|
|
684
|
+
Track and monitor AWS S3 costs in real-time:
|
|
865
685
|
|
|
866
686
|
```javascript
|
|
867
|
-
import { CostsPlugin } from "s3db.js";
|
|
868
|
-
|
|
869
687
|
const s3db = new S3db({
|
|
870
|
-
|
|
688
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
871
689
|
plugins: [CostsPlugin]
|
|
872
690
|
});
|
|
873
691
|
|
|
874
|
-
//
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
692
|
+
// Track costs automatically
|
|
693
|
+
await users.insert({ name: "John", email: "john@example.com" });
|
|
694
|
+
await users.list();
|
|
695
|
+
|
|
696
|
+
// Get cost information
|
|
697
|
+
console.log(s3db.client.costs);
|
|
698
|
+
// { total: 0.000009, requests: { total: 3, get: 1, put: 1, list: 1 } }
|
|
878
699
|
```
|
|
879
700
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
## ๐จ Best Practices
|
|
883
|
-
|
|
884
|
-
### โ
Do's
|
|
701
|
+
#### ๐ Full-Text Search Plugin
|
|
702
|
+
Powerful text search with automatic indexing:
|
|
885
703
|
|
|
886
|
-
#### **๐ฏ Design for Document Storage**
|
|
887
704
|
```javascript
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
705
|
+
const s3db = new S3db({
|
|
706
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
707
|
+
plugins: [new FullTextPlugin({
|
|
708
|
+
enabled: true,
|
|
709
|
+
fields: ['name', 'description', 'content'], // Fields to index
|
|
710
|
+
minWordLength: 3,
|
|
711
|
+
maxResults: 50,
|
|
712
|
+
language: 'en-US'
|
|
713
|
+
})]
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// Create resource with searchable fields
|
|
717
|
+
const products = await s3db.createResource({
|
|
718
|
+
name: "products",
|
|
719
|
+
attributes: {
|
|
720
|
+
name: "string|required",
|
|
721
|
+
description: "string",
|
|
722
|
+
content: "string"
|
|
895
723
|
}
|
|
896
|
-
};
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// Insert data (automatically indexed)
|
|
727
|
+
await products.insert({
|
|
728
|
+
name: "JavaScript Book",
|
|
729
|
+
description: "Learn JavaScript programming",
|
|
730
|
+
content: "Comprehensive guide to modern JavaScript"
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// Search across all indexed fields
|
|
734
|
+
const results = await s3db.plugins.fulltext.searchRecords('products', 'javascript');
|
|
735
|
+
console.log(results); // Returns products with search scores
|
|
736
|
+
|
|
737
|
+
// Example of search results:
|
|
738
|
+
// [
|
|
739
|
+
// {
|
|
740
|
+
// id: "prod-123",
|
|
741
|
+
// name: "JavaScript Book",
|
|
742
|
+
// description: "Learn JavaScript programming",
|
|
743
|
+
// content: "Comprehensive guide to modern JavaScript",
|
|
744
|
+
// _searchScore: 0.85,
|
|
745
|
+
// _matchedFields: ["name", "description", "content"],
|
|
746
|
+
// _matchedWords: ["javascript"]
|
|
747
|
+
// },
|
|
748
|
+
// {
|
|
749
|
+
// id: "prod-456",
|
|
750
|
+
// name: "Web Development Guide",
|
|
751
|
+
// description: "Includes JavaScript, HTML, and CSS",
|
|
752
|
+
// content: "Complete web development with JavaScript",
|
|
753
|
+
// _searchScore: 0.72,
|
|
754
|
+
// _matchedFields: ["description", "content"],
|
|
755
|
+
// _matchedWords: ["javascript"]
|
|
756
|
+
// }
|
|
757
|
+
// ]
|
|
897
758
|
```
|
|
898
759
|
|
|
899
|
-
####
|
|
760
|
+
#### ๐ Metrics Plugin
|
|
761
|
+
Monitor performance and usage metrics:
|
|
762
|
+
|
|
900
763
|
```javascript
|
|
901
|
-
|
|
902
|
-
|
|
764
|
+
const s3db = new S3db({
|
|
765
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
766
|
+
plugins: [new MetricsPlugin({
|
|
767
|
+
enabled: true,
|
|
768
|
+
collectPerformance: true,
|
|
769
|
+
collectErrors: true,
|
|
770
|
+
collectUsage: true,
|
|
771
|
+
flushInterval: 60000 // Flush every minute
|
|
772
|
+
})]
|
|
773
|
+
});
|
|
903
774
|
|
|
904
|
-
//
|
|
905
|
-
|
|
775
|
+
// Metrics are collected automatically
|
|
776
|
+
await users.insert({ name: "John" });
|
|
777
|
+
await users.list();
|
|
778
|
+
|
|
779
|
+
// Get metrics
|
|
780
|
+
const metrics = await s3db.plugins.metrics.getMetrics();
|
|
781
|
+
console.log(metrics); // Performance and usage data
|
|
782
|
+
|
|
783
|
+
// Example of metrics object:
|
|
784
|
+
// {
|
|
785
|
+
// performance: {
|
|
786
|
+
// averageResponseTime: 245, // milliseconds
|
|
787
|
+
// totalRequests: 1250,
|
|
788
|
+
// requestsPerSecond: 12.5,
|
|
789
|
+
// slowestOperations: [
|
|
790
|
+
// { operation: "list", resource: "users", avgTime: 450, count: 50 },
|
|
791
|
+
// { operation: "get", resource: "products", avgTime: 320, count: 200 }
|
|
792
|
+
// ]
|
|
793
|
+
// },
|
|
794
|
+
// usage: {
|
|
795
|
+
// resources: {
|
|
796
|
+
// users: { inserts: 150, updates: 75, deletes: 10, reads: 800 },
|
|
797
|
+
// products: { inserts: 300, updates: 120, deletes: 25, reads: 1200 }
|
|
798
|
+
// },
|
|
799
|
+
// totalOperations: 2680,
|
|
800
|
+
// mostActiveResource: "products",
|
|
801
|
+
// peakUsageHour: "14:00"
|
|
802
|
+
// },
|
|
803
|
+
// errors: {
|
|
804
|
+
// total: 15,
|
|
805
|
+
// byType: {
|
|
806
|
+
// "ValidationError": 8,
|
|
807
|
+
// "NotFoundError": 5,
|
|
808
|
+
// "PermissionError": 2
|
|
809
|
+
// },
|
|
810
|
+
// byResource: {
|
|
811
|
+
// users: 10,
|
|
812
|
+
// products: 5
|
|
813
|
+
// }
|
|
814
|
+
// },
|
|
815
|
+
// cache: {
|
|
816
|
+
// hitRate: 0.78, // 78% cache hit rate
|
|
817
|
+
// totalHits: 980,
|
|
818
|
+
// totalMisses: 270,
|
|
819
|
+
// averageCacheTime: 120 // milliseconds
|
|
820
|
+
// }
|
|
821
|
+
// }
|
|
906
822
|
```
|
|
907
823
|
|
|
908
|
-
####
|
|
824
|
+
#### ๐ Replication Plugin
|
|
825
|
+
Replicate data to other buckets or regions:
|
|
826
|
+
|
|
909
827
|
```javascript
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
828
|
+
const s3db = new S3db({
|
|
829
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
830
|
+
plugins: [new ReplicationPlugin({
|
|
831
|
+
enabled: true,
|
|
832
|
+
replicators: [
|
|
833
|
+
{
|
|
834
|
+
driver: 's3db',
|
|
835
|
+
resources: ['users', 'products'],
|
|
836
|
+
config: {
|
|
837
|
+
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
|
|
838
|
+
}
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
driver: 'sqs',
|
|
842
|
+
resources: ['orders', 'users', 'products'],
|
|
843
|
+
config: {
|
|
844
|
+
// Resource-specific queues
|
|
845
|
+
queues: {
|
|
846
|
+
users: 'https://sqs.us-east-1.amazonaws.com/123456789012/users-events.fifo',
|
|
847
|
+
orders: 'https://sqs.us-east-1.amazonaws.com/123456789012/orders-events.fifo',
|
|
848
|
+
products: 'https://sqs.us-east-1.amazonaws.com/123456789012/products-events.fifo'
|
|
849
|
+
},
|
|
850
|
+
// Fallback queue for unspecified resources
|
|
851
|
+
defaultQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/default-events.fifo',
|
|
852
|
+
messageGroupId: 's3db-replication', // For FIFO queues
|
|
853
|
+
deduplicationId: true // Enable deduplication
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
driver: 'bigquery',
|
|
858
|
+
resources: ['users', 'orders'],
|
|
859
|
+
config: {
|
|
860
|
+
projectId: 'my-project',
|
|
861
|
+
datasetId: 'analytics',
|
|
862
|
+
tableId: 's3db_replication'
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
driver: 'postgres',
|
|
867
|
+
resources: ['users'],
|
|
868
|
+
config: {
|
|
869
|
+
connectionString: 'postgresql://user:pass@localhost:5432/analytics',
|
|
870
|
+
tableName: 's3db_replication'
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
],
|
|
874
|
+
syncInterval: 300000 // Sync every 5 minutes
|
|
875
|
+
})]
|
|
914
876
|
});
|
|
877
|
+
|
|
878
|
+
// Data is automatically replicated to all configured targets
|
|
879
|
+
await users.insert({ name: "John" }); // Synced to all replicators
|
|
880
|
+
|
|
881
|
+
**SQS Message Structure:**
|
|
882
|
+
|
|
883
|
+
The SQS replicator sends standardized messages with the following structure:
|
|
884
|
+
|
|
885
|
+
```javascript
|
|
886
|
+
// INSERT operation
|
|
887
|
+
{
|
|
888
|
+
resource: "users",
|
|
889
|
+
action: "insert",
|
|
890
|
+
data: { _v: 0, id: "user-001", name: "John", email: "john@example.com" },
|
|
891
|
+
timestamp: "2024-01-01T10:00:00.000Z",
|
|
892
|
+
source: "s3db-replication"
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// UPDATE operation (includes before/after data)
|
|
896
|
+
{
|
|
897
|
+
resource: "users",
|
|
898
|
+
action: "update",
|
|
899
|
+
before: { _v: 0, id: "user-001", name: "John", age: 30 },
|
|
900
|
+
data: { _v: 1, id: "user-001", name: "John", age: 31 },
|
|
901
|
+
timestamp: "2024-01-01T10:05:00.000Z",
|
|
902
|
+
source: "s3db-replication"
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// DELETE operation
|
|
906
|
+
{
|
|
907
|
+
resource: "users",
|
|
908
|
+
action: "delete",
|
|
909
|
+
data: { _v: 1, id: "user-001", name: "John", age: 31 },
|
|
910
|
+
timestamp: "2024-01-01T10:10:00.000Z",
|
|
911
|
+
source: "s3db-replication"
|
|
912
|
+
}
|
|
915
913
|
```
|
|
916
914
|
|
|
917
|
-
|
|
915
|
+
**Queue Routing:**
|
|
916
|
+
- Each resource can have its own dedicated queue
|
|
917
|
+
- Unspecified resources use the default queue
|
|
918
|
+
- FIFO queues supported with deduplication
|
|
919
|
+
- Messages are automatically routed to the appropriate queue
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
#### ๐ Audit Plugin
|
|
923
|
+
Log all operations for compliance and traceability:
|
|
924
|
+
|
|
918
925
|
```javascript
|
|
919
|
-
|
|
920
|
-
|
|
926
|
+
const s3db = new S3db({
|
|
927
|
+
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
928
|
+
plugins: [new AuditPlugin({
|
|
929
|
+
enabled: true,
|
|
930
|
+
trackOperations: ['insert', 'update', 'delete', 'get'],
|
|
931
|
+
includeData: false, // Don't log sensitive data
|
|
932
|
+
retentionDays: 90
|
|
933
|
+
})]
|
|
934
|
+
});
|
|
921
935
|
|
|
922
|
-
//
|
|
923
|
-
{
|
|
936
|
+
// All operations are logged
|
|
937
|
+
await users.insert({ name: "John" });
|
|
938
|
+
await users.update(userId, { age: 31 });
|
|
924
939
|
|
|
925
|
-
//
|
|
926
|
-
|
|
940
|
+
// Get audit logs
|
|
941
|
+
const logs = await s3db.plugins.audit.getAuditLogs({
|
|
942
|
+
resourceName: 'users',
|
|
943
|
+
operation: 'insert'
|
|
944
|
+
});
|
|
945
|
+
console.log(logs); // Audit trail
|
|
927
946
|
```
|
|
928
947
|
|
|
929
|
-
###
|
|
948
|
+
### ๐๏ธ Advanced Behaviors
|
|
949
|
+
|
|
950
|
+
Choose the right behavior strategy for your use case:
|
|
951
|
+
|
|
952
|
+
#### Behavior Comparison
|
|
953
|
+
|
|
954
|
+
| Behavior | Enforcement | Data Loss | Event Emission | Use Case |
|
|
955
|
+
|------------------|-------------|-----------|----------------|-------------------------|
|
|
956
|
+
| `user-managed` | None | Possible | Warns | Dev/Test/Advanced users |
|
|
957
|
+
| `enforce-limits` | Strict | No | Throws | Production |
|
|
958
|
+
| `data-truncate` | Truncates | Yes | Warns | Content Mgmt |
|
|
959
|
+
| `body-overflow` | Truncates/Splits | Yes | Warns | Large objects |
|
|
960
|
+
| `body-only` | Unlimited | No | No | Large JSON/Logs |
|
|
961
|
+
|
|
962
|
+
#### User Managed Behavior (Default)
|
|
963
|
+
|
|
964
|
+
The `user-managed` behavior is the default for s3db resources. It provides no automatic enforcement of S3 metadata or body size limits, and does not modify or truncate data. Instead, it emits warnings via the `exceedsLimit` event when S3 metadata limits are exceeded, but allows all operations to proceed.
|
|
965
|
+
|
|
966
|
+
**Purpose & Use Cases:**
|
|
967
|
+
- For development, testing, or advanced users who want full control over resource metadata and body size.
|
|
968
|
+
- Useful when you want to handle S3 metadata limits yourself, or implement custom logic for warnings.
|
|
969
|
+
- Not recommended for production unless you have custom enforcement or validation in place.
|
|
970
|
+
|
|
971
|
+
**How It Works:**
|
|
972
|
+
- Emits an `exceedsLimit` event (with details) when a resource's metadata size exceeds the S3 2KB limit.
|
|
973
|
+
- Does NOT block, truncate, or modify dataโoperations always proceed.
|
|
974
|
+
- No automatic enforcement of any limits; user is responsible for handling warnings and data integrity.
|
|
975
|
+
|
|
976
|
+
**Event Emission:**
|
|
977
|
+
- Event: `exceedsLimit`
|
|
978
|
+
- Payload:
|
|
979
|
+
- `operation`: 'insert' | 'update' | 'upsert'
|
|
980
|
+
- `id` (for update/upsert): resource id
|
|
981
|
+
- `totalSize`: total metadata size in bytes
|
|
982
|
+
- `limit`: S3 metadata limit (2048 bytes)
|
|
983
|
+
- `excess`: number of bytes over the limit
|
|
984
|
+
- `data`: the offending data object
|
|
930
985
|
|
|
931
|
-
#### **๐ซ Avoid Large Arrays in Documents**
|
|
932
986
|
```javascript
|
|
933
|
-
//
|
|
934
|
-
const
|
|
987
|
+
// Flexible behavior - warns but doesn't block
|
|
988
|
+
const users = await s3db.createResource({
|
|
989
|
+
name: "users",
|
|
990
|
+
attributes: { name: "string", bio: "string" },
|
|
991
|
+
behavior: "user-managed" // Default
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
// Listen for limit warnings
|
|
995
|
+
users.on('exceedsLimit', (data) => {
|
|
996
|
+
console.warn(`Data exceeds 2KB limit by ${data.excess} bytes`, data);
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// Operation continues despite warning
|
|
1000
|
+
await users.insert({
|
|
935
1001
|
name: "John",
|
|
936
|
-
|
|
937
|
-
};
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1002
|
+
bio: "A".repeat(3000) // > 2KB
|
|
1003
|
+
});
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
**Best Practices & Warnings:**
|
|
1007
|
+
- Exceeding S3 metadata limits will cause silent data loss or errors at the storage layer.
|
|
1008
|
+
- Use this behavior only if you have custom logic to handle warnings and enforce limits.
|
|
1009
|
+
- For production, prefer `enforce-limits` or `data-truncate` to avoid data loss.
|
|
1010
|
+
|
|
1011
|
+
**Migration Tips:**
|
|
1012
|
+
- To migrate to a stricter behavior, change the resource's behavior to `enforce-limits` or `data-truncate`.
|
|
1013
|
+
- Review emitted warnings to identify resources at risk of exceeding S3 limits.
|
|
1014
|
+
|
|
1015
|
+
#### Enforce Limits Behavior
|
|
1016
|
+
|
|
1017
|
+
```javascript
|
|
1018
|
+
// Strict validation - throws error if limit exceeded
|
|
1019
|
+
const settings = await s3db.createResource({
|
|
1020
|
+
name: "settings",
|
|
1021
|
+
attributes: { key: "string", value: "string" },
|
|
1022
|
+
behavior: "enforce-limits"
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// Throws error if data > 2KB
|
|
1026
|
+
await settings.insert({
|
|
1027
|
+
key: "large_setting",
|
|
1028
|
+
value: "A".repeat(3000) // Throws: "S3 metadata size exceeds 2KB limit"
|
|
1029
|
+
});
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
#### Data Truncate Behavior
|
|
1033
|
+
|
|
1034
|
+
```javascript
|
|
1035
|
+
// Smart truncation - preserves structure, truncates content
|
|
1036
|
+
const summaries = await s3db.createResource({
|
|
1037
|
+
name: "summaries",
|
|
1038
|
+
attributes: {
|
|
1039
|
+
title: "string",
|
|
1040
|
+
description: "string",
|
|
1041
|
+
content: "string"
|
|
1042
|
+
},
|
|
1043
|
+
behavior: "data-truncate"
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
// Automatically truncates to fit within 2KB
|
|
1047
|
+
const result = await summaries.insert({
|
|
1048
|
+
title: "Short Title",
|
|
1049
|
+
description: "A".repeat(1000),
|
|
1050
|
+
content: "B".repeat(2000) // Will be truncated with "..."
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
// Retrieved data shows truncation
|
|
1054
|
+
const retrieved = await summaries.get(result.id);
|
|
1055
|
+
console.log(retrieved.content); // "B...B..." (truncated)
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
#### Body Overflow Behavior
|
|
1059
|
+
|
|
1060
|
+
```javascript
|
|
1061
|
+
// Preserve all data by using S3 object body
|
|
1062
|
+
const blogs = await s3db.createResource({
|
|
1063
|
+
name: "blogs",
|
|
1064
|
+
attributes: {
|
|
1065
|
+
title: "string",
|
|
1066
|
+
content: "string", // Can be very large
|
|
1067
|
+
author: "string"
|
|
1068
|
+
},
|
|
1069
|
+
behavior: "body-overflow"
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
// Large content is automatically split between metadata and body
|
|
1073
|
+
const blog = await blogs.insert({
|
|
1074
|
+
title: "My Blog Post",
|
|
1075
|
+
content: "A".repeat(5000), // Large content
|
|
1076
|
+
author: "John Doe"
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
// All data is preserved and accessible
|
|
1080
|
+
const retrieved = await blogs.get(blog.id);
|
|
1081
|
+
console.log(retrieved.content.length); // 5000 (full content preserved)
|
|
1082
|
+
console.log(retrieved._hasContent); // true (indicates body usage)
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
#### Body Only Behavior
|
|
1086
|
+
|
|
1087
|
+
```javascript
|
|
1088
|
+
// Store all data in S3 object body as JSON, keeping only version in metadata
|
|
1089
|
+
const documents = await s3db.createResource({
|
|
1090
|
+
name: "documents",
|
|
1091
|
+
attributes: {
|
|
1092
|
+
title: "string",
|
|
1093
|
+
content: "string", // Can be extremely large
|
|
1094
|
+
metadata: "object"
|
|
1095
|
+
},
|
|
1096
|
+
behavior: "body-only"
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// Store large documents without any size limits
|
|
1100
|
+
const document = await documents.insert({
|
|
1101
|
+
title: "Large Document",
|
|
1102
|
+
content: "A".repeat(100000), // 100KB content
|
|
1103
|
+
metadata: {
|
|
1104
|
+
author: "John Doe",
|
|
1105
|
+
tags: ["large", "document"],
|
|
1106
|
+
version: "1.0"
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
// All data is stored in the S3 object body
|
|
1111
|
+
const retrieved = await documents.get(document.id);
|
|
1112
|
+
console.log(retrieved.content.length); // 100000 (full content preserved)
|
|
1113
|
+
console.log(retrieved.metadata.author); // "John Doe"
|
|
1114
|
+
console.log(retrieved._hasContent); // true (indicates body usage)
|
|
1115
|
+
|
|
1116
|
+
// Perfect for storing large JSON documents, logs, or any large content
|
|
1117
|
+
const logEntry = await documents.insert({
|
|
1118
|
+
title: "Application Log",
|
|
1119
|
+
content: JSON.stringify({
|
|
1120
|
+
timestamp: new Date().toISOString(),
|
|
1121
|
+
level: "INFO",
|
|
1122
|
+
message: "Application started",
|
|
1123
|
+
details: {
|
|
1124
|
+
// ... large log details
|
|
1125
|
+
}
|
|
1126
|
+
}),
|
|
1127
|
+
metadata: { source: "api-server", environment: "production" }
|
|
1128
|
+
});
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
### ๐ Advanced Streaming API
|
|
1132
|
+
|
|
1133
|
+
Handle large datasets efficiently with advanced streaming capabilities:
|
|
1134
|
+
|
|
1135
|
+
#### Readable Streams
|
|
1136
|
+
|
|
1137
|
+
```javascript
|
|
1138
|
+
// Configure streaming with custom batch size and concurrency
|
|
1139
|
+
const readableStream = await users.readable({
|
|
1140
|
+
batchSize: 50, // Process 50 items per batch
|
|
1141
|
+
concurrency: 10 // 10 concurrent operations
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// Process data as it streams
|
|
1145
|
+
readableStream.on('data', (user) => {
|
|
1146
|
+
console.log(`Processing user: ${user.name}`);
|
|
1147
|
+
// Process each user individually
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
readableStream.on('error', (error) => {
|
|
1151
|
+
console.error('Stream error:', error);
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
readableStream.on('end', () => {
|
|
1155
|
+
console.log('Stream completed');
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// Pause and resume streaming
|
|
1159
|
+
readableStream.pause();
|
|
1160
|
+
setTimeout(() => readableStream.resume(), 1000);
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
#### Writable Streams
|
|
1164
|
+
|
|
1165
|
+
```javascript
|
|
1166
|
+
// Configure writable stream for bulk operations
|
|
1167
|
+
const writableStream = await users.writable({
|
|
1168
|
+
batchSize: 25, // Write 25 items per batch
|
|
1169
|
+
concurrency: 5 // 5 concurrent writes
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// Write data to stream
|
|
1173
|
+
const userData = [
|
|
1174
|
+
{ name: 'User 1', email: 'user1@example.com' },
|
|
1175
|
+
{ name: 'User 2', email: 'user2@example.com' },
|
|
1176
|
+
// ... thousands more
|
|
944
1177
|
];
|
|
1178
|
+
|
|
1179
|
+
userData.forEach(user => {
|
|
1180
|
+
writableStream.write(user);
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// End stream and wait for completion
|
|
1184
|
+
writableStream.on('finish', () => {
|
|
1185
|
+
console.log('All users written successfully');
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
writableStream.on('error', (error) => {
|
|
1189
|
+
console.error('Write error:', error);
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
writableStream.end();
|
|
945
1193
|
```
|
|
946
1194
|
|
|
947
|
-
####
|
|
1195
|
+
#### Stream Error Handling
|
|
1196
|
+
|
|
948
1197
|
```javascript
|
|
949
|
-
//
|
|
950
|
-
const
|
|
1198
|
+
// Handle errors gracefully in streams
|
|
1199
|
+
const stream = await users.readable();
|
|
1200
|
+
|
|
1201
|
+
stream.on('error', (error, item) => {
|
|
1202
|
+
console.error(`Error processing item:`, error);
|
|
1203
|
+
console.log('Problematic item:', item);
|
|
1204
|
+
// Continue processing other items
|
|
1205
|
+
});
|
|
951
1206
|
|
|
952
|
-
//
|
|
953
|
-
|
|
1207
|
+
// Custom error handling for specific operations
|
|
1208
|
+
stream.on('data', async (user) => {
|
|
1209
|
+
try {
|
|
1210
|
+
await processUser(user);
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
console.error(`Failed to process user ${user.id}:`, error);
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
954
1215
|
```
|
|
955
1216
|
|
|
956
|
-
###
|
|
957
|
-
|
|
958
|
-
1. **Enable caching** for frequently accessed data:
|
|
959
|
-
```javascript
|
|
960
|
-
const s3db = new S3db({
|
|
961
|
-
uri: "s3://...",
|
|
962
|
-
cache: true,
|
|
963
|
-
ttl: 3600 // 1 hour
|
|
964
|
-
});
|
|
965
|
-
```
|
|
966
|
-
|
|
967
|
-
2. **Adjust parallelism** for bulk operations:
|
|
968
|
-
```javascript
|
|
969
|
-
const s3db = new S3db({
|
|
970
|
-
uri: "s3://...",
|
|
971
|
-
parallelism: 25 // Handle 25 concurrent operations
|
|
972
|
-
});
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
3. **Use partitions** for efficient queries:
|
|
976
|
-
```javascript
|
|
977
|
-
// Query specific partitions instead of scanning all data
|
|
978
|
-
const results = await users.list({
|
|
979
|
-
partition: "byRegion",
|
|
980
|
-
partitionValues: { region: "us-east" }
|
|
981
|
-
});
|
|
982
|
-
```
|
|
1217
|
+
### ๐ Binary Content Management
|
|
983
1218
|
|
|
984
|
-
|
|
1219
|
+
Store and manage binary content alongside your metadata:
|
|
985
1220
|
|
|
986
|
-
|
|
1221
|
+
#### Set Binary Content
|
|
987
1222
|
|
|
988
|
-
|
|
1223
|
+
```javascript
|
|
1224
|
+
import fs from 'fs';
|
|
1225
|
+
|
|
1226
|
+
// Set image content for user profile
|
|
1227
|
+
const imageBuffer = fs.readFileSync('profile.jpg');
|
|
1228
|
+
await users.setContent({
|
|
1229
|
+
id: 'user-123',
|
|
1230
|
+
buffer: imageBuffer,
|
|
1231
|
+
contentType: 'image/jpeg'
|
|
1232
|
+
});
|
|
989
1233
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1234
|
+
// Set document content
|
|
1235
|
+
const documentBuffer = fs.readFileSync('document.pdf');
|
|
1236
|
+
await users.setContent({
|
|
1237
|
+
id: 'user-123',
|
|
1238
|
+
buffer: documentBuffer,
|
|
1239
|
+
contentType: 'application/pdf'
|
|
1240
|
+
});
|
|
993
1241
|
|
|
994
|
-
|
|
995
|
-
|
|
1242
|
+
// Set text content
|
|
1243
|
+
await users.setContent({
|
|
1244
|
+
id: 'user-123',
|
|
1245
|
+
buffer: 'Hello World',
|
|
1246
|
+
contentType: 'text/plain'
|
|
1247
|
+
});
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
#### Retrieve Binary Content
|
|
1251
|
+
|
|
1252
|
+
```javascript
|
|
1253
|
+
// Get binary content
|
|
1254
|
+
const content = await users.content('user-123');
|
|
996
1255
|
|
|
997
|
-
|
|
998
|
-
|
|
1256
|
+
if (content.buffer) {
|
|
1257
|
+
console.log('Content type:', content.contentType);
|
|
1258
|
+
console.log('Content size:', content.buffer.length);
|
|
1259
|
+
|
|
1260
|
+
// Save to file
|
|
1261
|
+
fs.writeFileSync('downloaded.jpg', content.buffer);
|
|
1262
|
+
} else {
|
|
1263
|
+
console.log('No content found');
|
|
1264
|
+
}
|
|
999
1265
|
```
|
|
1000
1266
|
|
|
1001
|
-
|
|
1267
|
+
#### Content Management
|
|
1002
1268
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
- โ
**Security Tests** - Encryption and validation
|
|
1269
|
+
```javascript
|
|
1270
|
+
// Check if content exists
|
|
1271
|
+
const hasContent = await users.hasContent('user-123');
|
|
1272
|
+
console.log('Has content:', hasContent);
|
|
1008
1273
|
|
|
1009
|
-
|
|
1274
|
+
// Delete content but preserve metadata
|
|
1275
|
+
await users.deleteContent('user-123');
|
|
1276
|
+
// User metadata remains, but binary content is removed
|
|
1277
|
+
```
|
|
1010
1278
|
|
|
1011
|
-
|
|
1279
|
+
### ๐๏ธ Advanced Partitioning
|
|
1012
1280
|
|
|
1013
|
-
|
|
1281
|
+
Organize data efficiently with complex partitioning strategies:
|
|
1014
1282
|
|
|
1015
|
-
|
|
1283
|
+
#### Composite Partitions
|
|
1016
1284
|
|
|
1017
|
-
```
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1285
|
+
```javascript
|
|
1286
|
+
// Partition with multiple fields
|
|
1287
|
+
const analytics = await s3db.createResource({
|
|
1288
|
+
name: "analytics",
|
|
1289
|
+
attributes: {
|
|
1290
|
+
userId: "string",
|
|
1291
|
+
event: "string",
|
|
1292
|
+
timestamp: "date",
|
|
1293
|
+
region: "string",
|
|
1294
|
+
device: "string"
|
|
1295
|
+
},
|
|
1296
|
+
partitions: {
|
|
1297
|
+
// Single field partition
|
|
1298
|
+
byEvent: { fields: { event: "string" } },
|
|
1299
|
+
|
|
1300
|
+
// Two field partition
|
|
1301
|
+
byEventAndRegion: {
|
|
1302
|
+
fields: {
|
|
1303
|
+
event: "string",
|
|
1304
|
+
region: "string"
|
|
1305
|
+
}
|
|
1306
|
+
},
|
|
1307
|
+
|
|
1308
|
+
// Three field partition
|
|
1309
|
+
byEventRegionDevice: {
|
|
1310
|
+
fields: {
|
|
1311
|
+
event: "string",
|
|
1312
|
+
region: "string",
|
|
1313
|
+
device: "string"
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
```
|
|
1319
|
+
|
|
1320
|
+
#### Nested Field Partitions
|
|
1321
|
+
|
|
1322
|
+
```javascript
|
|
1323
|
+
// Partition by nested object fields
|
|
1324
|
+
const users = await s3db.createResource({
|
|
1325
|
+
name: "users",
|
|
1326
|
+
attributes: {
|
|
1327
|
+
name: "string",
|
|
1328
|
+
profile: {
|
|
1329
|
+
country: "string",
|
|
1330
|
+
city: "string",
|
|
1331
|
+
preferences: {
|
|
1332
|
+
theme: "string"
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
partitions: {
|
|
1337
|
+
byCountry: { fields: { "profile.country": "string" } },
|
|
1338
|
+
byCity: { fields: { "profile.city": "string" } },
|
|
1339
|
+
byTheme: { fields: { "profile.preferences.theme": "string" } }
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
// Query by nested field
|
|
1344
|
+
const usUsers = await users.list({
|
|
1345
|
+
partition: "byCountry",
|
|
1346
|
+
partitionValues: { "profile.country": "US" }
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// Note: The system automatically manages partition references internally
|
|
1350
|
+
// Users should use standard list() method with partition parameters
|
|
1351
|
+
|
|
1352
|
+
#### Automatic Timestamp Partitions
|
|
1021
1353
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1354
|
+
```javascript
|
|
1355
|
+
// Enable automatic timestamp partitions
|
|
1356
|
+
const events = await s3db.createResource({
|
|
1357
|
+
name: "events",
|
|
1358
|
+
attributes: {
|
|
1359
|
+
name: "string",
|
|
1360
|
+
data: "object"
|
|
1361
|
+
},
|
|
1362
|
+
timestamps: true // Automatically adds byCreatedDate and byUpdatedDate
|
|
1363
|
+
});
|
|
1024
1364
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1365
|
+
// Query by creation date
|
|
1366
|
+
const todayEvents = await events.list({
|
|
1367
|
+
partition: "byCreatedDate",
|
|
1368
|
+
partitionValues: { createdAt: "2024-01-15" }
|
|
1369
|
+
});
|
|
1027
1370
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1371
|
+
// Query by update date
|
|
1372
|
+
const recentlyUpdated = await events.list({
|
|
1373
|
+
partition: "byUpdatedDate",
|
|
1374
|
+
partitionValues: { updatedAt: "2024-01-15" }
|
|
1375
|
+
});
|
|
1030
1376
|
```
|
|
1031
1377
|
|
|
1032
|
-
|
|
1378
|
+
#### Partition Validation
|
|
1033
1379
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1380
|
+
```javascript
|
|
1381
|
+
// Partitions are automatically validated against attributes
|
|
1382
|
+
const users = await s3db.createResource({
|
|
1383
|
+
name: "users",
|
|
1384
|
+
attributes: {
|
|
1385
|
+
name: "string",
|
|
1386
|
+
email: "string",
|
|
1387
|
+
status: "string"
|
|
1388
|
+
},
|
|
1389
|
+
partitions: {
|
|
1390
|
+
byStatus: { fields: { status: "string" } }, // โ
Valid
|
|
1391
|
+
byEmail: { fields: { email: "string" } } // โ
Valid
|
|
1392
|
+
// byInvalid: { fields: { invalid: "string" } } // โ Would throw error
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
```
|
|
1041
1396
|
|
|
1042
|
-
###
|
|
1397
|
+
### ๐ฃ Advanced Hooks System
|
|
1043
1398
|
|
|
1044
|
-
|
|
1045
|
-
- Clear description of the problem
|
|
1046
|
-
- Steps to reproduce
|
|
1047
|
-
- Expected vs actual behavior
|
|
1048
|
-
- Your environment details
|
|
1399
|
+
Extend functionality with comprehensive hook system:
|
|
1049
1400
|
|
|
1050
|
-
|
|
1401
|
+
#### Hook Execution Order
|
|
1051
1402
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1403
|
+
```javascript
|
|
1404
|
+
const users = await s3db.createResource({
|
|
1405
|
+
name: "users",
|
|
1406
|
+
attributes: { name: "string", email: "string" },
|
|
1407
|
+
hooks: {
|
|
1408
|
+
preInsert: [
|
|
1409
|
+
async (data) => {
|
|
1410
|
+
console.log('1. Pre-insert hook 1');
|
|
1411
|
+
data.timestamp = new Date().toISOString();
|
|
1412
|
+
return data;
|
|
1413
|
+
},
|
|
1414
|
+
async (data) => {
|
|
1415
|
+
console.log('2. Pre-insert hook 2');
|
|
1416
|
+
data.processed = true;
|
|
1417
|
+
return data;
|
|
1418
|
+
}
|
|
1419
|
+
],
|
|
1420
|
+
afterInsert: [
|
|
1421
|
+
async (data) => {
|
|
1422
|
+
console.log('3. After-insert hook 1');
|
|
1423
|
+
await sendWelcomeEmail(data.email);
|
|
1424
|
+
},
|
|
1425
|
+
async (data) => {
|
|
1426
|
+
console.log('4. After-insert hook 2');
|
|
1427
|
+
await updateAnalytics(data);
|
|
1428
|
+
}
|
|
1429
|
+
]
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1056
1432
|
|
|
1057
|
-
|
|
1433
|
+
// Execution order: preInsert hooks โ insert โ afterInsert hooks
|
|
1434
|
+
```
|
|
1058
1435
|
|
|
1059
|
-
|
|
1436
|
+
#### Version-Specific Hooks
|
|
1060
1437
|
|
|
1061
|
-
|
|
1438
|
+
```javascript
|
|
1439
|
+
// Hooks that respond to version changes
|
|
1440
|
+
const users = await s3db.createResource({
|
|
1441
|
+
name: "users",
|
|
1442
|
+
attributes: { name: "string", email: "string" },
|
|
1443
|
+
versioningEnabled: true,
|
|
1444
|
+
hooks: {
|
|
1445
|
+
preInsert: [
|
|
1446
|
+
async (data) => {
|
|
1447
|
+
// Access resource context
|
|
1448
|
+
console.log('Current version:', this.version);
|
|
1449
|
+
return data;
|
|
1450
|
+
}
|
|
1451
|
+
]
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1062
1454
|
|
|
1063
|
-
|
|
1455
|
+
// Listen for version updates
|
|
1456
|
+
users.on('versionUpdated', ({ oldVersion, newVersion }) => {
|
|
1457
|
+
console.log(`Resource updated from ${oldVersion} to ${newVersion}`);
|
|
1458
|
+
});
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
#### Error Handling in Hooks
|
|
1462
|
+
|
|
1463
|
+
```javascript
|
|
1464
|
+
const users = await s3db.createResource({
|
|
1465
|
+
name: "users",
|
|
1466
|
+
attributes: { name: "string", email: "string" },
|
|
1467
|
+
hooks: {
|
|
1468
|
+
preInsert: [
|
|
1469
|
+
async (data) => {
|
|
1470
|
+
try {
|
|
1471
|
+
// Validate external service
|
|
1472
|
+
await validateEmail(data.email);
|
|
1473
|
+
return data;
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
// Transform error or add context
|
|
1476
|
+
throw new Error(`Email validation failed: ${error.message}`);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
],
|
|
1480
|
+
afterInsert: [
|
|
1481
|
+
async (data) => {
|
|
1482
|
+
try {
|
|
1483
|
+
await sendWelcomeEmail(data.email);
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
// Log but don't fail the operation
|
|
1486
|
+
console.error('Failed to send welcome email:', error);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
]
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
#### Hook Context and Binding
|
|
1495
|
+
|
|
1496
|
+
```javascript
|
|
1497
|
+
const users = await s3db.createResource({
|
|
1498
|
+
name: "users",
|
|
1499
|
+
attributes: { name: "string", email: "string" },
|
|
1500
|
+
hooks: {
|
|
1501
|
+
preInsert: [
|
|
1502
|
+
async function(data) {
|
|
1503
|
+
// 'this' is bound to the resource instance
|
|
1504
|
+
console.log('Resource name:', this.name);
|
|
1505
|
+
console.log('Resource version:', this.version);
|
|
1506
|
+
|
|
1507
|
+
// Access resource methods
|
|
1508
|
+
const exists = await this.exists(data.id);
|
|
1509
|
+
if (exists) {
|
|
1510
|
+
throw new Error('User already exists');
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
return data;
|
|
1514
|
+
}
|
|
1515
|
+
]
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
```
|
|
1064
1519
|
|
|
1065
1520
|
---
|
|
1066
1521
|
|
|
1067
|
-
|
|
1068
|
-
<strong>Made with โค๏ธ by developers, for developers</strong><br>
|
|
1069
|
-
<a href="https://github.com/forattini-dev/s3db.js">โญ Star us on GitHub</a> โข
|
|
1070
|
-
<a href="https://www.npmjs.com/package/s3db.js">๐ฆ View on NPM</a> โข
|
|
1071
|
-
<a href="https://github.com/forattini-dev/s3db.js/issues">๐ Report Issues</a>
|
|
1072
|
-
</p>
|
|
1522
|
+
## ๐ API Reference
|
|
1073
1523
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1524
|
+
### ๐ Database Operations
|
|
1525
|
+
|
|
1526
|
+
| Method | Description | Example |
|
|
1527
|
+
|--------|-------------|---------|
|
|
1528
|
+
| `connect()` | Connect to database | `await s3db.connect()` |
|
|
1529
|
+
| `createResource(config)` | Create new resource | `await s3db.createResource({...})` |
|
|
1530
|
+
| `resource(name)` | Get resource reference | `const users = s3db.resource("users")` |
|
|
1531
|
+
| `resourceExists(name)` | Check if resource exists | `s3db.resourceExists("users")` |
|
|
1532
|
+
|
|
1533
|
+
### ๐ Resource Operations
|
|
1534
|
+
|
|
1535
|
+
| Method | Description | Example |
|
|
1536
|
+
|--------|-------------|---------|
|
|
1537
|
+
| `insert(data)` | Create document | `await users.insert({name: "John"})` |
|
|
1538
|
+
| `get(id)` | Retrieve document | `await users.get("user-123")` |
|
|
1539
|
+
| `update(id, data)` | Update document | `await users.update("user-123", {age: 31})` |
|
|
1540
|
+
| `upsert(id, data)` | Insert or update | `await users.upsert("user-123", {...})` |
|
|
1541
|
+
| `delete(id)` | Delete document | `await users.delete("user-123")` |
|
|
1542
|
+
| `exists(id)` | Check existence | `await users.exists("user-123")` |
|
|
1543
|
+
| `setContent({id, buffer, contentType})` | Set binary content | `await users.setContent({id: "123", buffer: imageBuffer})` |
|
|
1544
|
+
| `content(id)` | Get binary content | `await users.content("user-123")` |
|
|
1545
|
+
| `hasContent(id)` | Check if has content | `await users.hasContent("user-123")` |
|
|
1546
|
+
| `deleteContent(id)` | Remove content | `await users.deleteContent("user-123")` |
|
|
1547
|
+
|
|
1548
|
+
### ๐ Query Operations
|
|
1549
|
+
|
|
1550
|
+
| Method | Description | Example |
|
|
1551
|
+
|--------|-------------|---------|
|
|
1552
|
+
| `list(options?)` | List documents with pagination & partitions | `await users.list({limit: 10, offset: 0})` |
|
|
1553
|
+
| `listIds(options?)` | List document IDs | `await users.listIds()` |
|
|
1554
|
+
| `count(options?)` | Count documents | `await users.count()` |
|
|
1555
|
+
| `page(options)` | Paginate results | `await users.page({offset: 0, size: 10})` |
|
|
1556
|
+
| `query(filter, options?)` | Filter documents | `await users.query({isActive: true})` |
|
|
1557
|
+
|
|
1558
|
+
#### ๐ List vs GetAll - When to Use Each
|
|
1559
|
+
|
|
1560
|
+
**`list(options?)`** - Advanced listing with full control:
|
|
1561
|
+
```javascript
|
|
1562
|
+
// Simple listing (equivalent to getAll)
|
|
1563
|
+
const allUsers = await users.list();
|
|
1564
|
+
|
|
1565
|
+
// With pagination
|
|
1566
|
+
const first10 = await users.list({ limit: 10, offset: 0 });
|
|
1567
|
+
|
|
1568
|
+
// With partitions
|
|
1569
|
+
const usUsers = await users.list({
|
|
1570
|
+
partition: "byCountry",
|
|
1571
|
+
partitionValues: { "profile.country": "US" }
|
|
1572
|
+
});
|
|
1573
|
+
```
|
|
1574
|
+
|
|
1575
|
+
**`getAll()`** - Simple listing for all documents:
|
|
1576
|
+
```javascript
|
|
1577
|
+
// Get all documents (no options, no pagination)
|
|
1578
|
+
const allUsers = await users.getAll();
|
|
1579
|
+
console.log(`Total users: ${allUsers.length}`);
|
|
1580
|
+
```
|
|
1581
|
+
|
|
1582
|
+
**Choose `getAll()` when:**
|
|
1583
|
+
- โ
You want all documents without pagination
|
|
1584
|
+
- โ
You don't need partition filtering
|
|
1585
|
+
- โ
You prefer simplicity over flexibility
|
|
1586
|
+
|
|
1587
|
+
**Choose `list()` when:**
|
|
1588
|
+
- โ
You need pagination control
|
|
1589
|
+
- โ
You want to filter by partitions
|
|
1590
|
+
- โ
You need more control over the query
|
|
1591
|
+
|
|
1592
|
+
### ๐ Bulk Operations
|
|
1593
|
+
|
|
1594
|
+
| Method | Description | Example |
|
|
1595
|
+
|--------|-------------|---------|
|
|
1596
|
+
| `insertMany(docs)` | Insert multiple | `await users.insertMany([{...}, {...}])` |
|
|
1597
|
+
| `getMany(ids)` | Get multiple | `await users.getMany(["id1", "id2"])` |
|
|
1598
|
+
| `deleteMany(ids)` | Delete multiple | `await users.deleteMany(["id1", "id2"])` |
|
|
1599
|
+
| `getAll()` | Get all documents | `await users.getAll()` |
|
|
1600
|
+
| `deleteAll()` | Delete all documents | `await users.deleteAll()` |
|
|
1601
|
+
|
|
1602
|
+
### ๐ Streaming Operations
|
|
1603
|
+
|
|
1604
|
+
| Method | Description | Example |
|
|
1605
|
+
|--------|-------------|---------|
|
|
1606
|
+
| `
|