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