s3db.js 4.1.8 โ 4.1.11
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 +786 -1706
- package/dist/s3db.cjs.js +549 -125
- package/dist/s3db.cjs.min.js +14 -12
- package/dist/s3db.es.js +537 -126
- package/dist/s3db.es.min.js +12 -10
- package/dist/s3db.iife.js +549 -125
- package/dist/s3db.iife.min.js +14 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,28 +1,139 @@
|
|
|
1
|
-
# s3db.js
|
|
1
|
+
# ๐๏ธ s3db.js
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img width="200" src="https://img.icons8.com/fluency/200/database.png" alt="s3db.js">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Transform AWS S3 into a powerful document database</strong><br>
|
|
9
|
+
<em>Zero-cost storage โข Automatic encryption โข ORM-like interface โข Streaming API</em>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/s3db.js"><img src="https://img.shields.io/npm/v/s3db.js.svg?style=flat&color=brightgreen" alt="npm version"></a>
|
|
14
|
+
|
|
15
|
+
<a href="https://github.com/forattini-dev/s3db.js"><img src="https://img.shields.io/github/stars/forattini-dev/s3db.js?style=flat&color=yellow" alt="GitHub stars"></a>
|
|
16
|
+
|
|
17
|
+
<a href="https://github.com/forattini-dev/s3db.js/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Unlicense-blue.svg?style=flat" alt="License"></a>
|
|
18
|
+
|
|
19
|
+
<a href="https://api.codeclimate.com/v1/badges/26e3dc46c42367d44f18/maintainability"><img src="https://api.codeclimate.com/v1/badges/26e3dc46c42367d44f18/maintainability" alt="Maintainability"></a>
|
|
20
|
+
|
|
21
|
+
<a href="https://coveralls.io/github/forattini-dev/s3db.js?branch=main"><img src="https://coveralls.io/repos/github/forattini-dev/s3db.js/badge.svg?branch=main&style=flat" alt="Coverage Status"></a>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<a href="https://github.com/forattini-dev/s3db.js"><img src="https://img.shields.io/badge/Built_with-Node.js-339933.svg?style=flat&logo=node.js" alt="Built with Node.js"></a>
|
|
26
|
+
|
|
27
|
+
<a href="https://aws.amazon.com/s3/"><img src="https://img.shields.io/badge/Powered_by-AWS_S3-FF9900.svg?style=flat&logo=amazon-aws" alt="Powered by AWS S3"></a>
|
|
28
|
+
|
|
29
|
+
<a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Runtime-Node.js-339933.svg?style=flat&logo=node.js" alt="Node.js Runtime"></a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<br>
|
|
33
|
+
|
|
34
|
+
## ๐ What is s3db.js?
|
|
35
|
+
|
|
36
|
+
**s3db.js** is a revolutionary document database that transforms AWS S3 into a fully functional database using S3's metadata capabilities. Instead of traditional storage methods, it stores document data in S3's metadata fields (up to 2KB), making it incredibly cost-effective while providing a familiar ORM-like interface.
|
|
37
|
+
|
|
38
|
+
**Perfect for:**
|
|
39
|
+
- ๐ **Serverless applications** - No database servers to manage
|
|
40
|
+
- ๐ฐ **Cost-conscious projects** - Pay only for what you use
|
|
41
|
+
- ๐ **Secure applications** - Built-in encryption and validation
|
|
42
|
+
- ๐ **Analytics platforms** - Efficient data streaming and processing
|
|
43
|
+
- ๐ **Rapid prototyping** - Get started in minutes, not hours
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## โจ Key Features
|
|
48
|
+
|
|
49
|
+
<table>
|
|
50
|
+
<tr>
|
|
51
|
+
<td width="50%">
|
|
52
|
+
|
|
53
|
+
### ๐ฏ **Database Operations**
|
|
54
|
+
- **ORM-like Interface** - Familiar CRUD operations
|
|
55
|
+
- **Schema Validation** - Automatic data validation
|
|
56
|
+
- **Streaming API** - Handle large datasets efficiently
|
|
57
|
+
- **Event System** - Real-time notifications
|
|
58
|
+
|
|
59
|
+
</td>
|
|
60
|
+
<td width="50%">
|
|
61
|
+
|
|
62
|
+
### ๐ **Security & Performance**
|
|
63
|
+
- **Field-level Encryption** - Secure sensitive data
|
|
64
|
+
- **Intelligent Caching** - Reduce API calls
|
|
65
|
+
- **Auto-generated Passwords** - Secure by default
|
|
66
|
+
- **Cost Tracking** - Monitor AWS expenses
|
|
67
|
+
|
|
68
|
+
</td>
|
|
69
|
+
</tr>
|
|
70
|
+
<tr>
|
|
71
|
+
<td width="50%">
|
|
72
|
+
|
|
73
|
+
### ๐ฆ **Data Management**
|
|
74
|
+
- **Partitions** - Organize data efficiently
|
|
75
|
+
- **Bulk Operations** - Handle multiple records
|
|
76
|
+
- **Nested Objects** - Complex data structures
|
|
77
|
+
- **Automatic Timestamps** - Track changes
|
|
78
|
+
|
|
79
|
+
</td>
|
|
80
|
+
<td width="50%">
|
|
81
|
+
|
|
82
|
+
### ๐ง **Extensibility**
|
|
83
|
+
- **Custom Behaviors** - Handle large documents
|
|
84
|
+
- **Hooks System** - Custom business logic
|
|
85
|
+
- **Plugin Architecture** - Extend functionality
|
|
86
|
+
- **Event System** - Real-time notifications
|
|
87
|
+
|
|
88
|
+
</td>
|
|
89
|
+
</tr>
|
|
90
|
+
</table>
|
|
91
|
+
|
|
92
|
+
---
|
|
2
93
|
|
|
3
|
-
|
|
94
|
+
## ๐ Table of Contents
|
|
4
95
|
|
|
5
|
-
|
|
96
|
+
- [๐ Quick Start](#-quick-start)
|
|
97
|
+
- [๐พ Installation](#-installation)
|
|
98
|
+
- [๐ฏ Core Concepts](#-core-concepts)
|
|
99
|
+
- [โก Advanced Features](#-advanced-features)
|
|
100
|
+
- [๐ API Reference](#-api-reference)
|
|
101
|
+
- [๐จ Examples](#-examples)
|
|
102
|
+
- [๐ Security](#-security)
|
|
103
|
+
- [๐ฐ Cost Analysis](#-cost-analysis)
|
|
104
|
+
- [๐จ Best Practices](#-best-practices)
|
|
105
|
+
- [๐งช Testing](#-testing)
|
|
106
|
+
- [๐ค Contributing](#-contributing)
|
|
107
|
+
- [๐ License](#-license)
|
|
6
108
|
|
|
7
|
-
|
|
109
|
+
---
|
|
8
110
|
|
|
9
111
|
## ๐ Quick Start
|
|
10
112
|
|
|
113
|
+
Get up and running in less than 5 minutes!
|
|
114
|
+
|
|
115
|
+
### 1. Install s3db.js
|
|
116
|
+
|
|
11
117
|
```bash
|
|
12
|
-
npm
|
|
118
|
+
npm install s3db.js
|
|
13
119
|
```
|
|
14
120
|
|
|
121
|
+
### 2. Connect to your S3 database
|
|
122
|
+
|
|
15
123
|
```javascript
|
|
16
124
|
import { S3db } from "s3db.js";
|
|
17
125
|
|
|
18
|
-
// Connect to your S3 database
|
|
19
126
|
const s3db = new S3db({
|
|
20
127
|
uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
|
|
21
128
|
});
|
|
22
129
|
|
|
23
130
|
await s3db.connect();
|
|
131
|
+
console.log("๐ Connected to S3 database!");
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### 3. Create your first resource
|
|
24
135
|
|
|
25
|
-
|
|
136
|
+
```javascript
|
|
26
137
|
const users = await s3db.createResource({
|
|
27
138
|
name: "users",
|
|
28
139
|
attributes: {
|
|
@@ -31,10 +142,29 @@ const users = await s3db.createResource({
|
|
|
31
142
|
age: "number|integer|positive",
|
|
32
143
|
isActive: "boolean",
|
|
33
144
|
createdAt: "date"
|
|
145
|
+
},
|
|
146
|
+
timestamps: true,
|
|
147
|
+
behavior: "user-management",
|
|
148
|
+
partitions: {
|
|
149
|
+
byRegion: { fields: { region: "string" } }
|
|
150
|
+
},
|
|
151
|
+
paranoid: true,
|
|
152
|
+
autoDecrypt: true,
|
|
153
|
+
cache: false,
|
|
154
|
+
parallelism: 10,
|
|
155
|
+
hooks: {
|
|
156
|
+
preInsert: [async (data) => {
|
|
157
|
+
console.log("Pre-insert:", data);
|
|
158
|
+
return data;
|
|
159
|
+
}]
|
|
34
160
|
}
|
|
35
161
|
});
|
|
162
|
+
```
|
|
36
163
|
|
|
37
|
-
|
|
164
|
+
### 4. Start storing data
|
|
165
|
+
|
|
166
|
+
```javascript
|
|
167
|
+
// Insert a user
|
|
38
168
|
const user = await users.insert({
|
|
39
169
|
name: "John Doe",
|
|
40
170
|
email: "john@example.com",
|
|
@@ -43,144 +173,74 @@ const user = await users.insert({
|
|
|
43
173
|
createdAt: new Date()
|
|
44
174
|
});
|
|
45
175
|
|
|
46
|
-
// Query
|
|
176
|
+
// Query the user
|
|
47
177
|
const foundUser = await users.get(user.id);
|
|
48
|
-
console.log(foundUser.name);
|
|
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)
|
|
178
|
+
console.log(`Hello, ${foundUser.name}! ๐`);
|
|
67
179
|
|
|
68
|
-
|
|
180
|
+
// Update the user
|
|
181
|
+
await users.update(user.id, { age: 31 });
|
|
69
182
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
183
|
+
// List all users
|
|
184
|
+
const allUsers = await users.list();
|
|
185
|
+
console.log(`Total users: ${allUsers.length}`);
|
|
186
|
+
```
|
|
87
187
|
|
|
88
|
-
|
|
188
|
+
**That's it!** You now have a fully functional document database running on AWS S3. ๐
|
|
89
189
|
|
|
90
|
-
|
|
190
|
+
---
|
|
91
191
|
|
|
92
|
-
|
|
93
|
-
- **Metadata**: Up to 2KB of UTF-8 encoded data
|
|
192
|
+
## ๐พ Installation
|
|
94
193
|
|
|
95
|
-
|
|
194
|
+
### Package Manager
|
|
96
195
|
|
|
97
|
-
|
|
196
|
+
```bash
|
|
197
|
+
# npm
|
|
198
|
+
npm install s3db.js
|
|
98
199
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
id: "user-123",
|
|
103
|
-
name: "John Doe",
|
|
104
|
-
email: "john@example.com",
|
|
105
|
-
age: 30
|
|
106
|
-
}
|
|
200
|
+
# pnpm
|
|
201
|
+
pnpm add s3db.js
|
|
107
202
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Metadata: { "name": "John Doe", "email": "john@example.com", "age": "30", "id": "user-123" }
|
|
203
|
+
# yarn
|
|
204
|
+
yarn add s3db.js
|
|
111
205
|
```
|
|
112
206
|
|
|
113
|
-
|
|
207
|
+
### Environment Setup
|
|
114
208
|
|
|
115
|
-
|
|
209
|
+
Create a `.env` file with your AWS credentials:
|
|
116
210
|
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
yarn add s3db.js
|
|
211
|
+
```env
|
|
212
|
+
AWS_ACCESS_KEY_ID=your_access_key
|
|
213
|
+
AWS_SECRET_ACCESS_KEY=your_secret_key
|
|
214
|
+
AWS_BUCKET=your_bucket_name
|
|
215
|
+
DATABASE_NAME=myapp
|
|
123
216
|
```
|
|
124
217
|
|
|
125
|
-
|
|
218
|
+
Then initialize s3db.js:
|
|
126
219
|
|
|
127
220
|
```javascript
|
|
128
221
|
import { S3db } from "s3db.js";
|
|
222
|
+
import dotenv from "dotenv";
|
|
129
223
|
|
|
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
224
|
dotenv.config();
|
|
143
225
|
|
|
144
|
-
import { S3db } from "s3db.js";
|
|
145
|
-
|
|
146
226
|
const s3db = new S3db({
|
|
147
227
|
uri: `s3://${process.env.AWS_ACCESS_KEY_ID}:${process.env.AWS_SECRET_ACCESS_KEY}@${process.env.AWS_BUCKET}/databases/${process.env.DATABASE_NAME}`
|
|
148
228
|
});
|
|
149
229
|
```
|
|
150
230
|
|
|
151
|
-
|
|
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
|
|
231
|
+
### Authentication Methods
|
|
165
232
|
|
|
166
|
-
|
|
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
|
|
233
|
+
<details>
|
|
234
|
+
<summary><strong>๐ Multiple authentication options</strong></summary>
|
|
175
235
|
|
|
236
|
+
#### 1. Access Keys (Development)
|
|
176
237
|
```javascript
|
|
177
238
|
const s3db = new S3db({
|
|
178
239
|
uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
|
|
179
240
|
});
|
|
180
241
|
```
|
|
181
242
|
|
|
182
|
-
#### 2.
|
|
183
|
-
|
|
243
|
+
#### 2. IAM Roles (Production - Recommended)
|
|
184
244
|
```javascript
|
|
185
245
|
// No credentials needed - uses IAM role permissions
|
|
186
246
|
const s3db = new S3db({
|
|
@@ -188,1157 +248,556 @@ const s3db = new S3db({
|
|
|
188
248
|
});
|
|
189
249
|
```
|
|
190
250
|
|
|
191
|
-
#### 3.
|
|
192
|
-
|
|
251
|
+
#### 3. S3-Compatible Services (MinIO, etc.)
|
|
193
252
|
```javascript
|
|
194
253
|
const s3db = new S3db({
|
|
195
254
|
uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
196
|
-
endpoint: "http://localhost:9000"
|
|
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
|
|
255
|
+
endpoint: "http://localhost:9000"
|
|
206
256
|
});
|
|
207
257
|
```
|
|
208
258
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
- **IAM Roles**: Use IAM roles instead of access keys when possible (EC2, EKS, Lambda)
|
|
212
|
-
- **Environment Variables**: Store credentials in environment variables, not in code
|
|
213
|
-
- **Bucket Permissions**: Ensure your IAM role/user has the necessary S3 permissions:
|
|
214
|
-
- `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject`, `s3:ListBucket`, `s3:GetBucketLocation`
|
|
215
|
-
|
|
216
|
-
### Advanced Configuration
|
|
217
|
-
|
|
218
|
-
```javascript
|
|
219
|
-
import fs from "fs";
|
|
220
|
-
|
|
221
|
-
const s3db = new S3db({
|
|
222
|
-
uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
223
|
-
parallelism: 25, // Handle 25 concurrent operations
|
|
224
|
-
passphrase: fs.readFileSync("./cert.pem"), // Custom encryption key
|
|
225
|
-
cache: true, // Enable caching
|
|
226
|
-
ttl: 3600, // 1 hour cache TTL
|
|
227
|
-
plugins: [CostsPlugin] // Enable cost tracking
|
|
228
|
-
});
|
|
229
|
-
```
|
|
259
|
+
</details>
|
|
230
260
|
|
|
231
|
-
|
|
261
|
+
---
|
|
232
262
|
|
|
233
|
-
|
|
263
|
+
## ๐ฏ Core Concepts
|
|
234
264
|
|
|
235
|
-
|
|
265
|
+
### ๐๏ธ Database
|
|
266
|
+
A logical container for your resources, stored in a specific S3 prefix.
|
|
236
267
|
|
|
237
268
|
```javascript
|
|
238
|
-
// This creates/connects to a database at:
|
|
239
|
-
// s3://bucket/databases/myapp/
|
|
240
269
|
const s3db = new S3db({
|
|
241
270
|
uri: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
|
|
242
271
|
});
|
|
272
|
+
// Creates/connects to: s3://bucket/databases/myapp/
|
|
243
273
|
```
|
|
244
274
|
|
|
245
|
-
###
|
|
275
|
+
### ๐ Resources (Collections)
|
|
276
|
+
Resources define the structure of your documents, similar to tables in traditional databases.
|
|
277
|
+
|
|
278
|
+
#### New Configuration Structure
|
|
246
279
|
|
|
247
|
-
|
|
280
|
+
The Resource class now uses a unified configuration object where all options are passed directly in the config object:
|
|
248
281
|
|
|
249
282
|
```javascript
|
|
250
283
|
const users = await s3db.createResource({
|
|
251
284
|
name: "users",
|
|
252
285
|
attributes: {
|
|
286
|
+
// Basic types
|
|
253
287
|
name: "string|min:2|max:100",
|
|
254
288
|
email: "email|unique",
|
|
255
289
|
age: "number|integer|positive",
|
|
290
|
+
isActive: "boolean",
|
|
291
|
+
|
|
292
|
+
// Nested objects
|
|
256
293
|
profile: {
|
|
257
294
|
bio: "string|optional",
|
|
258
|
-
avatar: "url|optional"
|
|
295
|
+
avatar: "url|optional",
|
|
296
|
+
preferences: {
|
|
297
|
+
theme: "string|enum:light,dark|default:light",
|
|
298
|
+
notifications: "boolean|default:true"
|
|
299
|
+
}
|
|
259
300
|
},
|
|
260
|
-
|
|
261
|
-
|
|
301
|
+
|
|
302
|
+
// Arrays
|
|
303
|
+
tags: "array|items:string|unique",
|
|
304
|
+
|
|
305
|
+
// Encrypted fields
|
|
306
|
+
password: "secret"
|
|
307
|
+
},
|
|
308
|
+
// All options are now at the root level
|
|
309
|
+
timestamps: true, // Automatic createdAt/updatedAt
|
|
310
|
+
behavior: "user-management", // How to handle large documents
|
|
311
|
+
partitions: { // Organize data for efficient queries
|
|
312
|
+
byRegion: { fields: { region: "string" } }
|
|
313
|
+
},
|
|
314
|
+
paranoid: true, // Security flag for dangerous operations
|
|
315
|
+
autoDecrypt: true, // Auto-decrypt secret fields
|
|
316
|
+
cache: false, // Enable caching
|
|
317
|
+
parallelism: 10, // Parallelism for bulk operations
|
|
318
|
+
hooks: { // Custom hooks
|
|
319
|
+
preInsert: [async (data) => {
|
|
320
|
+
console.log("Pre-insert:", data);
|
|
321
|
+
return data;
|
|
322
|
+
}]
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### ๐ Schema Validation
|
|
328
|
+
Built-in validation using [@icebob/fastest-validator](https://github.com/icebob/fastest-validator) for resource creation and partition validation. This powerful validation engine provides comprehensive rule support, excellent performance, and detailed error reporting for all your data validation needs.
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
const product = await products.insert({
|
|
332
|
+
name: "Wireless Headphones",
|
|
333
|
+
price: 99.99,
|
|
334
|
+
category: "electronics",
|
|
335
|
+
features: ["bluetooth", "noise-cancellation"],
|
|
336
|
+
specifications: {
|
|
337
|
+
battery: "30 hours",
|
|
338
|
+
connectivity: "Bluetooth 5.0"
|
|
262
339
|
}
|
|
263
340
|
});
|
|
264
341
|
```
|
|
265
342
|
|
|
266
|
-
|
|
343
|
+
**Validation Features powered by fastest-validator:**
|
|
344
|
+
- โ
**Comprehensive Rules** - String, number, array, object, date validation
|
|
345
|
+
- โ
**Nested Objects** - Deep validation for complex data structures
|
|
346
|
+
- โ
**Custom Rules** - Extend with your own validation logic
|
|
347
|
+
- โ
**Performance** - Optimized validation engine for speed
|
|
348
|
+
- โ
**Error Messages** - Detailed validation error reporting
|
|
267
349
|
|
|
268
|
-
|
|
350
|
+
---
|
|
269
351
|
|
|
270
|
-
|
|
271
|
-
// Listar todos os usuรกrios
|
|
272
|
-
const allUsers = await users.list();
|
|
352
|
+
## โก Advanced Features
|
|
273
353
|
|
|
274
|
-
|
|
275
|
-
const googleUsers = await users.list({
|
|
276
|
-
partition: 'byUtmSource',
|
|
277
|
-
partitionValues: { 'utm.source': 'google' }
|
|
278
|
-
});
|
|
354
|
+
s3db.js leverages [@icebob/fastest-validator](https://github.com/icebob/fastest-validator) as its core validation engine for both resource schemas and partition field validation, ensuring high-performance data validation with comprehensive rule support.
|
|
279
355
|
|
|
280
|
-
|
|
281
|
-
const googleUserIds = await users.listIds({
|
|
282
|
-
partition: 'byUtmSource',
|
|
283
|
-
partitionValues: { 'utm.source': 'google' }
|
|
284
|
-
});
|
|
356
|
+
### ๐ฆ Partitions
|
|
285
357
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
358
|
+
Organize data efficiently with partitions for faster queries:
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
const analytics = await s3db.createResource({
|
|
362
|
+
name: "analytics",
|
|
363
|
+
attributes: {
|
|
364
|
+
userId: "string",
|
|
365
|
+
event: "string",
|
|
366
|
+
timestamp: "date",
|
|
367
|
+
utm: {
|
|
368
|
+
source: "string",
|
|
369
|
+
medium: "string",
|
|
370
|
+
campaign: "string"
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
partitions: {
|
|
374
|
+
byDate: { fields: { timestamp: "date|maxlength:10" } },
|
|
375
|
+
byUtmSource: { fields: { "utm.source": "string" } },
|
|
376
|
+
byUserAndDate: {
|
|
377
|
+
fields: {
|
|
378
|
+
userId: "string",
|
|
379
|
+
timestamp: "date|maxlength:10"
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
304
383
|
});
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const count = await users.count({
|
|
311
|
-
partition: 'byUtmSource',
|
|
312
|
-
partitionValues: { 'utm.source': 'google' }
|
|
384
|
+
|
|
385
|
+
// Query by partition for better performance
|
|
386
|
+
const googleEvents = await analytics.list({
|
|
387
|
+
partition: "byUtmSource",
|
|
388
|
+
partitionValues: { "utm.source": "google" }
|
|
313
389
|
});
|
|
314
390
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
{
|
|
318
|
-
|
|
319
|
-
);
|
|
391
|
+
const todayEvents = await analytics.count({
|
|
392
|
+
partition: "byDate",
|
|
393
|
+
partitionValues: { timestamp: "2024-01-15" }
|
|
394
|
+
});
|
|
320
395
|
```
|
|
321
396
|
|
|
322
|
-
|
|
397
|
+
### ๐ฃ Hooks System
|
|
323
398
|
|
|
324
|
-
|
|
399
|
+
Add custom logic with pre/post operation hooks:
|
|
325
400
|
|
|
326
|
-
```
|
|
327
|
-
const
|
|
328
|
-
name:
|
|
401
|
+
```javascript
|
|
402
|
+
const products = await s3db.createResource({
|
|
403
|
+
name: "products",
|
|
329
404
|
attributes: {
|
|
330
|
-
name:
|
|
331
|
-
|
|
332
|
-
|
|
405
|
+
name: "string",
|
|
406
|
+
price: "number",
|
|
407
|
+
category: "string"
|
|
333
408
|
},
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
409
|
+
hooks: {
|
|
410
|
+
preInsert: [
|
|
411
|
+
async (data) => {
|
|
412
|
+
// Auto-generate SKU
|
|
413
|
+
data.sku = `${data.category.toUpperCase()}-${Date.now()}`;
|
|
414
|
+
return data;
|
|
415
|
+
}
|
|
416
|
+
],
|
|
417
|
+
afterInsert: [
|
|
418
|
+
async (data) => {
|
|
419
|
+
console.log(`๐ฆ Product ${data.name} created with SKU: ${data.sku}`);
|
|
420
|
+
// Send notification, update cache, etc.
|
|
421
|
+
}
|
|
422
|
+
],
|
|
423
|
+
preUpdate: [
|
|
424
|
+
async (id, data) => {
|
|
425
|
+
// Log price changes
|
|
426
|
+
if (data.price) {
|
|
427
|
+
console.log(`๐ฐ Price update for ${id}: $${data.price}`);
|
|
428
|
+
}
|
|
429
|
+
return data;
|
|
430
|
+
}
|
|
431
|
+
]
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
// Optional: Security settings (default: true)
|
|
435
|
+
paranoid: true,
|
|
436
|
+
|
|
437
|
+
// Optional: Schema options (default: false)
|
|
438
|
+
allNestedObjectsOptional: false,
|
|
439
|
+
|
|
440
|
+
// Optional: Encryption settings (default: true)
|
|
441
|
+
autoDecrypt: true,
|
|
442
|
+
|
|
443
|
+
// Optional: Caching (default: false)
|
|
444
|
+
cache: false
|
|
340
445
|
});
|
|
446
|
+
```
|
|
341
447
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
448
|
+
### ๐ Streaming API
|
|
449
|
+
|
|
450
|
+
Handle large datasets efficiently with streams:
|
|
451
|
+
|
|
452
|
+
```javascript
|
|
453
|
+
// Export all users to CSV
|
|
454
|
+
const readableStream = await users.readable();
|
|
455
|
+
const csvWriter = createObjectCsvWriter({
|
|
456
|
+
path: "users_export.csv",
|
|
457
|
+
header: [
|
|
458
|
+
{ id: "id", title: "ID" },
|
|
459
|
+
{ id: "name", title: "Name" },
|
|
460
|
+
{ id: "email", title: "Email" }
|
|
461
|
+
]
|
|
346
462
|
});
|
|
347
|
-
```
|
|
348
463
|
|
|
349
|
-
|
|
464
|
+
const records = [];
|
|
465
|
+
readableStream.on("data", (user) => {
|
|
466
|
+
records.push(user);
|
|
467
|
+
});
|
|
350
468
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
partitionName: 'byUtmSource',
|
|
355
|
-
id: 'user-123',
|
|
356
|
-
data: { utm: { source: 'google' } }
|
|
469
|
+
readableStream.on("end", async () => {
|
|
470
|
+
await csvWriter.writeRecords(records);
|
|
471
|
+
console.log("โ
Export completed: users_export.csv");
|
|
357
472
|
});
|
|
358
473
|
|
|
359
|
-
//
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
partitionValues: { 'utm.source': 'google' }
|
|
474
|
+
// Bulk import from stream
|
|
475
|
+
const writableStream = await users.writable();
|
|
476
|
+
importData.forEach(userData => {
|
|
477
|
+
writableStream.write(userData);
|
|
364
478
|
});
|
|
479
|
+
writableStream.end();
|
|
365
480
|
```
|
|
366
481
|
|
|
367
|
-
|
|
482
|
+
### ๐ก๏ธ Document Behaviors
|
|
368
483
|
|
|
369
|
-
|
|
484
|
+
Handle documents that exceed S3's 2KB metadata limit:
|
|
370
485
|
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
486
|
+
```javascript
|
|
487
|
+
// Preserve all data by storing overflow in S3 body
|
|
488
|
+
const blogs = await s3db.createResource({
|
|
489
|
+
name: "blogs",
|
|
490
|
+
attributes: {
|
|
491
|
+
title: "string",
|
|
492
|
+
content: "string", // Can be very large
|
|
493
|
+
author: "string"
|
|
494
|
+
},
|
|
495
|
+
behavior: "body-overflow" // Handles large content automatically
|
|
376
496
|
});
|
|
377
497
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
498
|
+
// Strict validation - throws error if limit exceeded
|
|
499
|
+
const settings = await s3db.createResource({
|
|
500
|
+
name: "settings",
|
|
501
|
+
attributes: {
|
|
502
|
+
key: "string",
|
|
503
|
+
value: "string"
|
|
504
|
+
},
|
|
505
|
+
behavior: "enforce-limits" // Ensures data stays within 2KB
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Smart truncation - preserves structure, truncates content
|
|
509
|
+
const summaries = await s3db.createResource({
|
|
510
|
+
name: "summaries",
|
|
511
|
+
attributes: {
|
|
512
|
+
title: "string",
|
|
513
|
+
description: "string"
|
|
514
|
+
},
|
|
515
|
+
behavior: "data-truncate" // Truncates to fit within limits
|
|
516
|
+
});
|
|
381
517
|
```
|
|
382
518
|
|
|
383
|
-
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## ๐ API Reference
|
|
384
522
|
|
|
385
|
-
|
|
523
|
+
### ๐ Database Operations
|
|
386
524
|
|
|
387
|
-
|
|
525
|
+
| Method | Description | Example |
|
|
526
|
+
|--------|-------------|---------|
|
|
527
|
+
| `connect()` | Connect to database | `await s3db.connect()` |
|
|
528
|
+
| `createResource(config)` | Create new resource | `await s3db.createResource({...})` |
|
|
529
|
+
| `resource(name)` | Get resource reference | `const users = s3db.resource("users")` |
|
|
530
|
+
| `resourceExists(name)` | Check if resource exists | `s3db.resourceExists("users")` |
|
|
388
531
|
|
|
389
|
-
|
|
390
|
-
|----------|-------------|----------|
|
|
391
|
-
| `user-management` | **Default** - Emits warnings but allows operations | Development and testing |
|
|
392
|
-
| `enforce-limits` | Throws errors when limit is exceeded | Strict data size control |
|
|
393
|
-
| `data-truncate` | Truncates data to fit within limits | Preserve structure, lose data |
|
|
394
|
-
| `body-overflow` | Stores excess data in S3 object body | Preserve all data |
|
|
532
|
+
### ๐ Resource Operations
|
|
395
533
|
|
|
396
|
-
|
|
534
|
+
| Method | Description | Example |
|
|
535
|
+
|--------|-------------|---------|
|
|
536
|
+
| `insert(data)` | Create document | `await users.insert({name: "John"})` |
|
|
537
|
+
| `get(id)` | Retrieve document | `await users.get("user-123")` |
|
|
538
|
+
| `update(id, data)` | Update document | `await users.update("user-123", {age: 31})` |
|
|
539
|
+
| `upsert(id, data)` | Insert or update | `await users.upsert("user-123", {...})` |
|
|
540
|
+
| `delete(id)` | Delete document | `await users.delete("user-123")` |
|
|
541
|
+
| `exists(id)` | Check existence | `await users.exists("user-123")` |
|
|
542
|
+
|
|
543
|
+
### ๐ Query Operations
|
|
544
|
+
|
|
545
|
+
| Method | Description | Example |
|
|
546
|
+
|--------|-------------|---------|
|
|
547
|
+
| `list(options?)` | List documents | `await users.list()` |
|
|
548
|
+
| `listIds(options?)` | List document IDs | `await users.listIds()` |
|
|
549
|
+
| `count(options?)` | Count documents | `await users.count()` |
|
|
550
|
+
| `page(options)` | Paginate results | `await users.page({offset: 0, size: 10})` |
|
|
551
|
+
| `query(filter, options?)` | Filter documents | `await users.query({isActive: true})` |
|
|
552
|
+
|
|
553
|
+
### ๐ Bulk Operations
|
|
554
|
+
|
|
555
|
+
| Method | Description | Example |
|
|
556
|
+
|--------|-------------|---------|
|
|
557
|
+
| `insertMany(docs)` | Insert multiple | `await users.insertMany([{...}, {...}])` |
|
|
558
|
+
| `getMany(ids)` | Get multiple | `await users.getMany(["id1", "id2"])` |
|
|
559
|
+
| `deleteMany(ids)` | Delete multiple | `await users.deleteMany(["id1", "id2"])` |
|
|
560
|
+
| `getAll()` | Get all documents | `await users.getAll()` |
|
|
561
|
+
| `deleteAll()` | Delete all documents | `await users.deleteAll()` |
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## ๐จ Examples
|
|
566
|
+
|
|
567
|
+
### ๐ Blog Platform
|
|
397
568
|
|
|
398
569
|
```javascript
|
|
399
|
-
|
|
400
|
-
|
|
570
|
+
// Create blog posts with body-overflow behavior for long content
|
|
571
|
+
const posts = await s3db.createResource({
|
|
572
|
+
name: "posts",
|
|
401
573
|
attributes: {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
574
|
+
title: "string|min:5|max:200",
|
|
575
|
+
content: "string",
|
|
576
|
+
author: "string",
|
|
577
|
+
tags: "array|items:string",
|
|
578
|
+
published: "boolean|default:false",
|
|
579
|
+
publishedAt: "date|optional"
|
|
406
580
|
},
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
fields: { region: "string" }
|
|
413
|
-
}
|
|
414
|
-
},
|
|
415
|
-
hooks: { // Custom operation hooks
|
|
416
|
-
preInsert: [async (data) => {
|
|
417
|
-
// Custom validation logic
|
|
418
|
-
return data;
|
|
419
|
-
}],
|
|
420
|
-
afterInsert: [async (data) => {
|
|
421
|
-
console.log("User created:", data.id);
|
|
422
|
-
}]
|
|
423
|
-
}
|
|
581
|
+
behavior: "body-overflow", // Handle long content
|
|
582
|
+
timestamps: true,
|
|
583
|
+
partitions: {
|
|
584
|
+
byAuthor: { fields: { author: "string" } },
|
|
585
|
+
byTag: { fields: { "tags.0": "string" } }
|
|
424
586
|
}
|
|
425
587
|
});
|
|
426
|
-
```
|
|
427
588
|
|
|
428
|
-
|
|
589
|
+
// Create a blog post
|
|
590
|
+
const post = await posts.insert({
|
|
591
|
+
title: "Getting Started with s3db.js",
|
|
592
|
+
content: "This is a comprehensive guide to using s3db.js for your next project...",
|
|
593
|
+
author: "john_doe",
|
|
594
|
+
tags: ["tutorial", "database", "s3"],
|
|
595
|
+
published: true,
|
|
596
|
+
publishedAt: new Date()
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
// Query posts by author
|
|
600
|
+
const johnsPosts = await posts.list({
|
|
601
|
+
partition: "byAuthor",
|
|
602
|
+
partitionValues: { author: "john_doe" }
|
|
603
|
+
});
|
|
604
|
+
```
|
|
429
605
|
|
|
430
|
-
|
|
606
|
+
### ๐ E-commerce Store
|
|
431
607
|
|
|
432
608
|
```javascript
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
609
|
+
// Products with detailed specifications
|
|
610
|
+
const products = await s3db.createResource({
|
|
611
|
+
name: "products",
|
|
612
|
+
attributes: {
|
|
613
|
+
name: "string|min:2|max:200",
|
|
614
|
+
description: "string",
|
|
615
|
+
price: "number|positive",
|
|
616
|
+
category: "string",
|
|
617
|
+
inventory: {
|
|
618
|
+
stock: "number|integer|min:0",
|
|
619
|
+
reserved: "number|integer|min:0|default:0"
|
|
620
|
+
},
|
|
621
|
+
specifications: "object|optional",
|
|
622
|
+
images: "array|items:url"
|
|
623
|
+
},
|
|
624
|
+
behavior: "body-overflow",
|
|
625
|
+
timestamps: true,
|
|
626
|
+
partitions: {
|
|
627
|
+
byCategory: { fields: { category: "string" } }
|
|
628
|
+
}
|
|
437
629
|
});
|
|
438
630
|
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
631
|
+
// Orders with customer information
|
|
632
|
+
const orders = await s3db.createResource({
|
|
633
|
+
name: "orders",
|
|
634
|
+
attributes: {
|
|
635
|
+
customerId: "string",
|
|
636
|
+
items: "array|items:object",
|
|
637
|
+
total: "number|positive",
|
|
638
|
+
status: "string|enum:pending,processing,shipped,delivered",
|
|
639
|
+
shipping: {
|
|
640
|
+
address: "string",
|
|
641
|
+
city: "string",
|
|
642
|
+
country: "string",
|
|
643
|
+
zipCode: "string"
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
behavior: "enforce-limits",
|
|
647
|
+
timestamps: true
|
|
446
648
|
});
|
|
447
649
|
|
|
448
|
-
//
|
|
449
|
-
const
|
|
450
|
-
name: "
|
|
451
|
-
|
|
452
|
-
|
|
650
|
+
// Create a product
|
|
651
|
+
const product = await products.insert({
|
|
652
|
+
name: "Premium Wireless Headphones",
|
|
653
|
+
description: "High-quality audio with active noise cancellation",
|
|
654
|
+
price: 299.99,
|
|
655
|
+
category: "electronics",
|
|
656
|
+
inventory: { stock: 50 },
|
|
657
|
+
specifications: {
|
|
658
|
+
brand: "AudioTech",
|
|
659
|
+
model: "AT-WH1000",
|
|
660
|
+
features: ["ANC", "Bluetooth 5.0", "30h battery"]
|
|
661
|
+
},
|
|
662
|
+
images: ["https://example.com/headphones-1.jpg"]
|
|
453
663
|
});
|
|
454
|
-
```
|
|
455
664
|
|
|
456
|
-
|
|
665
|
+
// Create an order
|
|
666
|
+
const order = await orders.insert({
|
|
667
|
+
customerId: "customer-123",
|
|
668
|
+
items: [
|
|
669
|
+
{ productId: product.id, quantity: 1, price: 299.99 }
|
|
670
|
+
],
|
|
671
|
+
total: 299.99,
|
|
672
|
+
status: "pending",
|
|
673
|
+
shipping: {
|
|
674
|
+
address: "123 Main St",
|
|
675
|
+
city: "New York",
|
|
676
|
+
country: "USA",
|
|
677
|
+
zipCode: "10001"
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
```
|
|
457
681
|
|
|
458
|
-
|
|
682
|
+
### ๐ฅ User Management System
|
|
459
683
|
|
|
460
684
|
```javascript
|
|
685
|
+
// Users with authentication
|
|
461
686
|
const users = await s3db.createResource({
|
|
462
687
|
name: "users",
|
|
463
|
-
attributes: {
|
|
464
|
-
|
|
688
|
+
attributes: {
|
|
689
|
+
username: "string|min:3|max:50|unique",
|
|
690
|
+
email: "email|unique",
|
|
691
|
+
password: "secret", // Automatically encrypted
|
|
692
|
+
role: "string|enum:user,admin,moderator|default:user",
|
|
693
|
+
profile: {
|
|
694
|
+
firstName: "string",
|
|
695
|
+
lastName: "string",
|
|
696
|
+
avatar: "url|optional",
|
|
697
|
+
bio: "string|max:500|optional"
|
|
698
|
+
},
|
|
699
|
+
preferences: {
|
|
700
|
+
theme: "string|enum:light,dark|default:light",
|
|
701
|
+
language: "string|default:en",
|
|
702
|
+
notifications: "boolean|default:true"
|
|
703
|
+
},
|
|
704
|
+
lastLogin: "date|optional"
|
|
705
|
+
},
|
|
706
|
+
behavior: "enforce-limits",
|
|
707
|
+
timestamps: true,
|
|
708
|
+
hooks: {
|
|
709
|
+
preInsert: [async (data) => {
|
|
710
|
+
// Auto-generate secure password if not provided
|
|
711
|
+
if (!data.password) {
|
|
712
|
+
data.password = generateSecurePassword();
|
|
713
|
+
}
|
|
714
|
+
return data;
|
|
715
|
+
}],
|
|
716
|
+
afterInsert: [async (data) => {
|
|
717
|
+
console.log(`Welcome ${data.username}! ๐`);
|
|
718
|
+
}]
|
|
719
|
+
}
|
|
465
720
|
});
|
|
466
721
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
722
|
+
// Register a new user
|
|
723
|
+
const user = await users.insert({
|
|
724
|
+
username: "jane_smith",
|
|
725
|
+
email: "jane@example.com",
|
|
726
|
+
profile: {
|
|
727
|
+
firstName: "Jane",
|
|
728
|
+
lastName: "Smith"
|
|
729
|
+
},
|
|
730
|
+
preferences: {
|
|
731
|
+
theme: "dark",
|
|
732
|
+
notifications: true
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Password was auto-generated and encrypted
|
|
737
|
+
console.log("Generated password:", user.password);
|
|
477
738
|
```
|
|
478
739
|
|
|
479
|
-
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## ๐ Security
|
|
480
743
|
|
|
481
|
-
|
|
744
|
+
### ๐ Field-Level Encryption
|
|
745
|
+
|
|
746
|
+
Sensitive data is automatically encrypted using the `"secret"` type:
|
|
482
747
|
|
|
483
748
|
```javascript
|
|
484
749
|
const users = await s3db.createResource({
|
|
485
750
|
name: "users",
|
|
486
|
-
attributes: {
|
|
487
|
-
|
|
751
|
+
attributes: {
|
|
752
|
+
email: "email",
|
|
753
|
+
password: "secret", // ๐ Encrypted
|
|
754
|
+
apiKey: "secret", // ๐ Encrypted
|
|
755
|
+
creditCard: "secret" // ๐ Encrypted
|
|
756
|
+
}
|
|
488
757
|
});
|
|
489
758
|
|
|
490
759
|
const user = await users.insert({
|
|
491
|
-
name: "John Doe",
|
|
492
760
|
email: "john@example.com",
|
|
493
|
-
|
|
761
|
+
password: "my_secure_password",
|
|
762
|
+
apiKey: "sk_live_123456789",
|
|
763
|
+
creditCard: "4111111111111111"
|
|
494
764
|
});
|
|
495
765
|
|
|
496
|
-
|
|
497
|
-
|
|
766
|
+
// Data is automatically decrypted when retrieved
|
|
767
|
+
const retrieved = await users.get(user.id);
|
|
768
|
+
console.log(retrieved.password); // "my_secure_password" โ
|
|
498
769
|
```
|
|
499
770
|
|
|
500
|
-
|
|
771
|
+
### ๐ฒ Auto-Generated Secure Passwords
|
|
501
772
|
|
|
502
|
-
|
|
773
|
+
s3db.js automatically generates secure passwords for `secret` fields when not provided:
|
|
503
774
|
|
|
504
775
|
```javascript
|
|
505
|
-
const
|
|
506
|
-
name: "
|
|
507
|
-
attributes: { name: "string", email: "email", bio: "string" },
|
|
508
|
-
options: { behavior: "body-overflow" }
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
const user = await users.insert({
|
|
512
|
-
name: "John Doe",
|
|
513
|
-
email: "john@example.com",
|
|
514
|
-
bio: "This is a very long biography that will be stored in the S3 object body..."
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
// All data is preserved and automatically merged when retrieved
|
|
518
|
-
console.log(user.bio); // Full biography preserved
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
**How Body Overflow Works:**
|
|
522
|
-
- Small attributes stay in metadata for fast access
|
|
523
|
-
- Large attributes are moved to S3 object body
|
|
524
|
-
- Data is automatically merged when retrieved
|
|
525
|
-
- Maintains full data integrity
|
|
526
|
-
|
|
527
|
-
##### Complete Resource Configuration Reference
|
|
528
|
-
|
|
529
|
-
```javascript
|
|
530
|
-
const resource = await s3db.createResource({
|
|
531
|
-
// Required: Resource name (unique within database)
|
|
532
|
-
name: "users",
|
|
533
|
-
|
|
534
|
-
// Required: Schema definition
|
|
535
|
-
attributes: {
|
|
536
|
-
// Basic types
|
|
537
|
-
name: "string|min:2|max:100",
|
|
538
|
-
email: "email|unique",
|
|
539
|
-
age: "number|integer|positive",
|
|
540
|
-
isActive: "boolean",
|
|
541
|
-
|
|
542
|
-
// Advanced types
|
|
543
|
-
website: "url",
|
|
544
|
-
uuid: "uuid",
|
|
545
|
-
createdAt: "date",
|
|
546
|
-
price: "currency|symbol:$",
|
|
547
|
-
|
|
548
|
-
// Encrypted fields
|
|
549
|
-
password: "secret",
|
|
550
|
-
apiKey: "secret",
|
|
551
|
-
|
|
552
|
-
// Nested objects
|
|
553
|
-
address: {
|
|
554
|
-
street: "string",
|
|
555
|
-
city: "string",
|
|
556
|
-
country: "string",
|
|
557
|
-
zipCode: "string|optional"
|
|
558
|
-
},
|
|
559
|
-
|
|
560
|
-
// Complex nested structures
|
|
561
|
-
profile: {
|
|
562
|
-
bio: "string|max:500|optional",
|
|
563
|
-
avatar: "url|optional",
|
|
564
|
-
birthDate: "date|optional",
|
|
565
|
-
preferences: {
|
|
566
|
-
theme: "string|enum:light,dark|default:light",
|
|
567
|
-
language: "string|enum:en,es,fr|default:en",
|
|
568
|
-
notifications: "boolean|default:true"
|
|
569
|
-
}
|
|
570
|
-
},
|
|
571
|
-
|
|
572
|
-
// Nested objects with validation
|
|
573
|
-
contact: {
|
|
574
|
-
phone: {
|
|
575
|
-
mobile: "string|pattern:^\\+?[1-9]\\d{1,14}$|optional",
|
|
576
|
-
work: "string|pattern:^\\+?[1-9]\\d{1,14}$|optional"
|
|
577
|
-
},
|
|
578
|
-
social: {
|
|
579
|
-
twitter: "string|optional",
|
|
580
|
-
linkedin: "url|optional",
|
|
581
|
-
github: "url|optional"
|
|
582
|
-
}
|
|
583
|
-
},
|
|
584
|
-
|
|
585
|
-
// Arrays
|
|
586
|
-
tags: "array|items:string|unique",
|
|
587
|
-
scores: "array|items:number|min:1",
|
|
588
|
-
|
|
589
|
-
// Multiple types
|
|
590
|
-
id: ["string", "number"],
|
|
591
|
-
|
|
592
|
-
// Complex nested structures
|
|
593
|
-
metadata: {
|
|
594
|
-
settings: "object|optional",
|
|
595
|
-
preferences: "object|optional"
|
|
596
|
-
},
|
|
597
|
-
|
|
598
|
-
// Analytics and tracking
|
|
599
|
-
analytics: {
|
|
600
|
-
utm: {
|
|
601
|
-
source: "string|optional",
|
|
602
|
-
medium: "string|optional",
|
|
603
|
-
campaign: "string|optional",
|
|
604
|
-
term: "string|optional",
|
|
605
|
-
content: "string|optional"
|
|
606
|
-
},
|
|
607
|
-
events: "array|items:object|optional",
|
|
608
|
-
lastVisit: "date|optional"
|
|
609
|
-
}
|
|
610
|
-
},
|
|
611
|
-
|
|
612
|
-
// Optional: Resource configuration
|
|
613
|
-
options: {
|
|
614
|
-
// Behavior strategy for handling 2KB metadata limits
|
|
615
|
-
behavior: "user-management", // "user-management" | "enforce-limits" | "data-truncate" | "body-overflow"
|
|
616
|
-
|
|
617
|
-
// Enable automatic timestamps
|
|
618
|
-
timestamps: true, // Adds createdAt and updatedAt fields
|
|
619
|
-
|
|
620
|
-
// Define data partitions for efficient querying
|
|
621
|
-
partitions: {
|
|
622
|
-
byRegion: {
|
|
623
|
-
fields: { region: "string" }
|
|
624
|
-
},
|
|
625
|
-
byAgeGroup: {
|
|
626
|
-
fields: { ageGroup: "string" }
|
|
627
|
-
},
|
|
628
|
-
byDate: {
|
|
629
|
-
fields: { createdAt: "date|maxlength:10" }
|
|
630
|
-
}
|
|
631
|
-
},
|
|
632
|
-
|
|
633
|
-
// Custom operation hooks
|
|
634
|
-
hooks: {
|
|
635
|
-
// Pre-operation hooks (can modify data)
|
|
636
|
-
preInsert: [
|
|
637
|
-
async (data) => {
|
|
638
|
-
// Validate or transform data before insert
|
|
639
|
-
if (!data.email.includes("@")) {
|
|
640
|
-
throw new Error("Invalid email format");
|
|
641
|
-
}
|
|
642
|
-
return data;
|
|
643
|
-
}
|
|
644
|
-
],
|
|
645
|
-
preUpdate: [
|
|
646
|
-
async (id, data) => {
|
|
647
|
-
// Validate or transform data before update
|
|
648
|
-
return data;
|
|
649
|
-
}
|
|
650
|
-
],
|
|
651
|
-
preDelete: [
|
|
652
|
-
async (id) => {
|
|
653
|
-
// Validate before deletion
|
|
654
|
-
return true; // Return false to abort
|
|
655
|
-
}
|
|
656
|
-
],
|
|
657
|
-
|
|
658
|
-
// Post-operation hooks (cannot modify data)
|
|
659
|
-
afterInsert: [
|
|
660
|
-
async (data) => {
|
|
661
|
-
console.log("User created:", data.id);
|
|
662
|
-
}
|
|
663
|
-
],
|
|
664
|
-
afterUpdate: [
|
|
665
|
-
async (id, data) => {
|
|
666
|
-
console.log("User updated:", id);
|
|
667
|
-
}
|
|
668
|
-
],
|
|
669
|
-
afterDelete: [
|
|
670
|
-
async (id) => {
|
|
671
|
-
console.log("User deleted:", id);
|
|
672
|
-
}
|
|
673
|
-
]
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
});
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
### 3. Schema Validation
|
|
680
|
-
|
|
681
|
-
`s3db.js` uses [fastest-validator](https://github.com/icebob/fastest-validator) for schema validation with robust handling of edge cases:
|
|
682
|
-
|
|
683
|
-
```javascript
|
|
684
|
-
const attributes = {
|
|
685
|
-
// Basic types
|
|
686
|
-
name: "string|min:2|max:100|trim",
|
|
687
|
-
email: "email|nullable",
|
|
688
|
-
age: "number|integer|positive",
|
|
689
|
-
isActive: "boolean",
|
|
690
|
-
|
|
691
|
-
// Advanced types
|
|
692
|
-
website: "url",
|
|
693
|
-
uuid: "uuid",
|
|
694
|
-
createdAt: "date",
|
|
695
|
-
price: "currency|symbol:$",
|
|
696
|
-
|
|
697
|
-
// Custom s3db types
|
|
698
|
-
password: "secret", // Encrypted field
|
|
699
|
-
|
|
700
|
-
// Nested objects (supports empty objects and null values)
|
|
701
|
-
address: {
|
|
702
|
-
street: "string",
|
|
703
|
-
city: "string",
|
|
704
|
-
country: "string",
|
|
705
|
-
zipCode: "string|optional"
|
|
706
|
-
},
|
|
707
|
-
|
|
708
|
-
// Arrays (robust serialization with special character handling)
|
|
709
|
-
tags: "array|items:string|unique", // Handles empty arrays: []
|
|
710
|
-
scores: "array|items:number|min:1", // Handles null arrays
|
|
711
|
-
categories: "array|items:string", // Handles arrays with pipe characters: ['tag|special', 'normal']
|
|
712
|
-
|
|
713
|
-
// Multiple types
|
|
714
|
-
id: ["string", "number"],
|
|
715
|
-
|
|
716
|
-
// Complex nested structures
|
|
717
|
-
metadata: {
|
|
718
|
-
settings: "object|optional", // Can be empty: {}
|
|
719
|
-
preferences: "object|optional" // Can be null
|
|
720
|
-
}
|
|
721
|
-
};
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
#### Nested Object Validation
|
|
725
|
-
|
|
726
|
-
Nested objects support comprehensive validation rules at each level:
|
|
727
|
-
|
|
728
|
-
```javascript
|
|
729
|
-
const users = await s3db.createResource({
|
|
730
|
-
name: "users",
|
|
731
|
-
attributes: {
|
|
732
|
-
name: "string|min:2|max:100",
|
|
733
|
-
email: "email|unique",
|
|
734
|
-
|
|
735
|
-
// Simple nested object
|
|
736
|
-
profile: {
|
|
737
|
-
bio: "string|max:500|optional",
|
|
738
|
-
avatar: "url|optional",
|
|
739
|
-
birthDate: "date|optional"
|
|
740
|
-
},
|
|
741
|
-
|
|
742
|
-
// Complex nested structure with validation
|
|
743
|
-
contact: {
|
|
744
|
-
phone: {
|
|
745
|
-
mobile: "string|pattern:^\\+?[1-9]\\d{1,14}$|optional",
|
|
746
|
-
work: "string|pattern:^\\+?[1-9]\\d{1,14}$|optional"
|
|
747
|
-
},
|
|
748
|
-
social: {
|
|
749
|
-
twitter: "string|optional",
|
|
750
|
-
linkedin: "url|optional"
|
|
751
|
-
}
|
|
752
|
-
},
|
|
753
|
-
|
|
754
|
-
// Nested object with arrays
|
|
755
|
-
preferences: {
|
|
756
|
-
categories: "array|items:string|unique|optional",
|
|
757
|
-
notifications: {
|
|
758
|
-
email: "boolean|default:true",
|
|
759
|
-
sms: "boolean|default:false",
|
|
760
|
-
push: "boolean|default:true"
|
|
761
|
-
}
|
|
762
|
-
},
|
|
763
|
-
|
|
764
|
-
// Deep nesting with validation
|
|
765
|
-
analytics: {
|
|
766
|
-
tracking: {
|
|
767
|
-
utm: {
|
|
768
|
-
source: "string|optional",
|
|
769
|
-
medium: "string|optional",
|
|
770
|
-
campaign: "string|optional"
|
|
771
|
-
},
|
|
772
|
-
events: "array|items:object|optional"
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
// Insert data with complex nested structure
|
|
779
|
-
const user = await users.insert({
|
|
780
|
-
name: "John Doe",
|
|
781
|
-
email: "john@example.com",
|
|
782
|
-
profile: {
|
|
783
|
-
bio: "Software developer with 10+ years of experience",
|
|
784
|
-
avatar: "https://example.com/avatar.jpg",
|
|
785
|
-
birthDate: new Date("1990-01-15")
|
|
786
|
-
},
|
|
787
|
-
contact: {
|
|
788
|
-
phone: {
|
|
789
|
-
mobile: "+1234567890",
|
|
790
|
-
work: "+1987654321"
|
|
791
|
-
},
|
|
792
|
-
social: {
|
|
793
|
-
twitter: "@johndoe",
|
|
794
|
-
linkedin: "https://linkedin.com/in/johndoe"
|
|
795
|
-
}
|
|
796
|
-
},
|
|
797
|
-
preferences: {
|
|
798
|
-
categories: ["technology", "programming", "web-development"],
|
|
799
|
-
notifications: {
|
|
800
|
-
email: true,
|
|
801
|
-
sms: false,
|
|
802
|
-
push: true
|
|
803
|
-
}
|
|
804
|
-
},
|
|
805
|
-
analytics: {
|
|
806
|
-
tracking: {
|
|
807
|
-
utm: {
|
|
808
|
-
source: "google",
|
|
809
|
-
medium: "organic",
|
|
810
|
-
campaign: "brand"
|
|
811
|
-
},
|
|
812
|
-
events: [
|
|
813
|
-
{ type: "page_view", timestamp: new Date() },
|
|
814
|
-
{ type: "signup", timestamp: new Date() }
|
|
815
|
-
]
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
});
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
#### Enhanced Array and Object Handling
|
|
822
|
-
|
|
823
|
-
s3db.js now provides robust serialization for complex data structures:
|
|
824
|
-
|
|
825
|
-
```javascript
|
|
826
|
-
// โ
Supported: Empty arrays and objects
|
|
827
|
-
const user = await users.insert({
|
|
828
|
-
name: "John Doe",
|
|
829
|
-
tags: [], // Empty array - properly serialized
|
|
830
|
-
metadata: {}, // Empty object - properly handled
|
|
831
|
-
preferences: null // Null object - correctly preserved
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
// โ
Supported: Arrays with special characters
|
|
835
|
-
const product = await products.insert({
|
|
836
|
-
name: "Widget",
|
|
837
|
-
categories: ["electronics|gadgets", "home|office"], // Pipe characters escaped
|
|
838
|
-
tags: ["tag|with|pipes", "normal-tag"] // Multiple pipes handled
|
|
839
|
-
});
|
|
840
|
-
```
|
|
841
|
-
|
|
842
|
-
## ๐ ๏ธ API Reference
|
|
843
|
-
|
|
844
|
-
### Database Operations
|
|
845
|
-
|
|
846
|
-
#### Connect to Database
|
|
847
|
-
|
|
848
|
-
```javascript
|
|
849
|
-
await s3db.connect();
|
|
850
|
-
// Emits 'connected' event when ready
|
|
851
|
-
```
|
|
852
|
-
|
|
853
|
-
#### Create Resource
|
|
854
|
-
|
|
855
|
-
```javascript
|
|
856
|
-
const resource = await s3db.createResource({
|
|
857
|
-
name: "users",
|
|
858
|
-
attributes: {
|
|
859
|
-
name: "string",
|
|
860
|
-
email: "email"
|
|
861
|
-
}
|
|
862
|
-
});
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
#### Check Resource Existence
|
|
866
|
-
|
|
867
|
-
```javascript
|
|
868
|
-
// Check if a resource exists by name
|
|
869
|
-
const exists = s3db.resourceExists("users");
|
|
870
|
-
console.log(exists); // true or false
|
|
871
|
-
```
|
|
872
|
-
|
|
873
|
-
#### Create Resource If Not Exists
|
|
874
|
-
|
|
875
|
-
```javascript
|
|
876
|
-
// Create a resource only if it doesn't exist with the same definition hash
|
|
877
|
-
const result = await s3db.createResourceIfNotExists({
|
|
878
|
-
name: "users",
|
|
879
|
-
attributes: {
|
|
880
|
-
name: "string|required",
|
|
881
|
-
email: "email|required"
|
|
882
|
-
},
|
|
883
|
-
options: { timestamps: true },
|
|
884
|
-
behavior: "user-management"
|
|
885
|
-
});
|
|
886
|
-
|
|
887
|
-
console.log(result);
|
|
888
|
-
// {
|
|
889
|
-
// resource: Resource,
|
|
890
|
-
// created: true, // or false if already existed
|
|
891
|
-
// reason: "New resource created" // or "Resource already exists with same definition hash"
|
|
892
|
-
// }
|
|
893
|
-
|
|
894
|
-
// If the resource already exists with the same hash, it returns the existing resource
|
|
895
|
-
const result2 = await s3db.createResourceIfNotExists({
|
|
896
|
-
name: "users",
|
|
897
|
-
attributes: {
|
|
898
|
-
name: "string|required",
|
|
899
|
-
email: "email|required"
|
|
900
|
-
}
|
|
901
|
-
});
|
|
902
|
-
|
|
903
|
-
console.log(result2.created); // false
|
|
904
|
-
console.log(result2.reason); // "Resource already exists with same definition hash"
|
|
905
|
-
```
|
|
906
|
-
|
|
907
|
-
#### Get Resource Reference
|
|
908
|
-
|
|
909
|
-
```javascript
|
|
910
|
-
const users = s3db.resource("users");
|
|
911
|
-
// or
|
|
912
|
-
const users = s3db.resources.users
|
|
913
|
-
```
|
|
914
|
-
|
|
915
|
-
### Resource Operations
|
|
916
|
-
|
|
917
|
-
#### Insert Document
|
|
918
|
-
|
|
919
|
-
```javascript
|
|
920
|
-
// With custom ID
|
|
921
|
-
const user = await users.insert({
|
|
922
|
-
id: "user-123",
|
|
923
|
-
name: "John Doe",
|
|
924
|
-
email: "john@example.com"
|
|
925
|
-
});
|
|
926
|
-
|
|
927
|
-
// Auto-generated ID
|
|
928
|
-
const user = await users.insert({
|
|
929
|
-
name: "Jane Doe",
|
|
930
|
-
email: "jane@example.com"
|
|
931
|
-
});
|
|
932
|
-
// ID will be auto-generated using nanoid
|
|
933
|
-
```
|
|
934
|
-
|
|
935
|
-
#### Get Document
|
|
936
|
-
|
|
937
|
-
```javascript
|
|
938
|
-
const user = await users.get("user-123");
|
|
939
|
-
console.log(user.name); // "John Doe"
|
|
940
|
-
```
|
|
941
|
-
|
|
942
|
-
#### Update Document
|
|
943
|
-
|
|
944
|
-
```javascript
|
|
945
|
-
const updatedUser = await users.update("user-123", {
|
|
946
|
-
name: "John Smith",
|
|
947
|
-
age: 31
|
|
948
|
-
});
|
|
949
|
-
// Only specified fields are updated
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
#### Upsert Document
|
|
953
|
-
|
|
954
|
-
```javascript
|
|
955
|
-
// Insert if doesn't exist, update if exists
|
|
956
|
-
const user = await users.upsert("user-123", {
|
|
957
|
-
name: "John Doe",
|
|
958
|
-
email: "john@example.com",
|
|
959
|
-
age: 30
|
|
960
|
-
});
|
|
961
|
-
```
|
|
962
|
-
|
|
963
|
-
#### Delete Document
|
|
964
|
-
|
|
965
|
-
```javascript
|
|
966
|
-
await users.delete("user-123");
|
|
967
|
-
```
|
|
968
|
-
|
|
969
|
-
#### Count Documents
|
|
970
|
-
|
|
971
|
-
```javascript
|
|
972
|
-
const count = await users.count();
|
|
973
|
-
console.log(`Total users: ${count}`);
|
|
974
|
-
```
|
|
975
|
-
|
|
976
|
-
#### Page Documents
|
|
977
|
-
|
|
978
|
-
The `page()` method provides efficient pagination with optional total count for performance optimization.
|
|
979
|
-
|
|
980
|
-
```javascript
|
|
981
|
-
// Basic pagination with total count
|
|
982
|
-
const page = await users.page({
|
|
983
|
-
offset: 0,
|
|
984
|
-
size: 10
|
|
985
|
-
});
|
|
986
|
-
|
|
987
|
-
console.log(page.items); // Array of user objects
|
|
988
|
-
console.log(page.totalItems); // Total number of items
|
|
989
|
-
console.log(page.totalPages); // Total number of pages
|
|
990
|
-
console.log(page.page); // Current page number (0-based)
|
|
991
|
-
console.log(page.pageSize); // Items per page
|
|
992
|
-
console.log(page._debug); // Debug information
|
|
993
|
-
|
|
994
|
-
// Pagination with partition filtering
|
|
995
|
-
const googleUsersPage = await users.page({
|
|
996
|
-
partition: 'byUtmSource',
|
|
997
|
-
partitionValues: { 'utm.source': 'google' },
|
|
998
|
-
offset: 0,
|
|
999
|
-
size: 5
|
|
1000
|
-
});
|
|
1001
|
-
|
|
1002
|
-
// Skip total count for better performance on large collections
|
|
1003
|
-
const fastPage = await users.page({
|
|
1004
|
-
offset: 0,
|
|
1005
|
-
size: 100,
|
|
1006
|
-
skipCount: true // Skips counting total items
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
console.log(fastPage.totalItems); // null (not counted)
|
|
1010
|
-
console.log(fastPage.totalPages); // null (not calculated)
|
|
1011
|
-
console.log(fastPage._debug.skipCount); // true
|
|
1012
|
-
```
|
|
1013
|
-
|
|
1014
|
-
**Page Response Structure:**
|
|
1015
|
-
|
|
1016
|
-
```javascript
|
|
1017
|
-
{
|
|
1018
|
-
items: Array, // Array of document objects
|
|
1019
|
-
totalItems: number, // Total count (null if skipCount: true)
|
|
1020
|
-
page: number, // Current page number (0-based)
|
|
1021
|
-
pageSize: number, // Number of items per page
|
|
1022
|
-
totalPages: number, // Total pages (null if skipCount: true)
|
|
1023
|
-
_debug: { // Debug information
|
|
1024
|
-
requestedSize: number,
|
|
1025
|
-
requestedOffset: number,
|
|
1026
|
-
actualItemsReturned: number,
|
|
1027
|
-
skipCount: boolean,
|
|
1028
|
-
hasTotalItems: boolean
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
```
|
|
1032
|
-
|
|
1033
|
-
**Parameters:**
|
|
1034
|
-
|
|
1035
|
-
| Parameter | Type | Default | Description |
|
|
1036
|
-
|-----------|------|---------|-------------|
|
|
1037
|
-
| `offset` | `number` | `0` | Number of items to skip |
|
|
1038
|
-
| `size` | `number` | `100` | Number of items per page |
|
|
1039
|
-
| `partition` | `string` | `null` | Partition name to filter by |
|
|
1040
|
-
| `partitionValues` | `object` | `{}` | Partition field values |
|
|
1041
|
-
| `skipCount` | `boolean` | `false` | Skip total count for performance |
|
|
1042
|
-
|
|
1043
|
-
### Bulk Operations
|
|
1044
|
-
|
|
1045
|
-
#### Insert Many
|
|
1046
|
-
|
|
1047
|
-
```javascript
|
|
1048
|
-
const users = [
|
|
1049
|
-
{ name: "User 1", email: "user1@example.com" },
|
|
1050
|
-
{ name: "User 2", email: "user2@example.com" },
|
|
1051
|
-
{ name: "User 3", email: "user3@example.com" }
|
|
1052
|
-
];
|
|
1053
|
-
|
|
1054
|
-
await users.insertMany(users);
|
|
1055
|
-
```
|
|
1056
|
-
|
|
1057
|
-
#### Get Many
|
|
1058
|
-
|
|
1059
|
-
```javascript
|
|
1060
|
-
const userList = await users.getMany(["user-1", "user-2", "user-3"]);
|
|
1061
|
-
```
|
|
1062
|
-
|
|
1063
|
-
#### Delete Many
|
|
1064
|
-
|
|
1065
|
-
```javascript
|
|
1066
|
-
await users.deleteMany(["user-1", "user-2", "user-3"]);
|
|
1067
|
-
```
|
|
1068
|
-
|
|
1069
|
-
#### Get All
|
|
1070
|
-
|
|
1071
|
-
```javascript
|
|
1072
|
-
const allUsers = await users.getAll();
|
|
1073
|
-
// Returns all documents in the resource
|
|
1074
|
-
```
|
|
1075
|
-
|
|
1076
|
-
#### List IDs
|
|
1077
|
-
|
|
1078
|
-
```javascript
|
|
1079
|
-
const userIds = await users.listIds();
|
|
1080
|
-
// Returns array of all document IDs
|
|
1081
|
-
```
|
|
1082
|
-
|
|
1083
|
-
#### Delete All
|
|
1084
|
-
|
|
1085
|
-
```javascript
|
|
1086
|
-
await users.deleteAll();
|
|
1087
|
-
// โ ๏ธ Destructive operation - removes all documents
|
|
1088
|
-
```
|
|
1089
|
-
|
|
1090
|
-
## ๐ Examples
|
|
1091
|
-
|
|
1092
|
-
### E-commerce Application
|
|
1093
|
-
|
|
1094
|
-
```javascript
|
|
1095
|
-
// Create product resource with body-overflow behavior for long descriptions
|
|
1096
|
-
const products = await s3db.createResource({
|
|
1097
|
-
name: "products",
|
|
1098
|
-
attributes: {
|
|
1099
|
-
name: "string|min:2|max:200",
|
|
1100
|
-
description: "string|optional",
|
|
1101
|
-
price: "number|positive",
|
|
1102
|
-
category: "string",
|
|
1103
|
-
tags: "array|items:string",
|
|
1104
|
-
inStock: "boolean",
|
|
1105
|
-
images: "array|items:url",
|
|
1106
|
-
metadata: "object|optional"
|
|
1107
|
-
},
|
|
1108
|
-
options: {
|
|
1109
|
-
behavior: "body-overflow", // Handle long product descriptions
|
|
1110
|
-
timestamps: true // Track creation and update times
|
|
1111
|
-
}
|
|
1112
|
-
});
|
|
1113
|
-
|
|
1114
|
-
// Create order resource with enforce-limits for strict data control
|
|
1115
|
-
const orders = await s3db.createResource({
|
|
1116
|
-
name: "orders",
|
|
1117
|
-
attributes: {
|
|
1118
|
-
customerId: "string",
|
|
1119
|
-
products: "array|items:string",
|
|
1120
|
-
total: "number|positive",
|
|
1121
|
-
status: "string|enum:pending,paid,shipped,delivered",
|
|
1122
|
-
shippingAddress: {
|
|
1123
|
-
street: "string",
|
|
1124
|
-
city: "string",
|
|
1125
|
-
country: "string",
|
|
1126
|
-
zipCode: "string"
|
|
1127
|
-
},
|
|
1128
|
-
createdAt: "date"
|
|
1129
|
-
},
|
|
1130
|
-
options: {
|
|
1131
|
-
behavior: "enforce-limits", // Strict validation for order data
|
|
1132
|
-
timestamps: true
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
// Insert products (long descriptions will be handled by body-overflow)
|
|
1137
|
-
const product = await products.insert({
|
|
1138
|
-
name: "Wireless Headphones",
|
|
1139
|
-
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.",
|
|
1140
|
-
price: 99.99,
|
|
1141
|
-
category: "electronics",
|
|
1142
|
-
tags: ["wireless", "bluetooth", "audio", "noise-cancellation"],
|
|
1143
|
-
inStock: true,
|
|
1144
|
-
images: ["https://example.com/headphones.jpg"]
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
// Create order (enforce-limits ensures data integrity)
|
|
1148
|
-
const order = await orders.insert({
|
|
1149
|
-
customerId: "customer-123",
|
|
1150
|
-
products: [product.id],
|
|
1151
|
-
total: 99.99,
|
|
1152
|
-
status: "pending",
|
|
1153
|
-
shippingAddress: {
|
|
1154
|
-
street: "123 Main St",
|
|
1155
|
-
city: "New York",
|
|
1156
|
-
country: "USA",
|
|
1157
|
-
zipCode: "10001"
|
|
1158
|
-
},
|
|
1159
|
-
createdAt: new Date()
|
|
1160
|
-
});
|
|
1161
|
-
```
|
|
1162
|
-
|
|
1163
|
-
### User Authentication System
|
|
1164
|
-
|
|
1165
|
-
```javascript
|
|
1166
|
-
// Create users resource with encrypted password and strict validation
|
|
1167
|
-
const users = await s3db.createResource({
|
|
1168
|
-
name: "users",
|
|
1169
|
-
attributes: {
|
|
1170
|
-
username: "string|min:3|max:50|unique",
|
|
1171
|
-
email: "email|unique",
|
|
1172
|
-
password: "secret", // Encrypted field
|
|
1173
|
-
role: "string|enum:user,admin,moderator",
|
|
1174
|
-
isActive: "boolean",
|
|
1175
|
-
lastLogin: "date|optional",
|
|
1176
|
-
profile: {
|
|
1177
|
-
firstName: "string",
|
|
1178
|
-
lastName: "string",
|
|
1179
|
-
avatar: "url|optional",
|
|
1180
|
-
bio: "string|optional"
|
|
1181
|
-
}
|
|
1182
|
-
},
|
|
1183
|
-
options: {
|
|
1184
|
-
behavior: "enforce-limits", // Strict validation for user data
|
|
1185
|
-
timestamps: true // Track account creation and updates
|
|
1186
|
-
}
|
|
1187
|
-
});
|
|
1188
|
-
|
|
1189
|
-
// Create sessions resource with body-overflow for session data
|
|
1190
|
-
const sessions = await s3db.createResource({
|
|
1191
|
-
name: "sessions",
|
|
1192
|
-
attributes: {
|
|
1193
|
-
userId: "string",
|
|
1194
|
-
token: "secret", // Encrypted session token
|
|
1195
|
-
expiresAt: "date",
|
|
1196
|
-
userAgent: "string|optional",
|
|
1197
|
-
ipAddress: "string|optional",
|
|
1198
|
-
sessionData: "object|optional" // Additional session metadata
|
|
1199
|
-
},
|
|
1200
|
-
options: {
|
|
1201
|
-
behavior: "body-overflow", // Handle large session data
|
|
1202
|
-
timestamps: true
|
|
1203
|
-
}
|
|
1204
|
-
});
|
|
1205
|
-
|
|
1206
|
-
// Register user (enforce-limits ensures data integrity)
|
|
1207
|
-
const user = await users.insert({
|
|
1208
|
-
username: "john_doe",
|
|
1209
|
-
email: "john@example.com",
|
|
1210
|
-
password: "secure_password_123",
|
|
1211
|
-
role: "user",
|
|
1212
|
-
isActive: true,
|
|
1213
|
-
profile: {
|
|
1214
|
-
firstName: "John",
|
|
1215
|
-
lastName: "Doe"
|
|
1216
|
-
}
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
// Create session (body-overflow preserves all session data)
|
|
1220
|
-
const session = await sessions.insert({
|
|
1221
|
-
userId: user.id,
|
|
1222
|
-
token: "jwt_token_here",
|
|
1223
|
-
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
|
|
1224
|
-
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
1225
|
-
ipAddress: "192.168.1.1",
|
|
1226
|
-
sessionData: {
|
|
1227
|
-
preferences: { theme: "dark", language: "en" },
|
|
1228
|
-
lastActivity: new Date(),
|
|
1229
|
-
deviceInfo: { type: "desktop", os: "Windows" }
|
|
1230
|
-
}
|
|
1231
|
-
});
|
|
1232
|
-
```
|
|
1233
|
-
|
|
1234
|
-
## ๐ Streaming
|
|
1235
|
-
|
|
1236
|
-
For large datasets, use streams to process data efficiently:
|
|
1237
|
-
|
|
1238
|
-
### Readable Stream
|
|
1239
|
-
|
|
1240
|
-
```javascript
|
|
1241
|
-
const readableStream = await users.readable();
|
|
1242
|
-
|
|
1243
|
-
readableStream.on("id", (id) => {
|
|
1244
|
-
console.log("Processing user ID:", id);
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
readableStream.on("data", (user) => {
|
|
1248
|
-
console.log("User:", user.name);
|
|
1249
|
-
// Process each user
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
readableStream.on("end", () => {
|
|
1253
|
-
console.log("Finished processing all users");
|
|
1254
|
-
});
|
|
1255
|
-
|
|
1256
|
-
readableStream.on("error", (error) => {
|
|
1257
|
-
console.error("Stream error:", error);
|
|
1258
|
-
});
|
|
1259
|
-
```
|
|
1260
|
-
|
|
1261
|
-
### Writable Stream
|
|
1262
|
-
|
|
1263
|
-
```javascript
|
|
1264
|
-
const writableStream = await users.writable();
|
|
1265
|
-
|
|
1266
|
-
// Write data to stream
|
|
1267
|
-
writableStream.write({
|
|
1268
|
-
name: "User 1",
|
|
1269
|
-
email: "user1@example.com"
|
|
1270
|
-
});
|
|
1271
|
-
|
|
1272
|
-
writableStream.write({
|
|
1273
|
-
name: "User 2",
|
|
1274
|
-
email: "user2@example.com"
|
|
1275
|
-
});
|
|
1276
|
-
|
|
1277
|
-
// End stream
|
|
1278
|
-
writableStream.end();
|
|
1279
|
-
```
|
|
1280
|
-
|
|
1281
|
-
### Stream to CSV
|
|
1282
|
-
|
|
1283
|
-
```javascript
|
|
1284
|
-
import fs from "fs";
|
|
1285
|
-
import { createObjectCsvWriter } from "csv-writer";
|
|
1286
|
-
|
|
1287
|
-
const csvWriter = createObjectCsvWriter({
|
|
1288
|
-
path: "users.csv",
|
|
1289
|
-
header: [
|
|
1290
|
-
{ id: "id", title: "ID" },
|
|
1291
|
-
{ id: "name", title: "Name" },
|
|
1292
|
-
{ id: "email", title: "Email" }
|
|
1293
|
-
]
|
|
1294
|
-
});
|
|
1295
|
-
|
|
1296
|
-
const readableStream = await users.readable();
|
|
1297
|
-
const records = [];
|
|
1298
|
-
|
|
1299
|
-
readableStream.on("data", (user) => {
|
|
1300
|
-
records.push(user);
|
|
1301
|
-
});
|
|
1302
|
-
|
|
1303
|
-
readableStream.on("end", async () => {
|
|
1304
|
-
await csvWriter.writeRecords(records);
|
|
1305
|
-
console.log("CSV file created successfully");
|
|
1306
|
-
});
|
|
1307
|
-
```
|
|
1308
|
-
|
|
1309
|
-
## ๐ Security & Encryption
|
|
1310
|
-
|
|
1311
|
-
### Field-Level Encryption
|
|
1312
|
-
|
|
1313
|
-
Use the `"secret"` type for sensitive data:
|
|
1314
|
-
|
|
1315
|
-
```javascript
|
|
1316
|
-
const users = await s3db.createResource({
|
|
1317
|
-
name: "users",
|
|
776
|
+
const accounts = await s3db.createResource({
|
|
777
|
+
name: "accounts",
|
|
1318
778
|
attributes: {
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
apiKey: "secret", // Encrypted
|
|
1323
|
-
creditCard: "secret" // Encrypted
|
|
779
|
+
name: "string",
|
|
780
|
+
password: "secret", // Auto-generated if not provided
|
|
781
|
+
apiKey: "secret" // Auto-generated if not provided
|
|
1324
782
|
}
|
|
1325
783
|
});
|
|
1326
784
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
email: "john@example.com",
|
|
1331
|
-
password: "my_secure_password", // Stored encrypted
|
|
1332
|
-
apiKey: "sk_live_123456789", // Stored encrypted
|
|
1333
|
-
creditCard: "4111111111111111" // Stored encrypted
|
|
785
|
+
const account = await accounts.insert({
|
|
786
|
+
name: "Service Account"
|
|
787
|
+
// password and apiKey will be auto-generated
|
|
1334
788
|
});
|
|
1335
789
|
|
|
1336
|
-
//
|
|
1337
|
-
|
|
1338
|
-
console.log(retrieved.password); // "my_secure_password" (decrypted)
|
|
790
|
+
console.log(account.password); // "Ax7Kp9mN2qR3" (12-char secure password)
|
|
791
|
+
console.log(account.apiKey); // "Bc8Lq0nO3sS4" (12-char secure key)
|
|
1339
792
|
```
|
|
1340
793
|
|
|
1341
|
-
|
|
794
|
+
**Features:**
|
|
795
|
+
- ๐ฏ **12-character passwords** with cryptographically secure randomness
|
|
796
|
+
- ๐ซ **No confusing characters** (excludes 0, O, 1, l, I)
|
|
797
|
+
- ๐ **Unique every time** using nanoid generation
|
|
798
|
+
- ๐ก๏ธ **Custom passwords supported** when explicitly provided
|
|
799
|
+
|
|
800
|
+
### ๐ Custom Encryption Keys
|
|
1342
801
|
|
|
1343
802
|
```javascript
|
|
1344
803
|
import fs from "fs";
|
|
@@ -1349,44 +808,60 @@ const s3db = new S3db({
|
|
|
1349
808
|
});
|
|
1350
809
|
```
|
|
1351
810
|
|
|
811
|
+
---
|
|
812
|
+
|
|
1352
813
|
## ๐ฐ Cost Analysis
|
|
1353
814
|
|
|
1354
|
-
### Understanding S3 Costs
|
|
815
|
+
### ๐ Understanding S3 Costs
|
|
1355
816
|
|
|
1356
|
-
-
|
|
1357
|
-
- **GET Requests**: $0.0000004 per 1,000 requests
|
|
1358
|
-
- **Data Transfer**: $0.09 per GB
|
|
1359
|
-
- **Storage**: $0.023 per GB (but s3db.js uses 0-byte files)
|
|
817
|
+
s3db.js is incredibly cost-effective because it uses S3 metadata instead of file storage:
|
|
1360
818
|
|
|
1361
|
-
|
|
819
|
+
| Operation | AWS Cost | s3db.js Usage |
|
|
820
|
+
|-----------|----------|---------------|
|
|
821
|
+
| **PUT Requests** | $0.0005 per 1,000 | Document inserts/updates |
|
|
822
|
+
| **GET Requests** | $0.0004 per 1,000 | Document retrievals |
|
|
823
|
+
| **Storage** | $0.023 per GB | ~$0 (uses 0-byte files) |
|
|
824
|
+
| **Data Transfer** | $0.09 per GB | Minimal (metadata only) |
|
|
1362
825
|
|
|
1363
|
-
|
|
826
|
+
### ๐ก Cost Examples
|
|
827
|
+
|
|
828
|
+
<details>
|
|
829
|
+
<summary><strong>๐ Small Application (1,000 users)</strong></summary>
|
|
1364
830
|
|
|
1365
831
|
```javascript
|
|
1366
|
-
//
|
|
1367
|
-
const setupCost = 0.
|
|
832
|
+
// One-time setup cost
|
|
833
|
+
const setupCost = 0.0005; // 1,000 PUT requests = $0.0005
|
|
1368
834
|
|
|
1369
|
-
// Monthly
|
|
1370
|
-
const
|
|
835
|
+
// Monthly operations (10 reads per user)
|
|
836
|
+
const monthlyReads = 0.004; // 10,000 GET requests = $0.004
|
|
837
|
+
const monthlyUpdates = 0.0005; // 1,000 PUT requests = $0.0005
|
|
1371
838
|
|
|
1372
|
-
|
|
1373
|
-
console.log(`Monthly
|
|
839
|
+
const totalMonthlyCost = monthlyReads + monthlyUpdates;
|
|
840
|
+
console.log(`Monthly cost: $${totalMonthlyCost.toFixed(4)}`); // $0.0045
|
|
1374
841
|
```
|
|
1375
842
|
|
|
1376
|
-
|
|
843
|
+
</details>
|
|
844
|
+
|
|
845
|
+
<details>
|
|
846
|
+
<summary><strong>๐ Large Application (1,000,000 users)</strong></summary>
|
|
1377
847
|
|
|
1378
848
|
```javascript
|
|
1379
|
-
//
|
|
1380
|
-
const setupCost =
|
|
849
|
+
// One-time setup cost
|
|
850
|
+
const setupCost = 0.50; // 1,000,000 PUT requests = $0.50
|
|
1381
851
|
|
|
1382
|
-
// Monthly
|
|
1383
|
-
const
|
|
852
|
+
// Monthly operations (10 reads per user)
|
|
853
|
+
const monthlyReads = 4.00; // 10,000,000 GET requests = $4.00
|
|
854
|
+
const monthlyUpdates = 0.50; // 1,000,000 PUT requests = $0.50
|
|
1384
855
|
|
|
1385
|
-
|
|
1386
|
-
console.log(`Monthly
|
|
856
|
+
const totalMonthlyCost = monthlyReads + monthlyUpdates;
|
|
857
|
+
console.log(`Monthly cost: $${totalMonthlyCost.toFixed(2)}`); // $4.50
|
|
1387
858
|
```
|
|
1388
859
|
|
|
1389
|
-
|
|
860
|
+
</details>
|
|
861
|
+
|
|
862
|
+
### ๐ Cost Tracking
|
|
863
|
+
|
|
864
|
+
Monitor your expenses with the built-in cost tracking plugin:
|
|
1390
865
|
|
|
1391
866
|
```javascript
|
|
1392
867
|
import { CostsPlugin } from "s3db.js";
|
|
@@ -1397,600 +872,205 @@ const s3db = new S3db({
|
|
|
1397
872
|
});
|
|
1398
873
|
|
|
1399
874
|
// After operations
|
|
1400
|
-
console.log("Total cost:", s3db.client.costs.total.toFixed(4), "USD");
|
|
1401
|
-
console.log("Requests made:", s3db.client.costs.requests.total);
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
## ๐๏ธ Advanced Features
|
|
1405
|
-
|
|
1406
|
-
### AutoEncrypt / AutoDecrypt
|
|
1407
|
-
|
|
1408
|
-
Fields with the type `secret` are automatically encrypted and decrypted using the resource's passphrase. This ensures sensitive data is protected at rest.
|
|
1409
|
-
|
|
1410
|
-
```js
|
|
1411
|
-
const users = await s3db.createResource({
|
|
1412
|
-
name: "users",
|
|
1413
|
-
attributes: {
|
|
1414
|
-
username: "string",
|
|
1415
|
-
password: "secret" // Will be encrypted
|
|
1416
|
-
}
|
|
1417
|
-
});
|
|
1418
|
-
|
|
1419
|
-
const user = await users.insert({
|
|
1420
|
-
username: "john_doe",
|
|
1421
|
-
password: "my_secret_password"
|
|
1422
|
-
});
|
|
1423
|
-
|
|
1424
|
-
// The password is stored encrypted in S3, but automatically decrypted when retrieved
|
|
1425
|
-
const retrieved = await users.get(user.id);
|
|
1426
|
-
console.log(retrieved.password); // "my_secret_password"
|
|
1427
|
-
```
|
|
1428
|
-
|
|
1429
|
-
### Resource Events
|
|
1430
|
-
|
|
1431
|
-
All resources emit events for key operations. You can listen to these events for logging, analytics, or custom workflows.
|
|
1432
|
-
|
|
1433
|
-
```js
|
|
1434
|
-
users.on("insert", (data) => console.log("User inserted:", data.id));
|
|
1435
|
-
users.on("get", (data) => console.log("User retrieved:", data.id));
|
|
1436
|
-
users.on("update", (attrs, data) => console.log("User updated:", data.id));
|
|
1437
|
-
users.on("delete", (id) => console.log("User deleted:", id));
|
|
1438
|
-
```
|
|
1439
|
-
|
|
1440
|
-
### Resource Schema Export/Import
|
|
1441
|
-
|
|
1442
|
-
You can export and import resource schemas for backup, migration, or versioning purposes.
|
|
1443
|
-
|
|
1444
|
-
```js
|
|
1445
|
-
// Export schema
|
|
1446
|
-
const schemaData = users.schema.export();
|
|
1447
|
-
|
|
1448
|
-
// Import schema
|
|
1449
|
-
const importedSchema = Schema.import(schemaData);
|
|
875
|
+
console.log("๐ฐ Total cost:", s3db.client.costs.total.toFixed(4), "USD");
|
|
876
|
+
console.log("๐ Requests made:", s3db.client.costs.requests.total);
|
|
877
|
+
console.log("๐ Cost breakdown:", s3db.client.costs.breakdown);
|
|
1450
878
|
```
|
|
1451
879
|
|
|
1452
|
-
|
|
880
|
+
---
|
|
1453
881
|
|
|
1454
|
-
|
|
882
|
+
## ๐จ Best Practices
|
|
1455
883
|
|
|
1456
|
-
###
|
|
884
|
+
### โ
Do's
|
|
1457
885
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
ageGroup: "string",
|
|
1468
|
-
createdAt: "date"
|
|
1469
|
-
},
|
|
1470
|
-
options: {
|
|
1471
|
-
partitions: {
|
|
1472
|
-
byRegion: {
|
|
1473
|
-
fields: { region: "string" }
|
|
1474
|
-
},
|
|
1475
|
-
byAgeGroup: {
|
|
1476
|
-
fields: { ageGroup: "string" }
|
|
1477
|
-
},
|
|
1478
|
-
byDate: {
|
|
1479
|
-
fields: { createdAt: "date|maxlength:10" }
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
886
|
+
#### **๐ฏ Design for Document Storage**
|
|
887
|
+
```javascript
|
|
888
|
+
// โ
Good: Well-structured documents
|
|
889
|
+
const user = {
|
|
890
|
+
id: "user-123",
|
|
891
|
+
name: "John Doe",
|
|
892
|
+
profile: {
|
|
893
|
+
bio: "Software developer",
|
|
894
|
+
preferences: { theme: "dark" }
|
|
1482
895
|
}
|
|
1483
|
-
}
|
|
1484
|
-
```
|
|
1485
|
-
|
|
1486
|
-
### Querying by partition
|
|
1487
|
-
|
|
1488
|
-
Partitions are automatically created when you insert documents, and you can query them using specific methods that accept partition parameters:
|
|
1489
|
-
|
|
1490
|
-
#### List IDs by partition
|
|
1491
|
-
|
|
1492
|
-
```js
|
|
1493
|
-
// Get all user IDs in the 'south' region
|
|
1494
|
-
const userIds = await users.listIds({
|
|
1495
|
-
partition: "byRegion",
|
|
1496
|
-
partitionValues: { region: "south" }
|
|
1497
|
-
});
|
|
1498
|
-
|
|
1499
|
-
// Get all user IDs in the 'adult' age group
|
|
1500
|
-
const adultIds = await users.listIds({
|
|
1501
|
-
partition: "byAgeGroup",
|
|
1502
|
-
partitionValues: { ageGroup: "adult" }
|
|
1503
|
-
});
|
|
1504
|
-
```
|
|
1505
|
-
|
|
1506
|
-
#### Count documents by partition
|
|
1507
|
-
|
|
1508
|
-
```js
|
|
1509
|
-
// Count users in the 'south' region
|
|
1510
|
-
const count = await users.count({
|
|
1511
|
-
partition: "byRegion",
|
|
1512
|
-
partitionValues: { region: "south" }
|
|
1513
|
-
});
|
|
1514
|
-
|
|
1515
|
-
// Count adult users
|
|
1516
|
-
const adultCount = await users.count({
|
|
1517
|
-
partition: "byAgeGroup",
|
|
1518
|
-
partitionValues: { ageGroup: "adult" }
|
|
1519
|
-
});
|
|
896
|
+
};
|
|
1520
897
|
```
|
|
1521
898
|
|
|
1522
|
-
####
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
const usersSouth = await users.listByPartition({
|
|
1527
|
-
partition: "byRegion",
|
|
1528
|
-
partitionValues: { region: "south" }
|
|
1529
|
-
});
|
|
899
|
+
#### **๐ Use Sequential IDs for Performance**
|
|
900
|
+
```javascript
|
|
901
|
+
// โ
Best: Sequential IDs
|
|
902
|
+
const productIds = ["00001", "00002", "00003"];
|
|
1530
903
|
|
|
1531
|
-
//
|
|
1532
|
-
const
|
|
1533
|
-
{ partition: "byAgeGroup", partitionValues: { ageGroup: "adult" } },
|
|
1534
|
-
{ limit: 10, offset: 0 }
|
|
1535
|
-
);
|
|
904
|
+
// โ
Good: UUIDs with sufficient entropy
|
|
905
|
+
const userIds = ["a1b2c3d4", "e5f6g7h8", "i9j0k1l2"];
|
|
1536
906
|
```
|
|
1537
907
|
|
|
1538
|
-
####
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
partitionValues: { region: "south" }
|
|
908
|
+
#### **๐ Leverage Streaming for Large Operations**
|
|
909
|
+
```javascript
|
|
910
|
+
// โ
Good: Process large datasets with streams
|
|
911
|
+
const stream = await users.readable();
|
|
912
|
+
stream.on("data", (user) => {
|
|
913
|
+
// Process each user individually
|
|
1545
914
|
});
|
|
1546
|
-
|
|
1547
|
-
console.log(page.items); // Array of user objects
|
|
1548
|
-
console.log(page.totalItems); // Total count in this partition
|
|
1549
|
-
console.log(page.totalPages); // Total pages available
|
|
1550
915
|
```
|
|
1551
916
|
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
name: "logs",
|
|
1557
|
-
attributes: {
|
|
1558
|
-
message: "string",
|
|
1559
|
-
level: "string",
|
|
1560
|
-
createdAt: "date"
|
|
1561
|
-
},
|
|
1562
|
-
options: {
|
|
1563
|
-
partitions: {
|
|
1564
|
-
byDate: {
|
|
1565
|
-
fields: { createdAt: "date|maxlength:10" }
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
});
|
|
1570
|
-
|
|
1571
|
-
// Insert logs (partitions are created automatically)
|
|
1572
|
-
await logs.insert({
|
|
1573
|
-
message: "User login",
|
|
1574
|
-
level: "info",
|
|
1575
|
-
createdAt: new Date("2024-06-27")
|
|
1576
|
-
});
|
|
917
|
+
#### **๐๏ธ Choose the Right Behavior Strategy**
|
|
918
|
+
```javascript
|
|
919
|
+
// โ
Development: Flexible with warnings
|
|
920
|
+
{ behavior: "user-management" }
|
|
1577
921
|
|
|
1578
|
-
//
|
|
1579
|
-
|
|
1580
|
-
partition: "byDate",
|
|
1581
|
-
partitionValues: { createdAt: "2024-06-27" }
|
|
1582
|
-
});
|
|
922
|
+
// โ
Production: Strict validation
|
|
923
|
+
{ behavior: "enforce-limits" }
|
|
1583
924
|
|
|
1584
|
-
//
|
|
1585
|
-
|
|
1586
|
-
partition: "byDate",
|
|
1587
|
-
partitionValues: { createdAt: "2024-06-27" }
|
|
1588
|
-
});
|
|
925
|
+
// โ
Content: Preserve all data
|
|
926
|
+
{ behavior: "body-overflow" }
|
|
1589
927
|
```
|
|
1590
928
|
|
|
1591
|
-
###
|
|
1592
|
-
|
|
1593
|
-
`s3db.js` supports partitions using nested object fields using dot notation, just like the schema mapper:
|
|
1594
|
-
|
|
1595
|
-
```js
|
|
1596
|
-
const users = await s3db.createResource({
|
|
1597
|
-
name: "users",
|
|
1598
|
-
attributes: {
|
|
1599
|
-
name: "string|required",
|
|
1600
|
-
utm: {
|
|
1601
|
-
source: "string|required",
|
|
1602
|
-
medium: "string|required",
|
|
1603
|
-
campaign: "string|required"
|
|
1604
|
-
},
|
|
1605
|
-
address: {
|
|
1606
|
-
country: "string|required",
|
|
1607
|
-
state: "string|required",
|
|
1608
|
-
city: "string|required"
|
|
1609
|
-
},
|
|
1610
|
-
metadata: {
|
|
1611
|
-
category: "string|required",
|
|
1612
|
-
priority: "string|required"
|
|
1613
|
-
}
|
|
1614
|
-
},
|
|
1615
|
-
options: {
|
|
1616
|
-
partitions: {
|
|
1617
|
-
byUtmSource: {
|
|
1618
|
-
fields: {
|
|
1619
|
-
"utm.source": "string"
|
|
1620
|
-
}
|
|
1621
|
-
},
|
|
1622
|
-
byAddressCountry: {
|
|
1623
|
-
fields: {
|
|
1624
|
-
"address.country": "string|maxlength:2"
|
|
1625
|
-
}
|
|
1626
|
-
},
|
|
1627
|
-
byAddressState: {
|
|
1628
|
-
fields: {
|
|
1629
|
-
"address.country": "string|maxlength:2",
|
|
1630
|
-
"address.state": "string"
|
|
1631
|
-
}
|
|
1632
|
-
},
|
|
1633
|
-
byUtmAndAddress: {
|
|
1634
|
-
fields: {
|
|
1635
|
-
"utm.source": "string",
|
|
1636
|
-
"utm.medium": "string",
|
|
1637
|
-
"address.country": "string|maxlength:2"
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
});
|
|
1643
|
-
|
|
1644
|
-
// Insert user with nested data
|
|
1645
|
-
await users.insert({
|
|
1646
|
-
name: "John Doe",
|
|
1647
|
-
utm: {
|
|
1648
|
-
source: "google",
|
|
1649
|
-
medium: "cpc",
|
|
1650
|
-
campaign: "brand"
|
|
1651
|
-
},
|
|
1652
|
-
address: {
|
|
1653
|
-
country: "US",
|
|
1654
|
-
state: "California",
|
|
1655
|
-
city: "San Francisco"
|
|
1656
|
-
},
|
|
1657
|
-
metadata: {
|
|
1658
|
-
category: "premium",
|
|
1659
|
-
priority: "high"
|
|
1660
|
-
}
|
|
1661
|
-
});
|
|
1662
|
-
|
|
1663
|
-
// Query by nested UTM source
|
|
1664
|
-
const googleUsers = await users.listIds({
|
|
1665
|
-
partition: "byUtmSource",
|
|
1666
|
-
partitionValues: { "utm.source": "google" }
|
|
1667
|
-
});
|
|
1668
|
-
|
|
1669
|
-
// Query by nested address country
|
|
1670
|
-
const usUsers = await users.listIds({
|
|
1671
|
-
partition: "byAddressCountry",
|
|
1672
|
-
partitionValues: { "address.country": "US" }
|
|
1673
|
-
});
|
|
1674
|
-
|
|
1675
|
-
// Query by multiple nested fields
|
|
1676
|
-
const usCaliforniaUsers = await users.listIds({
|
|
1677
|
-
partition: "byAddressState",
|
|
1678
|
-
partitionValues: {
|
|
1679
|
-
"address.country": "US",
|
|
1680
|
-
"address.state": "California"
|
|
1681
|
-
}
|
|
1682
|
-
});
|
|
1683
|
-
|
|
1684
|
-
// Complex query with UTM and address
|
|
1685
|
-
const googleCpcUsUsers = await users.listIds({
|
|
1686
|
-
partition: "byUtmAndAddress",
|
|
1687
|
-
partitionValues: {
|
|
1688
|
-
"utm.source": "google",
|
|
1689
|
-
"utm.medium": "cpc",
|
|
1690
|
-
"address.country": "US"
|
|
1691
|
-
}
|
|
1692
|
-
});
|
|
929
|
+
### โ Don'ts
|
|
1693
930
|
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
931
|
+
#### **๐ซ Avoid Large Arrays in Documents**
|
|
932
|
+
```javascript
|
|
933
|
+
// โ Bad: Large arrays can exceed 2KB limit
|
|
934
|
+
const user = {
|
|
935
|
+
name: "John",
|
|
936
|
+
purchaseHistory: [/* hundreds of orders */]
|
|
937
|
+
};
|
|
1699
938
|
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
}
|
|
939
|
+
// โ
Better: Use separate resource with references
|
|
940
|
+
const user = { name: "John", id: "user-123" };
|
|
941
|
+
const orders = [
|
|
942
|
+
{ userId: "user-123", product: "...", date: "..." },
|
|
943
|
+
// Store orders separately
|
|
944
|
+
];
|
|
1704
945
|
```
|
|
1705
946
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
- **Mixed partitions**: Combine nested and flat fields in the same partition
|
|
1711
|
-
- **Rules support**: Apply maxlength, date formatting, etc. to nested fields
|
|
1712
|
-
- **Automatic flattening**: Uses the same flattening logic as the schema mapper
|
|
1713
|
-
|
|
1714
|
-
### Partition rules and transformations
|
|
1715
|
-
|
|
1716
|
-
Partitions support various field rules that automatically transform values:
|
|
1717
|
-
|
|
1718
|
-
```js
|
|
1719
|
-
const products = await s3db.createResource({
|
|
1720
|
-
name: "products",
|
|
1721
|
-
attributes: {
|
|
1722
|
-
name: "string",
|
|
1723
|
-
category: "string",
|
|
1724
|
-
price: "number",
|
|
1725
|
-
createdAt: "date"
|
|
1726
|
-
},
|
|
1727
|
-
options: {
|
|
1728
|
-
partitions: {
|
|
1729
|
-
byCategory: {
|
|
1730
|
-
fields: { category: "string" }
|
|
1731
|
-
},
|
|
1732
|
-
byDate: {
|
|
1733
|
-
fields: { createdAt: "date|maxlength:10" } // Truncates to YYYY-MM-DD
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
});
|
|
947
|
+
#### **๐ซ Don't Load Everything at Once**
|
|
948
|
+
```javascript
|
|
949
|
+
// โ Bad: Memory intensive
|
|
950
|
+
const allUsers = await users.getAll();
|
|
1738
951
|
|
|
1739
|
-
//
|
|
1740
|
-
await
|
|
1741
|
-
name: "Widget",
|
|
1742
|
-
category: "electronics",
|
|
1743
|
-
price: 99.99,
|
|
1744
|
-
createdAt: new Date("2024-06-27T15:30:00Z") // Will be stored as "2024-06-27"
|
|
1745
|
-
});
|
|
952
|
+
// โ
Better: Use pagination or streaming
|
|
953
|
+
const page = await users.page({ offset: 0, size: 100 });
|
|
1746
954
|
```
|
|
1747
955
|
|
|
1748
|
-
###
|
|
1749
|
-
|
|
1750
|
-
1. **Automatic creation**: Partitions are automatically created when you insert documents
|
|
1751
|
-
2. **Performance**: Partition queries are more efficient than filtering all documents
|
|
1752
|
-
3. **Storage**: Each partition creates additional S3 objects, increasing storage costs
|
|
1753
|
-
4. **Consistency**: Partition data is automatically kept in sync with main resource data
|
|
1754
|
-
5. **Field requirements**: All partition fields must exist in your resource attributes
|
|
1755
|
-
|
|
1756
|
-
### Available partition-aware methods
|
|
1757
|
-
|
|
1758
|
-
| Method | Description | Partition Support |
|
|
1759
|
-
|--------|-------------|-------------------|
|
|
1760
|
-
| `listIds()` | Get array of document IDs | โ
`{ partition, partitionValues }` |
|
|
1761
|
-
| `count()` | Count documents | โ
`{ partition, partitionValues }` |
|
|
1762
|
-
| `listByPartition()` | List documents by partition | โ
`{ partition, partitionValues }` |
|
|
1763
|
-
| `page()` | Paginate documents | โ
`{ partition, partitionValues, skipCount }` |
|
|
1764
|
-
| `getFromPartition()` | Get single document from partition | โ
Direct partition access |
|
|
1765
|
-
| `query()` | Filter documents in memory | โ No partition support |
|
|
1766
|
-
|
|
1767
|
-
## Hooks
|
|
1768
|
-
|
|
1769
|
-
`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.
|
|
956
|
+
### ๐ฏ Performance Tips
|
|
1770
957
|
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
958
|
+
1. **Enable caching** for frequently accessed data:
|
|
959
|
+
```javascript
|
|
960
|
+
const s3db = new S3db({
|
|
961
|
+
uri: "s3://...",
|
|
962
|
+
cache: true,
|
|
963
|
+
ttl: 3600 // 1 hour
|
|
964
|
+
});
|
|
965
|
+
```
|
|
1775
966
|
|
|
1776
|
-
|
|
1777
|
-
|
|
967
|
+
2. **Adjust parallelism** for bulk operations:
|
|
968
|
+
```javascript
|
|
969
|
+
const s3db = new S3db({
|
|
970
|
+
uri: "s3://...",
|
|
971
|
+
parallelism: 25 // Handle 25 concurrent operations
|
|
972
|
+
});
|
|
973
|
+
```
|
|
1778
974
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
return data;
|
|
1788
|
-
}],
|
|
1789
|
-
afterInsert: [async (data) => {
|
|
1790
|
-
console.log("User inserted:", data.id);
|
|
1791
|
-
}]
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
});
|
|
1795
|
-
|
|
1796
|
-
// Or dynamically:
|
|
1797
|
-
users.addHook('preInsert', async (data) => {
|
|
1798
|
-
// Custom logic
|
|
1799
|
-
return data;
|
|
1800
|
-
});
|
|
1801
|
-
```
|
|
975
|
+
3. **Use partitions** for efficient queries:
|
|
976
|
+
```javascript
|
|
977
|
+
// Query specific partitions instead of scanning all data
|
|
978
|
+
const results = await users.list({
|
|
979
|
+
partition: "byRegion",
|
|
980
|
+
partitionValues: { region: "us-east" }
|
|
981
|
+
});
|
|
982
|
+
```
|
|
1802
983
|
|
|
1803
|
-
|
|
1804
|
-
- Internal hooks run first, user hooks run last (in the order they were added).
|
|
1805
|
-
- Hooks can be async and can modify the data (for `pre*` hooks).
|
|
1806
|
-
- If a hook throws, the operation is aborted.
|
|
984
|
+
---
|
|
1807
985
|
|
|
1808
|
-
##
|
|
986
|
+
## ๐งช Testing
|
|
1809
987
|
|
|
1810
|
-
|
|
988
|
+
s3db.js includes a comprehensive test suite. Run tests with:
|
|
1811
989
|
|
|
1812
|
-
|
|
990
|
+
```bash
|
|
991
|
+
# Run all tests
|
|
992
|
+
npm test
|
|
1813
993
|
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
setup(s3db) {
|
|
1817
|
-
console.log("Plugin setup");
|
|
1818
|
-
},
|
|
1819
|
-
start() {
|
|
1820
|
-
console.log("Plugin started");
|
|
1821
|
-
},
|
|
1822
|
-
onUserCreated(user) {
|
|
1823
|
-
console.log("New user created:", user.id);
|
|
1824
|
-
}
|
|
1825
|
-
};
|
|
994
|
+
# Run specific test file
|
|
995
|
+
npm test -- --testNamePattern="Resource"
|
|
1826
996
|
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
plugins: [MyPlugin]
|
|
1830
|
-
});
|
|
997
|
+
# Run with coverage
|
|
998
|
+
npm run test:coverage
|
|
1831
999
|
```
|
|
1832
1000
|
|
|
1833
|
-
|
|
1001
|
+
### Test Coverage
|
|
1834
1002
|
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
4. **Sequential IDs**: Best performance with sequential IDs (00001, 00002, etc.)
|
|
1841
|
-
5. **No Transactions**: No ACID transactions across multiple operations
|
|
1842
|
-
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
|
|
1843
|
-
|
|
1844
|
-
**๐ก Overcoming the 2KB Limit**: Use resource behaviors to handle documents that exceed the 2KB metadata limit:
|
|
1845
|
-
- **`body-overflow`**: Stores excess data in S3 object body (preserves all data)
|
|
1846
|
-
- **`data-truncate`**: Intelligently truncates data to fit within limits
|
|
1847
|
-
- **`enforce-limits`**: Strict validation to prevent oversized documents
|
|
1848
|
-
- **`user-management`**: Default behavior with warnings and monitoring
|
|
1849
|
-
|
|
1850
|
-
### Best Practices
|
|
1851
|
-
|
|
1852
|
-
#### 1. Design for Document Storage
|
|
1853
|
-
|
|
1854
|
-
```javascript
|
|
1855
|
-
// โ
Good: Nested structure is fine
|
|
1856
|
-
const user = {
|
|
1857
|
-
id: "user-123",
|
|
1858
|
-
name: "John Doe",
|
|
1859
|
-
email: "john@example.com",
|
|
1860
|
-
profile: {
|
|
1861
|
-
bio: "Software developer",
|
|
1862
|
-
avatar: "https://example.com/avatar.jpg",
|
|
1863
|
-
preferences: {
|
|
1864
|
-
theme: "dark",
|
|
1865
|
-
notifications: true
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
};
|
|
1003
|
+
- โ
**Unit Tests** - Individual component testing
|
|
1004
|
+
- โ
**Integration Tests** - End-to-end workflows
|
|
1005
|
+
- โ
**Behavior Tests** - Document handling strategies
|
|
1006
|
+
- โ
**Performance Tests** - Large dataset operations
|
|
1007
|
+
- โ
**Security Tests** - Encryption and validation
|
|
1869
1008
|
|
|
1870
|
-
|
|
1871
|
-
const user = {
|
|
1872
|
-
id: "user-123",
|
|
1873
|
-
name: "John Doe",
|
|
1874
|
-
// This could exceed metadata limits
|
|
1875
|
-
purchaseHistory: [
|
|
1876
|
-
{ id: "order-1", date: "2023-01-01", total: 99.99 },
|
|
1877
|
-
{ id: "order-2", date: "2023-01-15", total: 149.99 },
|
|
1878
|
-
// ... many more items
|
|
1879
|
-
]
|
|
1880
|
-
};
|
|
1881
|
-
```
|
|
1009
|
+
---
|
|
1882
1010
|
|
|
1883
|
-
|
|
1011
|
+
## ๐ค Contributing
|
|
1884
1012
|
|
|
1885
|
-
|
|
1886
|
-
// โ
Good: Sequential IDs for better performance
|
|
1887
|
-
const users = ["00001", "00002", "00003", "00004"];
|
|
1013
|
+
We'd love your help making s3db.js even better! Here's how you can contribute:
|
|
1888
1014
|
|
|
1889
|
-
|
|
1890
|
-
const users = ["abc123", "def456", "ghi789", "jkl012"];
|
|
1015
|
+
### ๐ ๏ธ Development Setup
|
|
1891
1016
|
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1017
|
+
```bash
|
|
1018
|
+
# Clone the repository
|
|
1019
|
+
git clone https://github.com/forattini-dev/s3db.js.git
|
|
1020
|
+
cd s3db.js
|
|
1895
1021
|
|
|
1896
|
-
|
|
1022
|
+
# Install dependencies
|
|
1023
|
+
npm install
|
|
1897
1024
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
const order = {
|
|
1901
|
-
id: "order-123",
|
|
1902
|
-
customerId: "customer-456",
|
|
1903
|
-
customerName: "John Doe", // Denormalized for quick access
|
|
1904
|
-
items: ["product-1", "product-2"],
|
|
1905
|
-
total: 99.99
|
|
1906
|
-
};
|
|
1025
|
+
# Run tests
|
|
1026
|
+
npm test
|
|
1907
1027
|
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
id: "order-123",
|
|
1911
|
-
customerId: "customer-456", // Requires separate lookup
|
|
1912
|
-
items: ["product-1", "product-2"]
|
|
1913
|
-
};
|
|
1028
|
+
# Start development server
|
|
1029
|
+
npm run dev
|
|
1914
1030
|
```
|
|
1915
1031
|
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
```javascript
|
|
1919
|
-
// โ
Good: Use streams for large operations
|
|
1920
|
-
const readableStream = await users.readable();
|
|
1921
|
-
readableStream.on("data", (user) => {
|
|
1922
|
-
// Process each user individually
|
|
1923
|
-
});
|
|
1032
|
+
### ๐ Contribution Guidelines
|
|
1924
1033
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1034
|
+
1. **๐ด Fork** the repository
|
|
1035
|
+
2. **๐ฟ Create** a feature branch (`git checkout -b feature/amazing-feature`)
|
|
1036
|
+
3. **โจ Make** your changes with tests
|
|
1037
|
+
4. **โ
Ensure** all tests pass (`npm test`)
|
|
1038
|
+
5. **๐ Commit** your changes (`git commit -m 'Add amazing feature'`)
|
|
1039
|
+
6. **๐ Push** to your branch (`git push origin feature/amazing-feature`)
|
|
1040
|
+
7. **๐ Open** a Pull Request
|
|
1928
1041
|
|
|
1929
|
-
|
|
1042
|
+
### ๐ Bug Reports
|
|
1930
1043
|
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
if (error.message.includes("does not exist")) {
|
|
1937
|
-
console.log("User not found");
|
|
1938
|
-
} else {
|
|
1939
|
-
console.error("Unexpected error:", error);
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
// Method 2: Check existence first (โ ๏ธ Additional request cost)
|
|
1944
|
-
const userId = "user-123";
|
|
1945
|
-
if (await users.exists(userId)) {
|
|
1946
|
-
const user = await users.get(userId);
|
|
1947
|
-
console.log("User found:", user.name);
|
|
1948
|
-
} else {
|
|
1949
|
-
console.log("User not found");
|
|
1950
|
-
}
|
|
1951
|
-
```
|
|
1044
|
+
Found a bug? Please open an issue with:
|
|
1045
|
+
- Clear description of the problem
|
|
1046
|
+
- Steps to reproduce
|
|
1047
|
+
- Expected vs actual behavior
|
|
1048
|
+
- Your environment details
|
|
1952
1049
|
|
|
1953
|
-
|
|
1050
|
+
### ๐ก Feature Requests
|
|
1954
1051
|
|
|
1955
|
-
|
|
1052
|
+
Have an idea? We'd love to hear it! Open an issue describing:
|
|
1053
|
+
- The problem you're trying to solve
|
|
1054
|
+
- Your proposed solution
|
|
1055
|
+
- Any alternatives you've considered
|
|
1956
1056
|
|
|
1957
|
-
|
|
1958
|
-
// โ
For development and testing - allows flexibility
|
|
1959
|
-
const devUsers = await s3db.createResource({
|
|
1960
|
-
name: "users",
|
|
1961
|
-
attributes: { name: "string", email: "email" },
|
|
1962
|
-
options: { behavior: "user-management" }
|
|
1963
|
-
});
|
|
1057
|
+
---
|
|
1964
1058
|
|
|
1965
|
-
|
|
1966
|
-
const prodUsers = await s3db.createResource({
|
|
1967
|
-
name: "users",
|
|
1968
|
-
attributes: { name: "string", email: "email" },
|
|
1969
|
-
options: { behavior: "enforce-limits" }
|
|
1970
|
-
});
|
|
1059
|
+
## ๐ License
|
|
1971
1060
|
|
|
1972
|
-
|
|
1973
|
-
const blogPosts = await s3db.createResource({
|
|
1974
|
-
name: "posts",
|
|
1975
|
-
attributes: { title: "string", content: "string", author: "string" },
|
|
1976
|
-
options: { behavior: "body-overflow" }
|
|
1977
|
-
});
|
|
1061
|
+
This project is licensed under the **Unlicense** - see the [LICENSE](LICENSE) file for details.
|
|
1978
1062
|
|
|
1979
|
-
|
|
1980
|
-
const productDescriptions = await s3db.createResource({
|
|
1981
|
-
name: "products",
|
|
1982
|
-
attributes: { name: "string", description: "string", price: "number" },
|
|
1983
|
-
options: { behavior: "data-truncate" }
|
|
1984
|
-
});
|
|
1985
|
-
```
|
|
1063
|
+
This means you can use, modify, and distribute this software for any purpose without any restrictions. It's truly free and open source! ๐
|
|
1986
1064
|
|
|
1987
|
-
|
|
1988
|
-
- **`user-management`**: Development, testing, or when you want full control
|
|
1989
|
-
- **`enforce-limits`**: Production systems requiring strict data validation
|
|
1990
|
-
- **`body-overflow`**: When data integrity is critical and you need to preserve all information
|
|
1991
|
-
- **`data-truncate`**: When you can afford to lose some data but want to maintain structure
|
|
1065
|
+
---
|
|
1992
1066
|
|
|
1993
|
-
|
|
1067
|
+
<p align="center">
|
|
1068
|
+
<strong>Made with โค๏ธ by developers, for developers</strong><br>
|
|
1069
|
+
<a href="https://github.com/forattini-dev/s3db.js">โญ Star us on GitHub</a> โข
|
|
1070
|
+
<a href="https://www.npmjs.com/package/s3db.js">๐ฆ View on NPM</a> โข
|
|
1071
|
+
<a href="https://github.com/forattini-dev/s3db.js/issues">๐ Report Issues</a>
|
|
1072
|
+
</p>
|
|
1994
1073
|
|
|
1995
|
-
|
|
1996
|
-
|
|
1074
|
+
<p align="center">
|
|
1075
|
+
<sub>Built with Node.js โข Powered by AWS S3 โข Streaming Ready</sub>
|
|
1076
|
+
</p>
|