@arcaelas/dynamite 1.0.10 → 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.
Files changed (69) hide show
  1. package/README.md +1165 -152
  2. package/package.json +8 -10
  3. package/SECURITY.md +0 -41
  4. package/build/__tests__/crud.spec.d.ts +0 -7
  5. package/build/__tests__/crud.spec.js +0 -287
  6. package/build/__tests__/crud.spec.js.map +0 -1
  7. package/build/__tests__/debug-decorators.spec.d.ts +0 -7
  8. package/build/__tests__/debug-decorators.spec.js +0 -143
  9. package/build/__tests__/debug-decorators.spec.js.map +0 -1
  10. package/build/__tests__/decorators.spec.d.ts +0 -7
  11. package/build/__tests__/decorators.spec.js +0 -203
  12. package/build/__tests__/decorators.spec.js.map +0 -1
  13. package/build/__tests__/instance-crud.spec.d.ts +0 -7
  14. package/build/__tests__/instance-crud.spec.js +0 -184
  15. package/build/__tests__/instance-crud.spec.js.map +0 -1
  16. package/build/src/core/client.d.ts +0 -65
  17. package/build/src/core/client.js +0 -152
  18. package/build/src/core/client.js.map +0 -1
  19. package/build/src/core/table.d.ts +0 -164
  20. package/build/src/core/table.js +0 -406
  21. package/build/src/core/table.js.map +0 -1
  22. package/build/src/core/wrapper.d.ts +0 -54
  23. package/build/src/core/wrapper.js +0 -27
  24. package/build/src/core/wrapper.js.map +0 -1
  25. package/build/src/decorators/created_at.d.ts +0 -8
  26. package/build/src/decorators/created_at.js +0 -18
  27. package/build/src/decorators/created_at.js.map +0 -1
  28. package/build/src/decorators/default.d.ts +0 -8
  29. package/build/src/decorators/default.js +0 -57
  30. package/build/src/decorators/default.js.map +0 -1
  31. package/build/src/decorators/index.d.ts +0 -8
  32. package/build/src/decorators/index.js +0 -26
  33. package/build/src/decorators/index.js.map +0 -1
  34. package/build/src/decorators/index_sort.d.ts +0 -8
  35. package/build/src/decorators/index_sort.js +0 -30
  36. package/build/src/decorators/index_sort.js.map +0 -1
  37. package/build/src/decorators/mutate.d.ts +0 -9
  38. package/build/src/decorators/mutate.js +0 -60
  39. package/build/src/decorators/mutate.js.map +0 -1
  40. package/build/src/decorators/name.d.ts +0 -8
  41. package/build/src/decorators/name.js +0 -42
  42. package/build/src/decorators/name.js.map +0 -1
  43. package/build/src/decorators/not_null.d.ts +0 -8
  44. package/build/src/decorators/not_null.js +0 -20
  45. package/build/src/decorators/not_null.js.map +0 -1
  46. package/build/src/decorators/primary_key.d.ts +0 -8
  47. package/build/src/decorators/primary_key.js +0 -26
  48. package/build/src/decorators/primary_key.js.map +0 -1
  49. package/build/src/decorators/updated_at.d.ts +0 -8
  50. package/build/src/decorators/updated_at.js +0 -18
  51. package/build/src/decorators/updated_at.js.map +0 -1
  52. package/build/src/decorators/validate.d.ts +0 -8
  53. package/build/src/decorators/validate.js +0 -60
  54. package/build/src/decorators/validate.js.map +0 -1
  55. package/build/src/index.d.ts +0 -13
  56. package/build/src/index.js +0 -38
  57. package/build/src/index.js.map +0 -1
  58. package/build/src/utils/batch-relations.d.ts +0 -14
  59. package/build/src/utils/batch-relations.js +0 -130
  60. package/build/src/utils/batch-relations.js.map +0 -1
  61. package/build/src/utils/naming.d.ts +0 -8
  62. package/build/src/utils/naming.js +0 -18
  63. package/build/src/utils/naming.js.map +0 -1
  64. package/build/src/utils/projection.d.ts +0 -12
  65. package/build/src/utils/projection.js +0 -50
  66. package/build/src/utils/projection.js.map +0 -1
  67. package/build/src/utils/relations.d.ts +0 -23
  68. package/build/src/utils/relations.js +0 -205
  69. package/build/src/utils/relations.js.map +0 -1
