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 +1815 -97
- package/dist/s3db.cjs.js +67 -6
- package/dist/s3db.cjs.min.js +6 -6
- package/dist/s3db.es.js +68 -7
- package/dist/s3db.es.min.js +6 -6
- package/dist/s3db.iife.js +67 -6
- package/dist/s3db.iife.min.js +8 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,150 +1,1868 @@
|
|
|
1
1
|
# s3db.js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](http://unlicense.org/) [](https://www.npmjs.com/package/s3db.js) [](https://codeclimate.com/github/forattini-dev/s3db.js/maintainability) [](https://coveralls.io/github/forattini-dev/s3db.js?branch=main)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
216
|
+
### Advanced Configuration
|
|
14
217
|
|
|
15
218
|
```javascript
|
|
16
|
-
import
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
245
|
+
### 2. Resources (Collections)
|
|
26
246
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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:
|
|
38
|
-
email:
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
1168
|
+
## ๐ Security & Encryption
|
|
1169
|
+
|
|
1170
|
+
### Field-Level Encryption
|
|
1171
|
+
|
|
1172
|
+
Use the `"secret"` type for sensitive data:
|
|
48
1173
|
|
|
49
1174
|
```javascript
|
|
50
|
-
const
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
###
|
|
1200
|
+
### Custom Encryption Key
|
|
61
1201
|
|
|
62
|
-
```
|
|
63
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
##
|
|
1211
|
+
## ๐ฐ Cost Analysis
|
|
87
1212
|
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
1220
|
+
### Cost Examples
|
|
100
1221
|
|
|
101
|
-
|
|
1222
|
+
#### Small Application (1,000 users)
|
|
102
1223
|
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1347
|
+
Partitions are automatically created when you insert documents, and you can query them using specific methods that accept partition parameters:
|
|
122
1348
|
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
####
|
|
1365
|
+
#### Count documents by partition
|
|
134
1366
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1865
|
+
### Performance Tips
|
|
149
1866
|
|
|
150
|
-
|
|
1867
|
+
1. **Enable Caching**: Use `cache: true` for frequently accessed data
|
|
1868
|
+
2. **Adjust Parallelism**: Increase `parallelism`
|