@arcaelas/dynamite 1.0.13 → 1.0.14
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 +1165 -152
- package/package.json +6 -9
- package/LICENSE.txt +0 -32
- package/README.txt +0 -206
- package/SECURITY.md +0 -41
- package/src/@types/index.d.ts +0 -96
- package/src/@types/index.js +0 -9
- package/src/core/client.d.ts +0 -69
- package/src/core/client.js +0 -164
- package/src/core/table.d.ts +0 -98
- package/src/core/table.js +0 -459
- package/src/core/wrapper.d.ts +0 -17
- package/src/core/wrapper.js +0 -46
- package/src/decorators/belongs_to.d.ts +0 -1
- package/src/decorators/belongs_to.js +0 -24
- package/src/decorators/created_at.d.ts +0 -1
- package/src/decorators/created_at.js +0 -11
- package/src/decorators/default.d.ts +0 -1
- package/src/decorators/default.js +0 -47
- package/src/decorators/has_many.d.ts +0 -1
- package/src/decorators/has_many.js +0 -24
- package/src/decorators/index.d.ts +0 -11
- package/src/decorators/index.js +0 -36
- package/src/decorators/index_sort.d.ts +0 -12
- package/src/decorators/index_sort.js +0 -43
- package/src/decorators/mutate.d.ts +0 -2
- package/src/decorators/mutate.js +0 -51
- package/src/decorators/name.d.ts +0 -1
- package/src/decorators/name.js +0 -28
- package/src/decorators/not_null.d.ts +0 -1
- package/src/decorators/not_null.js +0 -13
- package/src/decorators/primary_key.d.ts +0 -6
- package/src/decorators/primary_key.js +0 -30
- package/src/decorators/updated_at.d.ts +0 -12
- package/src/decorators/updated_at.js +0 -26
- package/src/decorators/validate.d.ts +0 -1
- package/src/decorators/validate.js +0 -53
- package/src/index.d.ts +0 -22
- package/src/index.js +0 -47
- package/src/utils/batch-relations.d.ts +0 -14
- package/src/utils/batch-relations.js +0 -131
- package/src/utils/circular-detector.d.ts +0 -82
- package/src/utils/circular-detector.js +0 -209
- package/src/utils/memory-manager.d.ts +0 -42
- package/src/utils/memory-manager.js +0 -108
- package/src/utils/naming.d.ts +0 -8
- package/src/utils/naming.js +0 -18
- package/src/utils/projection.d.ts +0 -12
- package/src/utils/projection.js +0 -51
- package/src/utils/relations.d.ts +0 -17
- package/src/utils/relations.js +0 -166
- package/src/utils/security-validator.d.ts +0 -49
- package/src/utils/security-validator.js +0 -152
- package/src/utils/throttle-manager.d.ts +0 -78
- package/src/utils/throttle-manager.js +0 -196
- package/src/utils/transaction-manager.d.ts +0 -88
- package/src/utils/transaction-manager.js +0 -298
package/README.md
CHANGED
|
@@ -1,259 +1,1272 @@
|
|
|
1
1
|

|
|
2
2
|