package/README.md CHANGED
@@ -1,259 +1,1272 @@
1
1
  ![Arcaelas Insiders](https://raw.githubusercontent.com/arcaelas/dist/main/banner/svg/dark.svg#gh-dark-mode-only)
2
2
  ![Arcaelas Insiders](https://raw.githubusercontent.com/arcaelas/dist/main/banner/svg/light.svg#gh-light-mode-only)
3
3
 
4
- # Dinamite ORM
4
+ # @arcaelas/dynamite
5
5
 
6
- > A **decoratorfirst**, zero‑boilerplate ORM for DynamoDB (AWS SDK v3).
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/dinamite"><img src="https://img.shields.io/npm/v/@arcaelas/dinamite?color=cb3837" alt="npm"></a>
12
- <img src="https://img.shields.io/bundlephobia/minzip/@arcaelas/dinamite?label=gzip" alt="size">
13
- <img src="https://img.shields.io/github/license/arcaelas/dinamite" alt="MIT">
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
- - [Install](#install)
21
- - [Hello World](#hello-world)
22
- - [Decorators Reference](#decorators-reference)
23
- - [Model API](#model-api)
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
- - [Static CRUD](#static-crud)
26
- - [Instance CRUD](#instance-crud)
27
- - [Serialization](#serialization)
67
+ @Default(() => "customer")
68
+ declare role: CreationOptional<string>;
28
69
 
29
- - [Configuration](#configuration)
70
+ @CreatedAt()
71
+ declare createdAt: CreationOptional<string>;
30
72
 
31
- - [Connection](#connection)
32
- - [Naming rules & pluralisation](#naming-rules--pluralisation)
33
- - [Running on DynamoDB Local](#running-on-dynamodb-local)
73
+ @UpdatedAt()
74
+ declare updatedAt: CreationOptional<string>;
34
75
 
35
- - [Type Reference](#type-reference)
36
- - [Recipes](#recipes)
37
- - [Troubleshooting](#troubleshooting)
38
- - [Contributing](#contributing)
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
- ## Install
104
+ ## 📦 Installation
43
105
 
44
106
  ```bash
45
- npm i @arcaelas/dinamite
46
- # peer deps (unless already installed)
47
- npm i @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb pluralize
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
- ## Hello World
115
+ ## ⚡ Basic Usage
53
116
 
54
- ```ts
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
- connect({
65
- region: "us-east-1",
66
- // DynamoDB Local example
67
- endpoint: "http://localhost:7007",
68
- credentials: { accessKeyId: "x", secretAccessKey: "x" },
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
- class User extends Table {
72
- @Index() // Partition Key
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
- @CreatedAt() // ISO‑string timestamp
79
- declare created: string;
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 updated: string;
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
- const bob = await User.create({ id: "u1", name: "Bob" });
199
+ ### Data Decorators
86
200
 
87
- bob.name = "Robert";
88
- await bob.save(); // upsert
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
- console.log(await User.where());
91
- await bob.destroy();
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
- First call auto‑creates a table `users` (`user` → **snake + plural**).
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
- ## Decorators Reference
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
- | Decorator | Purpose | Extras |
101
- | ------------------ | ----------------------------------------------------- | ------ |
102
- | `@Index()` | **Partition key**. Exactly one «PK» per model. | |
103
- | `@IndexSort()` | **Sort key**. Requires previous `@Index()`. | |
104
- | `@PrimaryKey()` | Shortcut: _PK + SK on same property_. | |
105
- | `@Default(fn)` | Lazy default value, evaluated once per instance. | |
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
- Execution order: **Default → Mutate\[] → Validate\[]**
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
- ## Model API
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
- ### Static CRUD
452
+ class User extends Table<User> {
453
+ @PrimaryKey()
454
+ declare id: string;
120
455
 
121
- ```ts
122
- User.create(data); // PutItem (auto‑table‑creation)
123
- User.update(id, patch); // PutItem replacement
124
- User.destroy(id); // DeleteItem
125
- User.where(); // Scan User[]
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
- ### Instance CRUD
505
+ ### Relationship Types
506
+
507
+ #### `HasMany<T>`
129
508
 
130
- ```ts
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
- u.name = "The One";
135
- await u.save(); // updates
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
- await u.update({ name: "Thomas" });
138
- await u.destroy();
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
- ### Serialization
566
+ #### `BelongsTo<T>`
567
+
568
+ Defines a many-to-one relationship where the model belongs to a single parent instance.
142
569
 
143
- `model.toJSON()` → **only fields declared via decorators** are included.
144
- Undefined values are stripped before `marshall()` (`removeUndefinedValues`).
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
- ## Configuration
818
+ ## 🛠️ Advanced Features
149
819
 
150
- ### Connection
820
+ ### Data Validation and Transformation
151
821
 
152
- ```ts
153
- connect({
154
- region: "…",
155
- endpoint: "https://…", // optional – for DynamoDB Local
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
- ### Naming rules & pluralisation
943
+ ### Environment Variables
164
944
 
165
- - `PascalCase` / `camelCase` → `snake_case`
166
- - Singular → **plural** using [`pluralize`](https://www.npmjs.com/package/pluralize)
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
- Override with `@Name("my_table")`.
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
- ### Running on DynamoDB Local
965
+ ### Docker Setup for Development
171
966
 
172
967
  ```bash
173
- docker run -p 7007:8000 amazon/dynamodb-local
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
- ## Type Reference
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
- ```ts
181
- type Inmutable = string | number | boolean | null | object;
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
- type Mutate = (value: any) => Inmutable;
184
- type Default = Inmutable | (() => Inmutable);
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
- interface Column {
188
- name: string;
189
- default?: Default;
190
- mutate?: Mutate[];
191
- validate?: Validate[];
192
- index?: true; // PK
193
- indexSort?: true; // SK
194
- unique?: true; // not yet enforced
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
- interface WrapperEntry {
198
- name: string; // physical table
199
- columns: Map<string | symbol, Column>; // property → Column
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
- Internal state lives in **`src/core/wrapper.ts`**.
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
- ## Recipes
1107
+ ## 🔧 Development Setup
208
1108
 
209
- ### Soft‑delete flag
1109
+ ### Project Structure
210
1110
 
211
- ```ts
212
- class Post extends Table {
213
- @Index() declare id: string;
214
- @Default(() => false) declare deleted: boolean;
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
- async softDelete() {
217
- this.deleted = true;
218
- await this.save();
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
- ### Custom mutator – email normalisation
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
- ```ts
226
- import { Mutate } from "@arcaelas/dinamite";
1187
+ ---
227
1188
 
228
- const lower: Mutate = (v) => String(v).toLowerCase();
1189
+ ## Troubleshooting
229
1190
 
230
- class Subscriber extends Table {
231
- @Index() declare id: string;
232
- @Mutate(lower) declare email: string;
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
- ### Using DynamoDB Streams + Lambda
1220
+ ### Debugging
237
1221
 
238
- Because tables are created at runtime you can safely deploy stacks without `resources`, then subscribe Lambdas to the **physical table names** emitted by Dinamite (`User` → `users`, unless overridden).
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
- ## Troubleshooting
1246
+ ## 📄 License
243
1247
 
244
- | Error | Explanation & fix |
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 feature → PR. Conventional commits (`feat:`, `fix:`…).
256
- 2. `yarn test` must pass (Jest + ESLint).
257
- 3. Document new features in this README.
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
- _Made with ❤️ by [Miguel Alejandro](https://github.com/arcaelas)_ MIT License.
1272
+ **Made with ❤️ by [Miguel Alejandro](https://github.com/arcaelas) - [Arcaelas Insiders](https://github.com/arcaelas)**