@arcaelas/dynamite 1.0.19 → 1.0.24
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 +240 -1051
- package/package.json +11 -42
- package/src/scripts/load_seed.d.ts +5 -0
- package/src/scripts/load_seed.js +54 -0
- package/src/src/@types/index.d.ts +188 -0
- package/src/src/@types/index.js +9 -0
- package/src/{core → src/core}/client.d.ts +4 -16
- package/src/{core → src/core}/client.js +115 -38
- package/src/{core → src/core}/decorator.d.ts +0 -15
- package/src/{core → src/core}/decorator.js +5 -35
- package/src/src/core/table.d.ts +81 -0
- package/src/src/core/table.js +892 -0
- package/src/{decorators → src/decorators}/indexes.d.ts +1 -1
- package/src/{decorators → src/decorators}/indexes.js +12 -20
- package/src/{decorators → src/decorators}/relations.d.ts +20 -1
- package/src/{decorators → src/decorators}/relations.js +32 -2
- package/src/{decorators → src/decorators}/timestamps.d.ts +4 -4
- package/src/{decorators → src/decorators}/timestamps.js +11 -6
- package/src/{decorators → src/decorators}/transforms.d.ts +17 -4
- package/src/{decorators → src/decorators}/transforms.js +40 -28
- package/src/src/index.d.ts +15 -0
- package/src/{index.js → src/index.js} +8 -22
- package/src/src/index.test.d.ts +6 -0
- package/src/src/index.test.js +789 -0
- package/src/{utils → src/utils}/relations.d.ts +5 -3
- package/src/src/utils/relations.js +216 -0
- package/src/@types/index.d.ts +0 -137
- package/src/core/method.d.ts +0 -73
- package/src/core/method.js +0 -140
- package/src/core/table.d.ts +0 -56
- package/src/core/table.js +0 -659
- package/src/index.d.ts +0 -16
- package/src/index.test.d.ts +0 -13
- package/src/index.test.js +0 -1992
- package/src/utils/relations.js +0 -141
package/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
# @arcaelas/dynamite
|
|
5
|
-
|
|
6
|
-
> **A modern, decorator-first ORM for DynamoDB with TypeScript support**
|
|
7
|
-
> Full-featured • Type-safe • Relationship support • Auto table creation • Zero boilerplate
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/cover.png" alt="Dynamite ORM - Arcaelas Insiders for DynamoDB" width="100%">
|
|
3
|
+
</p>
|
|
8
4
|
|
|
9
5
|
<p align="center">
|
|
10
6
|
<a href="https://www.npmjs.com/package/@arcaelas/dynamite"><img src="https://img.shields.io/npm/v/@arcaelas/dynamite?color=cb3837" alt="npm"></a>
|
|
@@ -14,46 +10,17 @@
|
|
|
14
10
|
<img src="https://img.shields.io/badge/TypeScript-5.x-blue" alt="TypeScript">
|
|
15
11
|
</p>
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
# @arcaelas/dynamite
|
|
18
14
|
|
|
19
|
-
|
|
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)
|
|
15
|
+
> **A modern, decorator-first ORM for DynamoDB with TypeScript support**
|
|
16
|
+
> Full-featured | Type-safe | Relationships | Auto table creation | Transactions
|
|
33
17
|
|
|
34
18
|
---
|
|
35
19
|
|
|
36
|
-
##
|
|
20
|
+
## Quick Start
|
|
37
21
|
|
|
38
22
|
```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
|
-
});
|
|
23
|
+
import { Dynamite, Table, PrimaryKey, Default, CreatedAt, UpdatedAt, CreationOptional } from "@arcaelas/dynamite";
|
|
57
24
|
|
|
58
25
|
// Define your model
|
|
59
26
|
class User extends Table<User> {
|
|
@@ -61,1212 +28,434 @@ class User extends Table<User> {
|
|
|
61
28
|
@Default(() => crypto.randomUUID())
|
|
62
29
|
declare id: CreationOptional<string>;
|
|
63
30
|
|
|
64
|
-
|
|
65
|
-
declare
|
|
31
|
+
declare name: string;
|
|
32
|
+
declare email: string;
|
|
66
33
|
|
|
67
34
|
@Default(() => "customer")
|
|
68
35
|
declare role: CreationOptional<string>;
|
|
69
36
|
|
|
70
37
|
@CreatedAt()
|
|
71
|
-
declare
|
|
72
|
-
|
|
73
|
-
@UpdatedAt()
|
|
74
|
-
declare
|
|
75
|
-
|
|
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
|
-
}
|
|
38
|
+
declare created_at: CreationOptional<string>;
|
|
39
|
+
|
|
40
|
+
@UpdatedAt()
|
|
41
|
+
declare updated_at: CreationOptional<string>;
|
|
88
42
|
}
|
|
89
43
|
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
44
|
+
// Connect to DynamoDB
|
|
45
|
+
const dynamite = new Dynamite({
|
|
46
|
+
region: "us-east-1",
|
|
47
|
+
endpoint: "http://localhost:8000", // DynamoDB Local
|
|
48
|
+
credentials: { accessKeyId: "test", secretAccessKey: "test" },
|
|
49
|
+
tables: [User]
|
|
94
50
|
});
|
|
95
51
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
52
|
+
await dynamite.connect();
|
|
53
|
+
|
|
54
|
+
// Use it!
|
|
55
|
+
const user = await User.create({ name: "John Doe", email: "john@example.com" });
|
|
56
|
+
console.log(user.id); // "a1b2c3d4-..."
|
|
57
|
+
console.log(user.role); // "customer"
|
|
58
|
+
console.log(user.created_at); // "2025-01-15T10:30:00.000Z"
|
|
100
59
|
```
|
|
101
60
|
|
|
102
61
|
---
|
|
103
62
|
|
|
104
|
-
##
|
|
63
|
+
## Installation
|
|
105
64
|
|
|
106
65
|
```bash
|
|
107
66
|
npm install @arcaelas/dynamite
|
|
108
|
-
|
|
109
|
-
# Peer dependencies (if not already installed)
|
|
110
|
-
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
111
67
|
```
|
|
112
68
|
|
|
113
69
|
---
|
|
114
70
|
|
|
115
|
-
##
|
|
71
|
+
## Decorators
|
|
116
72
|
|
|
117
|
-
###
|
|
73
|
+
### Index Decorators
|
|
118
74
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
Validate,
|
|
125
|
-
Mutate,
|
|
126
|
-
NotNull,
|
|
127
|
-
Name
|
|
128
|
-
} from "@arcaelas/dynamite";
|
|
75
|
+
| Decorator | Description |
|
|
76
|
+
|-----------|-------------|
|
|
77
|
+
| `@PrimaryKey()` | Primary key (partition key) |
|
|
78
|
+
| `@Index()` | Partition key for GSI |
|
|
79
|
+
| `@IndexSort()` | Sort key |
|
|
129
80
|
|
|
130
|
-
|
|
131
|
-
class User extends Table<User> {
|
|
132
|
-
@PrimaryKey()
|
|
133
|
-
declare id: string;
|
|
81
|
+
### Data Decorators
|
|
134
82
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
83
|
+
| Decorator | Description |
|
|
84
|
+
|-----------|-------------|
|
|
85
|
+
| `@Default(value \| fn)` | Default value (static or dynamic) |
|
|
86
|
+
| `@Mutate(fn)` | Transform value before save |
|
|
87
|
+
| `@Validate(fn)` | Validate value before save |
|
|
88
|
+
| `@Serialize(fromDB, toDB)` | Bidirectional transformation |
|
|
89
|
+
| `@NotNull()` | Required field validation |
|
|
90
|
+
| `@Name("custom")` | Custom column/table name |
|
|
91
|
+
| `@Column()` | Column configuration |
|
|
139
92
|
|
|
140
|
-
|
|
141
|
-
declare name: string;
|
|
93
|
+
### Timestamp Decorators
|
|
142
94
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
95
|
+
| Decorator | Description |
|
|
96
|
+
|-----------|-------------|
|
|
97
|
+
| `@CreatedAt()` | Auto-set on creation |
|
|
98
|
+
| `@UpdatedAt()` | Auto-set on every update |
|
|
99
|
+
| `@DeleteAt()` | Soft delete timestamp |
|
|
146
100
|
|
|
147
|
-
|
|
148
|
-
declare active: boolean;
|
|
101
|
+
### Relationship Decorators
|
|
149
102
|
|
|
150
|
-
|
|
151
|
-
|
|
103
|
+
| Decorator | Description |
|
|
104
|
+
|-----------|-------------|
|
|
105
|
+
| `@HasMany(() => Model, "foreign_key")` | One-to-many |
|
|
106
|
+
| `@HasOne(() => Model, "foreign_key")` | One-to-one |
|
|
107
|
+
| `@BelongsTo(() => Model, "local_key")` | Many-to-one |
|
|
108
|
+
| `@ManyToMany(() => Model, config)` | Many-to-many with pivot table |
|
|
152
109
|
|
|
153
|
-
|
|
154
|
-
declare updatedAt: string;
|
|
155
|
-
}
|
|
156
|
-
```
|
|
110
|
+
---
|
|
157
111
|
|
|
158
|
-
|
|
112
|
+
## TypeScript Types
|
|
159
113
|
|
|
160
114
|
```typescript
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
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();
|
|
115
|
+
import {
|
|
116
|
+
CreationOptional, // Optional during create(), required after
|
|
117
|
+
NonAttribute, // Excluded from database (computed/relations)
|
|
118
|
+
InferAttributes, // Extract DB attributes from model
|
|
119
|
+
InferRelations, // Extract relations from model
|
|
120
|
+
CreateInput, // Input type for create()
|
|
121
|
+
UpdateInput, // Input type for update()
|
|
122
|
+
WhereOptions, // Query options type
|
|
123
|
+
QueryOperator // Available operators
|
|
124
|
+
} from "@arcaelas/dynamite";
|
|
184
125
|
```
|
|
185
126
|
|
|
186
|
-
|
|
127
|
+
### CreationOptional
|
|
187
128
|
|
|
188
|
-
|
|
129
|
+
Use for fields that are optional during creation but exist after:
|
|
189
130
|
|
|
190
|
-
|
|
131
|
+
```typescript
|
|
132
|
+
class User extends Table<User> {
|
|
133
|
+
@PrimaryKey()
|
|
134
|
+
@Default(() => crypto.randomUUID())
|
|
135
|
+
declare id: CreationOptional<string>; // Optional in create()
|
|
191
136
|
|
|
192
|
-
|
|
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;` |
|
|
137
|
+
declare name: string; // Required in create()
|
|
198
138
|
|
|
199
|
-
|
|
139
|
+
@CreatedAt()
|
|
140
|
+
declare created_at: CreationOptional<string>; // Auto-generated
|
|
141
|
+
}
|
|
142
|
+
```
|
|
200
143
|
|
|
201
|
-
|
|
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;` |
|
|
144
|
+
### NonAttribute
|
|
207
145
|
|
|
208
|
-
|
|
146
|
+
Use for computed properties and relations (not stored in DB):
|
|
209
147
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
148
|
+
```typescript
|
|
149
|
+
class User extends Table<User> {
|
|
150
|
+
declare first_name: string;
|
|
151
|
+
declare last_name: string;
|
|
214
152
|
|
|
215
|
-
|
|
153
|
+
// Computed property - not stored
|
|
154
|
+
declare full_name: NonAttribute<string>;
|
|
216
155
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
156
|
+
// Relations - loaded via include
|
|
157
|
+
@HasMany(() => Order, "user_id")
|
|
158
|
+
declare orders: NonAttribute<Order[]>;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
221
161
|
|
|
222
162
|
---
|
|
223
163
|
|
|
224
|
-
##
|
|
164
|
+
## Query Operations
|
|
225
165
|
|
|
226
166
|
### Basic Queries
|
|
227
167
|
|
|
228
168
|
```typescript
|
|
229
|
-
// Get all
|
|
169
|
+
// Get all
|
|
230
170
|
const users = await User.where({});
|
|
231
171
|
|
|
232
172
|
// Filter by field
|
|
233
|
-
const
|
|
234
|
-
const
|
|
173
|
+
const admins = await User.where({ role: "admin" });
|
|
174
|
+
const user = await User.where("email", "john@example.com");
|
|
235
175
|
|
|
236
|
-
//
|
|
237
|
-
const
|
|
238
|
-
const
|
|
176
|
+
// First/Last
|
|
177
|
+
const first = await User.first({ active: true });
|
|
178
|
+
const last = await User.last({});
|
|
239
179
|
```
|
|
240
180
|
|
|
241
|
-
###
|
|
181
|
+
### Query Operators
|
|
242
182
|
|
|
243
183
|
```typescript
|
|
244
|
-
// Comparison
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// Not equal
|
|
255
|
-
const nonAdmins = await User.where("role", "!=", "admin");
|
|
184
|
+
// Comparison
|
|
185
|
+
await User.where("age", ">=", 18);
|
|
186
|
+
await User.where("age", "<", 65);
|
|
187
|
+
await User.where("status", "!=", "banned");
|
|
188
|
+
|
|
189
|
+
// Array membership
|
|
190
|
+
await User.where("role", "in", ["admin", "moderator"]);
|
|
191
|
+
|
|
192
|
+
// String contains
|
|
193
|
+
await User.where("email", "$include", "gmail");
|
|
256
194
|
```
|
|
257
195
|
|
|
196
|
+
**Available operators:** `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `in`, `$include`
|
|
197
|
+
|
|
258
198
|
### Query Options
|
|
259
199
|
|
|
260
200
|
```typescript
|
|
261
|
-
// Pagination and limiting
|
|
262
201
|
const users = await User.where({}, {
|
|
263
202
|
limit: 10,
|
|
264
|
-
skip: 20
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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"
|
|
203
|
+
skip: 20,
|
|
204
|
+
order: "DESC",
|
|
205
|
+
attributes: ["id", "name", "email"],
|
|
206
|
+
include: {
|
|
207
|
+
orders: {
|
|
208
|
+
where: { status: "completed" },
|
|
209
|
+
limit: 5
|
|
210
|
+
}
|
|
211
|
+
}
|
|
291
212
|
});
|
|
292
213
|
```
|
|
293
214
|
|
|
294
215
|
---
|
|
295
216
|
|
|
296
|
-
##
|
|
217
|
+
## Relationships
|
|
297
218
|
|
|
298
|
-
### Defining
|
|
219
|
+
### Defining Relations
|
|
299
220
|
|
|
300
221
|
```typescript
|
|
301
|
-
// User model
|
|
302
222
|
class User extends Table<User> {
|
|
303
223
|
@PrimaryKey()
|
|
304
224
|
declare id: string;
|
|
305
225
|
|
|
306
226
|
@HasMany(() => Order, "user_id")
|
|
307
|
-
declare orders:
|
|
227
|
+
declare orders: NonAttribute<Order[]>;
|
|
228
|
+
|
|
229
|
+
@HasOne(() => Profile, "user_id")
|
|
230
|
+
declare profile: NonAttribute<Profile | null>;
|
|
308
231
|
|
|
309
|
-
@
|
|
310
|
-
|
|
232
|
+
@ManyToMany(() => Role, {
|
|
233
|
+
pivotTable: "user_roles",
|
|
234
|
+
foreignKey: "user_id",
|
|
235
|
+
relatedKey: "role_id"
|
|
236
|
+
})
|
|
237
|
+
declare roles: NonAttribute<Role[]>;
|
|
311
238
|
}
|
|
312
239
|
|
|
313
|
-
// Order model
|
|
314
240
|
class Order extends Table<Order> {
|
|
315
241
|
@PrimaryKey()
|
|
316
242
|
declare id: string;
|
|
317
243
|
|
|
318
|
-
@NotNull()
|
|
319
244
|
declare user_id: string;
|
|
320
245
|
|
|
321
246
|
@BelongsTo(() => User, "user_id")
|
|
322
|
-
declare user:
|
|
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;
|
|
341
|
-
|
|
342
|
-
@BelongsTo(() => Product, "product_id")
|
|
343
|
-
declare product: any;
|
|
247
|
+
declare user: NonAttribute<User | null>;
|
|
344
248
|
}
|
|
345
249
|
```
|
|
346
250
|
|
|
347
|
-
### Loading
|
|
251
|
+
### Loading Relations
|
|
348
252
|
|
|
349
253
|
```typescript
|
|
350
|
-
|
|
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({}, {
|
|
254
|
+
const users = await User.where({}, {
|
|
385
255
|
include: {
|
|
386
|
-
orders: {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
256
|
+
orders: { where: { status: "completed" } },
|
|
257
|
+
profile: {},
|
|
258
|
+
roles: {}
|
|
390
259
|
}
|
|
391
260
|
});
|
|
392
261
|
```
|
|
393
262
|
|
|
394
|
-
|
|
395
|
-
|
|
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.
|
|
263
|
+
### ManyToMany Operations
|
|
405
264
|
|
|
406
265
|
```typescript
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
class User extends Table<User> {
|
|
410
|
-
// Always CreationOptional - auto-generated ID
|
|
411
|
-
@PrimaryKey()
|
|
412
|
-
@Default(() => crypto.randomUUID())
|
|
413
|
-
declare id: CreationOptional<string>;
|
|
266
|
+
const user = await User.first({ id: "user-1" });
|
|
414
267
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
declare email: string;
|
|
268
|
+
// Attach relation
|
|
269
|
+
await user.attach(Role, "role-123");
|
|
418
270
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
declare role: CreationOptional<string>;
|
|
271
|
+
// Detach relation
|
|
272
|
+
await user.detach(Role, "role-123");
|
|
422
273
|
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
});
|
|
274
|
+
// Sync relations (replace all)
|
|
275
|
+
await user.sync(Role, ["role-1", "role-2", "role-3"]);
|
|
437
276
|
```
|
|
438
277
|
|
|
439
|
-
|
|
440
|
-
- `@PrimaryKey()` with `@Default()` → Always optional
|
|
441
|
-
- `@CreatedAt()` → Always optional
|
|
442
|
-
- `@UpdatedAt()` → Always optional
|
|
443
|
-
- Any field with `@Default()` → Always optional
|
|
278
|
+
---
|
|
444
279
|
|
|
445
|
-
|
|
280
|
+
## CRUD Operations
|
|
446
281
|
|
|
447
|
-
|
|
282
|
+
### Create
|
|
448
283
|
|
|
449
284
|
```typescript
|
|
450
|
-
import { Table, PrimaryKey, NonAttribute } from "@arcaelas/dynamite";
|
|
451
|
-
|
|
452
|
-
class User extends Table<User> {
|
|
453
|
-
@PrimaryKey()
|
|
454
|
-
declare id: string;
|
|
455
|
-
|
|
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
285
|
const user = await User.create({
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
lastName: "Doe",
|
|
497
|
-
birthDate: "1990-01-01"
|
|
286
|
+
name: "John Doe",
|
|
287
|
+
email: "john@example.com"
|
|
498
288
|
});
|
|
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"
|
|
503
289
|
```
|
|
504
290
|
|
|
505
|
-
###
|
|
506
|
-
|
|
507
|
-
#### `HasMany<T>`
|
|
508
|
-
|
|
509
|
-
Defines a one-to-many relationship where the model can have multiple related instances.
|
|
291
|
+
### Read
|
|
510
292
|
|
|
511
293
|
```typescript
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
class User extends Table<User> {
|
|
515
|
-
@PrimaryKey()
|
|
516
|
-
declare id: string;
|
|
517
|
-
|
|
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
|
|
294
|
+
const users = await User.where({ active: true });
|
|
295
|
+
const user = await User.first({ id: "user-123" });
|
|
564
296
|
```
|
|
565
297
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
Defines a many-to-one relationship where the model belongs to a single parent instance.
|
|
298
|
+
### Update
|
|
569
299
|
|
|
570
300
|
```typescript
|
|
571
|
-
|
|
301
|
+
// Static update (bulk)
|
|
302
|
+
await User.update({ role: "premium" }, { id: "user-123" });
|
|
572
303
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
});
|
|
304
|
+
// Instance update
|
|
305
|
+
user.name = "Jane Doe";
|
|
306
|
+
await user.save();
|
|
621
307
|
|
|
622
|
-
//
|
|
623
|
-
|
|
624
|
-
console.log(orderWithRelations[0].user.name); // string
|
|
625
|
-
}
|
|
626
|
-
if (orderWithRelations[0].category) {
|
|
627
|
-
console.log(orderWithRelations[0].category.name); // string
|
|
628
|
-
}
|
|
308
|
+
// Or
|
|
309
|
+
await user.update({ name: "Jane Doe" });
|
|
629
310
|
```
|
|
630
311
|
|
|
631
|
-
###
|
|
632
|
-
|
|
633
|
-
#### Complete Model Example
|
|
312
|
+
### Delete
|
|
634
313
|
|
|
635
314
|
```typescript
|
|
636
|
-
|
|
637
|
-
|
|
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>;
|
|
315
|
+
// Static delete (bulk)
|
|
316
|
+
await User.delete({ status: "inactive" });
|
|
672
317
|
|
|
673
|
-
|
|
674
|
-
|
|
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
|
|
318
|
+
// Instance delete (soft delete if @DeleteAt present)
|
|
319
|
+
await user.destroy();
|
|
777
320
|
|
|
778
|
-
|
|
779
|
-
|
|
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);
|
|
321
|
+
// Force hard delete
|
|
322
|
+
await user.forceDestroy();
|
|
814
323
|
```
|
|
815
324
|
|
|
816
325
|
---
|
|
817
326
|
|
|
818
|
-
##
|
|
819
|
-
|
|
820
|
-
### Data Validation and Transformation
|
|
327
|
+
## Soft Deletes
|
|
821
328
|
|
|
822
329
|
```typescript
|
|
823
|
-
class
|
|
330
|
+
class Post extends Table<Post> {
|
|
824
331
|
@PrimaryKey()
|
|
825
332
|
declare id: string;
|
|
826
333
|
|
|
827
|
-
|
|
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;
|
|
334
|
+
declare title: string;
|
|
841
335
|
|
|
842
|
-
|
|
843
|
-
|
|
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;
|
|
336
|
+
@DeleteAt()
|
|
337
|
+
declare deleted_at: CreationOptional<string | null>;
|
|
847
338
|
}
|
|
848
|
-
```
|
|
849
339
|
|
|
850
|
-
|
|
340
|
+
// Soft delete
|
|
341
|
+
await post.destroy(); // Sets deleted_at timestamp
|
|
851
342
|
|
|
852
|
-
|
|
853
|
-
|
|
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
|
-
```
|
|
343
|
+
// Query including soft-deleted
|
|
344
|
+
const all = await Post.withTrashed({});
|
|
864
345
|
|
|
865
|
-
|
|
346
|
+
// Query only soft-deleted
|
|
347
|
+
const trashed = await Post.onlyTrashed({});
|
|
866
348
|
|
|
867
|
-
|
|
868
|
-
|
|
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");
|
|
349
|
+
// Force hard delete
|
|
350
|
+
await post.forceDestroy();
|
|
883
351
|
```
|
|
884
352
|
|
|
885
|
-
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Transactions
|
|
886
356
|
|
|
887
357
|
```typescript
|
|
888
|
-
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
]);
|
|
894
|
-
|
|
895
|
-
// Batch update
|
|
896
|
-
await Promise.all(users.map(user => {
|
|
897
|
-
user.active = false;
|
|
898
|
-
return user.save();
|
|
899
|
-
}));
|
|
358
|
+
await dynamite.tx(async (tx) => {
|
|
359
|
+
const user = await User.create({ name: "John" }, tx);
|
|
360
|
+
await Order.create({ user_id: user.id, total: 100 }, tx);
|
|
361
|
+
// If any operation fails, all are rolled back
|
|
362
|
+
});
|
|
900
363
|
```
|
|
901
364
|
|
|
902
365
|
---
|
|
903
366
|
|
|
904
|
-
##
|
|
367
|
+
## Configuration
|
|
905
368
|
|
|
906
|
-
###
|
|
369
|
+
### DynamoDB Local
|
|
907
370
|
|
|
908
371
|
```typescript
|
|
909
|
-
|
|
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({
|
|
372
|
+
const dynamite = new Dynamite({
|
|
922
373
|
region: "us-east-1",
|
|
923
374
|
endpoint: "http://localhost:8000",
|
|
924
|
-
credentials: {
|
|
925
|
-
|
|
926
|
-
secretAccessKey: "test"
|
|
927
|
-
}
|
|
375
|
+
credentials: { accessKeyId: "test", secretAccessKey: "test" },
|
|
376
|
+
tables: [User, Order, Product]
|
|
928
377
|
});
|
|
929
378
|
|
|
930
|
-
|
|
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"
|
|
937
|
-
},
|
|
938
|
-
maxAttempts: 3,
|
|
939
|
-
requestTimeout: 3000
|
|
940
|
-
});
|
|
379
|
+
await dynamite.connect();
|
|
941
380
|
```
|
|
942
381
|
|
|
943
|
-
###
|
|
944
|
-
|
|
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
|
-
```
|
|
382
|
+
### AWS DynamoDB
|
|
952
383
|
|
|
953
384
|
```typescript
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
region: process.env.AWS_REGION!,
|
|
957
|
-
endpoint: process.env.DYNAMODB_ENDPOINT,
|
|
385
|
+
const dynamite = new Dynamite({
|
|
386
|
+
region: "us-east-1",
|
|
958
387
|
credentials: {
|
|
959
388
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
960
389
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
|
|
961
|
-
}
|
|
390
|
+
},
|
|
391
|
+
tables: [User, Order, Product]
|
|
962
392
|
});
|
|
393
|
+
|
|
394
|
+
await dynamite.connect();
|
|
963
395
|
```
|
|
964
396
|
|
|
965
|
-
### Docker Setup
|
|
397
|
+
### Docker Setup
|
|
966
398
|
|
|
967
399
|
```bash
|
|
968
|
-
# Start DynamoDB Local
|
|
969
400
|
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:
|
|
989
401
|
```
|
|
990
402
|
|
|
991
403
|
---
|
|
992
404
|
|
|
993
|
-
##
|
|
405
|
+
## API Reference
|
|
994
406
|
|
|
995
|
-
###
|
|
996
|
-
|
|
997
|
-
#### Static Methods
|
|
407
|
+
### Static Methods
|
|
998
408
|
|
|
999
409
|
```typescript
|
|
1000
|
-
// CRUD
|
|
1001
|
-
static
|
|
1002
|
-
static
|
|
1003
|
-
static
|
|
1004
|
-
|
|
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[]>
|
|
1009
|
-
|
|
1010
|
-
static async first<T>(filters?: Partial<InferAttributes<T>>): Promise<T | undefined>
|
|
1011
|
-
static async last<T>(filters?: Partial<InferAttributes<T>>): Promise<T | undefined>
|
|
1012
|
-
|
|
1013
|
-
// Utility Methods
|
|
1014
|
-
static async count<T>(filters?: Partial<InferAttributes<T>>): Promise<number>
|
|
1015
|
-
static async exists<T>(id: string): Promise<boolean>
|
|
1016
|
-
```
|
|
410
|
+
// CRUD
|
|
411
|
+
static create(data, tx?): Promise<T>
|
|
412
|
+
static update(data, filters, tx?): Promise<number>
|
|
413
|
+
static delete(filters, tx?): Promise<number>
|
|
1017
414
|
|
|
1018
|
-
|
|
415
|
+
// Query
|
|
416
|
+
static where(filters, options?): Promise<T[]>
|
|
417
|
+
static where(field, value): Promise<T[]>
|
|
418
|
+
static where(field, operator, value): Promise<T[]>
|
|
419
|
+
static first(filters?, options?): Promise<T | undefined>
|
|
420
|
+
static last(filters?, options?): Promise<T | undefined>
|
|
1019
421
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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>
|
|
422
|
+
// Soft deletes
|
|
423
|
+
static withTrashed(filters?, options?): Promise<T[]>
|
|
424
|
+
static onlyTrashed(filters?, options?): Promise<T[]>
|
|
1029
425
|
```
|
|
1030
426
|
|
|
1031
|
-
###
|
|
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
|
|
427
|
+
### Instance Methods
|
|
1047
428
|
|
|
1048
429
|
```typescript
|
|
1049
|
-
//
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
|
|
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
|
-
}
|
|
430
|
+
// CRUD
|
|
431
|
+
save(): Promise<boolean>
|
|
432
|
+
update(data): Promise<boolean>
|
|
433
|
+
destroy(): Promise<null>
|
|
434
|
+
forceDestroy(): Promise<null>
|
|
1101
435
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
## 🔧 Development Setup
|
|
1108
|
-
|
|
1109
|
-
### Project Structure
|
|
1110
|
-
|
|
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
|
-
```
|
|
1137
|
-
|
|
1138
|
-
### Running Tests
|
|
1139
|
-
|
|
1140
|
-
```bash
|
|
1141
|
-
# Start DynamoDB Local
|
|
1142
|
-
docker run -d -p 8000:8000 amazon/dynamodb-local
|
|
436
|
+
// ManyToMany
|
|
437
|
+
attach(Model, id, pivotData?): Promise<void>
|
|
438
|
+
detach(Model, id): Promise<void>
|
|
439
|
+
sync(Model, ids): Promise<void>
|
|
1143
440
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
# Run specific test
|
|
1148
|
-
npm test -- --testNamePattern="should handle relationships"
|
|
1149
|
-
|
|
1150
|
-
# Run with coverage
|
|
1151
|
-
npm test -- --coverage
|
|
1152
|
-
```
|
|
1153
|
-
|
|
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
|
-
});
|
|
441
|
+
// Serialization
|
|
442
|
+
toJSON(): Record<string, unknown>
|
|
1185
443
|
```
|
|
1186
444
|
|
|
1187
445
|
---
|
|
1188
446
|
|
|
1189
|
-
##
|
|
1190
|
-
|
|
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
|
|
447
|
+
## Documentation
|
|
1202
448
|
|
|
1203
|
-
|
|
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
|
-
});
|
|
449
|
+
For complete documentation, examples, and guides:
|
|
1214
450
|
|
|
1215
|
-
|
|
1216
|
-
const activeUsers = await User.where({ active: true }); // Good
|
|
1217
|
-
const allUsers = (await User.where({})).filter(u => u.active); // Bad
|
|
1218
|
-
```
|
|
1219
|
-
|
|
1220
|
-
### Debugging
|
|
1221
|
-
|
|
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
|
|
451
|
+
**[arcaelas.github.io/dynamite](https://arcaelas.github.io/dynamite)**
|
|
1243
452
|
|
|
1244
453
|
---
|
|
1245
454
|
|
|
1246
|
-
##
|
|
455
|
+
## License
|
|
1247
456
|
|
|
1248
457
|
MIT License - see [LICENSE](LICENSE) file for details.
|
|
1249
458
|
|
|
1250
459
|
---
|
|
1251
460
|
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
---
|
|
1271
|
-
|
|
1272
|
-
**Made with ❤️ by [Miguel Alejandro](https://github.com/arcaelas) - [Arcaelas Insiders](https://github.com/arcaelas)**
|
|
461
|
+
**Made with care by [Miguel Alejandro](https://github.com/arcaelas) - [Arcaelas Insiders](https://github.com/arcaelas)**
|