|
|
3
3
|
|
|
4
|
-
#
|
|
4
|
+
# @arcaelas/dynamite
|
|
5
5
|
|
|
6
|
-
> A
|
|
7
|
-
>
|
|
8
|
-
> _Auto‑provisions tables · Runs anywhere Node.js runs · Written in TypeScript only_
|
|
6
|
+
> **A modern, decorator-first ORM for DynamoDB with TypeScript support**
|
|
7
|
+
> Full-featured • Type-safe • Relationship support • Auto table creation • Zero boilerplate
|
|
9
8
|
|
|
10
9
|
<p align="center">
|
|
11
|
-
<a href="https://www.npmjs.com/package/@arcaelas/
|
|
12
|
-
<img src="https://img.shields.io/bundlephobia/minzip/@arcaelas/
|
|
13
|
-
<img src="https://img.shields.io/github/license/arcaelas/
|
|
10
|
+
<a href="https://www.npmjs.com/package/@arcaelas/dynamite"><img src="https://img.shields.io/npm/v/@arcaelas/dynamite?color=cb3837" alt="npm"></a>
|
|
11
|
+
<img src="https://img.shields.io/bundlephobia/minzip/@arcaelas/dynamite?label=gzip" alt="size">
|
|
12
|
+
<img src="https://img.shields.io/github/license/arcaelas/dynamite" alt="MIT">
|
|
13
|
+
<img src="https://img.shields.io/badge/AWS%20SDK-v3-orange" alt="AWS SDK v3">
|
|
14
|
+
<img src="https://img.shields.io/badge/TypeScript-5.x-blue" alt="TypeScript">
|
|
14
15
|
</p>
|
|
15
16
|
|
|
16
17
|
---
|
|
17
18
|
|
|
18
|
-
## Contents
|
|
19
|
+
## 📚 Table of Contents
|
|
20
|
+
|
|
21
|
+
- [🚀 Quick Start](#-quick-start)
|
|
22
|
+
- [📦 Installation](#-installation)
|
|
23
|
+
- [⚡ Basic Usage](#-basic-usage)
|
|
24
|
+
- [🎯 Decorators Reference](#-decorators-reference)
|
|
25
|
+
- [🔍 Query Operations](#-query-operations)
|
|
26
|
+
- [🔗 Relationships](#-relationships)
|
|
27
|
+
- [📝 TypeScript Types](#-typescript-types)
|
|
28
|
+
- [🛠️ Advanced Features](#-advanced-features)
|
|
29
|
+
- [⚙️ Configuration](#-configuration)
|
|
30
|
+
- [📖 API Reference](#-api-reference)
|
|
31
|
+
- [🔧 Development Setup](#-development-setup)
|
|
32
|
+
- [❓ Troubleshooting](#-troubleshooting)
|
|
19
33
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 🚀 Quick Start
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import {
|
|
40
|
+
Table,
|
|
41
|
+
PrimaryKey,
|
|
42
|
+
Default,
|
|
43
|
+
CreatedAt,
|
|
44
|
+
UpdatedAt,
|
|
45
|
+
CreationOptional,
|
|
46
|
+
NonAttribute
|
|
47
|
+
} from "@arcaelas/dynamite";
|
|
48
|
+
import { Dynamite } from "@arcaelas/dynamite";
|
|
49
|
+
|
|
50
|
+
// Configure connection
|
|
51
|
+
Dynamite.config({
|
|
52
|
+
region: "us-east-1",
|
|
53
|
+
// For local development
|
|
54
|
+
endpoint: "http://localhost:8000",
|
|
55
|
+
credentials: { accessKeyId: "test", secretAccessKey: "test" }
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Define your model
|
|
59
|
+
class User extends Table<User> {
|
|
60
|
+
@PrimaryKey()
|
|
61
|
+
@Default(() => crypto.randomUUID())
|
|
62
|
+
declare id: CreationOptional<string>;
|
|
63
|
+
|
|
64
|
+
@Default(() => "")
|
|
65
|
+
declare name: CreationOptional<string>;
|
|
24
66
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- [Serialization](#serialization)
|
|
67
|
+
@Default(() => "customer")
|
|
68
|
+
declare role: CreationOptional<string>;
|
|
28
69
|
|
|
29
|
-
|
|
70
|
+
@CreatedAt()
|
|
71
|
+
declare createdAt: CreationOptional<string>;
|
|
30
72
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- [Running on DynamoDB Local](#running-on-dynamodb-local)
|
|
73
|
+
@UpdatedAt()
|
|
74
|
+
declare updatedAt: CreationOptional<string>;
|
|
34
75
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
76
|
+
// Computed property (not stored in database)
|
|
77
|
+
declare displayName: NonAttribute<string>;
|
|
78
|
+
|
|
79
|
+
constructor(data?: any) {
|
|
80
|
+
super(data);
|
|
81
|
+
|
|
82
|
+
// Define computed property
|
|
83
|
+
Object.defineProperty(this, 'displayName', {
|
|
84
|
+
get: () => `${this.name} (${this.role})`,
|
|
85
|
+
enumerable: true
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Use it!
|
|
91
|
+
const user = await User.create({
|
|
92
|
+
name: "John Doe"
|
|
93
|
+
// id, role, createdAt, updatedAt are optional (CreationOptional)
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log(user.name); // "John Doe"
|
|
97
|
+
console.log(user.role); // "customer"
|
|
98
|
+
console.log(user.displayName); // "John Doe (customer)"
|
|
99
|
+
console.log(user.createdAt); // "2023-12-01T10:30:00.000Z"
|
|
100
|
+
```
|
|
39
101
|
|
|
40
102
|
---
|
|
41
103
|
|
|
42
|
-
##
|
|
104
|
+
## 📦 Installation
|
|
43
105
|
|
|
44
106
|
```bash
|
|
45
|
-
npm
|
|
46
|
-
|
|
47
|
-
|
|
107
|
+
npm install @arcaelas/dynamite
|
|
108
|
+
|
|
109
|
+
# Peer dependencies (if not already installed)
|
|
110
|
+
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
48
111
|
```
|
|
49
112
|
|
|
50
113
|
---
|
|
51
114
|
|
|
52
|
-
##
|
|
115
|
+
## ⚡ Basic Usage
|
|
53
116
|
|
|
54
|
-
|
|
55
|
-
import {
|
|
56
|
-
connect,
|
|
57
|
-
Table,
|
|
58
|
-
Index, // PK
|
|
59
|
-
CreatedAt,
|
|
60
|
-
UpdatedAt,
|
|
61
|
-
Default,
|
|
62
|
-
} from "@arcaelas/dinamite";
|
|
117
|
+
### Table Definition
|
|
63
118
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
119
|
+
```typescript
|
|
120
|
+
import {
|
|
121
|
+
Table,
|
|
122
|
+
PrimaryKey,
|
|
123
|
+
Default,
|
|
124
|
+
Validate,
|
|
125
|
+
Mutate,
|
|
126
|
+
NotNull,
|
|
127
|
+
Name
|
|
128
|
+
} from "@arcaelas/dynamite";
|
|
70
129
|
|
|
71
|
-
|
|
72
|
-
|
|
130
|
+
@Name("custom_users") // Override table name
|
|
131
|
+
class User extends Table<User> {
|
|
132
|
+
@PrimaryKey()
|
|
73
133
|
declare id: string;
|
|
74
134
|
|
|
135
|
+
@NotNull()
|
|
136
|
+
@Mutate((value) => (value as string).toLowerCase().trim())
|
|
137
|
+
@Validate((value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value as string) || "Invalid email")
|
|
138
|
+
declare email: string;
|
|
139
|
+
|
|
75
140
|
@Default(() => "")
|
|
76
141
|
declare name: string;
|
|
77
142
|
|
|
78
|
-
@
|
|
79
|
-
|
|
143
|
+
@Default(() => 18)
|
|
144
|
+
@Validate((value) => (value as number) >= 0 || "Age must be positive")
|
|
145
|
+
declare age: number;
|
|
146
|
+
|
|
147
|
+
@Default(() => true)
|
|
148
|
+
declare active: boolean;
|
|
149
|
+
|
|
150
|
+
@CreatedAt()
|
|
151
|
+
declare createdAt: string;
|
|
80
152
|
|
|
81
153
|
@UpdatedAt()
|
|
82
|
-
declare
|
|
154
|
+
declare updatedAt: string;
|
|
83
155
|
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### CRUD Operations
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// CREATE
|
|
162
|
+
const user = await User.create({
|
|
163
|
+
id: "user-123",
|
|
164
|
+
email: "john@example.com",
|
|
165
|
+
name: "John Doe",
|
|
166
|
+
age: 25
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// READ
|
|
170
|
+
const allUsers = await User.where({});
|
|
171
|
+
const activeUsers = await User.where({ active: true });
|
|
172
|
+
const userById = await User.first({ id: "user-123" });
|
|
173
|
+
|
|
174
|
+
// UPDATE
|
|
175
|
+
await User.update("user-123", { name: "John Smith" });
|
|
176
|
+
// or
|
|
177
|
+
user.name = "John Smith";
|
|
178
|
+
await user.save();
|
|
179
|
+
|
|
180
|
+
// DELETE
|
|
181
|
+
await User.delete("user-123");
|
|
182
|
+
// or
|
|
183
|
+
await user.destroy();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 🎯 Decorators Reference
|
|
189
|
+
|
|
190
|
+
### Core Decorators
|
|
191
|
+
|
|
192
|
+
| Decorator | Purpose | Example |
|
|
193
|
+
|-----------|---------|---------|
|
|
194
|
+
| `@PrimaryKey()` | Primary key (partition key) | `@PrimaryKey() declare id: string;` |
|
|
195
|
+
| `@Index()` | Partition key (alias for PrimaryKey) | `@Index() declare userId: string;` |
|
|
196
|
+
| `@IndexSort()` | Sort key | `@IndexSort() declare timestamp: string;` |
|
|
197
|
+
| `@Name("custom")` | Custom column/table name | `@Name("user_email") declare email: string;` |
|
|
84
198
|
|
|
85
|
-
|
|
199
|
+
### Data Decorators
|
|
86
200
|
|
|
87
|
-
|
|
88
|
-
|
|
201
|
+
| Decorator | Purpose | Example |
|
|
202
|
+
|-----------|---------|---------|
|
|
203
|
+
| `@Default(value\|fn)` | Default value | `@Default(() => uuid()) declare id: string;` |
|
|
204
|
+
| `@Mutate(fn)` | Transform value | `@Mutate((v) => v.toLowerCase()) declare email: string;` |
|
|
205
|
+
| `@Validate(fn)` | Validation function | `@Validate((v) => v.length > 0 \|\| "Required") declare name: string;` |
|
|
206
|
+
| `@NotNull()` | Not null validation | `@NotNull() declare email: string;` |
|
|
89
207
|
|
|
90
|
-
|
|
91
|
-
|
|
208
|
+
### Timestamp Decorators
|
|
209
|
+
|
|
210
|
+
| Decorator | Purpose | Example |
|
|
211
|
+
|-----------|---------|---------|
|
|
212
|
+
| `@CreatedAt()` | Set on creation | `@CreatedAt() declare createdAt: string;` |
|
|
213
|
+
| `@UpdatedAt()` | Set on every update | `@UpdatedAt() declare updatedAt: string;` |
|
|
214
|
+
|
|
215
|
+
### Relationship Decorators
|
|
216
|
+
|
|
217
|
+
| Decorator | Purpose | Example |
|
|
218
|
+
|-----------|---------|---------|
|
|
219
|
+
| `@HasMany(Model, foreignKey)` | One-to-many | `@HasMany(() => Order, "user_id") declare orders: any;` |
|
|
220
|
+
| `@BelongsTo(Model, localKey)` | Many-to-one | `@BelongsTo(() => User, "user_id") declare user: any;` |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 🔍 Query Operations
|
|
225
|
+
|
|
226
|
+
### Basic Queries
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Get all records
|
|
230
|
+
const users = await User.where({});
|
|
231
|
+
|
|
232
|
+
// Filter by field
|
|
233
|
+
const activeUsers = await User.where({ active: true });
|
|
234
|
+
const johnUsers = await User.where({ name: "John" });
|
|
235
|
+
|
|
236
|
+
// Get first/last record
|
|
237
|
+
const firstUser = await User.first({ active: true });
|
|
238
|
+
const lastUser = await User.last({ active: true });
|
|
92
239
|
```
|
|
93
240
|
|
|
94
|
-
|
|
241
|
+
### Advanced Queries with Operators
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// Comparison operators
|
|
245
|
+
const adults = await User.where("age", ">=", 18);
|
|
246
|
+
const youngAdults = await User.where("age", "<", 30);
|
|
247
|
+
const specificAges = await User.where("age", "in", [25, 30, 35]);
|
|
248
|
+
const excludeAges = await User.where("age", "not-in", [16, 17]);
|
|
249
|
+
|
|
250
|
+
// String operators
|
|
251
|
+
const gmailUsers = await User.where("email", "contains", "gmail");
|
|
252
|
+
const usersByPrefix = await User.where("name", "begins-with", "John");
|
|
253
|
+
|
|
254
|
+
// Not equal
|
|
255
|
+
const nonAdmins = await User.where("role", "!=", "admin");
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Query Options
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// Pagination and limiting
|
|
262
|
+
const users = await User.where({}, {
|
|
263
|
+
limit: 10,
|
|
264
|
+
skip: 20
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Sorting
|
|
268
|
+
const users = await User.where({}, {
|
|
269
|
+
order: "ASC" // or "DESC"
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Select specific attributes
|
|
273
|
+
const users = await User.where({}, {
|
|
274
|
+
attributes: ["id", "name", "email"]
|
|
275
|
+
});
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Method Chaining Alternative
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// Using query builder style
|
|
282
|
+
const users = await User
|
|
283
|
+
.where("age", ">=", 18)
|
|
284
|
+
.where("active", true);
|
|
285
|
+
|
|
286
|
+
// Complex conditions
|
|
287
|
+
const users = await User.where({
|
|
288
|
+
age: 25,
|
|
289
|
+
active: true,
|
|
290
|
+
role: "customer"
|
|
291
|
+
});
|
|
292
|
+
```
|
|
95
293
|
|
|
96
294
|
---
|
|
97
295
|
|
|
98
|
-
##
|
|
296
|
+
## 🔗 Relationships
|
|
297
|
+
|
|
298
|
+
### Defining Relationships
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// User model
|
|
302
|
+
class User extends Table<User> {
|
|
303
|
+
@PrimaryKey()
|
|
304
|
+
declare id: string;
|
|
305
|
+
|
|
306
|
+
@HasMany(() => Order, "user_id")
|
|
307
|
+
declare orders: any;
|
|
308
|
+
|
|
309
|
+
@HasMany(() => Review, "user_id")
|
|
310
|
+
declare reviews: any;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Order model
|
|
314
|
+
class Order extends Table<Order> {
|
|
315
|
+
@PrimaryKey()
|
|
316
|
+
declare id: string;
|
|
317
|
+
|
|
318
|
+
@NotNull()
|
|
319
|
+
declare user_id: string;
|
|
320
|
+
|
|
321
|
+
@BelongsTo(() => User, "user_id")
|
|
322
|
+
declare user: any;
|
|
323
|
+
|
|
324
|
+
@HasMany(() => OrderItem, "order_id")
|
|
325
|
+
declare items: any;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// OrderItem model
|
|
329
|
+
class OrderItem extends Table<OrderItem> {
|
|
330
|
+
@PrimaryKey()
|
|
331
|
+
declare id: string;
|
|
332
|
+
|
|
333
|
+
@NotNull()
|
|
334
|
+
declare order_id: string;
|
|
335
|
+
|
|
336
|
+
@NotNull()
|
|
337
|
+
declare product_id: string;
|
|
338
|
+
|
|
339
|
+
@BelongsTo(() => Order, "order_id")
|
|
340
|
+
declare order: any;
|
|
99
341
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
| `@Mutate(fn)` | Sequential value transformer. Runs before validators. | |
|
|
107
|
-
| `@Validate(fn\[])` | Sync validator(s); return `true` or error `string`. | |
|
|
108
|
-
| `@NotNull()` | Built‑in not‑null / not‑empty validation. | |
|
|
109
|
-
| `@CreatedAt()` | Timestamp (ISO) on first assignment. | |
|
|
110
|
-
| `@UpdatedAt()` | Timestamp (ISO) **every** assignment. | |
|
|
111
|
-
| `@Name("alias")` | Override table _or_ column name. | |
|
|
342
|
+
@BelongsTo(() => Product, "product_id")
|
|
343
|
+
declare product: any;
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Loading Relationships
|
|
112
348
|
|
|
113
|
-
|
|
349
|
+
```typescript
|
|
350
|
+
// Load with relationships
|
|
351
|
+
const usersWithOrders = await User.where({}, {
|
|
352
|
+
include: {
|
|
353
|
+
orders: {}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Nested relationships
|
|
358
|
+
const usersWithCompleteData = await User.where({}, {
|
|
359
|
+
include: {
|
|
360
|
+
orders: {
|
|
361
|
+
include: {
|
|
362
|
+
items: {
|
|
363
|
+
include: {
|
|
364
|
+
product: {}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Filtered relationships
|
|
373
|
+
const usersWithRecentOrders = await User.where({}, {
|
|
374
|
+
include: {
|
|
375
|
+
orders: {
|
|
376
|
+
where: { status: "completed" },
|
|
377
|
+
limit: 5,
|
|
378
|
+
order: "DESC"
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Relationship with specific attributes
|
|
384
|
+
const usersWithOrderSummary = await User.where({}, {
|
|
385
|
+
include: {
|
|
386
|
+
orders: {
|
|
387
|
+
attributes: ["id", "total", "status"],
|
|
388
|
+
where: { status: "completed" }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
```
|
|
114
393
|
|
|
115
394
|
---
|
|
116
395
|
|
|
117
|
-
##
|
|
396
|
+
## 📝 TypeScript Types
|
|
397
|
+
|
|
398
|
+
Dynamite provides essential TypeScript types that are fundamental for proper model definition and type safety. These types help you define optional fields, exclude computed properties, and establish relationships.
|
|
399
|
+
|
|
400
|
+
### Core Types
|
|
401
|
+
|
|
402
|
+
#### `CreationOptional<T>`
|
|
403
|
+
|
|
404
|
+
Marks a field as optional during creation but required in the actual model instance. **Always use for auto-generated fields**: `id` (with @PrimaryKey), `createdAt` (@CreatedAt), `updatedAt` (@UpdatedAt), and any field with @Default decorator.
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { Table, PrimaryKey, Default, CreatedAt, UpdatedAt, CreationOptional } from "@arcaelas/dynamite";
|
|
408
|
+
|
|
409
|
+
class User extends Table<User> {
|
|
410
|
+
// Always CreationOptional - auto-generated ID
|
|
411
|
+
@PrimaryKey()
|
|
412
|
+
@Default(() => crypto.randomUUID())
|
|
413
|
+
declare id: CreationOptional<string>;
|
|
414
|
+
|
|
415
|
+
// Required fields during creation
|
|
416
|
+
declare name: string;
|
|
417
|
+
declare email: string;
|
|
418
|
+
|
|
419
|
+
// Always CreationOptional - has default value
|
|
420
|
+
@Default(() => "customer")
|
|
421
|
+
declare role: CreationOptional<string>;
|
|
422
|
+
|
|
423
|
+
// Always CreationOptional - auto-set timestamps
|
|
424
|
+
@CreatedAt()
|
|
425
|
+
declare createdAt: CreationOptional<string>;
|
|
426
|
+
|
|
427
|
+
@UpdatedAt()
|
|
428
|
+
declare updatedAt: CreationOptional<string>;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Usage - TypeScript knows exactly what's required
|
|
432
|
+
const user = await User.create({
|
|
433
|
+
name: "John Doe", // Required
|
|
434
|
+
email: "john@test.com" // Required
|
|
435
|
+
// id, role, createdAt, updatedAt are automatically optional
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Rule of thumb**: Use `CreationOptional<T>` for:
|
|
440
|
+
- `@PrimaryKey()` with `@Default()` → Always optional
|
|
441
|
+
- `@CreatedAt()` → Always optional
|
|
442
|
+
- `@UpdatedAt()` → Always optional
|
|
443
|
+
- Any field with `@Default()` → Always optional
|
|
444
|
+
|
|
445
|
+
#### `NonAttribute<T>`
|
|
446
|
+
|
|
447
|
+
Excludes a field from database operations while keeping it in the TypeScript interface. Used for computed properties, getters, or virtual fields.
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
import { Table, PrimaryKey, NonAttribute } from "@arcaelas/dynamite";
|
|
118
451
|
|
|
119
|
-
|
|
452
|
+
class User extends Table<User> {
|
|
453
|
+
@PrimaryKey()
|
|
454
|
+
declare id: string;
|
|
120
455
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
456
|
+
declare firstName: string;
|
|
457
|
+
declare lastName: string;
|
|
458
|
+
declare birthDate: string;
|
|
459
|
+
|
|
460
|
+
// Computed property - not stored in database
|
|
461
|
+
declare fullName: NonAttribute<string>;
|
|
462
|
+
declare age: NonAttribute<number>;
|
|
463
|
+
|
|
464
|
+
// Getter methods as non-attributes
|
|
465
|
+
declare getDisplayName: NonAttribute<() => string>;
|
|
466
|
+
|
|
467
|
+
constructor(data?: any) {
|
|
468
|
+
super(data);
|
|
469
|
+
|
|
470
|
+
// Define computed properties
|
|
471
|
+
Object.defineProperty(this, 'fullName', {
|
|
472
|
+
get: () => `${this.firstName} ${this.lastName}`,
|
|
473
|
+
enumerable: true
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
Object.defineProperty(this, 'age', {
|
|
477
|
+
get: () => {
|
|
478
|
+
const today = new Date();
|
|
479
|
+
const birth = new Date(this.birthDate);
|
|
480
|
+
return today.getFullYear() - birth.getFullYear();
|
|
481
|
+
},
|
|
482
|
+
enumerable: true
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
Object.defineProperty(this, 'getDisplayName', {
|
|
486
|
+
value: () => this.fullName.toUpperCase(),
|
|
487
|
+
enumerable: false
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Usage
|
|
493
|
+
const user = await User.create({
|
|
494
|
+
id: "user-1",
|
|
495
|
+
firstName: "John",
|
|
496
|
+
lastName: "Doe",
|
|
497
|
+
birthDate: "1990-01-01"
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
console.log(user.fullName); // "John Doe" (not stored in DB)
|
|
501
|
+
console.log(user.age); // 34 (computed)
|
|
502
|
+
console.log(user.getDisplayName()); // "JOHN DOE"
|
|
126
503
|
```
|
|
127
504
|
|
|
128
|
-
###
|
|
505
|
+
### Relationship Types
|
|
506
|
+
|
|
507
|
+
#### `HasMany<T>`
|
|
129
508
|
|
|
130
|
-
|
|
131
|
-
const u = new User({ id: "42", name: "Neo" });
|
|
132
|
-
await u.save(); // inserts
|
|
509
|
+
Defines a one-to-many relationship where the model can have multiple related instances.
|
|
133
510
|
|
|
134
|
-
|
|
135
|
-
|
|
511
|
+
```typescript
|
|
512
|
+
import { Table, PrimaryKey, HasMany, NonAttribute } from "@arcaelas/dynamite";
|
|
513
|
+
|
|
514
|
+
class User extends Table<User> {
|
|
515
|
+
@PrimaryKey()
|
|
516
|
+
declare id: string;
|
|
136
517
|
|
|
137
|
-
|
|
138
|
-
|
|
518
|
+
declare name: string;
|
|
519
|
+
declare email: string;
|
|
520
|
+
|
|
521
|
+
// One-to-many: User has many Orders
|
|
522
|
+
@HasMany(() => Order, "user_id")
|
|
523
|
+
declare orders: NonAttribute<HasMany<Order>>;
|
|
524
|
+
|
|
525
|
+
// One-to-many: User has many Reviews
|
|
526
|
+
@HasMany(() => Review, "user_id")
|
|
527
|
+
declare reviews: NonAttribute<HasMany<Review>>;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
class Order extends Table<Order> {
|
|
531
|
+
@PrimaryKey()
|
|
532
|
+
declare id: string;
|
|
533
|
+
|
|
534
|
+
declare user_id: string;
|
|
535
|
+
declare total: number;
|
|
536
|
+
declare status: string;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
class Review extends Table<Review> {
|
|
540
|
+
@PrimaryKey()
|
|
541
|
+
declare id: string;
|
|
542
|
+
|
|
543
|
+
declare user_id: string;
|
|
544
|
+
declare rating: number;
|
|
545
|
+
declare comment: string;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Usage
|
|
549
|
+
const userWithOrders = await User.where({ id: "user-1" }, {
|
|
550
|
+
include: {
|
|
551
|
+
orders: {
|
|
552
|
+
where: { status: "completed" },
|
|
553
|
+
limit: 10
|
|
554
|
+
},
|
|
555
|
+
reviews: {
|
|
556
|
+
where: { rating: { $gte: 4 } }
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// TypeScript knows these are arrays
|
|
562
|
+
console.log(userWithOrders[0].orders.length); // number
|
|
563
|
+
console.log(userWithOrders[0].reviews[0].rating); // number
|
|
139
564
|
```
|
|
140
565
|
|
|
141
|
-
|
|
566
|
+
#### `BelongsTo<T>`
|
|
567
|
+
|
|
568
|
+
Defines a many-to-one relationship where the model belongs to a single parent instance.
|
|
142
569
|
|
|
143
|
-
|
|
144
|
-
|
|
570
|
+
```typescript
|
|
571
|
+
import { Table, PrimaryKey, BelongsTo, NonAttribute } from "@arcaelas/dynamite";
|
|
572
|
+
|
|
573
|
+
class Order extends Table<Order> {
|
|
574
|
+
@PrimaryKey()
|
|
575
|
+
declare id: string;
|
|
576
|
+
|
|
577
|
+
// Foreign key
|
|
578
|
+
@NotNull()
|
|
579
|
+
declare user_id: string;
|
|
580
|
+
|
|
581
|
+
@NotNull()
|
|
582
|
+
declare category_id: string;
|
|
583
|
+
|
|
584
|
+
declare total: number;
|
|
585
|
+
declare status: string;
|
|
586
|
+
|
|
587
|
+
// Many-to-one: Order belongs to User
|
|
588
|
+
@BelongsTo(() => User, "user_id")
|
|
589
|
+
declare user: NonAttribute<BelongsTo<User>>;
|
|
590
|
+
|
|
591
|
+
// Many-to-one: Order belongs to Category
|
|
592
|
+
@BelongsTo(() => Category, "category_id")
|
|
593
|
+
declare category: NonAttribute<BelongsTo<Category>>;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
class User extends Table<User> {
|
|
597
|
+
@PrimaryKey()
|
|
598
|
+
declare id: string;
|
|
599
|
+
|
|
600
|
+
declare name: string;
|
|
601
|
+
declare email: string;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
class Category extends Table<Category> {
|
|
605
|
+
@PrimaryKey()
|
|
606
|
+
declare id: string;
|
|
607
|
+
|
|
608
|
+
declare name: string;
|
|
609
|
+
declare description: string;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Usage
|
|
613
|
+
const orderWithRelations = await Order.where({ id: "order-1" }, {
|
|
614
|
+
include: {
|
|
615
|
+
user: {
|
|
616
|
+
attributes: ["id", "name", "email"]
|
|
617
|
+
},
|
|
618
|
+
category: {}
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// TypeScript knows these can be null or the related type
|
|
623
|
+
if (orderWithRelations[0].user) {
|
|
624
|
+
console.log(orderWithRelations[0].user.name); // string
|
|
625
|
+
}
|
|
626
|
+
if (orderWithRelations[0].category) {
|
|
627
|
+
console.log(orderWithRelations[0].category.name); // string
|
|
628
|
+
}
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Advanced Type Combinations
|
|
632
|
+
|
|
633
|
+
#### Complete Model Example
|
|
634
|
+
|
|
635
|
+
```typescript
|
|
636
|
+
import {
|
|
637
|
+
Table,
|
|
638
|
+
PrimaryKey,
|
|
639
|
+
Default,
|
|
640
|
+
CreatedAt,
|
|
641
|
+
UpdatedAt,
|
|
642
|
+
HasMany,
|
|
643
|
+
BelongsTo,
|
|
644
|
+
CreationOptional,
|
|
645
|
+
NonAttribute
|
|
646
|
+
} from "@arcaelas/dynamite";
|
|
647
|
+
|
|
648
|
+
class User extends Table<User> {
|
|
649
|
+
// Always CreationOptional - auto-generated primary key
|
|
650
|
+
@PrimaryKey()
|
|
651
|
+
@Default(() => crypto.randomUUID())
|
|
652
|
+
declare id: CreationOptional<string>;
|
|
653
|
+
|
|
654
|
+
// Required fields during creation
|
|
655
|
+
declare firstName: string;
|
|
656
|
+
declare lastName: string;
|
|
657
|
+
declare email: string;
|
|
658
|
+
|
|
659
|
+
// Always CreationOptional - has default values
|
|
660
|
+
@Default(() => "customer")
|
|
661
|
+
declare role: CreationOptional<string>;
|
|
662
|
+
|
|
663
|
+
@Default(() => true)
|
|
664
|
+
declare active: CreationOptional<boolean>;
|
|
665
|
+
|
|
666
|
+
// Always CreationOptional - auto-set timestamps
|
|
667
|
+
@CreatedAt()
|
|
668
|
+
declare createdAt: CreationOptional<string>;
|
|
669
|
+
|
|
670
|
+
@UpdatedAt()
|
|
671
|
+
declare updatedAt: CreationOptional<string>;
|
|
672
|
+
|
|
673
|
+
// Computed properties (not stored)
|
|
674
|
+
declare fullName: NonAttribute<string>;
|
|
675
|
+
declare displayRole: NonAttribute<string>;
|
|
676
|
+
|
|
677
|
+
// Relationships (not stored directly)
|
|
678
|
+
@HasMany(() => Order, "user_id")
|
|
679
|
+
declare orders: NonAttribute<HasMany<Order>>;
|
|
680
|
+
|
|
681
|
+
@HasMany(() => Review, "user_id")
|
|
682
|
+
declare reviews: NonAttribute<HasMany<Review>>;
|
|
683
|
+
|
|
684
|
+
constructor(data?: any) {
|
|
685
|
+
super(data);
|
|
686
|
+
|
|
687
|
+
// Define computed properties
|
|
688
|
+
Object.defineProperty(this, 'fullName', {
|
|
689
|
+
get: () => `${this.firstName} ${this.lastName}`,
|
|
690
|
+
enumerable: true
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
Object.defineProperty(this, 'displayRole', {
|
|
694
|
+
get: () => this.role.charAt(0).toUpperCase() + this.role.slice(1),
|
|
695
|
+
enumerable: true
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
class Order extends Table<Order> {
|
|
701
|
+
// Always CreationOptional - auto-generated ID
|
|
702
|
+
@PrimaryKey()
|
|
703
|
+
@Default(() => crypto.randomUUID())
|
|
704
|
+
declare id: CreationOptional<string>;
|
|
705
|
+
|
|
706
|
+
// Required field during creation
|
|
707
|
+
declare user_id: string;
|
|
708
|
+
declare total: number;
|
|
709
|
+
|
|
710
|
+
// Always CreationOptional - has default value
|
|
711
|
+
@Default(() => "pending")
|
|
712
|
+
declare status: CreationOptional<string>;
|
|
713
|
+
|
|
714
|
+
// Always CreationOptional - auto-set timestamp
|
|
715
|
+
@CreatedAt()
|
|
716
|
+
declare createdAt: CreationOptional<string>;
|
|
717
|
+
|
|
718
|
+
// Relationship
|
|
719
|
+
@BelongsTo(() => User, "user_id")
|
|
720
|
+
declare user: NonAttribute<BelongsTo<User>>;
|
|
721
|
+
|
|
722
|
+
// Computed total with tax
|
|
723
|
+
declare totalWithTax: NonAttribute<number>;
|
|
724
|
+
|
|
725
|
+
constructor(data?: any) {
|
|
726
|
+
super(data);
|
|
727
|
+
|
|
728
|
+
Object.defineProperty(this, 'totalWithTax', {
|
|
729
|
+
get: () => this.total * 1.1, // 10% tax
|
|
730
|
+
enumerable: true
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Perfect TypeScript inference
|
|
736
|
+
const createUser = async () => {
|
|
737
|
+
// TypeScript knows what's required vs optional
|
|
738
|
+
const user = await User.create({
|
|
739
|
+
firstName: "John", // required
|
|
740
|
+
lastName: "Doe", // required
|
|
741
|
+
email: "john@test.com" // required
|
|
742
|
+
// id, role, active, createdAt, updatedAt are optional
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// Computed properties work immediately
|
|
746
|
+
console.log(user.fullName); // "John Doe"
|
|
747
|
+
console.log(user.displayRole); // "Customer"
|
|
748
|
+
|
|
749
|
+
return user;
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
// Load with relationships
|
|
753
|
+
const getUserWithOrders = async (userId: string) => {
|
|
754
|
+
const users = await User.where({ id: userId }, {
|
|
755
|
+
include: {
|
|
756
|
+
orders: {
|
|
757
|
+
include: {
|
|
758
|
+
user: {} // Recursive relationship
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const user = users[0];
|
|
765
|
+
if (user?.orders?.length > 0) {
|
|
766
|
+
console.log(`${user.fullName} has ${user.orders.length} orders`);
|
|
767
|
+
user.orders.forEach(order => {
|
|
768
|
+
console.log(`Order ${order.id}: $${order.totalWithTax}`);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return user;
|
|
773
|
+
};
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### Type Inference Benefits
|
|
777
|
+
|
|
778
|
+
```typescript
|
|
779
|
+
// TypeScript will infer all the correct types
|
|
780
|
+
type UserCreationAttributes = {
|
|
781
|
+
firstName: string; // Required
|
|
782
|
+
lastName: string; // Required
|
|
783
|
+
email: string; // Required
|
|
784
|
+
// All these are automatically optional (CreationOptional):
|
|
785
|
+
id?: string; // @PrimaryKey + @Default
|
|
786
|
+
role?: string; // @Default
|
|
787
|
+
active?: boolean; // @Default
|
|
788
|
+
createdAt?: string; // @CreatedAt (always optional)
|
|
789
|
+
updatedAt?: string; // @UpdatedAt (always optional)
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
type UserAttributes = {
|
|
793
|
+
// All these exist in the instance (required after creation)
|
|
794
|
+
id: string; // CreationOptional but exists after creation
|
|
795
|
+
firstName: string;
|
|
796
|
+
lastName: string;
|
|
797
|
+
email: string;
|
|
798
|
+
role: string; // CreationOptional but exists after creation
|
|
799
|
+
active: boolean; // CreationOptional but exists after creation
|
|
800
|
+
createdAt: string; // CreationOptional but exists after creation
|
|
801
|
+
updatedAt: string; // CreationOptional but exists after creation
|
|
802
|
+
fullName: string; // NonAttribute computed property
|
|
803
|
+
displayRole: string; // NonAttribute computed property
|
|
804
|
+
orders: Order[]; // HasMany relationship (NonAttribute)
|
|
805
|
+
reviews: Review[]; // HasMany relationship (NonAttribute)
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// Perfect type safety
|
|
809
|
+
const user: UserAttributes = await User.create({
|
|
810
|
+
firstName: "John",
|
|
811
|
+
lastName: "Doe",
|
|
812
|
+
email: "john@example.com"
|
|
813
|
+
} satisfies UserCreationAttributes);
|
|
814
|
+
```
|
|
145
815
|
|
|
146
816
|
---
|
|
147
817
|
|
|
148
|
-
##
|
|
818
|
+
## 🛠️ Advanced Features
|
|
149
819
|
|
|
150
|
-
###
|
|
820
|
+
### Data Validation and Transformation
|
|
151
821
|
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
822
|
+
```typescript
|
|
823
|
+
class User extends Table<User> {
|
|
824
|
+
@PrimaryKey()
|
|
825
|
+
declare id: string;
|
|
826
|
+
|
|
827
|
+
// Multiple transformations (executed in order)
|
|
828
|
+
@Mutate((value) => (value as string).trim())
|
|
829
|
+
@Mutate((value) => (value as string).toLowerCase())
|
|
830
|
+
@Validate((value) => /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/.test(value as string) || "Invalid email format")
|
|
831
|
+
declare email: string;
|
|
832
|
+
|
|
833
|
+
// Complex validation
|
|
834
|
+
@Validate((value) => {
|
|
835
|
+
const age = value as number;
|
|
836
|
+
if (age < 0) return "Age cannot be negative";
|
|
837
|
+
if (age > 150) return "Age seems unrealistic";
|
|
838
|
+
return true;
|
|
839
|
+
})
|
|
840
|
+
declare age: number;
|
|
841
|
+
|
|
842
|
+
// Multiple validators
|
|
843
|
+
@Validate((value) => (value as string).length >= 2 || "Name too short")
|
|
844
|
+
@Validate((value) => (value as string).length <= 50 || "Name too long")
|
|
845
|
+
@Validate((value) => /^[a-zA-Z\s]+$/.test(value as string) || "Name can only contain letters and spaces")
|
|
846
|
+
declare name: string;
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
### Custom Table Names
|
|
851
|
+
|
|
852
|
+
```typescript
|
|
853
|
+
// Table name override
|
|
854
|
+
@Name("custom_table_name")
|
|
855
|
+
class MyModel extends Table<MyModel> {
|
|
856
|
+
@PrimaryKey()
|
|
857
|
+
declare id: string;
|
|
858
|
+
|
|
859
|
+
// Column name override
|
|
860
|
+
@Name("custom_column")
|
|
861
|
+
declare myField: string;
|
|
862
|
+
}
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
### Complex Queries
|
|
866
|
+
|
|
867
|
+
```typescript
|
|
868
|
+
// Multiple conditions
|
|
869
|
+
const users = await User.where({
|
|
870
|
+
age: 25,
|
|
871
|
+
active: true,
|
|
872
|
+
role: "premium"
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// Range queries
|
|
876
|
+
const users = await User.where("createdAt", ">=", "2023-01-01");
|
|
877
|
+
|
|
878
|
+
// Array filtering
|
|
879
|
+
const premiumUsers = await User.where("role", "in", ["admin", "premium", "vip"]);
|
|
880
|
+
|
|
881
|
+
// Pattern matching
|
|
882
|
+
const testUsers = await User.where("email", "contains", "@test.com");
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
### Batch Operations
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
// Batch create
|
|
889
|
+
const users = await Promise.all([
|
|
890
|
+
User.create({ id: "1", name: "User 1" }),
|
|
891
|
+
User.create({ id: "2", name: "User 2" }),
|
|
892
|
+
User.create({ id: "3", name: "User 3" })
|
|
893
|
+
]);
|
|
894
|
+
|
|
895
|
+
// Batch update
|
|
896
|
+
await Promise.all(users.map(user => {
|
|
897
|
+
user.active = false;
|
|
898
|
+
return user.save();
|
|
899
|
+
}));
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
---
|
|
903
|
+
|
|
904
|
+
## ⚙️ Configuration
|
|
905
|
+
|
|
906
|
+
### Connection Setup
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
import { Dynamite } from "@arcaelas/dynamite";
|
|
910
|
+
|
|
911
|
+
// AWS DynamoDB
|
|
912
|
+
Dynamite.config({
|
|
913
|
+
region: "us-east-1",
|
|
914
|
+
credentials: {
|
|
915
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
916
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
// DynamoDB Local
|
|
921
|
+
Dynamite.config({
|
|
922
|
+
region: "us-east-1",
|
|
923
|
+
endpoint: "http://localhost:8000",
|
|
156
924
|
credentials: {
|
|
157
|
-
accessKeyId: "
|
|
158
|
-
secretAccessKey: "
|
|
925
|
+
accessKeyId: "test",
|
|
926
|
+
secretAccessKey: "test"
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// With custom configuration
|
|
931
|
+
Dynamite.config({
|
|
932
|
+
region: "us-east-1",
|
|
933
|
+
endpoint: "https://dynamodb.us-east-1.amazonaws.com",
|
|
934
|
+
credentials: {
|
|
935
|
+
accessKeyId: "your-key",
|
|
936
|
+
secretAccessKey: "your-secret"
|
|
159
937
|
},
|
|
938
|
+
maxAttempts: 3,
|
|
939
|
+
requestTimeout: 3000
|
|
160
940
|
});
|
|
161
941
|
```
|
|
162
942
|
|
|
163
|
-
###
|
|
943
|
+
### Environment Variables
|
|
164
944
|
|
|
165
|
-
|
|
166
|
-
|
|
945
|
+
```bash
|
|
946
|
+
# .env file
|
|
947
|
+
AWS_REGION=us-east-1
|
|
948
|
+
AWS_ACCESS_KEY_ID=your-access-key
|
|
949
|
+
AWS_SECRET_ACCESS_KEY=your-secret-key
|
|
950
|
+
DYNAMODB_ENDPOINT=http://localhost:8000 # for local development
|
|
951
|
+
```
|
|
167
952
|
|
|
168
|
-
|
|
953
|
+
```typescript
|
|
954
|
+
// Load from environment
|
|
955
|
+
Dynamite.config({
|
|
956
|
+
region: process.env.AWS_REGION!,
|
|
957
|
+
endpoint: process.env.DYNAMODB_ENDPOINT,
|
|
958
|
+
credentials: {
|
|
959
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
960
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
```
|
|
169
964
|
|
|
170
|
-
###
|
|
965
|
+
### Docker Setup for Development
|
|
171
966
|
|
|
172
967
|
```bash
|
|
173
|
-
|
|
968
|
+
# Start DynamoDB Local
|
|
969
|
+
docker run -d -p 8000:8000 amazon/dynamodb-local
|
|
970
|
+
|
|
971
|
+
# Or with Docker Compose
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
```yaml
|
|
975
|
+
# docker-compose.yml
|
|
976
|
+
version: '3.8'
|
|
977
|
+
services:
|
|
978
|
+
dynamodb-local:
|
|
979
|
+
image: amazon/dynamodb-local
|
|
980
|
+
ports:
|
|
981
|
+
- "8000:8000"
|
|
982
|
+
command: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-dbPath", "/home/dynamodblocal/data/"]
|
|
983
|
+
volumes:
|
|
984
|
+
- dynamodb_data:/home/dynamodblocal/data
|
|
985
|
+
working_dir: /home/dynamodblocal
|
|
986
|
+
|
|
987
|
+
volumes:
|
|
988
|
+
dynamodb_data:
|
|
174
989
|
```
|
|
175
990
|
|
|
176
991
|
---
|
|
177
992
|
|
|
178
|
-
##
|
|
993
|
+
## 📖 API Reference
|
|
994
|
+
|
|
995
|
+
### Table Class Methods
|
|
996
|
+
|
|
997
|
+
#### Static Methods
|
|
998
|
+
|
|
999
|
+
```typescript
|
|
1000
|
+
// CRUD Operations
|
|
1001
|
+
static async create<T>(data: Partial<InferAttributes<T>>): Promise<T>
|
|
1002
|
+
static async update<T>(id: string, data: Partial<InferAttributes<T>>): Promise<T>
|
|
1003
|
+
static async delete<T>(id: string): Promise<void>
|
|
179
1004
|
|
|
180
|
-
|
|
181
|
-
|
|
1005
|
+
// Query Methods
|
|
1006
|
+
static async where<T>(filters?: Partial<InferAttributes<T>>, options?: WhereOptions<T>): Promise<T[]>
|
|
1007
|
+
static async where<T>(field: keyof InferAttributes<T>, value: any): Promise<T[]>
|
|
1008
|
+
static async where<T>(field: keyof InferAttributes<T>, operator: QueryOperator, value: any): Promise<T[]>
|
|
182
1009
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
type Validate = (value: any) => true | string;
|
|
1010
|
+
static async first<T>(filters?: Partial<InferAttributes<T>>): Promise<T | undefined>
|
|
1011
|
+
static async last<T>(filters?: Partial<InferAttributes<T>>): Promise<T | undefined>
|
|
186
1012
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
1013
|
+
// Utility Methods
|
|
1014
|
+
static async count<T>(filters?: Partial<InferAttributes<T>>): Promise<number>
|
|
1015
|
+
static async exists<T>(id: string): Promise<boolean>
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
#### Instance Methods
|
|
1019
|
+
|
|
1020
|
+
```typescript
|
|
1021
|
+
// CRUD Operations
|
|
1022
|
+
async save(): Promise<this>
|
|
1023
|
+
async update(data: Partial<InferAttributes<T>>): Promise<this>
|
|
1024
|
+
async destroy(): Promise<void>
|
|
1025
|
+
async reload(): Promise<this>
|
|
1026
|
+
|
|
1027
|
+
// Serialization
|
|
1028
|
+
toJSON(): Record<string, any>
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### Query Operators
|
|
1032
|
+
|
|
1033
|
+
| Operator | Description | Example |
|
|
1034
|
+
|----------|-------------|---------|
|
|
1035
|
+
| `=` | Equal to (default) | `User.where("age", 25)` |
|
|
1036
|
+
| `!=` | Not equal to | `User.where("status", "!=", "deleted")` |
|
|
1037
|
+
| `<` | Less than | `User.where("age", "<", 18)` |
|
|
1038
|
+
| `<=` | Less than or equal | `User.where("age", "<=", 65)` |
|
|
1039
|
+
| `>` | Greater than | `User.where("score", ">", 100)` |
|
|
1040
|
+
| `>=` | Greater than or equal | `User.where("age", ">=", 18)` |
|
|
1041
|
+
| `in` | In array | `User.where("role", "in", ["admin", "user"])` |
|
|
1042
|
+
| `not-in` | Not in array | `User.where("status", "not-in", ["banned", "deleted"])` |
|
|
1043
|
+
| `contains` | String contains | `User.where("email", "contains", "gmail")` |
|
|
1044
|
+
| `begins-with` | String starts with | `User.where("name", "begins-with", "John")` |
|
|
1045
|
+
|
|
1046
|
+
### Type Definitions
|
|
1047
|
+
|
|
1048
|
+
```typescript
|
|
1049
|
+
// Core Types - Essential for model definition
|
|
1050
|
+
type InferAttributes<T> = {
|
|
1051
|
+
[K in keyof T]: T[K] extends NonAttribute<any> ? never : T[K]
|
|
195
1052
|
}
|
|
196
1053
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
1054
|
+
type CreationOptional<T> = T
|
|
1055
|
+
// Marks fields as optional during creation but required in instances
|
|
1056
|
+
// ALWAYS use for: @PrimaryKey + @Default, @CreatedAt, @UpdatedAt, any @Default
|
|
1057
|
+
// Example: @CreatedAt() declare createdAt: CreationOptional<string>
|
|
1058
|
+
|
|
1059
|
+
type NonAttribute<T> = T
|
|
1060
|
+
// Excludes fields from database operations
|
|
1061
|
+
// Example: declare fullName: NonAttribute<string>
|
|
1062
|
+
|
|
1063
|
+
// Relationship Types - Define model associations
|
|
1064
|
+
type HasMany<T> = T[]
|
|
1065
|
+
// One-to-many relationship: Parent has multiple children
|
|
1066
|
+
// Example: @HasMany(() => Order, "user_id") declare orders: NonAttribute<HasMany<Order>>
|
|
1067
|
+
|
|
1068
|
+
type BelongsTo<T> = T | null
|
|
1069
|
+
// Many-to-one relationship: Child belongs to parent
|
|
1070
|
+
// Example: @BelongsTo(() => User, "user_id") declare user: NonAttribute<BelongsTo<User>>
|
|
1071
|
+
|
|
1072
|
+
// Query Types
|
|
1073
|
+
type QueryOperator = "=" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "not-in" | "contains" | "begins-with"
|
|
1074
|
+
|
|
1075
|
+
type WhereOptions<T> = {
|
|
1076
|
+
limit?: number;
|
|
1077
|
+
skip?: number;
|
|
1078
|
+
order?: "ASC" | "DESC";
|
|
1079
|
+
attributes?: (keyof InferAttributes<T>)[];
|
|
1080
|
+
include?: {
|
|
1081
|
+
[K in keyof T]?: T[K] extends NonAttribute<HasMany<any> | BelongsTo<any>>
|
|
1082
|
+
? IncludeOptions | {}
|
|
1083
|
+
: never;
|
|
1084
|
+
};
|
|
200
1085
|
}
|
|
201
|
-
```
|
|
202
1086
|
|
|
203
|
-
|
|
1087
|
+
type IncludeOptions = {
|
|
1088
|
+
where?: Record<string, any>;
|
|
1089
|
+
limit?: number;
|
|
1090
|
+
order?: "ASC" | "DESC";
|
|
1091
|
+
attributes?: string[];
|
|
1092
|
+
include?: Record<string, IncludeOptions | {}>;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Creation and Update Types
|
|
1096
|
+
type CreationAttributes<T> = {
|
|
1097
|
+
[K in keyof InferAttributes<T>]: InferAttributes<T>[K] extends CreationOptional<infer U>
|
|
1098
|
+
? U | undefined
|
|
1099
|
+
: InferAttributes<T>[K]
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
type UpdateAttributes<T> = Partial<InferAttributes<T>>
|
|
1103
|
+
```
|
|
204
1104
|
|
|
205
1105
|
---
|
|
206
1106
|
|
|
207
|
-
##
|
|
1107
|
+
## 🔧 Development Setup
|
|
208
1108
|
|
|
209
|
-
###
|
|
1109
|
+
### Project Structure
|
|
210
1110
|
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
1111
|
+
```
|
|
1112
|
+
src/
|
|
1113
|
+
├── core/
|
|
1114
|
+
│ ├── client.ts # Dynamite client configuration
|
|
1115
|
+
│ ├── table.ts # Base Table class
|
|
1116
|
+
│ └── wrapper.ts # Metadata management
|
|
1117
|
+
├── decorators/
|
|
1118
|
+
│ ├── index.ts # @Index decorator
|
|
1119
|
+
│ ├── primary_key.ts # @PrimaryKey decorator
|
|
1120
|
+
│ ├── default.ts # @Default decorator
|
|
1121
|
+
│ ├── validate.ts # @Validate decorator
|
|
1122
|
+
│ ├── mutate.ts # @Mutate decorator
|
|
1123
|
+
│ ├── created_at.ts # @CreatedAt decorator
|
|
1124
|
+
│ ├── updated_at.ts # @UpdatedAt decorator
|
|
1125
|
+
│ ├── not_null.ts # @NotNull decorator
|
|
1126
|
+
│ ├── name.ts # @Name decorator
|
|
1127
|
+
│ ├── has_many.ts # @HasMany decorator
|
|
1128
|
+
│ └── belongs_to.ts # @BelongsTo decorator
|
|
1129
|
+
├── utils/
|
|
1130
|
+
│ ├── relations.ts # Relationship handling
|
|
1131
|
+
│ ├── naming.ts # Table/column naming
|
|
1132
|
+
│ └── projection.ts # Field projection
|
|
1133
|
+
├── @types/
|
|
1134
|
+
│ └── index.ts # TypeScript definitions
|
|
1135
|
+
└── index.ts # Public API exports
|
|
1136
|
+
```
|
|
215
1137
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
1138
|
+
### Running Tests
|
|
1139
|
+
|
|
1140
|
+
```bash
|
|
1141
|
+
# Start DynamoDB Local
|
|
1142
|
+
docker run -d -p 8000:8000 amazon/dynamodb-local
|
|
1143
|
+
|
|
1144
|
+
# Run tests
|
|
1145
|
+
npm test
|
|
1146
|
+
|
|
1147
|
+
# Run specific test
|
|
1148
|
+
npm test -- --testNamePattern="should handle relationships"
|
|
1149
|
+
|
|
1150
|
+
# Run with coverage
|
|
1151
|
+
npm test -- --coverage
|
|
221
1152
|
```
|
|
222
1153
|
|
|
223
|
-
###
|
|
1154
|
+
### Example Test
|
|
1155
|
+
|
|
1156
|
+
```typescript
|
|
1157
|
+
describe("User Model", () => {
|
|
1158
|
+
beforeEach(async () => {
|
|
1159
|
+
// Setup test data
|
|
1160
|
+
await User.create({
|
|
1161
|
+
id: "test-user",
|
|
1162
|
+
email: "test@example.com",
|
|
1163
|
+
name: "Test User"
|
|
1164
|
+
});
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it("should create user with defaults", async () => {
|
|
1168
|
+
const user = await User.create({
|
|
1169
|
+
id: "user-2",
|
|
1170
|
+
email: "user2@example.com"
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
expect(user.name).toBe("");
|
|
1174
|
+
expect(user.active).toBe(true);
|
|
1175
|
+
expect(user.createdAt).toBeDefined();
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
it("should validate email format", async () => {
|
|
1179
|
+
await expect(User.create({
|
|
1180
|
+
id: "user-3",
|
|
1181
|
+
email: "invalid-email"
|
|
1182
|
+
})).rejects.toThrow("Invalid email");
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
```
|
|
224
1186
|
|
|
225
|
-
|
|
226
|
-
import { Mutate } from "@arcaelas/dinamite";
|
|
1187
|
+
---
|
|
227
1188
|
|
|
228
|
-
|
|
1189
|
+
## ❓ Troubleshooting
|
|
229
1190
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
1191
|
+
### Common Errors
|
|
1192
|
+
|
|
1193
|
+
| Error | Cause | Solution |
|
|
1194
|
+
|-------|-------|----------|
|
|
1195
|
+
| `Metadata no encontrada` | Model imported before decorators executed | Ensure `connect()` runs first, avoid circular imports |
|
|
1196
|
+
| `PartitionKey faltante` | No `@PrimaryKey()` or `@Index()` in model | Add primary key decorator |
|
|
1197
|
+
| `Two keys can not have the same name` | PK & SK attribute name clash | Use different column names |
|
|
1198
|
+
| `UnrecognizedClientException` | Wrong credentials or DynamoDB Local not running | Check credentials, start DynamoDB Local |
|
|
1199
|
+
| `ValidationException` | Invalid attribute names or values | Check for reserved keywords, validate data |
|
|
1200
|
+
|
|
1201
|
+
### Performance Tips
|
|
1202
|
+
|
|
1203
|
+
```typescript
|
|
1204
|
+
// Use attributes to limit returned data
|
|
1205
|
+
const users = await User.where({}, {
|
|
1206
|
+
attributes: ["id", "name"] // Only return these fields
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
// Use pagination for large datasets
|
|
1210
|
+
const users = await User.where({}, {
|
|
1211
|
+
limit: 100,
|
|
1212
|
+
skip: 0
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
// Prefer specific queries over scanning all records
|
|
1216
|
+
const activeUsers = await User.where({ active: true }); // Good
|
|
1217
|
+
const allUsers = (await User.where({})).filter(u => u.active); // Bad
|
|
234
1218
|
```
|
|
235
1219
|
|
|
236
|
-
###
|
|
1220
|
+
### Debugging
|
|
237
1221
|
|
|
238
|
-
|
|
1222
|
+
```typescript
|
|
1223
|
+
// Enable debug logging (if available)
|
|
1224
|
+
Dynamite.config({
|
|
1225
|
+
region: "us-east-1",
|
|
1226
|
+
logger: console // Log all DynamoDB operations
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// Log query parameters
|
|
1230
|
+
const users = await User.where({ active: true });
|
|
1231
|
+
console.log("Found users:", users.length);
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
### Best Practices
|
|
1235
|
+
|
|
1236
|
+
1. **Always define a primary key** with `@PrimaryKey()` or `@Index()`
|
|
1237
|
+
2. **Use TypeScript strict mode** for better type safety
|
|
1238
|
+
3. **Validate user input** with `@Validate()` decorators
|
|
1239
|
+
4. **Use attributes selection** to limit data transfer
|
|
1240
|
+
5. **Handle relationships carefully** to avoid N+1 queries
|
|
1241
|
+
6. **Use transactions** for complex operations (if needed)
|
|
1242
|
+
7. **Monitor DynamoDB costs** in production
|
|
239
1243
|
|
|
240
1244
|
---
|
|
241
1245
|
|
|
242
|
-
##
|
|
1246
|
+
## 📄 License
|
|
243
1247
|
|
|
244
|
-
|
|
245
|
-
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
246
|
-
| `Metadata no encontrada` | Model file imported before decorators executed – avoid circular imports; ensure `connect()` runs **first**. |
|
|
247
|
-
| `PartitionKey faltante` | No `@Index()` in the model. Add one. |
|
|
248
|
-
| `Two keys can not have the same name` | PK & SK attribute clash. Use `@PrimaryKey()` or distinct column names. |
|
|
249
|
-
| `UnrecognizedClientException` | Wrong credentials / DynamoDB Local not running. |
|
|
1248
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
250
1249
|
|
|
251
1250
|
---
|
|
252
1251
|
|
|
253
|
-
## Contributing
|
|
1252
|
+
## 🤝 Contributing
|
|
254
1253
|
|
|
255
|
-
1. Fork
|
|
256
|
-
2.
|
|
257
|
-
3.
|
|
1254
|
+
1. Fork the repository
|
|
1255
|
+
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
|
1256
|
+
3. Make your changes and add tests
|
|
1257
|
+
4. Ensure tests pass: `npm test`
|
|
1258
|
+
5. Commit changes: `git commit -m 'feat: add amazing feature'`
|
|
1259
|
+
6. Push to branch: `git push origin feature/amazing-feature`
|
|
1260
|
+
7. Open a Pull Request
|
|
1261
|
+
|
|
1262
|
+
### Development Guidelines
|
|
1263
|
+
|
|
1264
|
+
- Follow TypeScript strict mode
|
|
1265
|
+
- Add tests for new features
|
|
1266
|
+
- Update documentation
|
|
1267
|
+
- Use conventional commits
|
|
1268
|
+
- Ensure backward compatibility
|
|
1269
|
+
|
|
1270
|
+
---
|
|
258
1271
|
|
|
259
|
-
|
|
1272
|
+
**Made with ❤️ by [Miguel Alejandro](https://github.com/arcaelas) - [Arcaelas Insiders](https://github.com/arcaelas)**
|