@arcaelas/dynamite 1.0.0
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/.eslintrc.js +0 -0
- package/.prettierrc +0 -0
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/__tests__/crud.spec.ts +77 -0
- package/__tests__/decorators.spec.ts +134 -0
- package/__tests__/instance-crud.spec.ts +86 -0
- package/jest.config.ts +23 -0
- package/package.json +29 -0
- package/src/core/table.ts +226 -0
- package/src/core/wrapper.ts +103 -0
- package/src/decorators/created_at.ts +17 -0
- package/src/decorators/default.ts +56 -0
- package/src/decorators/index.ts +26 -0
- package/src/decorators/index_sort.ts +32 -0
- package/src/decorators/mutate.ts +54 -0
- package/src/decorators/name.ts +50 -0
- package/src/decorators/not_null.ts +21 -0
- package/src/decorators/primary_key.ts +26 -0
- package/src/decorators/updated_at.ts +18 -0
- package/src/decorators/validate.ts +59 -0
- package/src/index.ts +14 -0
- package/src/utils/naming.ts +12 -0
- package/tsconfig.json +32 -0
package/.eslintrc.js
ADDED
|
File without changes
|
package/.prettierrc
ADDED
|
File without changes
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 arcaela
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+
|
|
4
|
+
# Dinamite ORM
|
|
5
|
+
|
|
6
|
+
> A **decorator‑first**, zero‑boilerplate ORM for DynamoDB (AWS SDK v3).
|
|
7
|
+
>
|
|
8
|
+
> _Auto‑provisions tables · Runs anywhere Node.js runs · Written in TypeScript only_
|
|
9
|
+
|
|
10
|
+
<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">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Install](#install)
|
|
21
|
+
- [Hello World](#hello-world)
|
|
22
|
+
- [Decorators Reference](#decorators-reference)
|
|
23
|
+
- [Model API](#model-api)
|
|
24
|
+
|
|
25
|
+
- [Static CRUD](#static-crud)
|
|
26
|
+
- [Instance CRUD](#instance-crud)
|
|
27
|
+
- [Serialization](#serialization)
|
|
28
|
+
|
|
29
|
+
- [Configuration](#configuration)
|
|
30
|
+
|
|
31
|
+
- [Connection](#connection)
|
|
32
|
+
- [Naming rules & pluralisation](#naming-rules--pluralisation)
|
|
33
|
+
- [Running on DynamoDB Local](#running-on-dynamodb-local)
|
|
34
|
+
|
|
35
|
+
- [Type Reference](#type-reference)
|
|
36
|
+
- [Recipes](#recipes)
|
|
37
|
+
- [Troubleshooting](#troubleshooting)
|
|
38
|
+
- [Contributing](#contributing)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm i @arcaelas/dinamite
|
|
46
|
+
# peer deps (unless already installed)
|
|
47
|
+
npm i @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb pluralize
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Hello World
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import {
|
|
56
|
+
connect,
|
|
57
|
+
Table,
|
|
58
|
+
Index, // PK
|
|
59
|
+
CreatedAt,
|
|
60
|
+
UpdatedAt,
|
|
61
|
+
Default,
|
|
62
|
+
} from "@arcaelas/dinamite";
|
|
63
|
+
|
|
64
|
+
connect({
|
|
65
|
+
region: "us-east-1",
|
|
66
|
+
// DynamoDB Local example
|
|
67
|
+
endpoint: "http://localhost:7007",
|
|
68
|
+
credentials: { accessKeyId: "x", secretAccessKey: "x" },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
class User extends Table {
|
|
72
|
+
@Index() // Partition Key
|
|
73
|
+
declare id: string;
|
|
74
|
+
|
|
75
|
+
@Default(() => "")
|
|
76
|
+
declare name: string;
|
|
77
|
+
|
|
78
|
+
@CreatedAt() // ISO‑string timestamp
|
|
79
|
+
declare created: string;
|
|
80
|
+
|
|
81
|
+
@UpdatedAt()
|
|
82
|
+
declare updated: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const bob = await User.create({ id: "u1", name: "Bob" });
|
|
86
|
+
|
|
87
|
+
bob.name = "Robert";
|
|
88
|
+
await bob.save(); // upsert
|
|
89
|
+
|
|
90
|
+
console.log(await User.where());
|
|
91
|
+
await bob.destroy();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
First call auto‑creates a table `users` (`user` → **snake + plural**).
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Decorators Reference
|
|
99
|
+
|
|
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. | |
|
|
112
|
+
|
|
113
|
+
Execution order: **Default → Mutate\[] → Validate\[]**
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Model API
|
|
118
|
+
|
|
119
|
+
### Static CRUD
|
|
120
|
+
|
|
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[]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Instance CRUD
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const u = new User({ id: "42", name: "Neo" });
|
|
132
|
+
await u.save(); // inserts
|
|
133
|
+
|
|
134
|
+
u.name = "The One";
|
|
135
|
+
await u.save(); // updates
|
|
136
|
+
|
|
137
|
+
await u.update({ name: "Thomas" });
|
|
138
|
+
await u.destroy();
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Serialization
|
|
142
|
+
|
|
143
|
+
`model.toJSON()` → **only fields declared via decorators** are included.
|
|
144
|
+
Undefined values are stripped before `marshall()` (`removeUndefinedValues`).
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Configuration
|
|
149
|
+
|
|
150
|
+
### Connection
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
connect({
|
|
154
|
+
region: "…",
|
|
155
|
+
endpoint: "https://…", // optional – for DynamoDB Local
|
|
156
|
+
credentials: {
|
|
157
|
+
accessKeyId: "…",
|
|
158
|
+
secretAccessKey: "…",
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Naming rules & pluralisation
|
|
164
|
+
|
|
165
|
+
- `PascalCase` / `camelCase` → `snake_case`
|
|
166
|
+
- Singular → **plural** using [`pluralize`](https://www.npmjs.com/package/pluralize)
|
|
167
|
+
|
|
168
|
+
Override with `@Name("my_table")`.
|
|
169
|
+
|
|
170
|
+
### Running on DynamoDB Local
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
docker run -p 7007:8000 amazon/dynamodb-local
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Type Reference
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
type Inmutable = string | number | boolean | null | object;
|
|
182
|
+
|
|
183
|
+
type Mutate = (value: any) => Inmutable;
|
|
184
|
+
type Default = Inmutable | (() => Inmutable);
|
|
185
|
+
type Validate = (value: any) => true | string;
|
|
186
|
+
|
|
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
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface WrapperEntry {
|
|
198
|
+
name: string; // physical table
|
|
199
|
+
columns: Map<string | symbol, Column>; // property → Column
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Internal state lives in **`src/core/wrapper.ts`**.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Recipes
|
|
208
|
+
|
|
209
|
+
### Soft‑delete flag
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
class Post extends Table {
|
|
213
|
+
@Index() declare id: string;
|
|
214
|
+
@Default(() => false) declare deleted: boolean;
|
|
215
|
+
|
|
216
|
+
async softDelete() {
|
|
217
|
+
this.deleted = true;
|
|
218
|
+
await this.save();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Custom mutator – email normalisation
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { Mutate } from "@arcaelas/dinamite";
|
|
227
|
+
|
|
228
|
+
const lower: Mutate = (v) => String(v).toLowerCase();
|
|
229
|
+
|
|
230
|
+
class Subscriber extends Table {
|
|
231
|
+
@Index() declare id: string;
|
|
232
|
+
@Mutate(lower) declare email: string;
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Using DynamoDB Streams + Lambda
|
|
237
|
+
|
|
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).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Troubleshooting
|
|
243
|
+
|
|
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. |
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Contributing
|
|
254
|
+
|
|
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.
|
|
258
|
+
|
|
259
|
+
_Made with ❤️ by [Miguel Alejandro](https://github.com/arcaelas)_ – MIT License.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* __tests__/crud.spec.ts
|
|
2
|
+
* -----------------------------------------------------------
|
|
3
|
+
* CRUD end-to-end contra DynamoDB Local (puerto 7007)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DeleteTableCommand, DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
7
|
+
|
|
8
|
+
import { connect, default as Table } from "../src/core/table";
|
|
9
|
+
import wrapper from "../src/core/wrapper";
|
|
10
|
+
|
|
11
|
+
import Default from "../src/decorators/default";
|
|
12
|
+
import Name from "../src/decorators/name";
|
|
13
|
+
import NotNull from "../src/decorators/not_null";
|
|
14
|
+
import PrimaryKey from "../src/decorators/primary_key";
|
|
15
|
+
|
|
16
|
+
const ddbCfg = {
|
|
17
|
+
region: "local",
|
|
18
|
+
endpoint: "http://localhost:7007",
|
|
19
|
+
credentials: { accessKeyId: "x", secretAccessKey: "x" },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
beforeAll(() => connect(ddbCfg));
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
/* Eliminar todas las tablas creadas por los tests */
|
|
26
|
+
const ddb = new DynamoDBClient(ddbCfg);
|
|
27
|
+
for (const e of wrapper.values()) {
|
|
28
|
+
try {
|
|
29
|
+
await ddb.send(new DeleteTableCommand({ TableName: e.name }));
|
|
30
|
+
} catch (_) {
|
|
31
|
+
/* tabla puede no existir */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
wrapper.clear();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("CRUD Dinamite ORM (DynamoDB Local)", () => {
|
|
38
|
+
jest.setTimeout(10_000);
|
|
39
|
+
|
|
40
|
+
it("create → update → where → destroy", async () => {
|
|
41
|
+
@Name("crud_users1")
|
|
42
|
+
class User extends Table {
|
|
43
|
+
@PrimaryKey()
|
|
44
|
+
declare id: string;
|
|
45
|
+
|
|
46
|
+
@NotNull()
|
|
47
|
+
declare email: string;
|
|
48
|
+
|
|
49
|
+
@Default(() => new Date().toISOString())
|
|
50
|
+
declare created_at: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* ---------- create (auto-crea tabla) ---------- */
|
|
54
|
+
const u1 = await User.create({ id: "u1", email: "a@b.com" });
|
|
55
|
+
expect(u1.email).toBe("a@b.com");
|
|
56
|
+
|
|
57
|
+
/* ---------- update ---------- */
|
|
58
|
+
await User.update("u1", { email: "c@d.com" });
|
|
59
|
+
const [after] = await User.where();
|
|
60
|
+
expect(after.email).toBe("c@d.com");
|
|
61
|
+
|
|
62
|
+
/* ---------- destroy ---------- */
|
|
63
|
+
await User.destroy("u1");
|
|
64
|
+
expect(await User.where()).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("where() devuelve [] si la tabla no existe", async () => {
|
|
68
|
+
@Name("ghosts")
|
|
69
|
+
class Ghost extends Table {
|
|
70
|
+
@PrimaryKey()
|
|
71
|
+
declare id: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rows = await Ghost.where();
|
|
75
|
+
expect(rows).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/* __tests__/decorators.spec.ts
|
|
2
|
+
* -----------------------------------------------------------
|
|
3
|
+
* Suite para verificar todos los decoradores Dinamite ORM.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import wrapper, { STORE } from "../src/core/wrapper";
|
|
7
|
+
|
|
8
|
+
import CreatedAt from "../src/decorators/created_at";
|
|
9
|
+
import Default from "../src/decorators/default";
|
|
10
|
+
import Index from "../src/decorators/index";
|
|
11
|
+
import IndexSort from "../src/decorators/index_sort";
|
|
12
|
+
import Mutate from "../src/decorators/mutate";
|
|
13
|
+
import Name from "../src/decorators/name";
|
|
14
|
+
import NotNull from "../src/decorators/not_null";
|
|
15
|
+
import PrimaryKey from "../src/decorators/primary_key";
|
|
16
|
+
import UpdatedAt from "../src/decorators/updated_at";
|
|
17
|
+
import Validate from "../src/decorators/validate";
|
|
18
|
+
|
|
19
|
+
describe("Decoradores Dinamite ORM", () => {
|
|
20
|
+
beforeEach(() => wrapper.clear());
|
|
21
|
+
|
|
22
|
+
/* ---------------------------------------------------------- */
|
|
23
|
+
it("nombre de tabla por defecto (snake_plural) cuando no se usa @Name", () => {
|
|
24
|
+
class Book {
|
|
25
|
+
@NotNull()
|
|
26
|
+
declare title: string;
|
|
27
|
+
}
|
|
28
|
+
const meta = wrapper.get(Book)!;
|
|
29
|
+
expect(meta.name).toBe("books"); // Book → books
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/* ---------------------------------------------------------- */
|
|
33
|
+
it("@Name (clase y propiedad)", () => {
|
|
34
|
+
@Name("usuarios")
|
|
35
|
+
class User {
|
|
36
|
+
@Name("correo")
|
|
37
|
+
declare email: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const meta = wrapper.get(User)!;
|
|
41
|
+
expect(meta.name).toBe("usuarios");
|
|
42
|
+
expect(meta.columns.get("email")!.name).toBe("correo");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/* ---------------------------------------------------------- */
|
|
46
|
+
it("@Index + @IndexSort (PK + SK)", () => {
|
|
47
|
+
class Post {
|
|
48
|
+
@Index() // Partition Key
|
|
49
|
+
declare slug: string;
|
|
50
|
+
|
|
51
|
+
@IndexSort() // Sort Key
|
|
52
|
+
declare created: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const meta = wrapper.get(Post)!;
|
|
56
|
+
const pkCol = meta.columns.get("slug")!;
|
|
57
|
+
const skCol = meta.columns.get("created")!;
|
|
58
|
+
|
|
59
|
+
expect(pkCol.index).toBe(true);
|
|
60
|
+
expect(skCol.indexSort).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/* ---------------------------------------------------------- */
|
|
64
|
+
it("@PrimaryKey (combinado)", () => {
|
|
65
|
+
class Comment {
|
|
66
|
+
@PrimaryKey()
|
|
67
|
+
declare id: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const meta = wrapper.get(Comment)!;
|
|
71
|
+
const col = meta.columns.get("id")!;
|
|
72
|
+
|
|
73
|
+
expect(col.index).toBe(true);
|
|
74
|
+
expect(col.indexSort).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/* ---------------------------------------------------------- */
|
|
78
|
+
it("@Default + @Mutate + @Validate pipeline", () => {
|
|
79
|
+
@Name("tests")
|
|
80
|
+
class Model {
|
|
81
|
+
@Default(() => 10)
|
|
82
|
+
@Mutate((v) => (v as number) * 2)
|
|
83
|
+
@Validate((v) => ((v as number) >= 20 ? true : "menor a 20"))
|
|
84
|
+
declare score: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const m = new Model();
|
|
88
|
+
(m as any).score = undefined; // dispara pipeline
|
|
89
|
+
expect(m.score).toBe(20);
|
|
90
|
+
expect((m as any)[STORE].score).toBe(20);
|
|
91
|
+
|
|
92
|
+
expect(() => {
|
|
93
|
+
m.score = 5 as any;
|
|
94
|
+
}).toThrow("menor a 20");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/* ---------------------------------------------------------- */
|
|
98
|
+
it("@NotNull rechaza null/undefined/cadena vacía", () => {
|
|
99
|
+
class File {
|
|
100
|
+
@NotNull()
|
|
101
|
+
declare path: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const f = new File();
|
|
105
|
+
expect(() => ((f as any).path = null)).toThrow();
|
|
106
|
+
expect(() => ((f as any).path = " ")).toThrow();
|
|
107
|
+
f.path = "/tmp/file";
|
|
108
|
+
expect(f.path).toBe("/tmp/file");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/* ---------------------------------------------------------- */
|
|
112
|
+
it("@CreatedAt y @UpdatedAt gestionan timestamps", () => {
|
|
113
|
+
class Audit {
|
|
114
|
+
@CreatedAt()
|
|
115
|
+
declare created: string;
|
|
116
|
+
|
|
117
|
+
@UpdatedAt()
|
|
118
|
+
declare updated: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const a = new Audit();
|
|
122
|
+
(a as any).created = undefined;
|
|
123
|
+
(a as any).updated = undefined;
|
|
124
|
+
|
|
125
|
+
const firstCreated = a.created;
|
|
126
|
+
const firstUpdated = a.updated;
|
|
127
|
+
|
|
128
|
+
return new Promise((r) => setTimeout(r, 10)).then(() => {
|
|
129
|
+
(a as any).updated = undefined;
|
|
130
|
+
expect(a.created).toBe(firstCreated);
|
|
131
|
+
expect(a.updated).not.toBe(firstUpdated);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* __tests__/instance-crud.spec.ts
|
|
2
|
+
* ------------------------------------------------------------------
|
|
3
|
+
* CRUD usando métodos de **instancia** de Dinamite ORM con DynamoDB Local.
|
|
4
|
+
* Se asume que DynamoDB Local está corriendo en http://localhost:7007.
|
|
5
|
+
* ------------------------------------------------------------------ */
|
|
6
|
+
import {
|
|
7
|
+
connect,
|
|
8
|
+
CreatedAt,
|
|
9
|
+
Name,
|
|
10
|
+
NotNull,
|
|
11
|
+
PrimaryKey,
|
|
12
|
+
Table,
|
|
13
|
+
UpdatedAt,
|
|
14
|
+
} from "../src";
|
|
15
|
+
|
|
16
|
+
// ---------- conexión DynamoDB Local ----------
|
|
17
|
+
beforeAll(() =>
|
|
18
|
+
connect({
|
|
19
|
+
region: "local",
|
|
20
|
+
endpoint: "http://localhost:7007",
|
|
21
|
+
credentials: { accessKeyId: "x", secretAccessKey: "x" },
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// ---------- Modelo de prueba ----------
|
|
26
|
+
@Name("users")
|
|
27
|
+
class User extends Table {
|
|
28
|
+
@PrimaryKey()
|
|
29
|
+
declare id: string;
|
|
30
|
+
|
|
31
|
+
@NotNull() // ← registra la columna en el wrapper
|
|
32
|
+
declare email: string;
|
|
33
|
+
|
|
34
|
+
@CreatedAt()
|
|
35
|
+
declare created: string;
|
|
36
|
+
|
|
37
|
+
@UpdatedAt()
|
|
38
|
+
declare updated: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("CRUD Dinamite ORM – instancia", () => {
|
|
42
|
+
it("where() antes de existir la tabla → []", async () => {
|
|
43
|
+
const rows = await User.where();
|
|
44
|
+
expect(rows).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("save() → crea, update() → modifica, destroy() → elimina", async () => {
|
|
48
|
+
/* ---------- save (create) ---------- */
|
|
49
|
+
const u = new User({ id: "u1", email: "a@b.com" });
|
|
50
|
+
await u.save();
|
|
51
|
+
|
|
52
|
+
let rows = await User.where();
|
|
53
|
+
expect(rows).toHaveLength(1);
|
|
54
|
+
expect(rows[0].email).toBe("a@b.com");
|
|
55
|
+
|
|
56
|
+
const firstCreated = rows[0].created;
|
|
57
|
+
const firstUpdated = rows[0].updated;
|
|
58
|
+
|
|
59
|
+
/* ---------- update ---------- */
|
|
60
|
+
await u.update({ email: "c@d.com" });
|
|
61
|
+
|
|
62
|
+
rows = await User.where();
|
|
63
|
+
expect(rows[0].email).toBe("c@d.com");
|
|
64
|
+
expect(rows[0].updated).not.toBe(firstUpdated);
|
|
65
|
+
expect(rows[0].created).toBe(firstCreated);
|
|
66
|
+
|
|
67
|
+
/* ---------- destroy ---------- */
|
|
68
|
+
await u.destroy();
|
|
69
|
+
|
|
70
|
+
rows = await User.where();
|
|
71
|
+
expect(rows).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("la tabla realmente contiene el ítem mientras existe", async () => {
|
|
75
|
+
const u = new User({ id: "u2", email: "real@test.com" });
|
|
76
|
+
await u.save();
|
|
77
|
+
|
|
78
|
+
let rows = await User.where();
|
|
79
|
+
expect(rows.map((x) => x.id)).toContain("u2");
|
|
80
|
+
|
|
81
|
+
await u.destroy();
|
|
82
|
+
|
|
83
|
+
rows = await User.where();
|
|
84
|
+
expect(rows.map((x) => x.id)).not.toContain("u2");
|
|
85
|
+
});
|
|
86
|
+
});
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// jest.config.ts
|
|
2
|
+
import type { Config } from "@jest/types";
|
|
3
|
+
|
|
4
|
+
const config: Config.InitialOptions = {
|
|
5
|
+
preset: "ts-jest",
|
|
6
|
+
testEnvironment: "node",
|
|
7
|
+
testMatch: ["**/__tests__/**/*.spec.ts"],
|
|
8
|
+
collectCoverageFrom: ["src/**/*.ts"],
|
|
9
|
+
coverageDirectory: "coverage",
|
|
10
|
+
coverageReporters: ["text", "lcov"],
|
|
11
|
+
verbose: true,
|
|
12
|
+
clearMocks: true, // Limpia mocks automáticamente
|
|
13
|
+
transform: {
|
|
14
|
+
"^.+\\.tsx?$": "ts-jest",
|
|
15
|
+
},
|
|
16
|
+
moduleNameMapper: {
|
|
17
|
+
"^@/(.*)$": "<rootDir>/src/$1",
|
|
18
|
+
},
|
|
19
|
+
// Ignorar node_modules y cobertura
|
|
20
|
+
testPathIgnorePatterns: ["/node_modules/", "/coverage/"],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default config;
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arcaelas/dynamite",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@aws-sdk/client-dynamodb": "3.329.0",
|
|
8
|
+
"@aws-sdk/lib-dynamodb": "3.329.0",
|
|
9
|
+
"pluralize": "^8.0.0",
|
|
10
|
+
"uuid": "^11.1.0"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "jest",
|
|
14
|
+
"test:watch": "jest --watch",
|
|
15
|
+
"test:coverage": "jest --coverage",
|
|
16
|
+
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/jest": "^30.0.0",
|
|
20
|
+
"@types/node": "^24.0.6",
|
|
21
|
+
"jest": "^30.0.3",
|
|
22
|
+
"reflect-metadata": "^0.2.2",
|
|
23
|
+
"ts-jest": "^29.4.0",
|
|
24
|
+
"ts-node": "^10.9.2",
|
|
25
|
+
"tsconfig-paths": "^4.2.0",
|
|
26
|
+
"tsx": "^4.20.3",
|
|
27
|
+
"typescript": "^5.8.3"
|
|
28
|
+
}
|
|
29
|
+
}
|