@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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/* src/core/table.ts
|
|
2
|
+
* Dinamite ORM — runtime
|
|
3
|
+
* --------------------------------------------------
|
|
4
|
+
* CRUD + autocreación de tablas (DynamoDB v3)
|
|
5
|
+
* Serialización estricta mediante toJSON()
|
|
6
|
+
* © 2025 Miguel Alejandro
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
CreateTableCommand,
|
|
10
|
+
DeleteItemCommand,
|
|
11
|
+
DynamoDBClient,
|
|
12
|
+
DynamoDBClientConfig,
|
|
13
|
+
PutItemCommand,
|
|
14
|
+
ScanCommand,
|
|
15
|
+
} from "@aws-sdk/client-dynamodb";
|
|
16
|
+
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
|
|
17
|
+
import wrapper, { STORE, WrapperEntry } from "./wrapper";
|
|
18
|
+
|
|
19
|
+
/* ────────────────────────── Conexión global ────────────────────────── */
|
|
20
|
+
let client: DynamoDBClient | undefined;
|
|
21
|
+
export function connect(cfg: DynamoDBClientConfig): void {
|
|
22
|
+
client = new DynamoDBClient(cfg);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ─────────── Creación automática de tablas a partir del wrapper ────── */
|
|
26
|
+
async function createTable(ctor: Function): Promise<void> {
|
|
27
|
+
if (!client) throw new Error("connect() no llamado");
|
|
28
|
+
const meta = wrapper.get(ctor);
|
|
29
|
+
if (!meta) throw new Error(`Clase ${ctor.name} no registrada en wrapper`);
|
|
30
|
+
|
|
31
|
+
const cols = [...meta.columns.values()];
|
|
32
|
+
const pk = cols.find((c) => c.index);
|
|
33
|
+
if (!pk) throw new Error(`PartitionKey faltante en ${ctor.name}`);
|
|
34
|
+
|
|
35
|
+
const sk = cols.find((c) => c.indexSort);
|
|
36
|
+
|
|
37
|
+
const attr = new Map<string, "S" | "N" | "B">();
|
|
38
|
+
attr.set(pk.name, "S");
|
|
39
|
+
if (sk) attr.set(sk.name, "S");
|
|
40
|
+
|
|
41
|
+
type KS = { AttributeName: string; KeyType: "HASH" | "RANGE" };
|
|
42
|
+
const schema: KS[] = [{ AttributeName: pk.name, KeyType: "HASH" }];
|
|
43
|
+
if (sk && sk.name !== pk.name)
|
|
44
|
+
schema.push({ AttributeName: sk.name, KeyType: "RANGE" });
|
|
45
|
+
|
|
46
|
+
await client.send(
|
|
47
|
+
new CreateTableCommand({
|
|
48
|
+
TableName: meta.name,
|
|
49
|
+
BillingMode: "PAY_PER_REQUEST",
|
|
50
|
+
AttributeDefinitions: [...attr].map(([AttributeName, AttributeType]) => ({
|
|
51
|
+
AttributeName,
|
|
52
|
+
AttributeType,
|
|
53
|
+
})),
|
|
54
|
+
KeySchema: schema,
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ────────────────────────────── Table ──────────────────────────────── */
|
|
60
|
+
export default class Table<T extends object = object> {
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
62
|
+
private [STORE]!: { [K in keyof T]?: T[K] };
|
|
63
|
+
|
|
64
|
+
constructor(data: Partial<T> = {}) {
|
|
65
|
+
requireClient();
|
|
66
|
+
const meta = mustMeta(Object.getPrototypeOf(this).constructor);
|
|
67
|
+
|
|
68
|
+
/* defaults via setters */
|
|
69
|
+
meta.columns.forEach((c) => {
|
|
70
|
+
if (!(c.name in data)) (this as any)[c.name] = undefined;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
Object.assign(this, data);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* -------- serializa SOLO columnas válidas -------- */
|
|
77
|
+
toJSON(): Record<string, unknown> {
|
|
78
|
+
const meta = mustMeta(Object.getPrototypeOf(this).constructor);
|
|
79
|
+
const buf = (this as any)[STORE] ?? {};
|
|
80
|
+
const out: Record<string, unknown> = {};
|
|
81
|
+
|
|
82
|
+
for (const [prop, col] of meta.columns) {
|
|
83
|
+
if (prop in buf) out[col.name] = buf[prop];
|
|
84
|
+
else if (prop in this) out[col.name] = (this as any)[prop];
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ────────────── Métodos de instancia ────────────── */
|
|
90
|
+
async save(): Promise<this> {
|
|
91
|
+
// @ts-ignore
|
|
92
|
+
const id: unknown = this.id;
|
|
93
|
+
const Ctor = this.constructor as typeof Table<any>;
|
|
94
|
+
const record = this.toJSON();
|
|
95
|
+
|
|
96
|
+
if (id === undefined || id === null) {
|
|
97
|
+
delete (record as any).id;
|
|
98
|
+
const fresh = await (Ctor as any).create(record);
|
|
99
|
+
Object.assign(this, fresh);
|
|
100
|
+
} else {
|
|
101
|
+
await (Ctor as any).update(String(id), record);
|
|
102
|
+
}
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async update(patch: Partial<T>): Promise<this> {
|
|
107
|
+
// @ts-ignore
|
|
108
|
+
const id: unknown = this.id;
|
|
109
|
+
if (id === undefined || id === null)
|
|
110
|
+
throw new Error("update() requiere id");
|
|
111
|
+
|
|
112
|
+
Object.assign(this, patch);
|
|
113
|
+
const Ctor = this.constructor as typeof Table<any>;
|
|
114
|
+
await (Ctor as any).update(String(id), this.toJSON());
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async destroy(): Promise<void> {
|
|
119
|
+
// @ts-ignore
|
|
120
|
+
const id: unknown = this.id;
|
|
121
|
+
if (id === undefined || id === null)
|
|
122
|
+
throw new Error("destroy() requiere id");
|
|
123
|
+
const Ctor = this.constructor as typeof Table<any>;
|
|
124
|
+
await (Ctor as any).destroy(String(id));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ─────────────── CRUD estáticos ──────────────── */
|
|
128
|
+
|
|
129
|
+
static async create<M extends Table>(
|
|
130
|
+
this: new (d?: Partial<M>) => M,
|
|
131
|
+
data: Partial<M>
|
|
132
|
+
): Promise<M> {
|
|
133
|
+
const meta = mustMeta(this);
|
|
134
|
+
const payload = new this(data).toJSON(); // filtrado
|
|
135
|
+
|
|
136
|
+
const put = () =>
|
|
137
|
+
client!.send(
|
|
138
|
+
new PutItemCommand({
|
|
139
|
+
TableName: meta.name,
|
|
140
|
+
Item: marshall(payload, { removeUndefinedValues: true }),
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
await put();
|
|
146
|
+
} catch (err: any) {
|
|
147
|
+
if (err?.name === "ResourceNotFoundException") {
|
|
148
|
+
await createTable(this);
|
|
149
|
+
await put();
|
|
150
|
+
} else throw err;
|
|
151
|
+
}
|
|
152
|
+
return new this(data);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static async update<M extends Table>(
|
|
156
|
+
this: new (d?: Partial<M>) => M,
|
|
157
|
+
id: string,
|
|
158
|
+
record: Partial<M> // ← ya es JSON completo desde instancia
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const meta = mustMeta(this);
|
|
161
|
+
const payload = { ...record, id };
|
|
162
|
+
|
|
163
|
+
const put = () =>
|
|
164
|
+
client!.send(
|
|
165
|
+
new PutItemCommand({
|
|
166
|
+
TableName: meta.name,
|
|
167
|
+
Item: marshall(payload, { removeUndefinedValues: true }),
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await put();
|
|
173
|
+
} catch (err: any) {
|
|
174
|
+
if (err?.name === "ResourceNotFoundException") {
|
|
175
|
+
await createTable(this);
|
|
176
|
+
await put();
|
|
177
|
+
} else throw err;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
static async destroy<M extends Table>(
|
|
182
|
+
this: new () => M,
|
|
183
|
+
id: string
|
|
184
|
+
): Promise<null> {
|
|
185
|
+
requireClient();
|
|
186
|
+
try {
|
|
187
|
+
await client!.send(
|
|
188
|
+
new DeleteItemCommand({
|
|
189
|
+
TableName: mustMeta(this).name,
|
|
190
|
+
Key: marshall({ id }),
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
} catch (err: any) {
|
|
194
|
+
if (err.name === "ResourceNotFoundException") return null;
|
|
195
|
+
throw err;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
static async where<M extends Table>(this: new (d?: any) => M): Promise<M[]> {
|
|
201
|
+
requireClient();
|
|
202
|
+
try {
|
|
203
|
+
const res = await client!.send(
|
|
204
|
+
new ScanCommand({ TableName: mustMeta(this).name })
|
|
205
|
+
);
|
|
206
|
+
return (res.Items ?? []).map((i) => new this(unmarshall(i)));
|
|
207
|
+
} catch (err: any) {
|
|
208
|
+
if (err.name === "ResourceNotFoundException") return [];
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ─────────────────── Utilidades internas ─────────────────── */
|
|
215
|
+
function requireClient(): void {
|
|
216
|
+
if (!client) throw new Error("connect() debe llamarse antes de usar Table");
|
|
217
|
+
}
|
|
218
|
+
function mustMeta(ctor: Function): WrapperEntry {
|
|
219
|
+
const meta = wrapper.get(ctor);
|
|
220
|
+
if (!meta) throw new Error(`Metadata no encontrada para ${ctor.name}`);
|
|
221
|
+
return meta;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ─────────────────── Exportaciones internas ───────────────── */
|
|
225
|
+
export { STORE };
|
|
226
|
+
export type { WrapperEntry };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* src/core/wrapper.ts
|
|
2
|
+
* -------------------------------------------------
|
|
3
|
+
* Registro central (in-memory) de la configuración
|
|
4
|
+
* declarativa de cada modelo. Agnóstico: no depende
|
|
5
|
+
* de DynamoDB ni de otros módulos de la librería.
|
|
6
|
+
*
|
|
7
|
+
* © 2025 Miguel Alejandro
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* ------------------------------------------------------------------ */
|
|
11
|
+
/* 1. Tipos utilitarios */
|
|
12
|
+
/* ------------------------------------------------------------------ */
|
|
13
|
+
export type Inmutable = string | number | boolean | null | object;
|
|
14
|
+
|
|
15
|
+
export type Mutate = (value: any) => Inmutable;
|
|
16
|
+
export type Default = Inmutable | (() => Inmutable);
|
|
17
|
+
export type Validate = (value: any) => true | string;
|
|
18
|
+
|
|
19
|
+
/* ------------------------------------------------------------------ */
|
|
20
|
+
/* 2. Descripción de columna y tabla */
|
|
21
|
+
/* ------------------------------------------------------------------ */
|
|
22
|
+
export interface Column {
|
|
23
|
+
/** nombre físico en la tabla (DynamoDB) */
|
|
24
|
+
name: string;
|
|
25
|
+
|
|
26
|
+
/* Decoradores básicos */
|
|
27
|
+
default?: Default;
|
|
28
|
+
mutate?: Mutate[];
|
|
29
|
+
validate?: Validate[];
|
|
30
|
+
|
|
31
|
+
/* Metadatos de índice / unicidad */
|
|
32
|
+
index?: true; // Partition Key
|
|
33
|
+
indexSort?: true; // Sort Key
|
|
34
|
+
unique?: true; // constraint lógico (no se valida en Dynamo)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Configuración completa por tabla
|
|
39
|
+
*/
|
|
40
|
+
export interface WrapperEntry {
|
|
41
|
+
/** Nombre físico de la tabla (snake_plural o @Name) */
|
|
42
|
+
name: string;
|
|
43
|
+
|
|
44
|
+
/** Columnas asociadas a la clase. key = propiedad (string|symbol) */
|
|
45
|
+
columns: Map<string | symbol, Column>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
/* 3. Contenedor global */
|
|
50
|
+
/* ------------------------------------------------------------------ */
|
|
51
|
+
export type Wrapper = Map<Function, WrapperEntry>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mapa singleton (clase → configuración)
|
|
55
|
+
* Exportado como default para uso interno de la librería.
|
|
56
|
+
*/
|
|
57
|
+
const wrapper: Wrapper = new Map();
|
|
58
|
+
export default wrapper;
|
|
59
|
+
|
|
60
|
+
/* ------------------------------------------------------------------ */
|
|
61
|
+
/* 4. Símbolo de almacenamiento de valores reales */
|
|
62
|
+
/* ------------------------------------------------------------------ */
|
|
63
|
+
/**
|
|
64
|
+
* Buffer privado en cada instancia de Table donde se guardan
|
|
65
|
+
* los valores procesados por los setters virtuales.
|
|
66
|
+
*
|
|
67
|
+
* Se exporta para que los decoradores puedan leer/escribir,
|
|
68
|
+
* pero **NO** se vuelve a exportar desde la raíz del paquete.
|
|
69
|
+
*/
|
|
70
|
+
export const STORE: unique symbol = Symbol("dynamite:values");
|
|
71
|
+
|
|
72
|
+
/* ------------------------------------------------------------------ */
|
|
73
|
+
/* 5. Pequeños helpers opcionales */
|
|
74
|
+
/* ------------------------------------------------------------------ */
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Asegura que exista la entrada en el wrapper para la clase dada.
|
|
78
|
+
* Devuelve la entrada (recién creada o existente).
|
|
79
|
+
*/
|
|
80
|
+
export function ensureConfig(ctor: Function, tableName: string): WrapperEntry {
|
|
81
|
+
let entry = wrapper.get(ctor);
|
|
82
|
+
if (!entry) {
|
|
83
|
+
entry = { name: tableName, columns: new Map() };
|
|
84
|
+
wrapper.set(ctor, entry);
|
|
85
|
+
}
|
|
86
|
+
return entry;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Obtiene (o crea) el objeto Column para una propiedad concreta.
|
|
91
|
+
*/
|
|
92
|
+
export function ensureColumn(
|
|
93
|
+
entry: WrapperEntry,
|
|
94
|
+
prop: string | symbol,
|
|
95
|
+
columnName: string
|
|
96
|
+
): Column {
|
|
97
|
+
let col = entry.columns.get(prop);
|
|
98
|
+
if (!col) {
|
|
99
|
+
col = { name: columnName, mutate: [], validate: [] };
|
|
100
|
+
entry.columns.set(prop, col);
|
|
101
|
+
}
|
|
102
|
+
return col;
|
|
103
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dinamite ORM — @CreatedAt Decorator (wrapper)
|
|
3
|
+
* -------------------------------------------
|
|
4
|
+
* Establece un valor por defecto con la fecha/hora actual (ISO‑string)
|
|
5
|
+
* usando @Default.
|
|
6
|
+
*
|
|
7
|
+
* © 2025 Miguel Alejandro
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Default from "./default";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Asigna automáticamente la fecha de creación en nuevas instancias.
|
|
14
|
+
*/
|
|
15
|
+
export default function CreatedAt(): PropertyDecorator {
|
|
16
|
+
return Default(() => new Date().toISOString());
|
|
17
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dinamite ORM — @Default
|
|
3
|
+
* -----------------------
|
|
4
|
+
* Registra un valor por defecto y crea (si falta) el
|
|
5
|
+
* getter/setter virtual de la propiedad.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Column, STORE, ensureColumn, ensureConfig } from "../core/wrapper";
|
|
9
|
+
import { toSnakePlural } from "../utils/naming";
|
|
10
|
+
|
|
11
|
+
export default function Default(factory: () => unknown): PropertyDecorator {
|
|
12
|
+
if (typeof factory !== "function") {
|
|
13
|
+
throw new TypeError("@Default requiere una función factory");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (target: object, prop: string | symbol): void => {
|
|
17
|
+
const ctor = (target as any).constructor;
|
|
18
|
+
const entry = ensureConfig(ctor, toSnakePlural(ctor.name));
|
|
19
|
+
const column = ensureColumn(entry, prop, String(prop));
|
|
20
|
+
|
|
21
|
+
if (column.default)
|
|
22
|
+
throw new Error(`@Default duplicado en '${String(prop)}'`);
|
|
23
|
+
column.default = factory;
|
|
24
|
+
|
|
25
|
+
if (!Object.getOwnPropertyDescriptor(ctor.prototype, prop)?.set) {
|
|
26
|
+
defineVirtual(ctor.prototype, column, prop);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ------------------------------------------------------------------ */
|
|
32
|
+
function defineVirtual(proto: any, col: Column, prop: string | symbol): void {
|
|
33
|
+
Object.defineProperty(proto, prop, {
|
|
34
|
+
get() {
|
|
35
|
+
return (this[STORE] ?? {})[prop];
|
|
36
|
+
},
|
|
37
|
+
set(val: unknown) {
|
|
38
|
+
const buf = (this[STORE] ??= {});
|
|
39
|
+
|
|
40
|
+
if (val === undefined && col.default !== undefined) {
|
|
41
|
+
val = typeof col.default === "function" ? col.default() : col.default;
|
|
42
|
+
}
|
|
43
|
+
if (col.mutate) for (const m of col.mutate) val = m(val);
|
|
44
|
+
if (col.validate) {
|
|
45
|
+
for (const v of col.validate) {
|
|
46
|
+
const r = v(val);
|
|
47
|
+
if (r !== true)
|
|
48
|
+
throw new Error(typeof r === "string" ? r : "Validación fallida");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
buf[prop] = val;
|
|
52
|
+
},
|
|
53
|
+
enumerable: true,
|
|
54
|
+
configurable: true,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dinamite ORM — @Index (Partition Key)
|
|
3
|
+
* -------------------------------------
|
|
4
|
+
* Marca la propiedad como clave de partición.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ensureColumn, ensureConfig } from "../core/wrapper";
|
|
8
|
+
import { toSnakePlural } from "../utils/naming";
|
|
9
|
+
|
|
10
|
+
export default function Index(): PropertyDecorator {
|
|
11
|
+
return (target: object, prop: string | symbol): void => {
|
|
12
|
+
const ctor = (target as any).constructor;
|
|
13
|
+
const entry = ensureConfig(ctor, toSnakePlural(ctor.name));
|
|
14
|
+
|
|
15
|
+
/* Evitar duplicados */
|
|
16
|
+
const already = [...entry.columns.values()].find((c) => c.index);
|
|
17
|
+
if (already && already !== entry.columns.get(prop)) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`La tabla ${ctor.name} ya tiene PartitionKey (${already.name})`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const col = ensureColumn(entry, prop, String(prop));
|
|
24
|
+
col.index = true;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dinamite ORM — @IndexSort (Sort Key)
|
|
3
|
+
* ------------------------------------
|
|
4
|
+
* Marca la propiedad como clave de ordenamiento.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ensureColumn, ensureConfig } from "../core/wrapper";
|
|
8
|
+
import { toSnakePlural } from "../utils/naming";
|
|
9
|
+
|
|
10
|
+
export default function IndexSort(): PropertyDecorator {
|
|
11
|
+
return (target: object, prop: string | symbol): void => {
|
|
12
|
+
const ctor = (target as any).constructor;
|
|
13
|
+
const entry = ensureConfig(ctor, toSnakePlural(ctor.name));
|
|
14
|
+
|
|
15
|
+
const pkExists = [...entry.columns.values()].some((c) => c.index);
|
|
16
|
+
if (!pkExists) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`PartitionKey no definido en ${ctor.name}; declara @Index primero`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const already = [...entry.columns.values()].find((c) => c.indexSort);
|
|
23
|
+
if (already && already !== entry.columns.get(prop)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`La tabla ${ctor.name} ya tiene SortKey (${already.name})`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const col = ensureColumn(entry, prop, String(prop));
|
|
30
|
+
col.indexSort = true;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dinamite ORM — @Mutate
|
|
3
|
+
* ----------------------
|
|
4
|
+
* Registra funciones transformadoras para un campo y virtualiza
|
|
5
|
+
* la propiedad si aún no existe.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Column, Mutate } from "../core/wrapper";
|
|
9
|
+
import { STORE, ensureColumn, ensureConfig } from "../core/wrapper";
|
|
10
|
+
import { toSnakePlural } from "../utils/naming";
|
|
11
|
+
|
|
12
|
+
export default function Mutate(fn: Mutate): PropertyDecorator {
|
|
13
|
+
if (typeof fn !== "function") throw new TypeError("@Mutate requiere función");
|
|
14
|
+
|
|
15
|
+
return (target: object, prop: string | symbol): void => {
|
|
16
|
+
const ctor = (target as any).constructor;
|
|
17
|
+
const entry = ensureConfig(ctor, toSnakePlural(ctor.name));
|
|
18
|
+
const col = ensureColumn(entry, prop, String(prop));
|
|
19
|
+
|
|
20
|
+
col.mutate ??= [];
|
|
21
|
+
col.mutate.push(fn);
|
|
22
|
+
|
|
23
|
+
if (!Object.getOwnPropertyDescriptor(ctor.prototype, prop)?.set) {
|
|
24
|
+
defineVirtual(ctor.prototype, col, prop);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* ------------------------------------------------------------------ */
|
|
30
|
+
function defineVirtual(proto: any, col: Column, prop: string | symbol): void {
|
|
31
|
+
Object.defineProperty(proto, prop, {
|
|
32
|
+
get() {
|
|
33
|
+
return (this[STORE] ?? {})[prop];
|
|
34
|
+
},
|
|
35
|
+
set(val: unknown) {
|
|
36
|
+
const buf = (this[STORE] ??= {});
|
|
37
|
+
|
|
38
|
+
if (val === undefined && col.default !== undefined) {
|
|
39
|
+
val = typeof col.default === "function" ? col.default() : col.default;
|
|
40
|
+
}
|
|
41
|
+
if (col.mutate) for (const m of col.mutate) val = m(val);
|
|
42
|
+
if (col.validate) {
|
|
43
|
+
for (const v of col.validate) {
|
|
44
|
+
const r = v(val);
|
|
45
|
+
if (r !== true)
|
|
46
|
+
throw new Error(typeof r === "string" ? r : "Validación fallida");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
buf[prop] = val;
|
|
50
|
+
},
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dinamite ORM — @Name
|
|
3
|
+
* --------------------
|
|
4
|
+
* • @Name("tabla") → nombre físico de la tabla
|
|
5
|
+
* • @Name("columna") → alias de columna
|
|
6
|
+
*
|
|
7
|
+
* Permite sobrescribir el nombre AUTO-generado (snake_plural)
|
|
8
|
+
* sin lanzar conflicto.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ensureColumn, ensureConfig } from "../core/wrapper";
|
|
12
|
+
import { toSnakePlural } from "../utils/naming";
|
|
13
|
+
|
|
14
|
+
export default function Name(
|
|
15
|
+
label: string
|
|
16
|
+
): ClassDecorator & PropertyDecorator {
|
|
17
|
+
if (!label || typeof label !== "string") {
|
|
18
|
+
throw new TypeError("@Name requiere una cadena no vacía");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (target: any, prop?: string | symbol): void => {
|
|
22
|
+
const ctor = prop === undefined ? target : target.constructor;
|
|
23
|
+
const entry = ensureConfig(ctor, toSnakePlural(ctor.name));
|
|
24
|
+
|
|
25
|
+
if (prop === undefined) {
|
|
26
|
+
/* ---------- Nombre de la tabla ---------- */
|
|
27
|
+
|
|
28
|
+
const auto = toSnakePlural(ctor.name);
|
|
29
|
+
|
|
30
|
+
// Solo error si ya se modificó conscientemente antes.
|
|
31
|
+
if (entry.name !== auto && entry.name !== label && entry.name) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`La clase ${ctor.name} ya tiene un @Name distinto (${entry.name})`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
entry.name = label;
|
|
37
|
+
} else {
|
|
38
|
+
/* ---------- Alias de columna ---------- */
|
|
39
|
+
|
|
40
|
+
const col = ensureColumn(entry, prop, label);
|
|
41
|
+
|
|
42
|
+
if (col.name && col.name !== label) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`La columna '${String(prop)}' ya tiene @Name distinto (${col.name})`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
col.name = label;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dinamite ORM — @NotNull Decorator (wrapper)
|
|
3
|
+
* ------------------------------------------
|
|
4
|
+
* Valida que el valor no sea null, undefined ni cadena vacía.
|
|
5
|
+
* Internamente aplica @Validate con una función sincrónica.
|
|
6
|
+
*
|
|
7
|
+
* © 2025 Miguel Alejandro
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Validate from "./validate";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Decorador wrapper que asegura no-null / no-empty.
|
|
14
|
+
*/
|
|
15
|
+
export default function NotNull(): PropertyDecorator {
|
|
16
|
+
return (Validate as any)((value, key) => {
|
|
17
|
+
if (value === null || value === undefined) return false;
|
|
18
|
+
if (typeof value === "string" && value.trim() === "") return false;
|
|
19
|
+
return true;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dinamite ORM — @PrimaryKey
|
|
3
|
+
* --------------------------
|
|
4
|
+
* Declara simultáneamente Partition Key y Sort Key
|
|
5
|
+
* sobre la misma propiedad.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Index from "./index";
|
|
9
|
+
import IndexSort from "./index_sort";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Atajo para definir clave primaria compuesta (PK + SK).
|
|
13
|
+
* El parámetro `name` queda reservado por si en el futuro
|
|
14
|
+
* se almacena un identificador lógico de índice, pero hoy
|
|
15
|
+
* NO se pasa a ningún decorador interno.
|
|
16
|
+
*/
|
|
17
|
+
export default function PrimaryKey(name = "primary"): PropertyDecorator {
|
|
18
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
19
|
+
throw new TypeError("@PrimaryKey requiere un nombre de índice válido");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (target: object, prop: string | symbol): void => {
|
|
23
|
+
Index()(target, prop); // Partition Key
|
|
24
|
+
IndexSort()(target, prop); // Sort Key
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dinamite ORM — @UpdatedAt Decorator (wrapper)
|
|
3
|
+
* --------------------------------------------
|
|
4
|
+
* Actualiza la fecha/hora cada vez que la propiedad recibe un nuevo valor
|
|
5
|
+
* (incluyendo actualizaciones mediante Model.save()).
|
|
6
|
+
* Internamente aplica @Mutate con una factory de timestamp ISO.
|
|
7
|
+
*
|
|
8
|
+
* © 2025 Miguel Alejandro
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import Mutate from "./mutate";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Actualiza automáticamente la marca temporal en cada asignación.
|
|
15
|
+
*/
|
|
16
|
+
export default function UpdatedAt(): PropertyDecorator {
|
|
17
|
+
return Mutate(() => new Date().toISOString());
|
|
18
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dinamite ORM — @Validate
|
|
3
|
+
* ------------------------
|
|
4
|
+
* Registra validadores y crea virtual si falta.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Column, STORE, ensureColumn, ensureConfig } from "../core/wrapper";
|
|
8
|
+
import { toSnakePlural } from "../utils/naming";
|
|
9
|
+
|
|
10
|
+
export default function Validate(
|
|
11
|
+
validators:
|
|
12
|
+
| ((v: unknown) => true | string)
|
|
13
|
+
| ((v: unknown) => true | string)[]
|
|
14
|
+
): PropertyDecorator {
|
|
15
|
+
const list = Array.isArray(validators) ? validators : [validators];
|
|
16
|
+
if (!list.length || list.some((v) => typeof v !== "function")) {
|
|
17
|
+
throw new TypeError("@Validate requiere funciones");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (target: object, prop: string | symbol): void => {
|
|
21
|
+
const ctor = (target as any).constructor;
|
|
22
|
+
const entry = ensureConfig(ctor, toSnakePlural(ctor.name));
|
|
23
|
+
const col = ensureColumn(entry, prop, String(prop));
|
|
24
|
+
|
|
25
|
+
col.validate ??= [];
|
|
26
|
+
col.validate.push(...list);
|
|
27
|
+
|
|
28
|
+
if (!Object.getOwnPropertyDescriptor(ctor.prototype, prop)?.set) {
|
|
29
|
+
defineVirtual(ctor.prototype, col, prop);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ------------------------------------------------------------------ */
|
|
35
|
+
function defineVirtual(proto: any, col: Column, prop: string | symbol): void {
|
|
36
|
+
Object.defineProperty(proto, prop, {
|
|
37
|
+
get() {
|
|
38
|
+
return (this[STORE] ?? {})[prop];
|
|
39
|
+
},
|
|
40
|
+
set(val: unknown) {
|
|
41
|
+
const buf = (this[STORE] ??= {});
|
|
42
|
+
|
|
43
|
+
if (val === undefined && col.default !== undefined) {
|
|
44
|
+
val = typeof col.default === "function" ? col.default() : col.default;
|
|
45
|
+
}
|
|
46
|
+
if (col.mutate) for (const m of col.mutate) val = m(val);
|
|
47
|
+
if (col.validate) {
|
|
48
|
+
for (const v of col.validate) {
|
|
49
|
+
const r = v(val);
|
|
50
|
+
if (r !== true)
|
|
51
|
+
throw new Error(typeof r === "string" ? r : "Validación fallida");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
buf[prop] = val;
|
|
55
|
+
},
|
|
56
|
+
enumerable: true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
});
|
|
59
|
+
}
|